From 0dddff308e519e3b63455b92b9c0e2b104622b9f Mon Sep 17 00:00:00 2001 From: Sosokker Date: Fri, 7 Mar 2025 01:56:30 +0700 Subject: [PATCH] ui: add global 404, loading and error page --- frontend/app/error.tsx | 103 ++++++++++++++++++++++++++ frontend/app/global-error.tsx | 51 +++++++++++++ frontend/app/globals.css | 2 +- frontend/app/layout.tsx | 15 ++-- frontend/app/loading.tsx | 27 +++++++ frontend/app/not-found.tsx | 92 +++++++++++++++++++++++ frontend/components/ui/alert.tsx | 59 +++++++++++++++ frontend/components/ui/hover-card.tsx | 29 ++++++++ frontend/next.config.ts | 3 + frontend/package.json | 2 + frontend/pnpm-lock.yaml | 71 ++++++++++++++++++ 11 files changed, 443 insertions(+), 11 deletions(-) create mode 100644 frontend/app/error.tsx create mode 100644 frontend/app/global-error.tsx create mode 100644 frontend/app/loading.tsx create mode 100644 frontend/app/not-found.tsx create mode 100644 frontend/components/ui/alert.tsx create mode 100644 frontend/components/ui/hover-card.tsx diff --git a/frontend/app/error.tsx b/frontend/app/error.tsx new file mode 100644 index 0000000..508896b --- /dev/null +++ b/frontend/app/error.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { AlertTriangle, RefreshCcw, Home, ArrowLeft, HelpCircle } from "lucide-react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; + +interface ErrorProps { + error: Error & { digest?: string }; + reset: () => void; +} + +export default function Error({ error, reset }: ErrorProps) { + useEffect(() => { + // Log the error to an error reporting service + console.error("Application error:", error); + }, [error]); + + const router = useRouter(); + + // Determine error type to show appropriate message + const getErrorMessage = () => { + if (error.message.includes("FARM_NOT_FOUND")) { + return "The farm you're looking for could not be found."; + } + if (error.message.includes("CROP_NOT_FOUND")) { + return "The crop you're looking for could not be found."; + } + if (error.message.includes("UNAUTHORIZED")) { + return "You don't have permission to access this resource."; + } + if (error.message.includes("NETWORK")) { + return "Network error. Please check your internet connection."; + } + return "We apologize for the inconvenience. An unexpected error has occurred."; + }; + + return ( +
+
+
+ {/* Decorative elements */} +
+
+ + {/* Main icon */} +
+ +
+
+ +

Something went wrong

+

{getErrorMessage()}

+ + {error.message && !["FARM_NOT_FOUND", "CROP_NOT_FOUND", "UNAUTHORIZED"].includes(error.message) && ( +
+

Error details:

+

{error.message}

+ {error.digest &&

Error ID: {error.digest}

} +
+ )} + +
+ + + +
+ +
+

+ Need help?{" "} + + Contact Support + +

+

+ + Support Code: {error.digest ? error.digest.substring(0, 8) : "Unknown"} +

