diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..16c1d89c64f112a8a71fa0b71b50ff18aded3b35 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +generated-icon.png filter=lfs diff=lfs merge=lfs -text diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..948f660e23ac00cd2501673423c1bff96ea65538 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Ammaar Reshi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 94f88fb0bae3684ee9e57c5f8c18d3f9cb7180ae..1f0d3775c2aabbdb70b7f8aa3ab35cc7f42b1cf7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,92 @@ ---- -title: Gsearch -emoji: 🦀 -colorFrom: indigo -colorTo: green -sdk: docker -pinned: false -short_description: gemini-search ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# Gemini Search + +A Perplexity-style search engine powered by Google's Gemini 2.0 Flash model with grounding through Google Search. Get AI-powered answers to your questions with real-time web sources and citations. + +Created by [@ammaar](https://x.com/ammaar) + +![Kapture 2025-01-04 at 14 35 14](https://github.com/user-attachments/assets/2302898e-03ae-40a6-a16c-301d6b91c5af) + + +## Features + +- 🔍 Real-time web search integration +- 🤖 Powered by Google's latest Gemini 2.0 Flash model +- 📚 Source citations and references for answers +- 💬 Follow-up questions in the same chat session +- 🎨 Clean, modern UI inspired by Perplexity +- ⚡ Fast response times + +## Tech Stack + +- Frontend: React + Vite + TypeScript + Tailwind CSS +- Backend: Express.js + TypeScript +- AI: Google Gemini 2.0 Flash API +- Search: Google Search API integration + +## Setup + +### Prerequisites + +- Node.js (v18 or higher recommended) +- npm or yarn +- A Google API key with access to Gemini API + +### Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/ammaarreshi/Gemini-Search.git + cd Gemini-Search + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +3. Create a `.env` file in the root directory: + + ``` + GOOGLE_API_KEY=your_api_key_here + ``` + +4. Start the development server: + + ```bash + npm run dev + ``` + +5. Open your browser and navigate to: + ``` + http://localhost:3000 + ``` + +## Environment Variables + +- `GOOGLE_API_KEY`: Your Google API key with access to Gemini API +- `NODE_ENV`: Set to "development" by default, use "production" for production builds + +## Development + +- `npm run dev`: Start the development server +- `npm run build`: Build for production +- `npm run start`: Run the production server +- `npm run check`: Run TypeScript type checking + +## Security Notes + +- Never commit your `.env` file or expose your API keys +- The `.gitignore` file is configured to exclude sensitive files +- If you fork this repository, make sure to use your own API keys + +## License + +MIT License - feel free to use this code for your own projects! + +## Acknowledgments + +- Inspired by [Perplexity](https://www.perplexity.ai/) +- Built with [Google's Gemini API](https://ai.google.dev/) +- UI components from [shadcn/ui](https://ui.shadcn.com/) diff --git a/client/apple-touch-icon.png b/client/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1b9475a962011a50097d7f264b381ed983189a08 Binary files /dev/null and b/client/apple-touch-icon.png differ diff --git a/client/favicon.png b/client/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8702719b27cd178d967340d0e26f09391609639c Binary files /dev/null and b/client/favicon.png differ diff --git a/client/favicon.svg b/client/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..4492afe01215529cc094a3a22d012d58e9bf5226 --- /dev/null +++ b/client/favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000000000000000000000000000000000000..594cb4bdd164eb3ed7c51ff055b20c4d75ab710c --- /dev/null +++ b/client/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Gemini Search + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/client/manifest.json b/client/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..aa9bedfa088674d3f5ac205ad4575374aa4fe88d --- /dev/null +++ b/client/manifest.json @@ -0,0 +1,28 @@ +{ + "name": "Gemini Search", + "short_name": "Gemini", + "description": "A search engine powered by Google's Gemini Flash", + "start_url": "/", + "display": "standalone", + "background_color": "#FCFCF9", + "theme_color": "#FCFCF9", + "orientation": "portrait", + "icons": [ + { + "src": "/favicon.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "/pwa-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} \ No newline at end of file diff --git a/client/og-image.png b/client/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..d60ad4392794fedbf98727b11e502e3db34e792c Binary files /dev/null and b/client/og-image.png differ diff --git a/client/public/apple-touch-icon.png b/client/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1b9475a962011a50097d7f264b381ed983189a08 Binary files /dev/null and b/client/public/apple-touch-icon.png differ diff --git a/client/public/favicon.png b/client/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8702719b27cd178d967340d0e26f09391609639c Binary files /dev/null and b/client/public/favicon.png differ diff --git a/client/public/manifest.json b/client/public/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..aa9bedfa088674d3f5ac205ad4575374aa4fe88d --- /dev/null +++ b/client/public/manifest.json @@ -0,0 +1,28 @@ +{ + "name": "Gemini Search", + "short_name": "Gemini", + "description": "A search engine powered by Google's Gemini Flash", + "start_url": "/", + "display": "standalone", + "background_color": "#FCFCF9", + "theme_color": "#FCFCF9", + "orientation": "portrait", + "icons": [ + { + "src": "/favicon.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "/pwa-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} \ No newline at end of file diff --git a/client/public/pwa-512x512.png b/client/public/pwa-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..c702c8dea32cb9f51b59dc88f777817b19945560 Binary files /dev/null and b/client/public/pwa-512x512.png differ diff --git a/client/public/splash/apple-splash-1125-2436.jpg b/client/public/splash/apple-splash-1125-2436.jpg new file mode 100644 index 0000000000000000000000000000000000000000..60dde158deee96a56e72a853a76ed9ec7a2171d3 Binary files /dev/null and b/client/public/splash/apple-splash-1125-2436.jpg differ diff --git a/client/public/splash/apple-splash-1170-2532.jpg b/client/public/splash/apple-splash-1170-2532.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3b9cc6b914b64d1fd50d26d6dee32253d1a93323 Binary files /dev/null and b/client/public/splash/apple-splash-1170-2532.jpg differ diff --git a/client/public/splash/apple-splash-1179-2556.jpg b/client/public/splash/apple-splash-1179-2556.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3b9cc6b914b64d1fd50d26d6dee32253d1a93323 Binary files /dev/null and b/client/public/splash/apple-splash-1179-2556.jpg differ diff --git a/client/public/splash/apple-splash-1290-2796.jpg b/client/public/splash/apple-splash-1290-2796.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cb44df8e0a5b06cf8b6bcdcffe1eb42010bb5b46 Binary files /dev/null and b/client/public/splash/apple-splash-1290-2796.jpg differ diff --git a/client/public/splash/apple-splash-1536-2048.jpg b/client/public/splash/apple-splash-1536-2048.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6d28e017f8c44d20f9b8d39c2494fe24031f9cc Binary files /dev/null and b/client/public/splash/apple-splash-1536-2048.jpg differ diff --git a/client/public/splash/apple-splash-1668-2388.jpg b/client/public/splash/apple-splash-1668-2388.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c863029975dc3b0daa76ab1fa7430a5870cad77 Binary files /dev/null and b/client/public/splash/apple-splash-1668-2388.jpg differ diff --git a/client/public/splash/apple-splash-2048-2732.jpg b/client/public/splash/apple-splash-2048-2732.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0bba1ffa9f5310f4cffcab78f2559dc67ebb97f4 Binary files /dev/null and b/client/public/splash/apple-splash-2048-2732.jpg differ diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..21eec0477250d6b37deafc2c9d1f9316c022bc5b --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,41 @@ +import { Switch, Route, useLocation } from "wouter"; +import { Home } from "@/pages/Home"; +import { Search } from "@/pages/Search"; +import { Card, CardContent } from "@/components/ui/card"; +import { AlertCircle } from "lucide-react"; +import { AnimatePresence } from "framer-motion"; + +function App() { + const [location] = useLocation(); + + return ( + + + + + + + + ); +} + +function NotFound() { + return ( +
+ + +
+ +

