intial real time face detection working
This commit is contained in:
		
							parent
							
								
									834fac76b9
								
							
						
					
					
						commit
						99f5e148b9
					
				@ -1,3 +1,66 @@
 | 
			
		||||
@tailwind base;
 | 
			
		||||
@tailwind components;
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
@layer base {
 | 
			
		||||
  :root {
 | 
			
		||||
    --background: 0 0% 100%;
 | 
			
		||||
    --foreground: 0 0% 3.9%;
 | 
			
		||||
    --card: 0 0% 100%;
 | 
			
		||||
    --card-foreground: 0 0% 3.9%;
 | 
			
		||||
    --popover: 0 0% 100%;
 | 
			
		||||
    --popover-foreground: 0 0% 3.9%;
 | 
			
		||||
    --primary: 0 0% 9%;
 | 
			
		||||
    --primary-foreground: 0 0% 98%;
 | 
			
		||||
    --secondary: 0 0% 96.1%;
 | 
			
		||||
    --secondary-foreground: 0 0% 9%;
 | 
			
		||||
    --muted: 0 0% 96.1%;
 | 
			
		||||
    --muted-foreground: 0 0% 45.1%;
 | 
			
		||||
    --accent: 0 0% 96.1%;
 | 
			
		||||
    --accent-foreground: 0 0% 9%;
 | 
			
		||||
    --destructive: 0 84.2% 60.2%;
 | 
			
		||||
    --destructive-foreground: 0 0% 98%;
 | 
			
		||||
    --border: 0 0% 89.8%;
 | 
			
		||||
    --input: 0 0% 89.8%;
 | 
			
		||||
    --ring: 0 0% 3.9%;
 | 
			
		||||
    --chart-1: 12 76% 61%;
 | 
			
		||||
    --chart-2: 173 58% 39%;
 | 
			
		||||
    --chart-3: 197 37% 24%;
 | 
			
		||||
    --chart-4: 43 74% 66%;
 | 
			
		||||
    --chart-5: 27 87% 67%;
 | 
			
		||||
    --radius: 0.5rem
 | 
			
		||||
  }
 | 
			
		||||
  .dark {
 | 
			
		||||
    --background: 0 0% 3.9%;
 | 
			
		||||
    --foreground: 0 0% 98%;
 | 
			
		||||
    --card: 0 0% 3.9%;
 | 
			
		||||
    --card-foreground: 0 0% 98%;
 | 
			
		||||
    --popover: 0 0% 3.9%;
 | 
			
		||||
    --popover-foreground: 0 0% 98%;
 | 
			
		||||
    --primary: 0 0% 98%;
 | 
			
		||||
    --primary-foreground: 0 0% 9%;
 | 
			
		||||
    --secondary: 0 0% 14.9%;
 | 
			
		||||
    --secondary-foreground: 0 0% 98%;
 | 
			
		||||
    --muted: 0 0% 14.9%;
 | 
			
		||||
    --muted-foreground: 0 0% 63.9%;
 | 
			
		||||
    --accent: 0 0% 14.9%;
 | 
			
		||||
    --accent-foreground: 0 0% 98%;
 | 
			
		||||
    --destructive: 0 62.8% 30.6%;
 | 
			
		||||
    --destructive-foreground: 0 0% 98%;
 | 
			
		||||
    --border: 0 0% 14.9%;
 | 
			
		||||
    --input: 0 0% 14.9%;
 | 
			
		||||
    --ring: 0 0% 83.1%;
 | 
			
		||||
    --chart-1: 220 70% 50%;
 | 
			
		||||
    --chart-2: 160 60% 45%;
 | 
			
		||||
    --chart-3: 30 80% 55%;
 | 
			
		||||
    --chart-4: 280 65% 60%;
 | 
			
		||||
    --chart-5: 340 75% 55%
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
    @apply border-border;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    @apply bg-background text-foreground;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import type { Metadata } from "next";
 | 
			
		||||
import { Geist, Geist_Mono } from "next/font/google";
 | 
			
		||||
import "./globals.css";
 | 
			
		||||
import { Toaster } from "@/components/ui/toaster";
 | 
			
		||||
 | 
			
		||||
const geistSans = Geist({
 | 
			
		||||
  variable: "--font-geist-sans",
 | 
			
		||||
@ -28,6 +29,7 @@ export default function RootLayout({
 | 
			
		||||
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
        <Toaster />
 | 
			
		||||
      </body>
 | 
			
		||||
    </html>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								components.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								components.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://ui.shadcn.com/schema.json",
 | 
			
		||||
  "style": "default",
 | 
			
		||||
  "rsc": true,
 | 
			
		||||
  "tsx": true,
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "config": "tailwind.config.ts",
 | 
			
		||||
    "css": "app/globals.css",
 | 
			
		||||
    "baseColor": "neutral",
 | 
			
		||||
    "cssVariables": true,
 | 
			
		||||
    "prefix": ""
 | 
			
		||||
  },
 | 
			
		||||
  "aliases": {
 | 
			
		||||
    "components": "@/components",
 | 
			
		||||
    "utils": "@/lib/utils",
 | 
			
		||||
    "ui": "@/components/ui",
 | 
			
		||||
    "lib": "@/lib",
 | 
			
		||||
    "hooks": "@/hooks"
 | 
			
		||||
  },
 | 
			
		||||
  "iconLibrary": "lucide"
 | 
			
		||||
}
 | 
			
		||||
@ -2,10 +2,13 @@
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import Register from "./register/Register";
 | 
			
		||||
import Search from "./search/Search";
 | 
			
		||||
import "./MainForm.css"; // Import CSS for styling
 | 
			
		||||
import "./MainForm.css";
 | 
			
		||||
import RealtimeFaceDetection from "./realtimeFaceDetection/RealtimeFaceDetection";
 | 
			
		||||
 | 
			
		||||
const MainForm: React.FC = () => {
 | 
			
		||||
  const [activeTab, setActiveTab] = useState<"register" | "search">("register");
 | 
			
		||||
  const [activeTab, setActiveTab] = useState<
 | 
			
		||||
    "register" | "search" | "realtime"
 | 
			
		||||
  >("register");
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="main-container">
 | 
			
		||||
@ -22,10 +25,17 @@ const MainForm: React.FC = () => {
 | 
			
		||||
        >
 | 
			
		||||
          Search
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
          className={`tab-button ${activeTab === "realtime" ? "active" : ""}`}
 | 
			
		||||
          onClick={() => setActiveTab("realtime")}
 | 
			
		||||
        >
 | 
			
		||||
          Realtime Detection
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="tab-content">
 | 
			
		||||
        {activeTab === "register" && <Register />}
 | 
			
		||||
        {activeTab === "search" && <Search />}
 | 
			
		||||
        {activeTab === "realtime" && <RealtimeFaceDetection />}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										124
									
								
								components/realtimeFaceDetection/RealtimeFaceDetection.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								components/realtimeFaceDetection/RealtimeFaceDetection.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import Webcam from "react-webcam";
 | 
			
		||||
import * as faceapi from "face-api.js";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Camera } from "lucide-react";
 | 
			
		||||
import { useToast } from "@/hooks/use-toast";
 | 
			
		||||
 | 
			
		||||
const MODEL_URL = "https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model";
 | 
			
		||||
 | 
			
		||||
const RealtimeFaceDetection = () => {
 | 
			
		||||
  const webcamRef = useRef<Webcam>(null);
 | 
			
		||||
  const canvasRef = useRef<HTMLCanvasElement>(null);
 | 
			
		||||
  const [isModelLoaded, setIsModelLoaded] = useState(false);
 | 
			
		||||
  const [isDetecting, setIsDetecting] = useState(false);
 | 
			
		||||
  const { toast } = useToast();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const loadModels = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
 | 
			
		||||
        await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
 | 
			
		||||
        await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL);
 | 
			
		||||
        setIsModelLoaded(true);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error("Error loading models:", error);
 | 
			
		||||
        toast({
 | 
			
		||||
          title: "Error",
 | 
			
		||||
          description: "Failed to load face detection models.",
 | 
			
		||||
          variant: "destructive",
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    loadModels();
 | 
			
		||||
  }, [toast]);
 | 
			
		||||
 | 
			
		||||
  const detectFace = async () => {
 | 
			
		||||
    if (!webcamRef.current || !webcamRef.current.video) return;
 | 
			
		||||
 | 
			
		||||
    const video = webcamRef.current.video;
 | 
			
		||||
    const canvas = document.createElement("canvas");
 | 
			
		||||
    const context = canvas.getContext("2d");
 | 
			
		||||
 | 
			
		||||
    if (!context) return;
 | 
			
		||||
 | 
			
		||||
    canvas.width = video.videoWidth;
 | 
			
		||||
    canvas.height = video.videoHeight;
 | 
			
		||||
    context.drawImage(video, 0, 0, canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
    // Convert the canvas to a Blob (image file)
 | 
			
		||||
    canvas.toBlob(async (blob) => {
 | 
			
		||||
      if (!blob) return;
 | 
			
		||||
 | 
			
		||||
      // Detect face
 | 
			
		||||
      const detections = await faceapi
 | 
			
		||||
        .detectSingleFace(video, new faceapi.TinyFaceDetectorOptions())
 | 
			
		||||
        .withFaceLandmarks()
 | 
			
		||||
        .withFaceDescriptor();
 | 
			
		||||
 | 
			
		||||
      if (detections) {
 | 
			
		||||
        sendFaceDataToAPI(blob);
 | 
			
		||||
      }
 | 
			
		||||
    }, "image/jpeg"); // Save image as JPEG
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const sendFaceDataToAPI = async (imageBlob: Blob) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append("image", imageBlob, "face.jpg");
 | 
			
		||||
 | 
			
		||||
      const response = await fetch(
 | 
			
		||||
        `${process.env.NEXT_PUBLIC_BASE_URL}/search`,
 | 
			
		||||
        {
 | 
			
		||||
          method: "POST",
 | 
			
		||||
          body: formData, // Send multipart/form-data
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const data = await response.json();
 | 
			
		||||
      toast({ title: data?.name, description: data.message });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error("Error sending face data:", error);
 | 
			
		||||
      toast({
 | 
			
		||||
        title: "Error",
 | 
			
		||||
        description: "Failed to send face data.",
 | 
			
		||||
        variant: "destructive",
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const startDetection = () => {
 | 
			
		||||
    if (!isModelLoaded) return;
 | 
			
		||||
    setIsDetecting(true);
 | 
			
		||||
    const interval = setInterval(detectFace, 1000);
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      clearInterval(interval);
 | 
			
		||||
      setIsDetecting(false);
 | 
			
		||||
    }, 100000); // Stops detection after 10 seconds
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="max-w-3xl mx-auto">
 | 
			
		||||
      <div className="relative">
 | 
			
		||||
        <Webcam ref={webcamRef} mirrored className="w-full rounded-lg" />
 | 
			
		||||
        <canvas
 | 
			
		||||
          ref={canvasRef}
 | 
			
		||||
          className="absolute top-0 left-0 w-full h-full"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="mt-6 flex justify-center">
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={startDetection}
 | 
			
		||||
          disabled={!isModelLoaded || isDetecting}
 | 
			
		||||
        >
 | 
			
		||||
          <Camera className="mr-2 h-4 w-4" />
 | 
			
		||||
          {isDetecting ? "Detecting..." : "Start Realtime Detection"}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default RealtimeFaceDetection;
 | 
			
		||||
							
								
								
									
										56
									
								
								components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const buttonVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
 | 
			
		||||
        ghost: "hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        link: "text-primary underline-offset-4 hover:underline",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-10 px-4 py-2",
 | 
			
		||||
        sm: "h-9 rounded-md px-3",
 | 
			
		||||
        lg: "h-11 rounded-md px-8",
 | 
			
		||||
        icon: "h-10 w-10",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export interface ButtonProps
 | 
			
		||||
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 | 
			
		||||
    VariantProps<typeof buttonVariants> {
 | 
			
		||||
  asChild?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
 | 
			
		||||
  ({ className, variant, size, asChild = false, ...props }, ref) => {
 | 
			
		||||
    const Comp = asChild ? Slot : "button"
 | 
			
		||||
    return (
 | 
			
		||||
      <Comp
 | 
			
		||||
        className={cn(buttonVariants({ variant, size, className }))}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
Button.displayName = "Button"
 | 
			
		||||
 | 
			
		||||
export { Button, buttonVariants }
 | 
			
		||||
							
								
								
									
										129
									
								
								components/ui/toast.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								components/ui/toast.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,129 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as ToastPrimitives from "@radix-ui/react-toast"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
import { X } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const ToastProvider = ToastPrimitives.Provider
 | 
			
		||||
 | 
			
		||||
const ToastViewport = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Viewport>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Viewport
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
 | 
			
		||||
 | 
			
		||||
const toastVariants = cva(
 | 
			
		||||
  "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "border bg-background text-foreground",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "destructive group border-destructive bg-destructive text-destructive-foreground",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const Toast = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
 | 
			
		||||
    VariantProps<typeof toastVariants>
 | 
			
		||||
>(({ className, variant, ...props }, ref) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <ToastPrimitives.Root
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(toastVariants({ variant }), className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
Toast.displayName = ToastPrimitives.Root.displayName
 | 
			
		||||
 | 
			
		||||
const ToastAction = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Action>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Action
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastAction.displayName = ToastPrimitives.Action.displayName
 | 
			
		||||
 | 
			
		||||
const ToastClose = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Close>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Close
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    toast-close=""
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <X className="h-4 w-4" />
 | 
			
		||||
  </ToastPrimitives.Close>
 | 
			
		||||
))
 | 
			
		||||
ToastClose.displayName = ToastPrimitives.Close.displayName
 | 
			
		||||
 | 
			
		||||
const ToastTitle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Title>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Title
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm font-semibold", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
 | 
			
		||||
 | 
			
		||||
const ToastDescription = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof ToastPrimitives.Description>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <ToastPrimitives.Description
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm opacity-90", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
 | 
			
		||||
 | 
			
		||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
 | 
			
		||||
 | 
			
		||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  type ToastProps,
 | 
			
		||||
  type ToastActionElement,
 | 
			
		||||
  ToastProvider,
 | 
			
		||||
  ToastViewport,
 | 
			
		||||
  Toast,
 | 
			
		||||
  ToastTitle,
 | 
			
		||||
  ToastDescription,
 | 
			
		||||
  ToastClose,
 | 
			
		||||
  ToastAction,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								components/ui/toaster.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								components/ui/toaster.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import { useToast } from "@/hooks/use-toast"
 | 
			
		||||
import {
 | 
			
		||||
  Toast,
 | 
			
		||||
  ToastClose,
 | 
			
		||||
  ToastDescription,
 | 
			
		||||
  ToastProvider,
 | 
			
		||||
  ToastTitle,
 | 
			
		||||
  ToastViewport,
 | 
			
		||||
} from "@/components/ui/toast"
 | 
			
		||||
 | 
			
		||||
export function Toaster() {
 | 
			
		||||
  const { toasts } = useToast()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToastProvider>
 | 
			
		||||
      {toasts.map(function ({ id, title, description, action, ...props }) {
 | 
			
		||||
        return (
 | 
			
		||||
          <Toast key={id} {...props}>
 | 
			
		||||
            <div className="grid gap-1">
 | 
			
		||||
              {title && <ToastTitle>{title}</ToastTitle>}
 | 
			
		||||
              {description && (
 | 
			
		||||
                <ToastDescription>{description}</ToastDescription>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
            {action}
 | 
			
		||||
            <ToastClose />
 | 
			
		||||
          </Toast>
 | 
			
		||||
        )
 | 
			
		||||
      })}
 | 
			
		||||
      <ToastViewport />
 | 
			
		||||
    </ToastProvider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
Subproject commit 300e624454be86585500a57bd54a1e0cb28b7a94
 | 
			
		||||
							
								
								
									
										194
									
								
								hooks/use-toast.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								hooks/use-toast.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,194 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
// Inspired by react-hot-toast library
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  ToastActionElement,
 | 
			
		||||
  ToastProps,
 | 
			
		||||
} from "@/components/ui/toast"
 | 
			
		||||
 | 
			
		||||
const TOAST_LIMIT = 1
 | 
			
		||||
const TOAST_REMOVE_DELAY = 1000000
 | 
			
		||||
 | 
			
		||||
type ToasterToast = ToastProps & {
 | 
			
		||||
  id: string
 | 
			
		||||
  title?: React.ReactNode
 | 
			
		||||
  description?: React.ReactNode
 | 
			
		||||
  action?: ToastActionElement
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const actionTypes = {
 | 
			
		||||
  ADD_TOAST: "ADD_TOAST",
 | 
			
		||||
  UPDATE_TOAST: "UPDATE_TOAST",
 | 
			
		||||
  DISMISS_TOAST: "DISMISS_TOAST",
 | 
			
		||||
  REMOVE_TOAST: "REMOVE_TOAST",
 | 
			
		||||
} as const
 | 
			
		||||
 | 
			
		||||
let count = 0
 | 
			
		||||
 | 
			
		||||
function genId() {
 | 
			
		||||
  count = (count + 1) % Number.MAX_SAFE_INTEGER
 | 
			
		||||
  return count.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ActionType = typeof actionTypes
 | 
			
		||||
 | 
			
		||||
type Action =
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["ADD_TOAST"]
 | 
			
		||||
      toast: ToasterToast
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["UPDATE_TOAST"]
 | 
			
		||||
      toast: Partial<ToasterToast>
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["DISMISS_TOAST"]
 | 
			
		||||
      toastId?: ToasterToast["id"]
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: ActionType["REMOVE_TOAST"]
 | 
			
		||||
      toastId?: ToasterToast["id"]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
interface State {
 | 
			
		||||
  toasts: ToasterToast[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
 | 
			
		||||
 | 
			
		||||
const addToRemoveQueue = (toastId: string) => {
 | 
			
		||||
  if (toastTimeouts.has(toastId)) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const timeout = setTimeout(() => {
 | 
			
		||||
    toastTimeouts.delete(toastId)
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: "REMOVE_TOAST",
 | 
			
		||||
      toastId: toastId,
 | 
			
		||||
    })
 | 
			
		||||
  }, TOAST_REMOVE_DELAY)
 | 
			
		||||
 | 
			
		||||
  toastTimeouts.set(toastId, timeout)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const reducer = (state: State, action: Action): State => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case "ADD_TOAST":
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    case "UPDATE_TOAST":
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: state.toasts.map((t) =>
 | 
			
		||||
          t.id === action.toast.id ? { ...t, ...action.toast } : t
 | 
			
		||||
        ),
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    case "DISMISS_TOAST": {
 | 
			
		||||
      const { toastId } = action
 | 
			
		||||
 | 
			
		||||
      // ! Side effects ! - This could be extracted into a dismissToast() action,
 | 
			
		||||
      // but I'll keep it here for simplicity
 | 
			
		||||
      if (toastId) {
 | 
			
		||||
        addToRemoveQueue(toastId)
 | 
			
		||||
      } else {
 | 
			
		||||
        state.toasts.forEach((toast) => {
 | 
			
		||||
          addToRemoveQueue(toast.id)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: state.toasts.map((t) =>
 | 
			
		||||
          t.id === toastId || toastId === undefined
 | 
			
		||||
            ? {
 | 
			
		||||
                ...t,
 | 
			
		||||
                open: false,
 | 
			
		||||
              }
 | 
			
		||||
            : t
 | 
			
		||||
        ),
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    case "REMOVE_TOAST":
 | 
			
		||||
      if (action.toastId === undefined) {
 | 
			
		||||
        return {
 | 
			
		||||
          ...state,
 | 
			
		||||
          toasts: [],
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        toasts: state.toasts.filter((t) => t.id !== action.toastId),
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const listeners: Array<(state: State) => void> = []
 | 
			
		||||
 | 
			
		||||
let memoryState: State = { toasts: [] }
 | 
			
		||||
 | 
			
		||||
function dispatch(action: Action) {
 | 
			
		||||
  memoryState = reducer(memoryState, action)
 | 
			
		||||
  listeners.forEach((listener) => {
 | 
			
		||||
    listener(memoryState)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Toast = Omit<ToasterToast, "id">
 | 
			
		||||
 | 
			
		||||
function toast({ ...props }: Toast) {
 | 
			
		||||
  const id = genId()
 | 
			
		||||
 | 
			
		||||
  const update = (props: ToasterToast) =>
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: "UPDATE_TOAST",
 | 
			
		||||
      toast: { ...props, id },
 | 
			
		||||
    })
 | 
			
		||||
  const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
 | 
			
		||||
 | 
			
		||||
  dispatch({
 | 
			
		||||
    type: "ADD_TOAST",
 | 
			
		||||
    toast: {
 | 
			
		||||
      ...props,
 | 
			
		||||
      id,
 | 
			
		||||
      open: true,
 | 
			
		||||
      onOpenChange: (open) => {
 | 
			
		||||
        if (!open) dismiss()
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    id: id,
 | 
			
		||||
    dismiss,
 | 
			
		||||
    update,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useToast() {
 | 
			
		||||
  const [state, setState] = React.useState<State>(memoryState)
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    listeners.push(setState)
 | 
			
		||||
    return () => {
 | 
			
		||||
      const index = listeners.indexOf(setState)
 | 
			
		||||
      if (index > -1) {
 | 
			
		||||
        listeners.splice(index, 1)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [state])
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    ...state,
 | 
			
		||||
    toast,
 | 
			
		||||
    dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { useToast, toast }
 | 
			
		||||
							
								
								
									
										6
									
								
								lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
import { clsx, type ClassValue } from "clsx"
 | 
			
		||||
import { twMerge } from "tailwind-merge"
 | 
			
		||||
 | 
			
		||||
export function cn(...inputs: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(inputs))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1054
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1054
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
								
							@ -9,19 +9,31 @@
 | 
			
		||||
    "lint": "next lint"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "react": "^19.0.0",
 | 
			
		||||
    "react-dom": "^19.0.0",
 | 
			
		||||
    "next": "15.1.6"
 | 
			
		||||
    "@radix-ui/react-slot": "^1.1.1",
 | 
			
		||||
    "@radix-ui/react-toast": "^1.2.5",
 | 
			
		||||
    "class-variance-authority": "^0.7.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "encoding": "^0.1.13",
 | 
			
		||||
    "face-api.js": "^0.22.2",
 | 
			
		||||
    "lucide-react": "^0.474.0",
 | 
			
		||||
    "next": "15.1.6",
 | 
			
		||||
    "react": "^18.3.1",
 | 
			
		||||
    "react-dom": "^18.3.1",
 | 
			
		||||
    "react-webcam": "^7.2.0",
 | 
			
		||||
    "tailwind-merge": "^3.0.1",
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "typescript": "^5",
 | 
			
		||||
    "@babel/core": "^7.26.7",
 | 
			
		||||
    "@babel/helper-plugin-utils": "^7.26.5",
 | 
			
		||||
    "@eslint/eslintrc": "^3",
 | 
			
		||||
    "@types/node": "^20",
 | 
			
		||||
    "@types/react": "^19",
 | 
			
		||||
    "@types/react-dom": "^19",
 | 
			
		||||
    "postcss": "^8",
 | 
			
		||||
    "tailwindcss": "^3.4.1",
 | 
			
		||||
    "eslint": "^9",
 | 
			
		||||
    "eslint-config-next": "15.1.6",
 | 
			
		||||
    "@eslint/eslintrc": "^3"
 | 
			
		||||
    "postcss": "^8",
 | 
			
		||||
    "tailwindcss": "^3.4.1",
 | 
			
		||||
    "typescript": "^5"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,62 @@
 | 
			
		||||
import type { Config } from "tailwindcss";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  content: [
 | 
			
		||||
    darkMode: ["class"],
 | 
			
		||||
    content: [
 | 
			
		||||
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
 | 
			
		||||
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
 | 
			
		||||
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
 | 
			
		||||
  ],
 | 
			
		||||
  theme: {
 | 
			
		||||
    extend: {
 | 
			
		||||
      colors: {
 | 
			
		||||
        background: "var(--background)",
 | 
			
		||||
        foreground: "var(--foreground)",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  	extend: {
 | 
			
		||||
  		colors: {
 | 
			
		||||
  			background: 'hsl(var(--background))',
 | 
			
		||||
  			foreground: 'hsl(var(--foreground))',
 | 
			
		||||
  			card: {
 | 
			
		||||
  				DEFAULT: 'hsl(var(--card))',
 | 
			
		||||
  				foreground: 'hsl(var(--card-foreground))'
 | 
			
		||||
  			},
 | 
			
		||||
  			popover: {
 | 
			
		||||
  				DEFAULT: 'hsl(var(--popover))',
 | 
			
		||||
  				foreground: 'hsl(var(--popover-foreground))'
 | 
			
		||||
  			},
 | 
			
		||||
  			primary: {
 | 
			
		||||
  				DEFAULT: 'hsl(var(--primary))',
 | 
			
		||||
  				foreground: 'hsl(var(--primary-foreground))'
 | 
			
		||||
  			},
 | 
			
		||||
  			secondary: {
 | 
			
		||||
  				DEFAULT: 'hsl(var(--secondary))',
 | 
			
		||||
  				foreground: 'hsl(var(--secondary-foreground))'
 | 
			
		||||
  			},
 | 
			
		||||
  			muted: {
 | 
			
		||||
  				DEFAULT: 'hsl(var(--muted))',
 | 
			
		||||
  				foreground: 'hsl(var(--muted-foreground))'
 | 
			
		||||
  			},
 | 
			
		||||
  			accent: {
 | 
			
		||||
  				DEFAULT: 'hsl(var(--accent))',
 | 
			
		||||
  				foreground: 'hsl(var(--accent-foreground))'
 | 
			
		||||
  			},
 | 
			
		||||
  			destructive: {
 | 
			
		||||
  				DEFAULT: 'hsl(var(--destructive))',
 | 
			
		||||
  				foreground: 'hsl(var(--destructive-foreground))'
 | 
			
		||||
  			},
 | 
			
		||||
  			border: 'hsl(var(--border))',
 | 
			
		||||
  			input: 'hsl(var(--input))',
 | 
			
		||||
  			ring: 'hsl(var(--ring))',
 | 
			
		||||
  			chart: {
 | 
			
		||||
  				'1': 'hsl(var(--chart-1))',
 | 
			
		||||
  				'2': 'hsl(var(--chart-2))',
 | 
			
		||||
  				'3': 'hsl(var(--chart-3))',
 | 
			
		||||
  				'4': 'hsl(var(--chart-4))',
 | 
			
		||||
  				'5': 'hsl(var(--chart-5))'
 | 
			
		||||
  			}
 | 
			
		||||
  		},
 | 
			
		||||
  		borderRadius: {
 | 
			
		||||
  			lg: 'var(--radius)',
 | 
			
		||||
  			md: 'calc(var(--radius) - 2px)',
 | 
			
		||||
  			sm: 'calc(var(--radius) - 4px)'
 | 
			
		||||
  		}
 | 
			
		||||
  	}
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [],
 | 
			
		||||
  plugins: [require("tailwindcss-animate")],
 | 
			
		||||
} satisfies Config;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user