+
+
+
+ ); +} diff --git a/frontend/app/global-error.tsx b/frontend/app/global-error.tsx new file mode 100644 index 0000000..f7e182f --- /dev/null +++ b/frontend/app/global-error.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { AlertTriangle, RefreshCcw, Home } from "lucide-react"; + +interface GlobalErrorProps { + error: Error & { digest?: string }; + reset: () => void; +} + +export default function GlobalError({ error, reset }: GlobalErrorProps) { + return ( + + +
+
+
+ +
+ +

Critical Error

+

The application has encountered a critical error and cannot continue.

+ + {error.message && ( +
+

Error details:

+

{error.message}

+ {error.digest &&

Error ID: {error.digest}

} +
+ )} + +
+ + +
+
+
+ + + ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 405715a..94ab5ed 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -3,7 +3,7 @@ @tailwind utilities; body { - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-poppins); } @layer base { diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 17285cf..eb91124 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,20 +1,15 @@ import type { Metadata } from "next"; -import { Open_Sans, Roboto_Mono } from "next/font/google"; +import { Poppins } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; import { SessionProvider } from "@/context/SessionContext"; -const openSans = Open_Sans({ +const poppins = Poppins({ subsets: ["latin"], display: "swap", - variable: "--font-opensans", -}); - -const robotoMono = Roboto_Mono({ - subsets: ["latin"], - display: "swap", - variable: "--font-roboto-mono", + variable: "--font-poppins", + weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], }); // const geistMono = Geist_Mono({ @@ -36,7 +31,7 @@ export default function RootLayout({ - +
{children}
diff --git a/frontend/app/loading.tsx b/frontend/app/loading.tsx new file mode 100644 index 0000000..1974619 --- /dev/null +++ b/frontend/app/loading.tsx @@ -0,0 +1,27 @@ +import { Leaf } from "lucide-react"; + +export default function Loading() { + return ( +
+
+
+
+ +
+
+
+ +
+

Loading...

+

+ We're preparing your farming data. This will only take a moment. +

+
+ +
+
+
+
+
+ ); +} diff --git a/frontend/app/not-found.tsx b/frontend/app/not-found.tsx new file mode 100644 index 0000000..7322eee --- /dev/null +++ b/frontend/app/not-found.tsx @@ -0,0 +1,92 @@ +"use client"; + +import type React from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Leaf, Home, Search, ArrowLeft, MapPin } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function NotFound() { + const [searchQuery, setSearchQuery] = useState(""); + const router = useRouter(); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery.trim()) { + // In a real app, this would navigate to search results + router.push(`/search?q=${encodeURIComponent(searchQuery)}`); + } + }; + + return ( +
+
+
+ {/* Decorative elements */} +
+
+ + {/* Main icon */} +
+ +
+
+ +

+ 404 +

+

Page Not Found

+

+ Looks like you've wandered into uncharted territory. This page doesn't exist or has been moved. +

+ +
+
+ + setSearchQuery(e.target.value)} + /> + + +
+ + +
+
+ +
+ + + View Farms + + + + Knowledge Hub + + + + Contact Support + +
+
+
+ ); +} diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/frontend/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/components/ui/hover-card.tsx b/frontend/components/ui/hover-card.tsx new file mode 100644 index 0000000..e54d91c --- /dev/null +++ b/frontend/components/ui/hover-card.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..b1b166f 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,6 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + devIndicators: { + buildActivity: false, + }, }; export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json index ab04322..5be4672 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.2", @@ -33,6 +34,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "framer-motion": "^12.4.10", "js-cookie": "^3.0.5", "lucide-react": "^0.475.0", "next": "15.1.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 09b6d99..3982219 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.6 version: 2.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-hover-card': + specifier: ^1.1.6 + version: 1.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -80,6 +83,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + framer-motion: + specifier: ^12.4.10 + version: 12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0) js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -605,6 +611,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-hover-card@1.1.6': + resolution: {integrity: sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -1510,6 +1529,20 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + framer-motion@12.4.10: + resolution: {integrity: sha512-3Msuyjcr1Pb5hjkn4EJcRe1HumaveP0Gbv4DBMKTPKcV/1GSMkQXj+Uqgneys+9DPcZM18Hac9qY9iUEF5LZtg==} + 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 + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1865,6 +1898,12 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + motion-dom@12.4.10: + resolution: {integrity: sha512-ISP5u6FTceoD6qKdLupIPU/LyXBrxGox+P2e3mBbm1+pLdlBbwv01YENJr7+1WZnW5ucVKzFScYsV1eXTCG4Xg==} + + motion-utils@12.4.10: + resolution: {integrity: sha512-NPwZd94V013SwRf++jMrk2+HEBgPkeIE2RiOzhAuuQlqxMJPkKt/LXVh6Upl+iN8oarSGD2dlY5/bqgsYXDABA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2918,6 +2957,23 @@ snapshots: '@types/react': 19.0.8 '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-hover-card@1.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-id@1.1.0(@types/react@19.0.8)(react@19.0.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0) @@ -4033,6 +4089,15 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + framer-motion@12.4.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + motion-dom: 12.4.10 + motion-utils: 12.4.10 + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + fsevents@2.3.3: optional: true @@ -4388,6 +4453,12 @@ snapshots: minipass@7.1.2: {} + motion-dom@12.4.10: + dependencies: + motion-utils: 12.4.10 + + motion-utils@12.4.10: {} + ms@2.1.3: {} mz@2.7.0: