refactor: add animations
This commit is contained in:
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Plus,
|
||||
FolderPlus,
|
||||
Layers,
|
||||
Package,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
@@ -45,7 +45,7 @@ const data = {
|
||||
{
|
||||
title: "Project",
|
||||
url: ROUTES.PROJECT,
|
||||
icon: FolderPlus,
|
||||
icon: Layers,
|
||||
},
|
||||
{
|
||||
title: "Package",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
|
||||
interface StatsCardProps {
|
||||
title: string
|
||||
@@ -19,34 +20,41 @@ export function StatsCard({
|
||||
isLoading = false
|
||||
}: StatsCardProps) {
|
||||
return (
|
||||
<Card className="py-6 px-6 hover:scale-105 transition-all duration-300 ease-in-out">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="h-10 w-24 bg-muted rounded-md mt-2 animate-pulse" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-3xl font-bold">
|
||||
{value}
|
||||
</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{subtitle}
|
||||
<motion.div
|
||||
whileHover={{ y: -4, scale: 1.02 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
className="h-full"
|
||||
>
|
||||
<Card className="h-full py-6 px-4 transition-colors cursor-pointer">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-col">
|
||||
{isLoading ? (
|
||||
<div className="h-10 w-24 bg-muted rounded-md mt-2 animate-pulse" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-3xl font-bold tracking-tight">
|
||||
{value}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-md bg-secondary text-secondary-foreground">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="p-3 rounded-xl bg-primary/10 text-primary shrink-0 group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { LucideIcon } from "lucide-react"
|
||||
import { Reveal } from "@/components/ui/reveal"
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string
|
||||
@@ -12,26 +13,29 @@ interface PageHeaderProps {
|
||||
|
||||
export function PageHeader({ title, description, icon: Icon, children, actions }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* <div className="p-3.5 rounded-xl bg-primary text-primary-foreground flex items-center justify-center shadow-lg shadow-primary/5 ring-1 ring-white/10">
|
||||
{Icon && <Icon className="h-7 w-7" />}
|
||||
{children}
|
||||
</div> */}
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl font-extrabold tracking-tight ">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm font-semibold tracking-wide opacity-80">
|
||||
{description}
|
||||
</p>
|
||||
<Reveal direction="down" className="w-full">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* <div className="p-3.5 rounded-xl bg-primary text-primary-foreground flex items-center justify-center shadow-lg shadow-primary/5 ring-1 ring-white/10">
|
||||
{Icon && <Icon className="h-7 w-7" />}
|
||||
{children}
|
||||
</div> */}
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl font-extrabold tracking-tight ">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm font-semibold tracking-wide opacity-80">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="flex items-center gap-4">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="flex items-center gap-4">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Reveal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
|
||||
setLoadingProjects(true)
|
||||
setError(null)
|
||||
const data = await fetchProjects()
|
||||
setProjects(data)
|
||||
setProjects(data.items)
|
||||
} catch (err) {
|
||||
console.error("Failed to load projects:", err)
|
||||
setError("Failed to load projects. Please check if the backend is running.")
|
||||
@@ -74,7 +74,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
|
||||
setSelectedLocation(null)
|
||||
setLocations([])
|
||||
const data = await fetchPackagesByProject(selectedProject.id)
|
||||
setPackages(data)
|
||||
setPackages(data.items)
|
||||
} catch (err) {
|
||||
console.error("Failed to load packages:", err)
|
||||
setError("Failed to load packages for the selected project.")
|
||||
@@ -99,7 +99,7 @@ export function ProjectSelectionSection({ onSelectionComplete }: ProjectSelectio
|
||||
setError(null)
|
||||
setSelectedLocation(null)
|
||||
const data = await fetchLocationsByPackage(selectedPackage.id)
|
||||
setLocations(data)
|
||||
setLocations(data.items)
|
||||
} catch (err) {
|
||||
console.error("Failed to load locations:", err)
|
||||
setError("Failed to load locations for the selected package.")
|
||||
|
||||
122
src/components/ui/reveal.tsx
Normal file
122
src/components/ui/reveal.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { motion, type HTMLMotionProps } from "motion/react";
|
||||
import React from "react";
|
||||
|
||||
interface RevealProps extends HTMLMotionProps<"div"> {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
direction?: "up" | "down" | "left" | "right" | "none";
|
||||
duration?: number;
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
export function Reveal({
|
||||
children,
|
||||
delay = 0,
|
||||
direction = "up",
|
||||
duration = 0.5,
|
||||
distance = 20,
|
||||
className,
|
||||
...props
|
||||
}: RevealProps) {
|
||||
const offsets = {
|
||||
up: { y: distance },
|
||||
down: { y: -distance },
|
||||
left: { x: distance },
|
||||
right: { x: -distance },
|
||||
none: {},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
...(direction !== "none" ? offsets[direction] : {})
|
||||
}}
|
||||
whileInView={{ opacity: 1, x: 0, y: 0 }}
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
transition={{
|
||||
duration,
|
||||
delay,
|
||||
ease: [0.21, 0.47, 0.32, 0.98],
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StaggerContainerProps extends HTMLMotionProps<"div"> {
|
||||
children: React.ReactNode;
|
||||
staggerChildren?: number;
|
||||
delayChildren?: number;
|
||||
}
|
||||
|
||||
export function StaggerContainer({
|
||||
children,
|
||||
staggerChildren = 0.1,
|
||||
delayChildren = 0,
|
||||
className,
|
||||
...props
|
||||
}: StaggerContainerProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren,
|
||||
delayChildren,
|
||||
},
|
||||
},
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StaggerItem({
|
||||
children,
|
||||
direction = "up",
|
||||
distance = 20,
|
||||
className,
|
||||
...props
|
||||
}: RevealProps) {
|
||||
const offsets = {
|
||||
up: { y: distance },
|
||||
down: { y: -distance },
|
||||
left: { x: distance },
|
||||
right: { x: -distance },
|
||||
none: {},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
...(direction !== "none" ? offsets[direction] : {})
|
||||
},
|
||||
show: { opacity: 1, x: 0, y: 0 },
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: [0.21, 0.47, 0.32, 0.98],
|
||||
}}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user