Compare commits

..

No commits in common. "96cc1bb68474e2efdfcff22fc9834deeffa31a06" and "73b31778dbaa1a9257f179e221c029c4f3a274dc" have entirely different histories.

11 changed files with 190 additions and 282 deletions

View File

@ -20,7 +20,6 @@ import {
XCircle, XCircle,
Wallet, Wallet,
} from "lucide-react"; } from "lucide-react";
import { ROUTES } from "@/lib/routes";
export default function EmployeePage() { export default function EmployeePage() {
const { user, logout, isLoading } = useAuth(); const { user, logout, isLoading } = useAuth();
@ -42,10 +41,8 @@ export default function EmployeePage() {
}, [user]); }, [user]);
useEffect(() => { useEffect(() => {
// if (!isLoading && !user) router.replace("/"); if (!isLoading && !user) router.replace("/");
// else if (!isLoading && user?.role === "manager") router.replace("/manager"); else if (!isLoading && user?.role === "manager") router.replace("/manager");
if (!isLoading && !user) router.replace(ROUTES.home);
else if (!isLoading && user?.role === "manager") router.replace(ROUTES.manager);
else if (!isLoading && user) loadData(); else if (!isLoading && user) loadData();
}, [user, isLoading, router, loadData]); }, [user, isLoading, router, loadData]);

View File

@ -3,12 +3,11 @@ import "./globals.css";
import { AuthProvider } from "@/context/AuthContext"; import { AuthProvider } from "@/context/AuthContext";
import { Geist } from "next/font/google"; import { Geist } from "next/font/google";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Toaster } from "@/components/ui/sonner";
const geist = Geist({subsets:['latin'],variable:'--font-sans'}); const geist = Geist({subsets:['latin'],variable:'--font-sans'});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "LeaveFlow - Leave Management", title: "LeaveFlow Leave Management",
description: "Streamlined leave management for teams", description: "Streamlined leave management for teams",
}; };
@ -25,7 +24,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</head> </head>
<body> <body>
<AuthProvider>{children}</AuthProvider> <AuthProvider>{children}</AuthProvider>
<Toaster richColors position="top-right" />
</body> </body>
</html> </html>
); );

View File

@ -14,8 +14,6 @@ import {
XCircle, XCircle,
LayoutDashboard, LayoutDashboard,
} from "lucide-react"; } from "lucide-react";
import { ROUTES } from "@/lib/routes";
import { toast } from "sonner";
type FilterStatus = "All" | "Pending" | "Approved" | "Rejected"; type FilterStatus = "All" | "Pending" | "Approved" | "Rejected";
@ -34,27 +32,23 @@ export default function ManagerPage() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!isLoading && !user) router.replace(ROUTES.home); if (!isLoading && !user) router.replace("/");
else if (!isLoading && user?.role === "employee") router.replace(ROUTES.employee); else if (!isLoading && user?.role === "employee") router.replace("/employee");
else if (!isLoading && user) loadData(); else if (!isLoading && user) loadData();
}, [user, isLoading, router, loadData]); }, [user, isLoading, router, loadData]);
const handleApprove = async (id: number) => { const handleApprove = async (id: number) => {
const leave = leaves.find((l) => l.id === id);
setLoadingId(id); setLoadingId(id);
await updateLeaveStatus(id, "Approved"); await updateLeaveStatus(id, "Approved");
await loadData(); await loadData();
setLoadingId(null); setLoadingId(null);
toast.success(`Leave approved for ${leave?.userName ?? "employee"}`);
}; };
const handleReject = async (id: number) => { const handleReject = async (id: number) => {
const leave = leaves.find((l) => l.id === id);
setLoadingId(id); setLoadingId(id);
await updateLeaveStatus(id, "Rejected"); await updateLeaveStatus(id, "Rejected");
await loadData(); await loadData();
setLoadingId(null); setLoadingId(null);
toast.error(`Leave rejected for ${leave?.userName ?? "employee"}`);
}; };
if (isLoading || !user || !dataLoaded) { if (isLoading || !user || !dataLoaded) {

View File

@ -7,34 +7,37 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CalendarDays, Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { ROUTES } from "@/lib/routes"; import { CalendarDays, AlertCircle, Loader2 } from "lucide-react";
import { toast } from "sonner";
export default function LoginPage() { export default function LoginPage() {
const { login, user, isLoading } = useAuth(); const { login, user, isLoading } = useAuth();
const router = useRouter(); const router = useRouter();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (!isLoading && user) { if (!isLoading && user) {
router.replace(user.role === "manager" ? ROUTES.manager : ROUTES.employee); router.replace(user.role === "manager" ? "/manager" : "/employee");
} }
}, [user, isLoading, router]); }, [user, isLoading, router]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!username || !password) { if (!username || !password) {
toast.error("Please enter username and password"); setError("Please enter username and password");
return; return;
} }
setLoading(true); setLoading(true);
setError("");
const result = await login(username, password); const result = await login(username, password);
setLoading(false); setLoading(false);
if (!result.ok) { if (!result.ok) {
toast.error(result.error ?? "Login failed"); setError(result.error ?? "Login failed");
} else if (result.ok) {
// redirect handled by useEffect
} }
}; };
@ -72,6 +75,7 @@ export default function LoginPage() {
<h1 className="text-2xl font-bold tracking-tight" style={{ color: "var(--text-primary)" }}> <h1 className="text-2xl font-bold tracking-tight" style={{ color: "var(--text-primary)" }}>
LeaveFlow LeaveFlow
</h1> </h1>
</div> </div>
<Card <Card
@ -117,6 +121,13 @@ export default function LoginPage() {
/> />
</div> </div>
{error && (
<Alert style={{ borderColor: "var(--color-danger-border)", background: "var(--color-danger-bg)", borderRadius: "var(--radius-sm)" }}>
<AlertCircle className="h-4 w-4" style={{ color: "var(--color-danger)" }} />
<AlertDescription style={{ color: "var(--color-danger)" }}>{error}</AlertDescription>
</Alert>
)}
<Button <Button
type="submit" type="submit"
className="w-full font-semibold" className="w-full font-semibold"

View File

@ -19,9 +19,9 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { applyLeave, daysBetween, isWeekend, LeaveRequest } from "@/lib/leavesStore"; import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react";
import { toast } from "sonner"; import { applyLeave } from "@/lib/leavesStore";
interface LeaveSheetProps { interface LeaveSheetProps {
open: boolean; open: boolean;
@ -38,13 +38,15 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
const [fromDate, setFromDate] = useState(""); const [fromDate, setFromDate] = useState("");
const [toDate, setToDate] = useState(""); const [toDate, setToDate] = useState("");
const [reason, setReason] = useState(""); const [reason, setReason] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const today = new Date().toISOString().split("T")[0]; const today = new Date().toISOString().split("T")[0];
const daysCount = const daysCount =
fromDate && toDate && toDate >= fromDate fromDate && toDate && toDate >= fromDate
? daysBetween(fromDate, toDate) ? Math.ceil((new Date(toDate).getTime() - new Date(fromDate).getTime()) / 86400000) + 1
: 0; : 0;
const reset = () => { const reset = () => {
@ -52,6 +54,8 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
setFromDate(""); setFromDate("");
setToDate(""); setToDate("");
setReason(""); setReason("");
setError("");
setSuccess(false);
}; };
const handleClose = () => { const handleClose = () => {
@ -61,24 +65,27 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError("");
if (!fromDate || !toDate || !reason.trim()) { if (!fromDate || !toDate || !reason.trim()) {
toast.error("All fields are required"); setError("All fields are required");
return; return;
} }
if (toDate < fromDate) { if (toDate < fromDate) {
toast.error("End date must be after start date"); setError("End date must be after start date");
return; return;
} }
setLoading(true); setLoading(true);
const result = await applyLeave(userId, { type, fromDate, toDate, reason }); const result = await applyLeave(userId, { type, fromDate, toDate, reason });
setLoading(false); setLoading(false);
if (!result.ok) { if (!result.ok) {
toast.error(result.error ?? "Failed to apply"); setError(result.error ?? "Failed to apply");
return; return;
} }
toast.success("Leave applied! Your request is pending approval."); setSuccess(true);
setTimeout(() => {
handleClose(); handleClose();
onSuccess(); onSuccess();
}, 1500);
}; };
return ( return (
@ -100,23 +107,43 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
{success ? (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<div
className="w-16 h-16 rounded-full flex items-center justify-center"
style={{ background: "var(--color-success-bg)" }}
>
<CheckCircle2 className="w-8 h-8" style={{ color: "var(--color-success)" }} />
</div>
<p className="font-semibold text-base" style={{ color: "var(--text-primary)" }}>
Leave Applied!
</p>
<p className="text-sm" style={{ color: "var(--text-muted)" }}>
Your request is pending approval
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-5 pt-2"> <form onSubmit={handleSubmit} className="space-y-5 pt-2">
{/* Leave Type */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}> <Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
Leave Type Leave Type
</Label> </Label>
<Select value={type} onValueChange={(v) => setType(v as LeaveRequest["type"])}> <Select value={type} onValueChange={(v) => setType(v as any)}>
<SelectTrigger style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}> <SelectTrigger style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{LEAVE_TYPES.map((t) => ( {LEAVE_TYPES.map((t) => (
<SelectItem key={t} value={t}>{t}</SelectItem> <SelectItem key={t} value={t}>
{t}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* Date range */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}> <Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
@ -127,13 +154,8 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
min={today} min={today}
value={fromDate} value={fromDate}
onChange={(e) => { onChange={(e) => {
const val = e.target.value; setFromDate(e.target.value);
if (isWeekend(val)) { if (toDate && e.target.value > toDate) setToDate("");
toast.error("Start date cannot be a Saturday or Sunday");
return;
}
setFromDate(val);
if (toDate && val > toDate) setToDate("");
}} }}
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }} style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
/> />
@ -146,14 +168,7 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
type="date" type="date"
min={fromDate || today} min={fromDate || today}
value={toDate} value={toDate}
onChange={(e) => { onChange={(e) => setToDate(e.target.value)}
const val = e.target.value;
if (isWeekend(val)) {
toast.error("End date cannot be a Saturday or Sunday");
return;
}
setToDate(val);
}}
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }} style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
/> />
</div> </div>
@ -173,6 +188,7 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
</div> </div>
)} )}
{/* Reason */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}> <Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
Reason Reason
@ -186,6 +202,13 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
/> />
</div> </div>
{error && (
<Alert style={{ borderColor: "var(--color-danger-border)", background: "var(--color-danger-bg)", borderRadius: "var(--radius-sm)" }}>
<AlertCircle className="h-4 w-4" style={{ color: "var(--color-danger)" }} />
<AlertDescription style={{ color: "var(--color-danger)" }}>{error}</AlertDescription>
</Alert>
)}
<div className="flex gap-2 pt-2"> <div className="flex gap-2 pt-2">
<Button <Button
type="button" type="button"
@ -210,6 +233,7 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
</Button> </Button>
</div> </div>
</form> </form>
)}
</SheetContent> </SheetContent>
</Sheet> </Sheet>
); );

