diff --git a/app/(main)/account/page.tsx b/app/(main)/account/page.tsx index e3d00ef..53156d0 100644 --- a/app/(main)/account/page.tsx +++ b/app/(main)/account/page.tsx @@ -1,8 +1,8 @@ import { BentoContainer, BentoContainerHeader, -} from "@/components/bento-container"; -import { Title, Description, SubTitle } from "@/components/texts"; +} from "@/components/reusables/bento-container"; +import { Title, Description, SubTitle } from "@/components/reusables/texts"; import AccountField from "@/components/account/account-field"; import { Button } from "@/components/ui/button"; import { LogOut } from "lucide-react"; diff --git a/app/(main)/day/[date]/loading.tsx b/app/(main)/day/[date]/loading.tsx index 23591f2..22ddb7d 100644 --- a/app/(main)/day/[date]/loading.tsx +++ b/app/(main)/day/[date]/loading.tsx @@ -1,9 +1,14 @@ +import { Skeleton } from "@/components/ui/skeleton"; + const Loading = () => { return ( -
- Loading... +
+ + + +
- ) + ); }; -export default Loading; \ No newline at end of file +export default Loading; diff --git a/app/(main)/day/[date]/page.tsx b/app/(main)/day/[date]/page.tsx index d0a9538..b0a125f 100644 --- a/app/(main)/day/[date]/page.tsx +++ b/app/(main)/day/[date]/page.tsx @@ -1,6 +1,4 @@ -import { Suspense } from "react"; import AttendanceTableServer from "@/components/attendance-table/attendance-table-server"; -import Loader from "@/components/loader"; interface SingleDayPageProps { // When a page component is async, Next.js may provide params as a thenable. @@ -11,19 +9,9 @@ const SingleDayPage = async ({ params }: SingleDayPageProps) => { return (
- - } - > - - +
); }; -export default SingleDayPage; +export default SingleDayPage; \ No newline at end of file diff --git a/app/(main)/day/range/loading.tsx b/app/(main)/day/range/loading.tsx deleted file mode 100644 index 23591f2..0000000 --- a/app/(main)/day/range/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const Loading = () => { - return ( -
- Loading... -
- ) -}; - -export default Loading; \ No newline at end of file diff --git a/app/(main)/error.tsx b/app/(main)/error.tsx new file mode 100644 index 0000000..6f6c537 --- /dev/null +++ b/app/(main)/error.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { ErrorBoundaryProps } from "@/lib/error-types"; +import FetchFailed from "@/components/error/fetch-failed"; + +const ErrorPage: React.FC = ({ error, reset }) => { + return ; +}; + +export default ErrorPage; \ No newline at end of file diff --git a/app/(main)/home/loading.tsx b/app/(main)/home/loading.tsx deleted file mode 100644 index 71fed28..0000000 --- a/app/(main)/home/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const LoadingHomePage = () => { - return ( -
- loading... -
- ) -}; - -export default LoadingHomePage; \ No newline at end of file diff --git a/app/(main)/home/page.tsx b/app/(main)/home/page.tsx index 7c95ea2..4ce5ba2 100644 --- a/app/(main)/home/page.tsx +++ b/app/(main)/home/page.tsx @@ -3,8 +3,8 @@ import { BentoContainer, BentoContainerHeader, -} from "@/components/bento-container"; -import { Description, Title } from "@/components/texts"; +} from "@/components/reusables/bento-container"; +import { Description, Title } from "@/components/reusables/texts"; import { useDates } from "@/context/dates-context"; import { DayCards, @@ -13,11 +13,15 @@ import { } from "@/components/days/day-cards"; import { groupDatesByMonth } from "@/lib/utils"; import { ScrollArea } from "@/components/ui/scroll-area"; -import AlertMessage from "@/components/alert-message"; +import AlertMessage from "@/components/reusables/alert-message"; +import { Skeleton } from "@/components/ui/skeleton"; const HomePage = () => { const { dates } = useDates(); - const groupedDates = groupDatesByMonth(dates); + const groupedDates = dates ? groupDatesByMonth(dates) : {}; + + const isLoading = dates === null; + const isEmpty = dates && dates.length === 0; return ( @@ -30,20 +34,45 @@ const HomePage = () => { - - - {Object.entries(groupedDates).map(([monthYear, days]) => ( - - - {days.map((item) => ( - - ))} - - - ))} + {isLoading && ( +
+ {Array.from({ length: 2 }).map((_, monthIndex) => ( +
+ +
+ {Array.from({ length: 4 }).map( + (_, dayIndex) => ( + + ) + )} +
+
+ ))} +
+ )} + + {isEmpty && ( + + )} + + {!isLoading && + !isEmpty && + Object.entries(groupedDates).map(([monthYear, days]) => ( + + + {days.map((item) => ( + + ))} + + + ))}
); }; -export default HomePage; + +export default HomePage; \ No newline at end of file diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 4239760..f99756e 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -1,33 +1,41 @@ import { LoadingProvider } from "@/context/loading-context"; +import { SidebarOpenProvider } from "@/context/sidebar-open-context"; +import { DatesProvider } from "@/context/dates-context"; import Header from "@/components/header"; -import { Toaster } from "sonner"; -import React from "react"; import DaysSidebarServer from "@/components/days/days-sidebar-server"; +import { Toaster } from "sonner"; import NextTopLoader from "nextjs-toploader"; -import { DatesProvider } from "@/context/dates-context"; +import React, { Suspense } from "react"; +import DaysSidebarLoader from "@/components/days/days-sidebar-loader"; export const dynamic = "force-dynamic"; interface MainLayoutProps { children: React.ReactNode; } + const MainLayout: React.FC = ({ children }) => { return ( -
- - -
-
- - -
- {children} -
-
+ + +
+ +
+ +
+ + }> + + +
+ {children} +
+
+
-
- -
+
+ + ); }; diff --git a/app/(main)/student/[studentId]/loading.tsx b/app/(main)/student/[studentId]/loading.tsx deleted file mode 100644 index fb0e670..0000000 --- a/app/(main)/student/[studentId]/loading.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Loader from "@/components/loader"; - -const LoadingHomePage = () => { - return ( -
- -
- ); -}; - -export default LoadingHomePage; diff --git a/app/(main)/student/[studentId]/page.tsx b/app/(main)/student/[studentId]/page.tsx index 4278d9a..85a35a8 100644 --- a/app/(main)/student/[studentId]/page.tsx +++ b/app/(main)/student/[studentId]/page.tsx @@ -1,22 +1,4 @@ -import { AttendanceRecordResponse } from "@/lib/types"; -import { - fetchJSON, - formatDateForRender, - formatTimeForRender, -} from "@/lib/utils"; - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { BentoContainer } from "@/components/bento-container"; -import { Description, SubTitle, Title } from "@/components/texts"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import NoDataMessage from "@/components/no-data-message"; +import StudentOnDate from "@/components/student/student-on-date"; interface StudentOnDatePageProps { // params of the studentId @@ -32,96 +14,7 @@ const StudentOnDatePage: React.FC = async ({ const studentId = (await params).studentId; const date = (await searchParams).date; - const route = - process.env.NEXT_PUBLIC_BASE_URL + - `/api/reports/student/${studentId}?date=${date}`; - - const data = await fetchJSON(route); - - if (!data.success) return - - const student = [ - { - label: "Partner Id", - value: - data.data.length > 0 ? data.data[0].partner_id : "Unknown ID", - }, - { - label: "Email", - value: - data.data.length > 0 - ? data.data[0].email_address - : "Unknown Email", - }, - { - label: "Section", - value: - data.data.length > 0 - ? data.data[0].department - : "Unknown Section", - }, - ]; - - // Format the data.data to just the checkIn and check_out fields - const formattedData = data.data.map((item) => { - return { - checkIn: item.checkIn, - check_out: item.check_out, - }; - }); - - return ( -
-
- - Student Record for {studentId} on{" "} - {formatDateForRender(date)} - - - If data is missing, it means the student did not check in or out - on that date. Refresh the page if you believe this is an error. - -
- - {/* STUDENT PROFILE */} -
- {student.map(({ value, label }) => ( - - {value} - {label} - - ))} -
- -
- - - - - Check In - Check Out - - - - {formattedData.map((item, index) => ( - - - {formatTimeForRender(item.checkIn)} - - - {formatTimeForRender(item.check_out)} - - - ))} - -
-
-
-
- ); + return ; }; export default StudentOnDatePage; diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..2352b72 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { ErrorBoundaryProps } from "@/lib/error-types"; +import FetchFailed from "@/components/error/fetch-failed"; + +const ErrorPage: React.FC = ({ error, reset }) => { + return ; +}; + +export default ErrorPage; diff --git a/app/layout.tsx b/app/layout.tsx index fde59c6..698db8f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { Toaster } from "sonner"; import { LoadingProvider } from "@/context/loading-context"; import NextTopLoader from "nextjs-toploader"; +import ContentTransition from "../components/content-transition"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -30,9 +31,11 @@ export default function RootLayout({ - - {children} - + + + {children} + + ); diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..2d7c7e0 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,47 @@ +import AudioWave from "@/components/reusables/audio-wave"; +import { Description, Title } from "@/components/reusables/texts"; +import { Frown } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import AnimoDevBadge from "@/components/reusables/animo-dev-badge"; + +const NotFoundPage = () => { + return ( +
+
+

+ Project Harmony +

+ +
+ +
+ + + 404 - Page Not Found <Frown />{" "} + + + The page you are looking for does not exist. + +
+ +
+
+ + + + + + +
+ + Contact support if you believe this is an error. + +
+
+ ); +}; + +export default NotFoundPage; diff --git a/app/page.tsx b/app/page.tsx index 26ef448..7e6378f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,14 +1,9 @@ -"use client"; - -import { Description, Title } from "@/components/texts"; +import { Description, Title } from "@/components/reusables/texts"; import { Button } from "@/components/ui/button"; -import { Book, Disc3 } from "lucide-react"; -import AudioWave from "../components/audio-wave"; -import { Badge } from "@/components/ui/badge"; -import { useIsTablet } from "@/hooks/use-tablet"; +import { Book } from "lucide-react"; +import AudioWave from "../components/reusables/audio-wave"; import ANIMODEVLOGO from "@/public/animo-dev-logo.jpg"; import Image from "next/image"; -import { useEffect, useState } from "react"; import { DropdownMenu, DropdownMenuContent, @@ -16,23 +11,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import Link from "next/link"; +import AnimoDevBadge from "@/components/reusables/animo-dev-badge"; const LandingPage = () => { - const isTablet = useIsTablet(); - const [discSize, setDiscSize] = useState(45); - - useEffect(() => { - if (isTablet) { - setDiscSize(30); - } else { - setDiscSize(45); - } - }, [isTablet]); - return (
-
- +
{/* HERO SECTION */}
@@ -50,14 +34,6 @@ const LandingPage = () => { musikalista logo{" "}
-

- Musikalista IoT-powered Room Tracker monitors - every entry and exit in the Music Club Room, - syncing data to this website's dashboard so - club heads and members always know who is - practicing, when the room's occupied, and - who last used it, all without manual logs. -

@@ -67,50 +43,43 @@ const LandingPage = () => { -
- - {" "} - Developed and maintained by - - {" "} - ANIMO.DEV - {" "} - - + <div className="mt-20 md:mt-0 space-y-10"> + <div className="w-full flex items-center justify-center md:justify-start lg:justify-start mb-4"> + <AnimoDevBadge /> + </div> + <Title className="text-5xl sm:text-6xl lg:text-7xl mb-4 font-bold break-words text-center md:text-left"> Never <span className="text-primary"> {" "} - L - <Disc3 - className="inline animate-spin" - size={discSize} - /> - se Track{" "} + Lose Track{" "} </span>{" "} - <br className="block md:hidden" /> of a Beat, <br className="hidden lg:block" /> or - <span className="text-primary"> - {" "} - Wh - <Disc3 - className="inline animate-spin" - size={discSize} - />{" "} - <br className="block md:hidden" /> - is{" "} - </span> + <span className="text-primary"> Who is </span> in the - <span className="text-primary"> Room</span>. + <span className="text-primary"> Room.</span>{" "} - + The official website for the Iot-powered Musikalista Room Tracker
{" "} that monitors check-ins and check-outs.
+ + Musikalista IoT-powered Room Tracker monitors every + entry and exit in the Music Club Room,{" "} +
syncing data to + this website's dashboard so club heads and + members always know who is practicing,{" "} +
when the + room's occupied, and who last used it, all + without manual logs. +
- - - +
+ + + + +
diff --git a/components/attendance-table/attendance-table-server.tsx b/components/attendance-table/attendance-table-server.tsx index e845f7a..2b54cc3 100644 --- a/components/attendance-table/attendance-table-server.tsx +++ b/components/attendance-table/attendance-table-server.tsx @@ -1,9 +1,8 @@ import { DateResponse } from "@/lib/types"; -import { BentoContainer } from "../bento-container"; -import { SubTitle, Description } from "../texts"; +import { BentoContainer } from "../reusables/bento-container"; +import { SubTitle, Description } from "../reusables/texts"; import AttendanceTable from "./attendance-table"; import { fetchJSON } from "@/lib/utils"; -import NoDataMessage from "../no-data-message"; interface AttendanceTableServerProps { className?: string; @@ -18,7 +17,8 @@ const AttendanceTableServer: React.FC = async ({ const route = `${process.env.NEXT_PUBLIC_BASE_URL}/api/reports/date/${date}`; const data = await fetchJSON(route); - if (!data.success) return + if (!data.success) + throw new Error(data.message || "Failed to fetch attendance data."); const formattedData = data.data.data?.map((item, index) => { @@ -37,7 +37,7 @@ const AttendanceTableServer: React.FC = async ({ className={className} date={date} data={formattedData} - /> + /> ); }; diff --git a/components/attendance-table/attendance-table.tsx b/components/attendance-table/attendance-table.tsx index cbd3af4..83fb6fc 100644 --- a/components/attendance-table/attendance-table.tsx +++ b/components/attendance-table/attendance-table.tsx @@ -2,10 +2,12 @@ import { ColumnDef } from "@tanstack/react-table"; import { DataTable } from "@/components/ui/data-table"; -import { BentoContainer } from "../bento-container"; -import { Description, SubTitle } from "../texts"; +import { + BentoContainer, + BentoContainerHeader, +} from "../reusables/bento-container"; +import { Description, SubTitle } from "../reusables/texts"; import React from "react"; -import ShareButton from "./share-button"; import { formatDateForRender, formatTimeForRender } from "@/lib/utils"; export interface Columns { @@ -51,7 +53,6 @@ interface AttendanceTableProps { tableClassName?: string; date: string; data: Columns[]; - withShareButton?: boolean; } const AttendanceTable: React.FC = ({ @@ -59,13 +60,12 @@ const AttendanceTable: React.FC = ({ tableClassName, date, data, - withShareButton, }) => { return ( -
+ {" "} Record for day: {formatDateForRender(date)}{" "} @@ -74,16 +74,14 @@ const AttendanceTable: React.FC = ({ {" "} List of all the checkins and outs for the selected day{" "} -
+ - {withShareButton && } - + />
); }; diff --git a/components/attendance-table/share-button.tsx b/components/attendance-table/share-button.tsx deleted file mode 100644 index 2406389..0000000 --- a/components/attendance-table/share-button.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import { usePathname, useSearchParams } from "next/navigation"; - -import { Button } from "../ui/button"; -import { Share } from "lucide-react"; -import { toast } from "sonner"; -import { DateType } from "@/lib/types"; - -interface ShareButtonProps extends React.ComponentProps { - fromHome? : boolean; - fromRange? : boolean; - date? : DateType; - children? : React.ReactNode; -} - -const ShareButton: React.FC = ( { fromHome, fromRange, date, children, ...buttonProps }) => { - const pathname = usePathname(); - const searchParams = useSearchParams(); - - const handleShareClick = () => { - if (fromHome && date) { - const shareUrl = `${ - process.env.NEXT_PUBLIC_BASE_URL - }/day/${date.text}`; - - navigator.clipboard.writeText(shareUrl); - toast.success("Link copied to clipboard!"); - return; - }; - - if (fromRange) { - const startDate = searchParams.get("startDate"); - const endDate = searchParams.get("endDate"); - - if (!startDate || !endDate) { - toast.error("Invalid date range"); - return; - } - - const shareUrl = `${ - process.env.NEXT_PUBLIC_BASE_URL - }/day/range?startDate=${startDate}&endDate=${endDate}`; - navigator.clipboard.writeText(shareUrl); - toast.success("Link copied to clipboard!"); - return; - } - - const shareUrl = `${ - process.env.NEXT_PUBLIC_BASE_URL - }${pathname}`; - - navigator.clipboard.writeText(shareUrl); - toast.success("Link copied to clipboard!"); - }; - - return ( - - ); -}; - -export default ShareButton; diff --git a/components/auth/auth-form.tsx b/components/auth/auth-form.tsx index 5715215..6c8c7d2 100644 --- a/components/auth/auth-form.tsx +++ b/components/auth/auth-form.tsx @@ -18,8 +18,8 @@ import { import { Input } from "@/components/ui/input"; -import { Title, Description } from "../texts"; -import { BentoContainer } from "../bento-container"; +import { Title, Description } from "../reusables/texts"; +import { BentoContainer } from "../reusables/bento-container"; const authSchema = z.object({ email: z.string().min(1, "Email is required"), diff --git a/components/content-transition.tsx b/components/content-transition.tsx new file mode 100644 index 0000000..fb8cbad --- /dev/null +++ b/components/content-transition.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +type Props = { + children: ReactNode; + className?: string; +}; + +export default function ContentTransition({ children, className }: Props) { + const [ready, setReady] = useState(false); + + useEffect(() => { + // Defer to next frame so initial styles paint before we transition + const id = requestAnimationFrame(() => setReady(true)); + return () => cancelAnimationFrame(id); + }, []); + + return ( +
+ {children} +
+ ); +} diff --git a/components/custom-shortcut/custom-shortcut.tsx b/components/custom-shortcut/custom-shortcut.tsx index cf5d736..fc64ff6 100644 --- a/components/custom-shortcut/custom-shortcut.tsx +++ b/components/custom-shortcut/custom-shortcut.tsx @@ -15,27 +15,46 @@ import { Button } from "../ui/button"; import React, { useState } from "react"; import { Dispatch, SetStateAction } from "react"; +import { useSidebarOpen } from "@/context/sidebar-open-context"; // Used by the items in the custom shortcut dropdown menu export interface DropdownCustomItemProps { setDropdownOpen: Dispatch>; + setSidebarOpen : (open: boolean) => void; } -type CustomShortcutProps = React.ComponentProps +interface CustomShortcutProps { + children?: React.ReactNode; + variant?: "default" | "link"; + className?: string; +} + +const CustomShortcut: React.FC = ({ + children, + variant, + className, +}) => { + // sidebar open handler + const { setSidebarOpen } = useSidebarOpen(); -const CustomShortcut : React.FC = (props) => { const [dropdownOpen, setDropdownOpen] = useState(false); return ( - + {children ? ( +
{children}
+ ) : ( + + )}
Custom Shortcuts - - + +
); diff --git a/components/custom-shortcut/dropdown-date-range-item.tsx b/components/custom-shortcut/dropdown-date-range-item.tsx index 64937de..8e9fbdb 100644 --- a/components/custom-shortcut/dropdown-date-range-item.tsx +++ b/components/custom-shortcut/dropdown-date-range-item.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import DatePicker from "../date-picker"; +import DatePicker from "../ui/date-picker"; import { Button } from "../ui/button"; import { DropdownMenuItem } from "../ui/dropdown-menu"; @@ -23,6 +23,7 @@ import { toast } from "sonner"; const DropdownDateRangeItem: React.FC = ({ setDropdownOpen, + setSidebarOpen }) => { const [startDate, setStartDate] = useState(undefined); const [endDate, setEndDate] = useState(undefined); @@ -35,6 +36,7 @@ const DropdownDateRangeItem: React.FC = ({ return; } + setSidebarOpen(false); setDropdownOpen(false); const formattedStartDate = formatDateAsYYYYMMDD(startDate); diff --git a/components/custom-shortcut/dropdown-student-on-date-item.tsx b/components/custom-shortcut/dropdown-student-on-date-item.tsx index 4c287e5..0cb3ce5 100644 --- a/components/custom-shortcut/dropdown-student-on-date-item.tsx +++ b/components/custom-shortcut/dropdown-student-on-date-item.tsx @@ -39,6 +39,7 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { toast } from "sonner"; + const formSchema = z.object({ studentId: z.string().min(1, "Student ID is required"), date: z.date({ error: "Date is required" }), @@ -46,6 +47,7 @@ const formSchema = z.object({ const DropdownStudentOnDateItem: React.FC = ({ setDropdownOpen, + setSidebarOpen, }) => { const [dialogOpen, setDialogOpen] = useState(false); @@ -65,6 +67,7 @@ const DropdownStudentOnDateItem: React.FC = ({ return; } + setSidebarOpen(false); setDropdownOpen(false); setDialogOpen(false); diff --git a/components/days/day-cards.tsx b/components/days/day-cards.tsx index 79521eb..f255422 100644 --- a/components/days/day-cards.tsx +++ b/components/days/day-cards.tsx @@ -1,29 +1,29 @@ -import {BentoContainer} from "../bento-container"; -import { SubTitle, Description, SubHeading } from "../texts"; +import { BentoContainer } from "../reusables/bento-container"; +import { SubTitle, Description, SubHeading } from "../reusables/texts"; import { ChevronRight, Sheet } from "lucide-react"; import Link from "next/link"; import { Button } from "../ui/button"; -import ShareButton from "../attendance-table/share-button"; import { formatDateForRender } from "@/lib/utils"; import React from "react"; import { DateType } from "@/lib/types"; interface DayCardsContainerProps { - children : React.ReactNode; - title : string; + children: React.ReactNode; + title: string; } -const DayCardsContainer : React.FC = ({ children, title }) => { +const DayCardsContainer: React.FC = ({ + children, + title, +}) => { return (
- - {title} - + {title} {children}
- ) -} + ); +}; interface DayCardsProps { children: React.ReactNode; @@ -31,21 +31,19 @@ interface DayCardsProps { const DayCards: React.FC = ({ children }) => { return ( -
    +
      {children}
    ); }; interface DayCardItemProps { - item : DateType; + item: DateType; } -const DayCardItem: React.FC = ({ - item, -}) => { +const DayCardItem: React.FC = ({ item }) => { return (
  • - +
    {formatDateForRender(item.text)} @@ -64,17 +62,17 @@ const DayCardItem: React.FC = ({
    -
    - - - - -
    + + +
  • ); }; -export { DayCardsContainer, DayCards, DayCardItem }; \ No newline at end of file +export { DayCardsContainer, DayCards, DayCardItem }; diff --git a/components/days/days-sidebar-loader.tsx b/components/days/days-sidebar-loader.tsx new file mode 100644 index 0000000..58d09ac --- /dev/null +++ b/components/days/days-sidebar-loader.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { Description, SubTitle} from "@/components/reusables/texts"; +import { Calendar } from "lucide-react"; +import { + BentoContainer, +} from "@/components/reusables/bento-container"; +import { Skeleton } from "@/components/ui/skeleton"; + +import { useIsTablet } from "@/hooks/use-tablet"; + +const DaysSidebarLoader = () => { + const isTablet = useIsTablet(); + + return ( + !isTablet && ( + +
    +
    +
    + + Choose Day +
    + +
    + + Select a day to view records. + +
    +
    + {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
    +
    + ) + ); +}; + +export default DaysSidebarLoader; diff --git a/components/days/days-sidebar-server.tsx b/components/days/days-sidebar-server.tsx index 7b682d0..a7853ac 100644 --- a/components/days/days-sidebar-server.tsx +++ b/components/days/days-sidebar-server.tsx @@ -1,8 +1,8 @@ import { fetchJSON, formatDatesWithIndexAsId } from "@/lib/utils"; import DaysSidebar from "./days-sidebar"; -import NoDataMessage from "../no-data-message"; import { AvailableDatesResponse } from "@/lib/types"; - +import FetchFailed from "../error/fetch-failed"; +import { Description } from "../reusables/texts"; interface DaysSidebarProps { className?: string; @@ -10,13 +10,29 @@ interface DaysSidebarProps { const DaysSidebarServer: React.FC = async ({ className }) => { const route = `${process.env.NEXT_PUBLIC_BASE_URL}/api/reports/dates`; - const data = await fetchJSON(route); + const data = await fetchJSON(route, { + cache: "default", + }); + + // since this component is not used by a page + // manually return the error component + if (!data.success) { + const error = new Error( + data.message || "Failed to fetch available dates" + ); + return ; + } - if (!data.success) return + // prevent unecessary client rendering + if (!data.dates || data.dates.length === 0) { + return No available dates yet. ; + } - const formattedDates = formatDatesWithIndexAsId(data.dates); + const formattedDates = formatDatesWithIndexAsId(data.dates); - return ; + return ( + + ); }; -export default DaysSidebarServer; \ No newline at end of file +export default DaysSidebarServer; diff --git a/components/days/days-sidebar.tsx b/components/days/days-sidebar.tsx index 69bd26f..f7654b5 100644 --- a/components/days/days-sidebar.tsx +++ b/components/days/days-sidebar.tsx @@ -1,8 +1,8 @@ "use client"; import React, { useEffect, useMemo } from "react"; -import { Description, SubTitle } from "../texts"; -import { BentoContainer } from "../bento-container"; +import { Description, SubTitle } from "../reusables/texts"; +import { BentoContainer } from "../reusables/bento-container"; import DaysSidebarItem from "./days-sidebar-item"; import { Calendar } from "lucide-react"; import { useDates } from "@/context/dates-context"; @@ -24,7 +24,7 @@ const DaysSidebar: React.FC = ({ }) => { const isTablet = useIsTablet(); - const { dates, setDates } = useDates(); + const { setDates } = useDates(); useEffect(() => { setDates(formattedDates); @@ -35,11 +35,11 @@ const DaysSidebar: React.FC = ({ // Memoitized the dates list to prevent unnecessary re-renders // Also check if the pathname equals the date and add an active class const datesMemo = useMemo(() => { - return dates.map((date) => ({ + return formattedDates.map((date) => ({ ...date, isActive: pathname === `/day/${date.text}`, })); - }, [dates, pathname]); + }, [formattedDates, pathname]); if (isTablet) { return null; @@ -72,11 +72,16 @@ const DaysSidebar: React.FC = ({ type="always" >
      - {datesMemo.map((date) => ( - + No available dates yet. + + ) : ( + datesMemo.map((date) => ( + = ({ ? "bg-accent text-primary hover:text-primary pl-4" : "" }`} - /> - ))} + /> + )) + )}
    diff --git a/components/error/fetch-failed.tsx b/components/error/fetch-failed.tsx new file mode 100644 index 0000000..a65439b --- /dev/null +++ b/components/error/fetch-failed.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { + BentoContainer, + BentoContainerHeader, +} from "../reusables/bento-container"; +import { twMerge } from "tailwind-merge"; +import { Description, Title } from "../reusables/texts"; +import { Button } from "../ui/button"; +import { ErrorBoundaryProps } from "@/lib/error-types"; +import { useRouter } from "next/navigation"; +import AnimoDevBadge from "../reusables/animo-dev-badge"; + +// extend the error boundary props to have the className +interface FetchFailedProps extends ErrorBoundaryProps { + className?: string; + showMessage?: boolean; +} + +const FetchFailed: React.FC = ({ + error, + reset, + className, +}) => { + const router = useRouter(); + + return ( +
    + + + + + Something went wrong: {error.name} + + + Unexpected error. Try again or contact support + + + + +
    + ); +}; + +export default FetchFailed; diff --git a/components/header.tsx b/components/header.tsx index 024134c..ddddcb0 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { Button } from "./ui/button"; -import { Home, Menu, User } from "lucide-react"; +import { CalendarCog, Home, Menu, User } from "lucide-react"; import { Sheet, @@ -16,9 +16,10 @@ import { import { useIsTablet } from "@/hooks/use-tablet"; import { useState } from "react"; import { useMemo } from "react"; -import DatePicker from "./date-picker"; +import DatePicker from "./ui/date-picker"; import { useRouter } from "next/navigation"; import { formatDateAsYYYYMMDD } from "@/lib/utils"; +import { useSidebarOpen } from "@/context/sidebar-open-context"; import CustomShortcut from "./custom-shortcut/custom-shortcut"; const Header = () => { @@ -32,7 +33,7 @@ const Header = () => { return (
    @@ -131,30 +134,34 @@ const MobileHeaderContent = () => { Quickly navigate to different sections. -
    -
    - - -
    +
    + + + Custom date + +