Compare commits
No commits in common. "96cc1bb68474e2efdfcff22fc9834deeffa31a06" and "73b31778dbaa1a9257f179e221c029c4f3a274dc" have entirely different histories.
96cc1bb684
...
73b31778db
@ -20,7 +20,6 @@ import {
|
||||
XCircle,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import { ROUTES } from "@/lib/routes";
|
||||
|
||||
export default function EmployeePage() {
|
||||
const { user, logout, isLoading } = useAuth();
|
||||
@ -42,10 +41,8 @@ export default function EmployeePage() {
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
// if (!isLoading && !user) router.replace("/");
|
||||
// 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);
|
||||
if (!isLoading && !user) router.replace("/");
|
||||
else if (!isLoading && user?.role === "manager") router.replace("/manager");
|
||||
else if (!isLoading && user) loadData();
|
||||
}, [user, isLoading, router, loadData]);
|
||||
|
||||
|
||||
@ -3,12 +3,11 @@ import "./globals.css";
|
||||
import { AuthProvider } from "@/context/AuthContext";
|
||||
import { Geist } from "next/font/google";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
const geist = Geist({subsets:['latin'],variable:'--font-sans'});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "LeaveFlow - Leave Management",
|
||||
title: "LeaveFlow – Leave Management",
|
||||
description: "Streamlined leave management for teams",
|
||||
};
|
||||
|
||||
@ -25,7 +24,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
</head>
|
||||
<body>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
<Toaster richColors position="top-right" />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -14,8 +14,6 @@ import {
|
||||
XCircle,
|
||||
LayoutDashboard,
|
||||
} from "lucide-react";
|
||||
import { ROUTES } from "@/lib/routes";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type FilterStatus = "All" | "Pending" | "Approved" | "Rejected";
|
||||
|
||||
@ -34,27 +32,23 @@ export default function ManagerPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !user) router.replace(ROUTES.home);
|
||||
else if (!isLoading && user?.role === "employee") router.replace(ROUTES.employee);
|
||||
if (!isLoading && !user) router.replace("/");
|
||||
else if (!isLoading && user?.role === "employee") router.replace("/employee");
|
||||
else if (!isLoading && user) loadData();
|
||||
}, [user, isLoading, router, loadData]);
|
||||
|
||||
const handleApprove = async (id: number) => {
|
||||
const leave = leaves.find((l) => l.id === id);
|
||||
setLoadingId(id);
|
||||
await updateLeaveStatus(id, "Approved");
|
||||
await loadData();
|
||||
setLoadingId(null);
|
||||
toast.success(`Leave approved for ${leave?.userName ?? "employee"}`);
|
||||
};
|
||||
|
||||
const handleReject = async (id: number) => {
|
||||
const leave = leaves.find((l) => l.id === id);
|
||||
setLoadingId(id);
|
||||
await updateLeaveStatus(id, "Rejected");
|
||||
await loadData();
|
||||
setLoadingId(null);
|
||||
toast.error(`Leave rejected for ${leave?.userName ?? "employee"}`);
|
||||
};
|
||||
|
||||
if (isLoading || !user || !dataLoaded) {
|
||||
|
||||
23
app/page.tsx
23
app/page.tsx
@ -7,34 +7,37 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { CalendarDays, Loader2 } from "lucide-react";
|
||||
import { ROUTES } from "@/lib/routes";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { CalendarDays, AlertCircle, Loader2 } from "lucide-react";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { login, user, isLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && user) {
|
||||
router.replace(user.role === "manager" ? ROUTES.manager : ROUTES.employee);
|
||||
router.replace(user.role === "manager" ? "/manager" : "/employee");
|
||||
}
|
||||
}, [user, isLoading, router]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!username || !password) {
|
||||
toast.error("Please enter username and password");
|
||||
setError("Please enter username and password");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const result = await login(username, password);
|
||||
setLoading(false);
|
||||
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)" }}>
|
||||
LeaveFlow
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
|
||||
<Card
|
||||
@ -117,6 +121,13 @@ export default function LoginPage() {
|
||||
/>
|
||||
</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
|
||||
type="submit"
|
||||
className="w-full font-semibold"
|
||||
|
||||
@ -19,9 +19,9 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { applyLeave, daysBetween, isWeekend, LeaveRequest } from "@/lib/leavesStore";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react";
|
||||
import { applyLeave } from "@/lib/leavesStore";
|
||||
|
||||
interface LeaveSheetProps {
|
||||
open: boolean;
|
||||
@ -38,13 +38,15 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
const [fromDate, setFromDate] = useState("");
|
||||
const [toDate, setToDate] = useState("");
|
||||
const [reason, setReason] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
|
||||
const daysCount =
|
||||
fromDate && toDate && toDate >= fromDate
|
||||
? daysBetween(fromDate, toDate)
|
||||
? Math.ceil((new Date(toDate).getTime() - new Date(fromDate).getTime()) / 86400000) + 1
|
||||
: 0;
|
||||
|
||||
const reset = () => {
|
||||
@ -52,6 +54,8 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
setFromDate("");
|
||||
setToDate("");
|
||||
setReason("");
|
||||
setError("");
|
||||
setSuccess(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@ -61,24 +65,27 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
if (!fromDate || !toDate || !reason.trim()) {
|
||||
toast.error("All fields are required");
|
||||
setError("All fields are required");
|
||||
return;
|
||||
}
|
||||
if (toDate < fromDate) {
|
||||
toast.error("End date must be after start date");
|
||||
setError("End date must be after start date");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const result = await applyLeave(userId, { type, fromDate, toDate, reason });
|
||||
setLoading(false);
|
||||
if (!result.ok) {
|
||||
toast.error(result.error ?? "Failed to apply");
|
||||
setError(result.error ?? "Failed to apply");
|
||||
return;
|
||||
}
|
||||
toast.success("Leave applied! Your request is pending approval.");
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
onSuccess();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -100,23 +107,43 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
</SheetDescription>
|
||||
</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">
|
||||
{/* Leave Type */}
|
||||
<div className="space-y-1.5">
|
||||
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
|
||||
Leave Type
|
||||
</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)" }}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LEAVE_TYPES.map((t) => (
|
||||
<SelectItem key={t} value={t}>{t}</SelectItem>
|
||||
<SelectItem key={t} value={t}>
|
||||
{t}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Date range */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<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}
|
||||
value={fromDate}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (isWeekend(val)) {
|
||||
toast.error("Start date cannot be a Saturday or Sunday");
|
||||
return;
|
||||
}
|
||||
setFromDate(val);
|
||||
if (toDate && val > toDate) setToDate("");
|
||||
setFromDate(e.target.value);
|
||||
if (toDate && e.target.value > toDate) setToDate("");
|
||||
}}
|
||||
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
|
||||
/>
|
||||
@ -146,14 +168,7 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
type="date"
|
||||
min={fromDate || today}
|
||||
value={toDate}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (isWeekend(val)) {
|
||||
toast.error("End date cannot be a Saturday or Sunday");
|
||||
return;
|
||||
}
|
||||
setToDate(val);
|
||||
}}
|
||||
onChange={(e) => setToDate(e.target.value)}
|
||||
style={{ borderColor: "var(--surface-border)", borderRadius: "var(--radius-sm)" }}
|
||||
/>
|
||||
</div>
|
||||
@ -173,6 +188,7 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reason */}
|
||||
<div className="space-y-1.5">
|
||||
<Label style={{ color: "var(--text-secondary)", fontSize: "0.8125rem", fontWeight: 500 }}>
|
||||
Reason
|
||||
@ -186,6 +202,13 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
/>
|
||||
</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">
|
||||
<Button
|
||||
type="button"
|
||||
@ -210,6 +233,7 @@ export function LeaveSheet({ open, onOpenChange, userId, leaveBalance, onSuccess
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@ -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 }
|
||||
@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
import { ROUTES } from "@/lib/routes";
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
@ -62,8 +61,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem("lms_user");
|
||||
// router.push("/");
|
||||
router.push(ROUTES.home);
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
"type": "Sick",
|
||||
"fromDate": "2026-04-25",
|
||||
"toDate": "2026-04-26",
|
||||
"days": 2,
|
||||
"reason": "Fever",
|
||||
"status": "Approved",
|
||||
"appliedAt": "2026-04-20"
|
||||
@ -16,7 +15,6 @@
|
||||
"type": "Casual",
|
||||
"fromDate": "2026-04-28",
|
||||
"toDate": "2026-04-30",
|
||||
"days": 3,
|
||||
"reason": "Family trip",
|
||||
"status": "Pending",
|
||||
"appliedAt": "2026-04-27"
|
||||
|
||||
@ -5,8 +5,6 @@ export interface LeaveRequest {
|
||||
type: "Sick" | "Casual" | "Annual" | "Unpaid";
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
/** Immutable day count stored at apply-time; used for all balance calculations. */
|
||||
days: number;
|
||||
reason: string;
|
||||
status: "Pending" | "Approved" | "Rejected";
|
||||
appliedAt: string;
|
||||
@ -68,46 +66,25 @@ export async function applyLeave(
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
await seedIfNeeded();
|
||||
|
||||
// Block if fromDate or toDate is a weekend
|
||||
if (isWeekend(data.fromDate) || isWeekend(data.toDate)) {
|
||||
return { ok: false, error: "Leave cannot start or end on a weekend (Saturday/Sunday)." };
|
||||
}
|
||||
// Calculate days
|
||||
const from = new Date(data.fromDate);
|
||||
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)
|
||||
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
|
||||
// Check balance
|
||||
const users = getUsers();
|
||||
const user = users.find((u) => u.id === userId);
|
||||
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) ?? "[]");
|
||||
|
||||
// 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 = {
|
||||
id: Date.now(),
|
||||
userId,
|
||||
type: data.type,
|
||||
fromDate: data.fromDate,
|
||||
toDate: data.toDate,
|
||||
days,
|
||||
reason: data.reason,
|
||||
status: "Pending",
|
||||
appliedAt: new Date().toISOString().split("T")[0],
|
||||
@ -131,16 +108,15 @@ export async function updateLeaveStatus(
|
||||
leaves[idx] = { ...leave, status };
|
||||
localStorage.setItem(LEAVES_KEY, JSON.stringify(leaves));
|
||||
|
||||
// Adjust balance using the immutable `days` stored at apply-time.
|
||||
// Fallback to daysBetween for legacy records that pre-date the `days` field.
|
||||
// Adjust balance
|
||||
const users = getUsers();
|
||||
const user = users.find((u) => u.id === leave.userId);
|
||||
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) {
|
||||
user.leaveBalance = (user.leaveBalance ?? 0) - days;
|
||||
} else if (status === "Rejected" && wasApproved) {
|
||||
@ -159,7 +135,6 @@ export async function updateLeaveStatus(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserBalance(userId: number): number {
|
||||
const users = getUsers();
|
||||
@ -167,22 +142,8 @@ export function getUserBalance(userId: number): number {
|
||||
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 (Mon–Fri) between two dates
|
||||
export function daysBetween(from: string, to: string): number {
|
||||
const start = new Date(from);
|
||||
const end = new Date(to);
|
||||
let count = 0;
|
||||
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;
|
||||
const f = new Date(from);
|
||||
const t = new Date(to);
|
||||
return Math.ceil((t.getTime() - f.getTime()) / 86400000) + 1;
|
||||
}
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@ -12,12 +12,10 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.11.0",
|
||||
"next": "16.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"shadcn": "^4.5.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.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": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@ -10195,16 +10183,6 @@
|
||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||
"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": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
||||
@ -13,12 +13,10 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.11.0",
|
||||
"next": "16.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"shadcn": "^4.5.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user