164 lines
6.1 KiB
Plaintext
164 lines
6.1 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-slate-200 border-primary dark:border-blue-700',
|
|
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" class:list={[timelineClass]}></div>
|
|
|
|
<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)}>
|
|
{year}
|
|
</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]}></div>
|
|
|
|
{/* 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 bg-white dark:bg-gray-900 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>
|
|
<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-6 z-0',
|
|
isEven ? 'right-0 bg-gradient-to-r' : 'left-0 bg-gradient-to-l',
|
|
'from-transparent to-primary/70 dark:to-blue-700/70'
|
|
)}></div>
|
|
</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.5s 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: 500px; /* 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> |