Files
365devnet/src/components/ui/ModernTimeline.astro
becarta 890d7b8670
Some checks failed
GitHub Actions / build (18) (push) Has been cancelled
GitHub Actions / build (20) (push) Has been cancelled
GitHub Actions / build (22) (push) Has been cancelled
GitHub Actions / check (push) Has been cancelled
Updated site completely
2025-03-29 22:32:31 +01:00

205 lines
7.0 KiB
Plaintext

---
import { Icon } from 'astro-icon/components';
import { twMerge } from 'tailwind-merge';
import type { Item } from '~/types';
export interface Props {
items?: Array<Item & { year?: string }>;
defaultIcon?: string;
classes?: Record<string, string>;
compact?: boolean;
}
const { items = [], classes = {}, defaultIcon, compact = false } = Astro.props as Props;
const {
container: containerClass = '',
panel: panelClass = '',
title: titleClass = '',
description: descriptionClass = '',
icon: defaultIconClass = 'text-primary dark:text-blue-300 border-primary dark:border-blue-500 dark:shadow-blue-500/40 dark:shadow-sm',
timeline: timelineClass = 'bg-primary/30 dark:bg-blue-700/30',
timelineDot: timelineDotClass = 'bg-primary dark:bg-blue-700',
year: yearClass = 'text-primary dark:text-blue-300',
} = classes;
---
{
items && items.length && (
<div class={twMerge('relative mx-auto max-w-5xl', containerClass)}>
{/* Main timeline line */}
<div
class="absolute left-4 md:left-1/2 top-0 h-full w-1 transform -translate-x-1/2 z-0 transition-all duration-300 ease-in-out shadow-sm bg-blue-700/15 dark:bg-gray-800"
class:list={[timelineClass]}
/>
<div class="relative">
{items.map((item, index) => {
const { title, description, icon, classes: itemClasses = {} } = item;
const isEven = index % 2 === 0;
// Use the year property if available, otherwise try to extract from date
let year = item.year;
// If year is not provided, try to extract from date in the description
if (!year && description) {
// Look for a date pattern like MM-YYYY
const dateMatch = description.match(/\d{2}-(\d{4})/);
if (dateMatch) {
year = dateMatch[1];
}
}
return (
<div class={`relative ${compact ? 'mb-6' : 'mb-12'}`}>
{/* Year marker (if available) */}
{year && (
<div
class={twMerge(
'absolute left-4 md:left-1/2 transform -translate-x-1/2 -top-4 font-bold text-sm z-10',
yearClass
)}
>
<span class="relative">
<span class="absolute inset-0 bg-white/20 backdrop-blur-sm rounded-md -z-10" />
{year}
</span>
</div>
)}
{/* Timeline dot */}
<div
class={`absolute left-4 md:left-1/2 transform -translate-x-1/2 top-5 ${compact ? 'w-3 h-3' : 'w-4 h-4'} rounded-full z-10 shadow-md transition-all duration-300 ease-in-out`}
class:list={[timelineDotClass]}
/>
{/* Content card */}
<div
class={twMerge(
'relative ml-10 md:ml-0 md:w-[45%]',
isEven ? 'md:mr-auto md:pr-6' : 'md:ml-auto md:pl-6',
'transition-all duration-300 ease-in-out'
)}
>
<div
class={twMerge(
`flex flex-col border border-gray-200 dark:border-gray-700 rounded-lg ${compact ? 'p-3' : 'p-4'} shadow-sm hover:shadow-md transition-all duration-300 ease-in-out backdrop-blur-sm bg-white/15 dark:bg-transparent hover:bg-white/30 hover:backdrop-blur-md dark:hover:backdrop-blur-md hover:translate-y-[-3px] group card-container`,
panelClass,
itemClasses?.panel
)}
>
<div class="flex items-center mb-2">
{(icon || defaultIcon) && (
<Icon
name={icon || defaultIcon}
class={twMerge(
`${compact ? 'w-6 h-6 p-1' : 'w-7 h-7 p-1.5'} rounded-full border-2 mr-2 flex-shrink-0`,
defaultIconClass,
itemClasses?.icon
)}
/>
)}
{title && (
<p
class={twMerge(
`${compact ? 'text-base' : 'text-lg'} font-bold`,
titleClass,
itemClasses?.title
)}
set:html={title}
/>
)}
</div>
{description && (
<div
class="max-h-0 group-hover:max-h-[500px] overflow-hidden transition-all duration-500 ease-in-out"
data-details
>
<div class="flex items-center justify-center mb-1 opacity-70 group-hover:opacity-0 transition-opacity duration-200 h-4">
<div class="w-6 h-1 bg-gray-300 dark:bg-gray-700 rounded-full" />
</div>
<div
class={twMerge(
`text-muted ${compact ? 'text-sm' : 'text-sm'} opacity-0 group-hover:opacity-100 transition-all duration-500 ease-in-out`,
descriptionClass,
itemClasses?.description
)}
set:html={description}
/>
</div>
)}
</div>
{/* Connector line to timeline (visible only on desktop) */}
<div
class={twMerge(
'absolute top-5 hidden md:block h-0.5 w-10 z-0',
isEven ? 'right-0 bg-gradient-to-r' : 'left-0 bg-gradient-to-l',
'from-transparent to-blue-700 dark:to-blue-700'
)}
/>
</div>
</div>
);
})}
</div>
</div>
)
}
<style>
/* Add some animation classes */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Card container styles */
.card-container {
min-height: 0;
height: auto;
transition:
transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
box-shadow 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
will-change: transform, box-shadow;
}
/* Hover effect for details */
[data-details] {
transform-origin: top;
transition:
max-height 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
will-change: max-height, opacity;
}
/* Add a subtle indicator that more content is available */
.group:not(:hover) [data-details] {
max-height: 0 !important;
overflow: hidden;
}
.group:hover [data-details] {
max-height: 550px; /* Large enough to fit content but still allows animation */
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.timeline-item {
width: calc(100% - 2.5rem);
margin-left: 2.5rem;
}
}
</style>