404 Page Not Found

+
+ +

+ The page you're looking for doesn't exist. +

+
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/client/src/components/FollowUpInput.tsx b/client/src/components/FollowUpInput.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6cd1c55e335a7006eeeaa5895badea03145b9b36 --- /dev/null +++ b/client/src/components/FollowUpInput.tsx @@ -0,0 +1,66 @@ +import { useState, KeyboardEvent } from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { MessageSquarePlus, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface FollowUpInputProps { + onSubmit: (query: string) => void; + isLoading?: boolean; +} + +export function FollowUpInput({ + onSubmit, + isLoading = false, +}: FollowUpInputProps) { + const [query, setQuery] = useState(''); + + const handleSubmit = () => { + if (query.trim() && !isLoading) { + onSubmit(query.trim()); + setQuery(''); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSubmit(); + } + }; + + return ( +
+
+ setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Ask a follow-up question..." + className={cn( + "transition-all duration-200", + "focus-visible:ring-1 focus-visible:ring-primary", + "placeholder:text-muted-foreground/70", + "w-full" + )} + disabled={isLoading} + /> +
+ + +
+ ); +} \ No newline at end of file diff --git a/client/src/components/Logo.tsx b/client/src/components/Logo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..44389b910e13769f6eadd836e83df573c71a1da8 --- /dev/null +++ b/client/src/components/Logo.tsx @@ -0,0 +1,130 @@ +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; + +interface LogoProps { + className?: string; + animate?: boolean; +} + +export function Logo({ className, animate = false }: LogoProps) { + return ( + + + + + {animate ? ( + <> + + + + + + + ) : ( + <> + + + + + + + )} + + + + ); +} \ No newline at end of file diff --git a/client/src/components/SearchInput.tsx b/client/src/components/SearchInput.tsx new file mode 100644 index 0000000000000000000000000000000000000000..751f573cfeab5cfbf919e21e7bfcf650e28637b2 --- /dev/null +++ b/client/src/components/SearchInput.tsx @@ -0,0 +1,75 @@ +import { useState, KeyboardEvent } from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Search, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface SearchInputProps { + onSearch: (query: string) => void; + isLoading?: boolean; + initialValue?: string; + autoFocus?: boolean; + large?: boolean; +} + +export function SearchInput({ + onSearch, + isLoading = false, + initialValue = '', + autoFocus = false, + large = false, +}: SearchInputProps) { + const [query, setQuery] = useState(initialValue); + + const handleSubmit = () => { + if (query.trim() && !isLoading) { + onSearch(query.trim()); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleSubmit(); + } + }; + + return ( +
+
+ + + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Ask anything..." + className={cn( + "pl-10 pr-4 transition-all duration-200", + large && "h-12 text-lg rounded-lg", + "focus-visible:ring-2 focus-visible:ring-primary" + )} + disabled={isLoading} + autoFocus={autoFocus} + /> +
+ + +
+ ); +} \ No newline at end of file diff --git a/client/src/components/SearchResults.tsx b/client/src/components/SearchResults.tsx new file mode 100644 index 0000000000000000000000000000000000000000..89f3663f6d41ec859811f7d0d818100ec5be755f --- /dev/null +++ b/client/src/components/SearchResults.tsx @@ -0,0 +1,132 @@ +import { useEffect, useRef } from 'react'; +import { Card } from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { AlertCircle } from 'lucide-react'; +import { Skeleton } from '@/components/ui/skeleton'; +import { cn } from '@/lib/utils'; +import { motion } from 'framer-motion'; +import { SourceList } from '@/components/SourceList'; +import { Logo } from '@/components/Logo'; + +interface SearchResultsProps { + query: string; + results: any; + isLoading: boolean; + error?: Error; + isFollowUp?: boolean; + originalQuery?: string; +} + +export function SearchResults({ + query, + results, + isLoading, + error, + isFollowUp, + originalQuery +}: SearchResultsProps) { + const contentRef = useRef(null); + + useEffect(() => { + if (results && contentRef.current) { + contentRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [results]); + + if (error) { + return ( + + + + {error.message || 'An error occurred while searching. Please try again.'} + + + ); + } + + if (isLoading) { + return ( +
+
+ +
+ + + + + + +
+ + +
+
+ ); + } + + if (!results) return null; + + return ( +
+ {/* Search Query Display */} + + {isFollowUp && originalQuery && ( + <> +
+ Original search: + "{originalQuery}" +
+
+ + )} +
+ {isFollowUp ? 'Follow-up question:' : ''} +

"{query}"

+
+ + + {/* Sources Section */} + {results.sources && results.sources.length > 0 && ( + + + + )} + + {/* Main Content */} + + +
+ + +
+ ); +} \ No newline at end of file diff --git a/client/src/components/SourceList.tsx b/client/src/components/SourceList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..47381a0ec137d66cda47b91c2ad6615343f62351 --- /dev/null +++ b/client/src/components/SourceList.tsx @@ -0,0 +1,75 @@ +import { Card } from '@/components/ui/card'; +import { ExternalLink, Link2 } from 'lucide-react'; +import { motion } from 'framer-motion'; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; + +interface Source { + title: string; + url: string; + snippet: string; +} + +interface SourceListProps { + sources: Source[]; +} + +export function SourceList({ sources }: SourceListProps) { + return ( +
+
+ +

Sources

+
+ + + + {sources.map((source, index) => ( + + window.open(source.url, '_blank')} + > +
+
+
+

+ {source.title.replace(/\*\*/g, '')} +

+ + {source.snippet && ( +

+ {source.snippet.replace(/\*\*/g, '')} +

+ )} + +
+ + {new URL(source.url).hostname.replace('www.', '')} + +
+
+ + +
+
+
+
+ ))} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/SourcePreviewModal.tsx b/client/src/components/SourcePreviewModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bc42f2d26580d7d03090f8701d6cfd6ca881c21f --- /dev/null +++ b/client/src/components/SourcePreviewModal.tsx @@ -0,0 +1,67 @@ +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { ExternalLink } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; + +interface Source { + title: string; + url: string; + snippet: string; +} + +interface SourcePreviewModalProps { + source: Source | null; + isOpen: boolean; + onClose: () => void; +} + +export function SourcePreviewModal({ source, isOpen, onClose }: SourcePreviewModalProps) { + if (!source) return null; + + return ( + + {isOpen && ( + + + + + + {source.title} + + + {source.url} + + + + + + + +
+

+ {source.snippet} +

+ {/* TODO: Add full content preview when available */} +
+
+
+
+
+ )} +
+ ); +} diff --git a/client/src/components/ThemeToggle.tsx b/client/src/components/ThemeToggle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bfe61185516c0a5b9ee9ce3a786ec188b09eefc6 --- /dev/null +++ b/client/src/components/ThemeToggle.tsx @@ -0,0 +1,34 @@ + +import { Moon, Sun } from "lucide-react"; +import { Button } from "./ui/button"; +import { useEffect, useState } from "react"; + +export function ThemeToggle() { + const [theme, setTheme] = useState("light"); + + useEffect(() => { + const isDark = document.documentElement.classList.contains("dark"); + setTheme(isDark ? "dark" : "light"); + }, []); + + function toggleTheme() { + const newTheme = theme === "light" ? "dark" : "light"; + setTheme(newTheme); + document.documentElement.classList.toggle("dark"); + } + + return ( + + ); +} diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e6a723d06574ee5cec8b00759b98f3fbe1ac7cc9 --- /dev/null +++ b/client/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8722561cf6bda62d62f9a0c67730aefda971873a --- /dev/null +++ b/client/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96 --- /dev/null +++ b/client/src/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 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + 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/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c4abbf37f217c715a0eaade7f45ac78600df419f --- /dev/null +++ b/client/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..991f56ecb117e96284bf0f6cad3b14ea2fdf5264 --- /dev/null +++ b/client/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f000e3ef5176395b067dfc3f3e1256a80c450015 --- /dev/null +++ b/client/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..60e6c96f72f0350d08b47e4730cab8f3975dc853 --- /dev/null +++ b/client/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>