View File

@ -1,49 +0,0 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: (
<CircleCheckIcon className="size-4" />
),
info: (
<InfoIcon className="size-4" />
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@ -1,5 +1,4 @@
"use client"; "use client";
import { ROUTES } from "@/lib/routes";
import React, { import React, {
createContext, createContext,
@ -62,8 +61,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const logout = () => { const logout = () => {
setUser(null); setUser(null);
localStorage.removeItem("lms_user"); localStorage.removeItem("lms_user");
// router.push("/"); router.push("/");
router.push(ROUTES.home);
}; };
return ( return (

View File

@ -5,7 +5,6 @@
"type": "Sick", "type": "Sick",
"fromDate": "2026-04-25", "fromDate": "2026-04-25",
"toDate": "2026-04-26", "toDate": "2026-04-26",
"days": 2,
"reason": "Fever", "reason": "Fever",
"status": "Approved", "status": "Approved",
"appliedAt": "2026-04-20" "appliedAt": "2026-04-20"
@ -16,7 +15,6 @@
"type": "Casual", "type": "Casual",
"fromDate": "2026-04-28", "fromDate": "2026-04-28",
"toDate": "2026-04-30", "toDate": "2026-04-30",
"days": 3,
"reason": "Family trip", "reason": "Family trip",
"status": "Pending", "status": "Pending",
"appliedAt": "2026-04-27" "appliedAt": "2026-04-27"

View File

@ -5,8 +5,6 @@ export interface LeaveRequest {
type: "Sick" | "Casual" | "Annual" | "Unpaid"; type: "Sick" | "Casual" | "Annual" | "Unpaid";
fromDate: string; fromDate: string;
toDate: string; toDate: string;
/** Immutable day count stored at apply-time; used for all balance calculations. */
days: number;
reason: string; reason: string;
status: "Pending" | "Approved" | "Rejected"; status: "Pending" | "Approved" | "Rejected";
appliedAt: string; appliedAt: string;
@ -68,46 +66,25 @@ export async function applyLeave(
): Promise<{ ok: boolean; error?: string }> { ): Promise<{ ok: boolean; error?: string }> {
await seedIfNeeded(); await seedIfNeeded();
// Block if fromDate or toDate is a weekend // Calculate days
if (isWeekend(data.fromDate) || isWeekend(data.toDate)) { const from = new Date(data.fromDate);
return { ok: false, error: "Leave cannot start or end on a weekend (Saturday/Sunday)." }; const to = new Date(data.toDate);
} const days = Math.ceil((to.getTime() - from.getTime()) / 86400000) + 1;
// Calculate days using the shared util (no inline duplication) // Check balance
const days = daysBetween(data.fromDate, data.toDate);
if (days === 0) {
return { ok: false, error: "Selected range has no working days." };
}
// Check balance — account for already-pending requests
const users = getUsers(); const users = getUsers();
const user = users.find((u) => u.id === userId); const user = users.find((u) => u.id === userId);
if (!user) return { ok: false, error: "User not found" }; if (!user) return { ok: false, error: "User not found" };
if ((user.leaveBalance ?? 0) < days)
return { ok: false, error: `Insufficient balance. You have ${user.leaveBalance} days left.` };
const leaves: LeaveRequest[] = JSON.parse(localStorage.getItem(LEAVES_KEY) ?? "[]"); const leaves: LeaveRequest[] = JSON.parse(localStorage.getItem(LEAVES_KEY) ?? "[]");
// Only check balance for non-Unpaid leave
if (data.type !== "Unpaid") {
const pendingDays = leaves
.filter((l) => l.userId === userId && l.status === "Pending" && l.type !== "Unpaid")
.reduce((sum, l) => sum + (l.days ?? daysBetween(l.fromDate, l.toDate)), 0);
const availableBalance = (user.leaveBalance ?? 0) - pendingDays;
if (availableBalance < days)
return {
ok: false,
error: `Insufficient balance. You have ${user.leaveBalance} total days, but ${pendingDays} are reserved by pending requests (${availableBalance} available).`,
};
}
const newLeave: LeaveRequest = { const newLeave: LeaveRequest = {
id: Date.now(), id: Date.now(),
userId, userId,
type: data.type, type: data.type,
fromDate: data.fromDate, fromDate: data.fromDate,
toDate: data.toDate, toDate: data.toDate,
days,
reason: data.reason, reason: data.reason,
status: "Pending", status: "Pending",
appliedAt: new Date().toISOString().split("T")[0], appliedAt: new Date().toISOString().split("T")[0],
@ -131,16 +108,15 @@ export async function updateLeaveStatus(
leaves[idx] = { ...leave, status }; leaves[idx] = { ...leave, status };
localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves)); localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves));
// Adjust balance using the immutable `days` stored at apply-time. // Adjust balance
// Fallback to daysBetween for legacy records that pre-date the `days` field.
const users = getUsers(); const users = getUsers();
const user = users.find((u) => u.id === leave.userId); const user = users.find((u) => u.id === leave.userId);
if (!user || user.role !== "employee") return; if (!user || user.role !== "employee") return;
const days = leave.days ?? daysBetween(leave.fromDate, leave.toDate); const from = new Date(leave.fromDate);
const to = new Date(leave.toDate);
const days = Math.ceil((to.getTime() - from.getTime()) / 86400000) + 1;
// Only adjust balance for non-Unpaid leave
if (leave.type !== "Unpaid") {
if (status === "Approved" && !wasApproved) { if (status === "Approved" && !wasApproved) {
user.leaveBalance = (user.leaveBalance ?? 0) - days; user.leaveBalance = (user.leaveBalance ?? 0) - days;
} else if (status === "Rejected" && wasApproved) { } else if (status === "Rejected" && wasApproved) {
@ -159,7 +135,6 @@ export async function updateLeaveStatus(
} }
} }
} }
}
export function getUserBalance(userId: number): number { export function getUserBalance(userId: number): number {
const users = getUsers(); const users = getUsers();
@ -167,22 +142,8 @@ export function getUserBalance(userId: number): number {
return u?.leaveBalance ?? 0; return u?.leaveBalance ?? 0;
} }
// Returns true if date is Saturday or Sunday
export function isWeekend(dateStr: string): boolean {
const day = new Date(dateStr).getDay();
return day === 0 || day === 6;
}
// Count only working days (MonFri) between two dates
export function daysBetween(from: string, to: string): number { export function daysBetween(from: string, to: string): number {
const start = new Date(from); const f = new Date(from);
const end = new Date(to); const t = new Date(to);
let count = 0; return Math.ceil((t.getTime() - f.getTime()) / 86400000) + 1;
const current = new Date(start);
while (current <= end) {
const day = current.getDay();
if (day !== 0 && day !== 6) count++;
current.setDate(current.getDate() + 1);
}
return count;
} }

22
package-lock.json generated
View File

@ -12,12 +12,10 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.11.0", "lucide-react": "^1.11.0",
"next": "16.2.4", "next": "16.2.4",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"shadcn": "^4.5.0", "shadcn": "^4.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },
@ -8616,16 +8614,6 @@
} }
} }
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -10195,16 +10183,6 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@ -13,12 +13,10 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.11.0", "lucide-react": "^1.11.0",
"next": "16.2.4", "next": "16.2.4",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"shadcn": "^4.5.0", "shadcn": "^4.5.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },