feat: framer-motion page transitions and UI polish
This commit is contained in:
parent
72d95065d0
commit
ec7869abbc
|
|
@ -4,6 +4,7 @@ import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|||
import { useAuth } from '@/hooks/useAuth';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
import AppNavigation from '@/components/layout/Sidebar';
|
||||
import PageTransition from '@/components/PageTransition';
|
||||
import { ReleaseNotesDialog } from '@/components/ReleaseNotesDialog';
|
||||
import CommandPalette from '@/components/CommandPalette';
|
||||
import LoginPage from '@/pages/LoginPage';
|
||||
|
|
@ -84,11 +85,15 @@ function RequireAuth({ children, role }) {
|
|||
}
|
||||
|
||||
function AdminShell({ children }) {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.10),transparent_34rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.28))] text-foreground">
|
||||
<AppNavigation adminMode />
|
||||
<main className="mx-auto w-full max-w-[1500px] px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
||||
{children}
|
||||
<PageTransition routeKey={location.pathname}>
|
||||
{children}
|
||||
</PageTransition>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1101,8 +1101,9 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
|
|||
Loading payment history...
|
||||
</div>
|
||||
) : payments.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border/70 px-3 py-8 text-center text-sm text-muted-foreground">
|
||||
No payments recorded for this bill.
|
||||
<div className="flex flex-col items-center gap-1.5 rounded-lg bg-muted/20 px-3 py-8 text-center">
|
||||
<p className="text-sm font-medium text-muted-foreground">No payments yet</p>
|
||||
<p className="text-xs text-muted-foreground/60">Use the form below to record the first payment.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-52 space-y-2 overflow-y-auto pr-1">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
|
||||
|
||||
export default function PageTransition({ children, routeKey }) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (reduceMotion) return children;
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={routeKey}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -6 }}
|
||||
transition={{ duration: 0.18, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom';
|
||||
import AppNavigation from './Sidebar';
|
||||
import { api } from '@/api';
|
||||
import PageTransition from '@/components/PageTransition';
|
||||
|
||||
function SimplefinBadge() {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
|
@ -23,6 +24,8 @@ function SimplefinBadge() {
|
|||
}
|
||||
|
||||
export default function Layout({ mainContentId }) {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.06),transparent_30rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.12))] text-foreground"
|
||||
role="main"
|
||||
|
|
@ -34,7 +37,9 @@ export default function Layout({ mainContentId }) {
|
|||
<div className="mx-auto w-full max-w-[1500px] px-4 py-6 sm:px-6 lg:px-8 lg:py-8"
|
||||
id={mainContentId}
|
||||
>
|
||||
<Outlet />
|
||||
<PageTransition routeKey={location.pathname}>
|
||||
<Outlet />
|
||||
</PageTransition>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="mx-auto flex w-full max-w-[1500px] flex-wrap items-center justify-center gap-x-4 gap-y-2 px-4 pb-6 text-xs text-muted-foreground sm:px-6 lg:px-8"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowDown, ArrowUp, GripVertical, Pencil, TrendingUp } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api.js';
|
||||
|
|
@ -144,7 +145,9 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill,
|
|||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
<motion.div
|
||||
layout="position"
|
||||
transition={{ layout: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
|
||||
draggable={dragProps?.draggable}
|
||||
onDragStart={dragProps?.onDragStart}
|
||||
onDragEnter={dragProps?.onDragEnter}
|
||||
|
|
@ -359,7 +362,7 @@ export function MobileTrackerRow({ row, year, month, refresh, index, onEditBill,
|
|||
<div className="rounded-md border border-border/50 bg-muted/25 px-2 py-1.5">
|
||||
<NotesCell row={{ ...row, year, month }} refresh={refresh} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{editPayment && (
|
||||
<PaymentModal
|
||||
|
|
|
|||
|
|
@ -103,9 +103,10 @@ export function PaymentLedgerDialog({ row, year, month, threshold, defaultPaymen
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-md border border-dashed border-border/70 px-3 py-8 text-center text-sm text-muted-foreground">
|
||||
No payments recorded for this month.
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-1 rounded-md bg-muted/20 px-3 py-7 text-center">
|
||||
<p className="text-sm font-medium text-muted-foreground">No payments yet</p>
|
||||
<p className="text-xs text-muted-foreground/60">Add one below.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from 'react';
|
||||
import { LayoutGroup } from 'framer-motion';
|
||||
import { ArrowDown, ArrowUp } from 'lucide-react';
|
||||
import { cn, fmt } from '@/lib/utils';
|
||||
import {
|
||||
|
|
@ -160,6 +161,7 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<LayoutGroup id={`tracker-bucket-mobile-${label}`}>
|
||||
<div className="grid gap-3 p-3 lg:hidden" aria-busy={loading ? 'true' : 'false'}>
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
|
|
@ -186,8 +188,8 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l
|
|||
</div>
|
||||
))
|
||||
) : rows.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border/70 bg-background/40 px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
No bills match this bucket and filter set.
|
||||
<div className="rounded-lg bg-muted/15 px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
No bills match — try adjusting your filters.
|
||||
</div>
|
||||
) : (
|
||||
rows.map((r, i) => (
|
||||
|
|
@ -206,6 +208,7 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l
|
|||
))
|
||||
)}
|
||||
</div>
|
||||
</LayoutGroup>
|
||||
|
||||
<div className="hidden lg:block" aria-busy={loading ? 'true' : 'false'}>
|
||||
<div className="overflow-x-auto">
|
||||
|
|
@ -255,24 +258,26 @@ export function TrackerBucket({ label, rows, year, month, refresh, onEditBill, l
|
|||
) : rows.length === 0 ? (
|
||||
<TableRow className="border-border/50">
|
||||
<TableCell colSpan={9} className="py-10 text-center text-sm text-muted-foreground">
|
||||
No bills match this bucket and filter set.
|
||||
No bills match — try adjusting your filters.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((r, i) => (
|
||||
<Row
|
||||
key={r.id}
|
||||
row={r}
|
||||
year={year}
|
||||
month={month}
|
||||
refresh={refresh}
|
||||
index={i}
|
||||
onEditBill={onEditBill}
|
||||
moveControls={moveControlsFor(r, i)}
|
||||
dragProps={dragPropsFor(r, i)}
|
||||
isDrifted={driftedIds.has(r.id)}
|
||||
/>
|
||||
))
|
||||
<LayoutGroup id={`tracker-bucket-table-${label}`}>
|
||||
{rows.map((r, i) => (
|
||||
<Row
|
||||
key={r.id}
|
||||
row={r}
|
||||
year={year}
|
||||
month={month}
|
||||
refresh={refresh}
|
||||
index={i}
|
||||
onEditBill={onEditBill}
|
||||
moveControls={moveControlsFor(r, i)}
|
||||
dragProps={dragPropsFor(r, i)}
|
||||
isDrifted={driftedIds.has(r.id)}
|
||||
/>
|
||||
))}
|
||||
</LayoutGroup>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
|
|||
|
|
@ -281,6 +281,8 @@ export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveC
|
|||
return (
|
||||
<>
|
||||
<TableRow
|
||||
layout="position"
|
||||
transition={{ layout: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
|
||||
data-tracker-row
|
||||
tabIndex={0}
|
||||
aria-rowindex={index + 1}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
|
|
@ -20,19 +21,29 @@ function AlertDialogOverlay({ className, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({ className, ...props }) {
|
||||
function AlertDialogContent({ className, children, ...props }) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
asChild
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
{...props}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: '-50%', y: '-48%', scale: 0.98 }}
|
||||
animate={{ opacity: 1, x: '-50%', y: '-50%', scale: 1 }}
|
||||
exit={{ opacity: 0, x: '-50%', y: '-48%', scale: 0.98 }}
|
||||
transition={{ duration: 0.16, ease: [0.22, 1, 0.36, 1] }}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-1rem)] max-w-md max-h-[calc(100svh-1rem)] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-2xl border border-border/70 bg-card p-4 text-card-foreground shadow-xl duration-200 sm:w-full sm:max-h-[calc(100svh-2rem)] sm:p-6 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-1rem)] max-w-md max-h-[calc(100svh-1rem)] gap-4 overflow-y-auto rounded-2xl border border-border/70 bg-card p-4 text-card-foreground shadow-xl duration-200 sm:w-full sm:max-h-[calc(100svh-2rem)] sm:p-6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AlertDialogPrimitive.Content>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { motion } from 'framer-motion';
|
||||
import { X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
|
|
@ -25,20 +26,28 @@ function DialogContent({ className, children, ref, ...props }) {
|
|||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
asChild
|
||||
ref={ref}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
{...props}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: '-50%', y: '-48%', scale: 0.98 }}
|
||||
animate={{ opacity: 1, x: '-50%', y: '-50%', scale: 1 }}
|
||||
exit={{ opacity: 0, x: '-50%', y: '-48%', scale: 0.98 }}
|
||||
transition={{ duration: 0.18, ease: [0.22, 1, 0.36, 1] }}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-1rem)] max-w-lg max-h-[calc(100svh-1rem)] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-2xl border border-border/70 bg-card p-4 text-card-foreground shadow-xl duration-200 sm:w-full sm:max-h-[calc(100svh-2rem)] sm:p-6 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-1rem)] max-w-lg max-h-[calc(100svh-1rem)] gap-4 overflow-y-auto rounded-2xl border border-border/70 bg-card p-4 text-card-foreground shadow-xl duration-200 sm:w-full sm:max-h-[calc(100svh-2rem)] sm:p-6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-full p-1 opacity-70 transition-all hover:bg-accent hover:opacity-100 focus:outline-none focus:ring-[3px] focus:ring-ring/50 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground" aria-label="Close dialog">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</motion.div>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Table({ className, ref, ...props }) {
|
||||
|
|
@ -58,7 +59,7 @@ function TableFooter({ className, ref, ...props }) {
|
|||
|
||||
function TableRow({ className, ref, ...props }) {
|
||||
return (
|
||||
<tr
|
||||
<motion.tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-b border-border/50 last:border-0',
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@ function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
|
|||
</div>
|
||||
|
||||
{ranges.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
<div className="rounded-lg bg-muted/20 px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
No ranges yet. Add a range to use selected history.
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -649,8 +649,8 @@ function DayDetailDialog({ day, open, onOpenChange, moneyMarker }) {
|
|||
<section>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Bills Due</h3>
|
||||
{day.bills_due.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border/70 p-4 text-sm text-muted-foreground">
|
||||
No bills are due on this day.
|
||||
<div className="rounded-lg bg-muted/15 p-4 text-sm text-muted-foreground">
|
||||
No bills due this day.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -151,10 +151,10 @@ function ExpandedBills({ category }) {
|
|||
if (!bills.length) {
|
||||
return (
|
||||
<div className="border-t border-border/60 bg-muted/15 px-4 py-5 sm:px-6">
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-dashed border-border/70 bg-background/65 p-4 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-3 rounded-lg bg-muted/20 p-4 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>No bills in this category yet.</span>
|
||||
<Button asChild variant="outline" size="sm" className="w-fit">
|
||||
<Link to="/bills">Open Bills</Link>
|
||||
<Link to="/bills">Add a bill →</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ function InputRow({ label, hint, children }) {
|
|||
|
||||
function EmptyDebts() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border/60 bg-muted/10 px-6 py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-xl bg-muted/20 px-6 py-16 text-center">
|
||||
<TrendingDown className="h-10 w-10 text-muted-foreground/40 mb-4" />
|
||||
<p className="text-sm font-medium text-foreground">No bills with a balance found</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
|
|
@ -125,7 +125,7 @@ function EmptyDebts() {
|
|||
|
||||
function NoSelection() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border/60 bg-muted/10 px-6 py-16 text-center">
|
||||
<div className="flex flex-col items-center justify-center rounded-xl bg-muted/20 px-6 py-16 text-center">
|
||||
<Calculator className="h-10 w-10 text-muted-foreground/40 mb-4" />
|
||||
<p className="text-sm font-medium text-foreground">Select a loan or debt to begin</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
|
|
@ -597,7 +597,7 @@ export default function PayoffPage() {
|
|||
startBalance={activeBalance}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center rounded-xl border border-dashed border-border/60 bg-muted/10 h-[300px] text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-center rounded-xl bg-muted/20 h-[300px] text-sm text-muted-foreground">
|
||||
{customNeedsBalance
|
||||
? 'Enter a balance and payment to see the chart'
|
||||
: simPaymentN <= 0
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ export default function RoadmapPage() {
|
|||
<p className="text-sm text-muted-foreground">{roadmapError}</p>
|
||||
</div>
|
||||
) : issues.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border/60 py-12 text-center text-sm text-muted-foreground">
|
||||
<div className="rounded-xl bg-muted/20 py-12 text-center text-sm text-muted-foreground">
|
||||
No open issues.
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -475,7 +475,7 @@ export default function RoadmapPage() {
|
|||
<p className="text-sm text-muted-foreground mt-1">{devLogError}</p>
|
||||
</div>
|
||||
) : devLogData && devLogData.entries?.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border/60 py-12 text-center text-sm text-muted-foreground">
|
||||
<div className="rounded-xl bg-muted/20 py-12 text-center text-sm text-muted-foreground">
|
||||
No activity log entries found.
|
||||
</div>
|
||||
) : devLogData ? (
|
||||
|
|
|
|||
|
|
@ -759,7 +759,7 @@ export default function SnowballPage() {
|
|||
|
||||
{/* Empty state */}
|
||||
{bills.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border/60 py-20 text-center gap-3">
|
||||
<div className="flex flex-col items-center justify-center rounded-xl bg-muted/20 py-20 text-center gap-3">
|
||||
<TrendingDown className="h-10 w-10 text-muted-foreground/30" />
|
||||
<p className="text-sm font-medium text-muted-foreground">No debt bills found</p>
|
||||
<p className="text-xs text-muted-foreground/70 max-w-sm">
|
||||
|
|
|
|||
|
|
@ -663,8 +663,9 @@ export default function SummaryPage() {
|
|||
</div>
|
||||
|
||||
{expenses.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border p-6 text-sm text-muted-foreground">
|
||||
No bills found for this month.
|
||||
<div className="flex flex-col items-center gap-2 rounded-xl bg-muted/20 px-6 py-10 text-center">
|
||||
<p className="text-sm font-medium text-muted-foreground">No bills for this month</p>
|
||||
<a href="/bills" className="text-xs text-primary underline underline-offset-4 hover:opacity-80 transition-opacity">Add a bill →</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-border/60 bg-background/70 px-3">
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"framer-motion": "^12.40.0",
|
||||
"lucide-react": "^0.456.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^8.0.9",
|
||||
|
|
@ -7007,6 +7008,33 @@
|
|||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.40.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz",
|
||||
"integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.40.0",
|
||||
"motion-utils": "^12.39.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
|
|
@ -9252,6 +9280,21 @@
|
|||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.40.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz",
|
||||
"integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.39.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.39.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
|
||||
"integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"framer-motion": "^12.40.0",
|
||||
"lucide-react": "^0.456.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^8.0.9",
|
||||
|
|
|
|||
Loading…
Reference in New Issue