Files

146 lines
5.7 KiB
TypeScript

"use client";
import { useState, useEffect, type FC, type ReactNode } from "react";
import { motion, AnimatePresence, MotionConfig } from "motion/react";
import { ChevronUpIcon } from "lucide-react";
export interface ActivityItemType {
icon: ReactNode;
title: string;
desc: string;
time: string;
}
export interface ActivitiesCardProps {
headerIcon: ReactNode;
title: string;
subtitle: string;
activities: ActivityItemType[];
}
const ActivityItem: FC<ActivityItemType> = ({ icon, title, desc, time }) => {
return (
<motion.div
layout
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="flex cursor-pointer items-center gap-3 px-3 py-3 transition-colors hover:bg-neutral-50 sm:gap-4 sm:px-5 dark:hover:bg-neutral-800/50"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-gray-100/50 bg-gradient-to-b from-[#f4f4f7]/90 to-[#E9EAF0]/90 text-gray-400 sm:h-12 sm:w-12 dark:border-neutral-700 dark:from-neutral-800 dark:to-neutral-900 dark:text-neutral-500">
{icon}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-[15px] leading-tight font-bold text-[#3E3E43] sm:text-[17px] dark:text-neutral-200">
{title}
</p>
<p className="truncate text-[13px] text-[#909092] sm:text-[15px] dark:text-neutral-500">
{desc}
</p>
</div>
<span className="pt-1 text-[11px] whitespace-nowrap text-[#9F9FA1] sm:text-[13px] dark:text-neutral-600">
{time}
</span>
</motion.div>
);
};
export const ActivitiesCard: FC<ActivitiesCardProps> = ({
headerIcon,
title,
subtitle,
activities,
}) => {
const [open, setOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 640);
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
return (
<MotionConfig transition={{ type: "spring", bounce: 0, duration: 0.6 }}>
<motion.div
layout
className="w-xs overflow-hidden rounded-xl border-2 border-[#e7e6e6]/60 bg-[#FEFEFE] shadow-lg sm:w-sm sm:rounded-[20px] dark:border-neutral-800 dark:bg-neutral-900"
>
<motion.button
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between gap-2 px-3 py-2 transition-colors sm:gap-3 sm:px-4 sm:py-3.5"
>
<div className="flex min-w-0 flex-1 items-center gap-3 text-left sm:gap-4">
<motion.div
initial={{
width: isMobile ? 48 : 60,
height: isMobile ? 48 : 60,
}}
animate={{
width: open ? (isMobile ? 36 : 48) : isMobile ? 48 : 60,
height: open ? (isMobile ? 36 : 48) : isMobile ? 48 : 60,
}}
className="relative flex shrink-0 items-center justify-center overflow-hidden rounded-lg border border-gray-100/50 bg-gradient-to-b from-[#f4f4f7] via-[#efeef2] to-[#E9EAF0] shadow-sm sm:rounded-xl dark:border-neutral-700 dark:from-neutral-700 dark:via-neutral-800 dark:to-neutral-900"
>
<motion.span className="pointer-events-none absolute inset-0 rounded-[inherit] shadow-[inset_1px_1px_2px_rgba(255,255,255,0.8),_inset_-1px_-1px_2px_rgba(165,172,190,0.2)] dark:shadow-[inset_1px_1px_1px_rgba(255,255,255,0.1),inset_-1px_-1px_3px_rgba(0,0,0,0.6)]" />
<motion.div animate={{ scale: open ? 0.7 : 1 }}>
{headerIcon}
</motion.div>
</motion.div>
<div className="flex min-w-0 flex-1 flex-col justify-center">
<motion.p
layout
className="truncate text-[16px] font-bold tracking-tight text-neutral-900 sm:text-[17px] dark:text-neutral-100"
>
{title}
</motion.p>
<AnimatePresence mode="popLayout" initial={false}>
{!open && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.3,
ease: "easeOut",
}}
className="truncate text-[14px] tracking-tight text-[#BFBFC2] sm:text-[15px] dark:text-neutral-500"
>
{subtitle}
</motion.p>
)}
</AnimatePresence>
</div>
</div>
<motion.div
animate={{ rotate: open ? 180 : 0 }}
className="flex size-6 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#9C97A8]/70 to-[#7A7596]/70 shadow-xs dark:from-neutral-700 dark:to-neutral-800"
>
<ChevronUpIcon className="size-5 text-white" />
</motion.div>
</motion.button>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="border-t-2 border-[#e7e6e6]/60 dark:border-neutral-800"
>
<div className="py-2">
{activities.map((item, i) => (
<ActivityItem key={i} {...item} />
))}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</MotionConfig>
);
};