Main page overhaul
This commit is contained in:
164
src/components/ui/ModernTimeline.astro
Normal file
164
src/components/ui/ModernTimeline.astro
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
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-xs 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-xs' : '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>
|
Reference in New Issue
Block a user