205 lines
7.0 KiB
Plaintext
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>
|