init
This commit is contained in:
		
						commit
						04267b3886
					
				
					 100 changed files with 16495 additions and 0 deletions
				
			
		
							
								
								
									
										42
									
								
								frontend/src/App.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								frontend/src/App.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| #root { | ||||
|   max-width: 1280px; | ||||
|   margin: 0 auto; | ||||
|   padding: 2rem; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .logo { | ||||
|   height: 6em; | ||||
|   padding: 1.5em; | ||||
|   will-change: filter; | ||||
|   transition: filter 300ms; | ||||
| } | ||||
| .logo:hover { | ||||
|   filter: drop-shadow(0 0 2em #646cffaa); | ||||
| } | ||||
| .logo.react:hover { | ||||
|   filter: drop-shadow(0 0 2em #61dafbaa); | ||||
| } | ||||
| 
 | ||||
| @keyframes logo-spin { | ||||
|   from { | ||||
|     transform: rotate(0deg); | ||||
|   } | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (prefers-reduced-motion: no-preference) { | ||||
|   a:nth-of-type(2) .logo { | ||||
|     animation: logo-spin infinite 20s linear; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card { | ||||
|   padding: 2em; | ||||
| } | ||||
| 
 | ||||
| .read-the-docs { | ||||
|   color: #888; | ||||
| } | ||||
							
								
								
									
										30
									
								
								frontend/src/App.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/App.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| 
 | ||||
| import { Toaster } from "@/components/ui/toaster"; | ||||
| import { Toaster as Sonner } from "@/components/ui/sonner"; | ||||
| import { TooltipProvider } from "@/components/ui/tooltip"; | ||||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||||
| import { BrowserRouter, Routes, Route } from "react-router-dom"; | ||||
| import { AuthProvider } from "@/hooks/useAuth"; | ||||
| import Index from "./pages/Index"; | ||||
| import NotFound from "./pages/NotFound"; | ||||
| 
 | ||||
| const queryClient = new QueryClient(); | ||||
| 
 | ||||
| const App = () => ( | ||||
|   <QueryClientProvider client={queryClient}> | ||||
|     <TooltipProvider> | ||||
|       <AuthProvider> | ||||
|         <Toaster /> | ||||
|         <Sonner /> | ||||
|         <BrowserRouter> | ||||
|           <Routes> | ||||
|             <Route path="/" element={<Index />} /> | ||||
|             <Route path="*" element={<NotFound />} /> | ||||
|           </Routes> | ||||
|         </BrowserRouter> | ||||
|       </AuthProvider> | ||||
|     </TooltipProvider> | ||||
|   </QueryClientProvider> | ||||
| ); | ||||
| 
 | ||||
| export default App; | ||||
							
								
								
									
										160
									
								
								frontend/src/components/AddItemDialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								frontend/src/components/AddItemDialog.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,160 @@ | |||
| 
 | ||||
| import { useState } from 'react'; | ||||
| import { Button } from '@/components/ui/button'; | ||||
| import { Input } from '@/components/ui/input'; | ||||
| import { Textarea } from '@/components/ui/textarea'; | ||||
| import { Badge } from '@/components/ui/badge'; | ||||
| import { | ||||
|   Dialog, | ||||
|   DialogContent, | ||||
|   DialogDescription, | ||||
|   DialogFooter, | ||||
|   DialogHeader, | ||||
|   DialogTitle, | ||||
| } from '@/components/ui/dialog'; | ||||
| import { Tag, InventoryItem } from '@/types/inventory'; | ||||
| import { inventoryService } from '@/services/inventoryService'; | ||||
| import { useAuth } from '@/hooks/useAuth'; | ||||
| import { useToast } from '@/hooks/use-toast'; | ||||
| 
 | ||||
| interface AddItemDialogProps { | ||||
|   open: boolean; | ||||
|   onOpenChange: (open: boolean) => void; | ||||
|   tags: Tag[]; | ||||
|   onItemAdded: (item: InventoryItem) => void; | ||||
| } | ||||
| 
 | ||||
| export const AddItemDialog = ({ open, onOpenChange, tags, onItemAdded }: AddItemDialogProps) => { | ||||
|   const [name, setName] = useState(''); | ||||
|   const [description, setDescription] = useState(''); | ||||
|   const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|    | ||||
|   const { user } = useAuth(); | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|      | ||||
|     if (!name.trim()) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Il nome dell'oggetto è obbligatorio", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     setIsLoading(true); | ||||
|      | ||||
|     try { | ||||
|       const newItem = await inventoryService.createItem({ | ||||
|         name: name.trim(), | ||||
|         description: description.trim(), | ||||
|         tagIds: selectedTagIds | ||||
|       }); | ||||
|        | ||||
|       onItemAdded(newItem); | ||||
|        | ||||
|       // Reset form
 | ||||
|       setName(''); | ||||
|       setDescription(''); | ||||
|       setSelectedTagIds([]); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Impossibile aggiungere l'oggetto", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const toggleTag = (tagId: string) => { | ||||
|     setSelectedTagIds(prev => | ||||
|       prev.includes(tagId) | ||||
|         ? prev.filter(id => id !== tagId) | ||||
|         : [...prev, tagId] | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog open={open} onOpenChange={onOpenChange}> | ||||
|       <DialogContent className="sm:max-w-md"> | ||||
|         <DialogHeader> | ||||
|           <DialogTitle>Aggiungi Nuovo Oggetto</DialogTitle> | ||||
|           <DialogDescription> | ||||
|             Inserisci i dettagli del nuovo oggetto per l'inventario | ||||
|           </DialogDescription> | ||||
|         </DialogHeader> | ||||
|          | ||||
|         <form onSubmit={handleSubmit} className="space-y-4"> | ||||
|           <div className="space-y-2"> | ||||
|             <label className="text-sm font-medium text-gray-700">Nome *</label> | ||||
|             <Input | ||||
|               value={name} | ||||
|               onChange={(e) => setName(e.target.value)} | ||||
|               placeholder="es. Cavo LAN Cat6" | ||||
|               required | ||||
|             /> | ||||
|           </div> | ||||
|            | ||||
|           <div className="space-y-2"> | ||||
|             <label className="text-sm font-medium text-gray-700">Descrizione</label> | ||||
|             <Textarea | ||||
|               value={description} | ||||
|               onChange={(e) => setDescription(e.target.value)} | ||||
|               placeholder="Descrizione dettagliata dell'oggetto..." | ||||
|               rows={3} | ||||
|             /> | ||||
|           </div> | ||||
|            | ||||
|           <div className="space-y-2"> | ||||
|             <label className="text-sm font-medium text-gray-700">Tag</label> | ||||
|             <div className="flex flex-wrap gap-2 p-3 border rounded-md bg-gray-50 min-h-[60px]"> | ||||
|               {tags.map(tag => ( | ||||
|                 <Badge | ||||
|                   key={tag._id} | ||||
|                   variant="outline" | ||||
|                   className={`cursor-pointer transition-all border`} | ||||
|                   style={{ | ||||
|                     backgroundColor: selectedTagIds.includes(tag._id) ? '#3b82f6' : tag.color, | ||||
|                     color: '#fff', | ||||
|                   }} | ||||
|                   onClick={() => toggleTag(tag._id)} | ||||
|                 > | ||||
|                   {tag.name} | ||||
|                   {selectedTagIds.includes(tag._id) && ( | ||||
|                     <span className="ml-1 font-bold">✓</span> | ||||
|                   )} | ||||
|                 </Badge> | ||||
|               ))} | ||||
|             </div> | ||||
|             <p className="text-xs text-gray-500"> | ||||
|               Clicca sui tag per selezionarli | ||||
|             </p> | ||||
|           </div> | ||||
| 
 | ||||
|           <DialogFooter> | ||||
|             <Button | ||||
|               type="button" | ||||
|               variant="outline" | ||||
|               onClick={() => onOpenChange(false)} | ||||
|               disabled={isLoading} | ||||
|             > | ||||
|               Annulla | ||||
|             </Button> | ||||
|             <Button | ||||
|               type="submit" | ||||
|               disabled={isLoading} | ||||
|               className="bg-green-600 hover:bg-green-700" | ||||
|             > | ||||
|               {isLoading ? "Aggiunta..." : "Aggiungi Oggetto"} | ||||
|             </Button> | ||||
|           </DialogFooter> | ||||
|         </form> | ||||
|       </DialogContent> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										57
									
								
								frontend/src/components/DeleteItemDialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								frontend/src/components/DeleteItemDialog.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | |||
| 
 | ||||
| import { | ||||
|   AlertDialog, | ||||
|   AlertDialogAction, | ||||
|   AlertDialogCancel, | ||||
|   AlertDialogContent, | ||||
|   AlertDialogDescription, | ||||
|   AlertDialogFooter, | ||||
|   AlertDialogHeader, | ||||
|   AlertDialogTitle, | ||||
| } from '@/components/ui/alert-dialog'; | ||||
| import { InventoryItem } from '@/types/inventory'; | ||||
| 
 | ||||
| interface DeleteItemDialogProps { | ||||
|   open: boolean; | ||||
|   onOpenChange: (open: boolean) => void; | ||||
|   item: InventoryItem | null; | ||||
|   onConfirm: () => void; | ||||
|   isLoading: boolean; | ||||
| } | ||||
| 
 | ||||
| export const DeleteItemDialog = ({  | ||||
|   open,  | ||||
|   onOpenChange,  | ||||
|   item,  | ||||
|   onConfirm,  | ||||
|   isLoading  | ||||
| }: DeleteItemDialogProps) => { | ||||
|   if (!item) return null; | ||||
| 
 | ||||
|   return ( | ||||
|     <AlertDialog open={open} onOpenChange={onOpenChange}> | ||||
|       <AlertDialogContent> | ||||
|         <AlertDialogHeader> | ||||
|           <AlertDialogTitle>Conferma eliminazione</AlertDialogTitle> | ||||
|           <AlertDialogDescription> | ||||
|             Sei sicuro di voler eliminare l'oggetto <strong>"{item.name}"</strong>? | ||||
|             <br /> | ||||
|             Questa azione non può essere annullata. | ||||
|           </AlertDialogDescription> | ||||
|         </AlertDialogHeader> | ||||
|         <AlertDialogFooter> | ||||
|           <AlertDialogCancel disabled={isLoading}> | ||||
|             Annulla | ||||
|           </AlertDialogCancel> | ||||
|           <AlertDialogAction  | ||||
|             onClick={onConfirm} | ||||
|             disabled={isLoading} | ||||
|             className="bg-red-600 hover:bg-red-700" | ||||
|           > | ||||
|             {isLoading ? "Eliminazione..." : "Elimina"} | ||||
|           </AlertDialogAction> | ||||
|         </AlertDialogFooter> | ||||
|       </AlertDialogContent> | ||||
|     </AlertDialog> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										368
									
								
								frontend/src/components/InventoryDashboard.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										368
									
								
								frontend/src/components/InventoryDashboard.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,368 @@ | |||
| 
 | ||||
| import { useState, useEffect, useMemo } from 'react'; | ||||
| import { Button } from '@/components/ui/button'; | ||||
| import { Input } from '@/components/ui/input'; | ||||
| import { Badge } from '@/components/ui/badge'; | ||||
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | ||||
| import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; | ||||
| import { Search, Plus, Tag as TagIcon, User, Trash2 } from 'lucide-react'; | ||||
| import { InventoryItem, Tag } from '@/types/inventory'; | ||||
| import { inventoryService } from '@/services/inventoryService'; | ||||
| import { useAuth } from '@/hooks/useAuth'; | ||||
| import { AddItemDialog } from '@/components/AddItemDialog'; | ||||
| import { ManageTagsDialog } from '@/components/ManageTagsDialog'; | ||||
| import { DeleteItemDialog } from '@/components/DeleteItemDialog'; | ||||
| import { useToast } from '@/hooks/use-toast'; | ||||
| 
 | ||||
| export const InventoryDashboard = () => { | ||||
|   const [items, setItems] = useState<InventoryItem[]>([]); | ||||
|   const [tags, setTags] = useState<Tag[]>([]); | ||||
|   const [searchQuery, setSearchQuery] = useState(''); | ||||
|   const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]); | ||||
|   const [isLoading, setIsLoading] = useState(true); | ||||
|   const [isSearching, setIsSearching] = useState(false); | ||||
|   const [showAddDialog, setShowAddDialog] = useState(false); | ||||
|   const [showTagsDialog, setShowTagsDialog] = useState(false); | ||||
|   const [itemToDelete, setItemToDelete] = useState<InventoryItem | null>(null); | ||||
|   const [isDeletingItem, setIsDeletingItem] = useState(false); | ||||
| 
 | ||||
|   const { user, logout } = useAuth(); | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     loadInitialData(); | ||||
|   }, []); | ||||
| 
 | ||||
|   const loadInitialData = async () => { | ||||
|     try { | ||||
|       const [itemsData, tagsData] = await Promise.all([ | ||||
|         inventoryService.getItems(), | ||||
|         inventoryService.getTags() | ||||
|       ]); | ||||
|       setItems(itemsData); | ||||
|       setTags(tagsData); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Impossibile caricare i dati dell'inventario", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleSearch = async () => { | ||||
|     setIsSearching(true); | ||||
|     try { | ||||
|       const results = await inventoryService.searchItems(searchQuery, selectedTagIds); | ||||
|       setItems(results); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore di ricerca", | ||||
|         description: "Impossibile completare la ricerca", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsSearching(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleAddItem = async (newItem: InventoryItem) => { | ||||
|     setItems(prev => [newItem, ...prev]); | ||||
|     setShowAddDialog(false); | ||||
|     toast({ | ||||
|       title: "Oggetto aggiunto", | ||||
|       description: `${newItem.name} è stato aggiunto all'inventario`, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const handleDeleteItem = async () => { | ||||
|     if (!itemToDelete) return; | ||||
| 
 | ||||
|     setIsDeletingItem(true); | ||||
|      | ||||
|     try { | ||||
|       await inventoryService.deleteItem(itemToDelete._id); | ||||
|       setItems(prev => prev.filter(item => item._id !== itemToDelete._id)); | ||||
|       setItemToDelete(null); | ||||
|        | ||||
|       toast({ | ||||
|         title: "Oggetto eliminato", | ||||
|         description: `${itemToDelete.name} è stato eliminato dall'inventario`, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Impossibile eliminare l'oggetto", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsDeletingItem(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleTagCreated = (newTag: Tag) => { | ||||
|     setTags(prev => [...prev, newTag]); | ||||
|   }; | ||||
| 
 | ||||
|   const handleTagDeleted = (tagId: string) => { | ||||
|     setTags(prev => prev.filter(tag => tag._id !== tagId)); | ||||
|     // Rimuovi il tag dai filtri se selezionato
 | ||||
|     setSelectedTagIds(prev => prev.filter(id => id !== tagId)); | ||||
|     // Ricarica gli items per aggiornare quelli che avevano questo tag
 | ||||
|     loadInitialData(); | ||||
|   }; | ||||
| 
 | ||||
|   const toggleTagFilter = (tagId: string) => { | ||||
|     setSelectedTagIds(prev => | ||||
|       prev.includes(tagId) | ||||
|         ? prev.filter(id => id !== tagId) | ||||
|         : [...prev, tagId] | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   const filteredItems = useMemo(() => { | ||||
|     if (!searchQuery && selectedTagIds.length === 0) return items; | ||||
| 
 | ||||
|     return items.filter(item => { | ||||
|       const matchesQuery = !searchQuery || | ||||
|         item.name.toLowerCase().includes(searchQuery.toLowerCase()) || | ||||
|         item.description.toLowerCase().includes(searchQuery.toLowerCase()) || | ||||
|         item.tags.some(tag => tag.name.toLowerCase().includes(searchQuery.toLowerCase())); | ||||
| 
 | ||||
|       const matchesTags = selectedTagIds.length === 0 || | ||||
|         selectedTagIds.some(tagId => item.tags.some(tag => tag._id === tagId)); | ||||
| 
 | ||||
|       return matchesQuery && matchesTags; | ||||
|     }); | ||||
|   }, [items, searchQuery, selectedTagIds]); | ||||
| 
 | ||||
|   const formatDate = (dateString: string) => { | ||||
|     return new Date(dateString).toLocaleDateString('it-IT', { | ||||
|       year: 'numeric', | ||||
|       month: 'short', | ||||
|       day: 'numeric' | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   if (isLoading) { | ||||
|     return ( | ||||
|       <div className="min-h-screen bg-slate-50 flex items-center justify-center"> | ||||
|         <div className="text-center"> | ||||
|           <div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-green-500 rounded-full animate-pulse mx-auto mb-4"></div> | ||||
|           <p className="text-gray-600">Caricamento inventario...</p> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen bg-slate-50"> | ||||
|       {/* Header */} | ||||
|       <div className="bg-white border-b shadow-sm"> | ||||
|         <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | ||||
|           <div className="flex justify-between items-center py-4"> | ||||
|             <div className="flex items-center space-x-4"> | ||||
|               <div className="w-10 h-10 bg-gradient-to-br from-blue-600 to-green-500 rounded-xl flex items-center justify-center"> | ||||
|                 <span className="text-white font-bold">HS</span> | ||||
|               </div> | ||||
|               <div> | ||||
|                 <h1 className="text-2xl font-bold text-gray-900">Inventario Hackinpovo</h1> | ||||
|                 <p className="text-sm text-gray-500">Gestisci oggetti e componenti</p> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div className="flex items-center space-x-4"> | ||||
|               <div className="flex items-center space-x-2 text-sm text-gray-600"> | ||||
|                 <User className="w-4 h-4" /> | ||||
|                 <span>{user?.name}</span> | ||||
|               </div> | ||||
|               <Button variant="outline" onClick={logout}> | ||||
|                 Logout | ||||
|               </Button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | ||||
|         {/* Controls */} | ||||
|         <Card className="mb-8"> | ||||
|           <CardContent className="pt-6"> | ||||
|             <div className="space-y-4"> | ||||
|               {/* Search Bar */} | ||||
|               <div className="flex gap-4"> | ||||
|                 <div className="flex-1 relative"> | ||||
|                   <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> | ||||
|                   <Input | ||||
|                     placeholder="Cerca oggetti per nome, descrizione o tag..." | ||||
|                     value={searchQuery} | ||||
|                     onChange={(e) => setSearchQuery(e.target.value)} | ||||
|                     className="pl-10 h-12" | ||||
|                     onKeyDown={(e) => { | ||||
|                       if (e.key === 'Enter') { | ||||
|                         handleSearch(); | ||||
|                       } | ||||
|                     }} | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <Button | ||||
|                   onClick={handleSearch} | ||||
|                   disabled={isSearching} | ||||
|                   className="h-12 px-6 bg-blue-600 hover:bg-blue-700" | ||||
|                 > | ||||
|                   {isSearching ? "Ricerca..." : "Cerca"} | ||||
|                 </Button> | ||||
|               </div> | ||||
| 
 | ||||
|               {/* Tag Filters */} | ||||
|               <div className="space-y-2"> | ||||
|                 <div className="flex items-center justify-between"> | ||||
|                   <label className="text-sm font-medium text-gray-700">Filtra per tag:</label> | ||||
|                   <div className="flex gap-2"> | ||||
|                     <Button | ||||
|                       variant="outline" | ||||
|                       size="sm" | ||||
|                       onClick={() => setShowTagsDialog(true)} | ||||
|                       className="text-xs" | ||||
|                     > | ||||
|                       <TagIcon className="w-3 h-3 mr-1" /> | ||||
|                       Gestisci Tag | ||||
|                     </Button> | ||||
|                     <Button | ||||
|                       variant="outline" | ||||
|                       size="sm" | ||||
|                       onClick={() => setShowAddDialog(true)} | ||||
|                       className="text-xs bg-green-50 hover:bg-green-100 text-green-700 border-green-300" | ||||
|                     > | ||||
|                       <Plus className="w-3 h-3 mr-1" /> | ||||
|                       Aggiungi Oggetto | ||||
|                     </Button> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <div className="flex flex-wrap gap-2"> | ||||
|                   {tags.map(tag => { | ||||
|                     console.log(tag); | ||||
|                     return <Badge | ||||
|                       key={tag._id} | ||||
|                       variant="outline" | ||||
|                       className={`cursor-pointer transition-all border`} | ||||
|                       style={{ | ||||
|                         backgroundColor: selectedTagIds.includes(tag._id) ? '#3b82f6' : tag.color, | ||||
|                         color: '#fff', | ||||
|                       }} | ||||
|                       onClick={() => toggleTagFilter(tag._id)} | ||||
|                     > | ||||
|                       {tag.name} | ||||
|                       {selectedTagIds.includes(tag._id) && ( | ||||
|                         <span className="ml-1 font-bold">✓</span> | ||||
|                       )} | ||||
|                     </Badge> | ||||
|                   })} | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </CardContent> | ||||
|         </Card> | ||||
| 
 | ||||
|         {/* Items Table */} | ||||
|         <Card> | ||||
|           <CardHeader> | ||||
|             <CardTitle className="flex items-center justify-between"> | ||||
|               <span>Oggetti ({filteredItems.length})</span> | ||||
|               {selectedTagIds.length > 0 && ( | ||||
|                 <Button | ||||
|                   variant="ghost" | ||||
|                   size="sm" | ||||
|                   onClick={() => setSelectedTagIds([])} | ||||
|                   className="text-xs text-gray-500" | ||||
|                 > | ||||
|                   Rimuovi filtri | ||||
|                 </Button> | ||||
|               )} | ||||
|             </CardTitle> | ||||
|           </CardHeader> | ||||
|           <CardContent> | ||||
|             <div className="rounded-md border overflow-hidden"> | ||||
|               <Table> | ||||
|                 <TableHeader> | ||||
|                   <TableRow className="bg-slate-50"> | ||||
|                     <TableHead className="font-semibold">Nome</TableHead> | ||||
|                     <TableHead className="font-semibold">Descrizione</TableHead> | ||||
|                     <TableHead className="font-semibold">Tag</TableHead> | ||||
|                     <TableHead className="font-semibold">Data Aggiunta</TableHead> | ||||
|                     <TableHead className="font-semibold">Aggiunto da</TableHead> | ||||
|                     <TableHead className="font-semibold">Azioni</TableHead> | ||||
|                   </TableRow> | ||||
|                 </TableHeader> | ||||
|                 <TableBody> | ||||
|                   {filteredItems.length === 0 ? ( | ||||
|                     <TableRow> | ||||
|                       <TableCell colSpan={6} className="text-center py-8 text-gray-500"> | ||||
|                         {searchQuery || selectedTagIds.length > 0 | ||||
|                           ? "Nessun oggetto trovato con i filtri applicati" | ||||
|                           : "Nessun oggetto nell'inventario" | ||||
|                         } | ||||
|                       </TableCell> | ||||
|                     </TableRow> | ||||
|                   ) : ( | ||||
|                     filteredItems.map((item) => ( | ||||
|                       <TableRow key={item._id} className="hover:bg-slate-50 transition-colors"> | ||||
|                         <TableCell className="font-medium">{item.name}</TableCell> | ||||
|                         <TableCell className="text-gray-600">{item.description}</TableCell> | ||||
|                         <TableCell> | ||||
|                           <div className="flex flex-wrap gap-1"> | ||||
|                             {item.tags.map(tag => ( | ||||
|                               <Badge key={tag._id} variant="outline" className={`text-xs text-white`} style={{ backgroundColor: tag.color }}> | ||||
|                                 {tag.name} | ||||
|                               </Badge> | ||||
|                             ))} | ||||
|                           </div> | ||||
|                         </TableCell> | ||||
|                         <TableCell className="text-gray-600">{formatDate(item.dateAdded)}</TableCell> | ||||
|                         <TableCell className="text-gray-600">{item.addedBy}</TableCell> | ||||
|                         <TableCell> | ||||
|                           <Button | ||||
|                             variant="ghost" | ||||
|                             size="sm" | ||||
|                             onClick={() => setItemToDelete(item)} | ||||
|                             className="text-red-600 hover:text-red-700 hover:bg-red-50" | ||||
|                           > | ||||
|                             <Trash2 className="w-4 h-4" /> | ||||
|                           </Button> | ||||
|                         </TableCell> | ||||
|                       </TableRow> | ||||
|                     )) | ||||
|                   )} | ||||
|                 </TableBody> | ||||
|               </Table> | ||||
|             </div> | ||||
|           </CardContent> | ||||
|         </Card> | ||||
|       </div> | ||||
| 
 | ||||
|       <AddItemDialog | ||||
|         open={showAddDialog} | ||||
|         onOpenChange={setShowAddDialog} | ||||
|         tags={tags} | ||||
|         onItemAdded={handleAddItem} | ||||
|       /> | ||||
| 
 | ||||
|       <ManageTagsDialog | ||||
|         open={showTagsDialog} | ||||
|         onOpenChange={setShowTagsDialog} | ||||
|         tags={tags} | ||||
|         onTagCreated={handleTagCreated} | ||||
|         onTagDeleted={handleTagDeleted} | ||||
|       /> | ||||
| 
 | ||||
|       <DeleteItemDialog | ||||
|         open={!!itemToDelete} | ||||
|         onOpenChange={() => setItemToDelete(null)} | ||||
|         item={itemToDelete} | ||||
|         onConfirm={handleDeleteItem} | ||||
|         isLoading={isDeletingItem} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										160
									
								
								frontend/src/components/LoginForm.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								frontend/src/components/LoginForm.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,160 @@ | |||
| 
 | ||||
| import { useState } from 'react'; | ||||
| import { Button } from '@/components/ui/button'; | ||||
| import { Input } from '@/components/ui/input'; | ||||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; | ||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | ||||
| import { useAuth } from '@/hooks/useAuth'; | ||||
| import { useToast } from '@/hooks/use-toast'; | ||||
| 
 | ||||
| export const LoginForm = () => { | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   const { login, register } = useAuth(); | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => { | ||||
|     e.preventDefault(); | ||||
|     setIsLoading(true); | ||||
|      | ||||
|     const formData = new FormData(e.currentTarget); | ||||
|     const email = formData.get('email') as string; | ||||
|     const password = formData.get('password') as string; | ||||
| 
 | ||||
|     try { | ||||
|       await login(email, password); | ||||
|       toast({ | ||||
|         title: "Login effettuato", | ||||
|         description: "Benvenuto nell'inventario Hackinpovo!", | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore di login", | ||||
|         description: "Controlla le tue credenziali.", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleRegister = async (e: React.FormEvent<HTMLFormElement>) => { | ||||
|     e.preventDefault(); | ||||
|     setIsLoading(true); | ||||
|      | ||||
|     const formData = new FormData(e.currentTarget); | ||||
|     const email = formData.get('email') as string; | ||||
|     const password = formData.get('password') as string; | ||||
|     const name = formData.get('name') as string; | ||||
| 
 | ||||
|     try { | ||||
|       await register(email, password, name); | ||||
|       toast({ | ||||
|         title: "Account creato", | ||||
|         description: "Benvenuto nell'inventario Hackinpovo!", | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore di registrazione", | ||||
|         description: "Si è verificato un errore durante la registrazione.", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-slate-800 flex items-center justify-center p-4"> | ||||
|       <Card className="w-full max-w-md shadow-2xl border-0 bg-white/95 backdrop-blur"> | ||||
|         <CardHeader className="text-center pb-2"> | ||||
|           <div className="w-16 h-16 bg-gradient-to-br from-blue-600 to-green-500 rounded-2xl mx-auto mb-4 flex items-center justify-center"> | ||||
|             <span className="text-white font-bold text-xl">HS</span> | ||||
|           </div> | ||||
|           <CardTitle className="text-2xl bg-gradient-to-r from-blue-600 to-green-500 bg-clip-text text-transparent"> | ||||
|             Inventario Hackinpovo | ||||
|           </CardTitle> | ||||
|           <CardDescription> | ||||
|             Gestisci l'inventario di Hackinpovo | ||||
|           </CardDescription> | ||||
|         </CardHeader> | ||||
|         <CardContent> | ||||
|           <Tabs defaultValue="login" className="w-full"> | ||||
|             <TabsList className="grid w-full grid-cols-2"> | ||||
|               <TabsTrigger value="login">Login</TabsTrigger> | ||||
|               <TabsTrigger value="register">Registrati</TabsTrigger> | ||||
|             </TabsList> | ||||
|              | ||||
|             <TabsContent value="login" className="space-y-4"> | ||||
|               <form onSubmit={handleLogin} className="space-y-4"> | ||||
|                 <div className="space-y-2"> | ||||
|                   <Input | ||||
|                     name="email" | ||||
|                     type="email" | ||||
|                     placeholder="Email" | ||||
|                     required | ||||
|                     className="h-11" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <div className="space-y-2"> | ||||
|                   <Input | ||||
|                     name="password" | ||||
|                     type="password" | ||||
|                     placeholder="Password" | ||||
|                     required | ||||
|                     className="h-11" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <Button  | ||||
|                   type="submit"  | ||||
|                   className="w-full h-11 bg-gradient-to-r from-blue-600 to-green-500 hover:from-blue-700 hover:to-green-600" | ||||
|                   disabled={isLoading} | ||||
|                 > | ||||
|                   {isLoading ? "Accesso in corso..." : "Accedi"} | ||||
|                 </Button> | ||||
|               </form> | ||||
|             </TabsContent> | ||||
|              | ||||
|             <TabsContent value="register" className="space-y-4"> | ||||
|               <form onSubmit={handleRegister} className="space-y-4"> | ||||
|                 <div className="space-y-2"> | ||||
|                   <Input | ||||
|                     name="name" | ||||
|                     type="text" | ||||
|                     placeholder="Nome completo" | ||||
|                     required | ||||
|                     className="h-11" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <div className="space-y-2"> | ||||
|                   <Input | ||||
|                     name="email" | ||||
|                     type="email" | ||||
|                     placeholder="Email" | ||||
|                     required | ||||
|                     className="h-11" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <div className="space-y-2"> | ||||
|                   <Input | ||||
|                     name="password" | ||||
|                     type="password" | ||||
|                     placeholder="Password" | ||||
|                     required | ||||
|                     className="h-11" | ||||
|                   /> | ||||
|                 </div> | ||||
|                 <Button  | ||||
|                   type="submit"  | ||||
|                   className="w-full h-11 bg-gradient-to-r from-blue-600 to-green-500 hover:from-blue-700 hover:to-green-600" | ||||
|                   disabled={isLoading} | ||||
|                 > | ||||
|                   {isLoading ? "Registrazione..." : "Registrati"} | ||||
|                 </Button> | ||||
|               </form> | ||||
|             </TabsContent> | ||||
|           </Tabs> | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										228
									
								
								frontend/src/components/ManageTagsDialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								frontend/src/components/ManageTagsDialog.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,228 @@ | |||
| import { useState } from 'react'; | ||||
| import { Button } from '@/components/ui/button'; | ||||
| import { Input } from '@/components/ui/input'; | ||||
| import { Badge } from '@/components/ui/badge'; | ||||
| import { | ||||
|   Dialog, | ||||
|   DialogContent, | ||||
|   DialogDescription, | ||||
|   DialogHeader, | ||||
|   DialogTitle, | ||||
| } from '@/components/ui/dialog'; | ||||
| import { | ||||
|   AlertDialog, | ||||
|   AlertDialogAction, | ||||
|   AlertDialogCancel, | ||||
|   AlertDialogContent, | ||||
|   AlertDialogDescription, | ||||
|   AlertDialogFooter, | ||||
|   AlertDialogHeader, | ||||
|   AlertDialogTitle, | ||||
| } from '@/components/ui/alert-dialog'; | ||||
| import { Tag as TagIcon, Plus, Trash2 } from 'lucide-react'; | ||||
| import { Tag } from '@/types/inventory'; | ||||
| import { inventoryService } from '@/services/inventoryService'; | ||||
| import { useToast } from '@/hooks/use-toast'; | ||||
| 
 | ||||
| interface ManageTagsDialogProps { | ||||
|   open: boolean; | ||||
|   onOpenChange: (open: boolean) => void; | ||||
|   tags: Tag[]; | ||||
|   onTagCreated: (tag: Tag) => void; | ||||
|   onTagDeleted: (tagId: string) => void; | ||||
| } | ||||
| 
 | ||||
| export const ManageTagsDialog = ({  | ||||
|   open,  | ||||
|   onOpenChange,  | ||||
|   tags,  | ||||
|   onTagCreated,  | ||||
|   onTagDeleted  | ||||
| }: ManageTagsDialogProps) => { | ||||
|   const [newTagName, setNewTagName] = useState(''); | ||||
|   const [newTagColor, setNewTagColor] = useState('#000000'); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   const [tagToDelete, setTagToDelete] = useState<Tag | null>(null); | ||||
|   const [isDeletingTag, setIsDeletingTag] = useState(false); | ||||
|    | ||||
|   const { toast } = useToast(); | ||||
| 
 | ||||
|   const handleCreateTag = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|      | ||||
|     if (!newTagName.trim()) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Il nome del tag è obbligatorio", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (tags.some(tag => tag.name.toLowerCase() === newTagName.trim().toLowerCase())) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Un tag con questo nome esiste già", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     setIsLoading(true); | ||||
|      | ||||
|     try { | ||||
|       const newTag = await inventoryService.createTag(newTagName.trim(), newTagColor.trim()); | ||||
|       onTagCreated(newTag); | ||||
|       setNewTagName(''); | ||||
|       setNewTagColor('#000000'); | ||||
|        | ||||
|       toast({ | ||||
|         title: "Tag creato", | ||||
|         description: `Il tag "${newTag.name}" è stato aggiunto`, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Impossibile creare il tag", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsLoading(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleDeleteTag = async () => { | ||||
|     if (!tagToDelete) return; | ||||
| 
 | ||||
|     setIsDeletingTag(true); | ||||
|      | ||||
|     try { | ||||
|       await inventoryService.deleteTag(tagToDelete._id); | ||||
|       onTagDeleted(tagToDelete._id); | ||||
|       setTagToDelete(null); | ||||
|        | ||||
|       toast({ | ||||
|         title: "Tag eliminato", | ||||
|         description: `Il tag "${tagToDelete.name}" è stato eliminato`, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       toast({ | ||||
|         title: "Errore", | ||||
|         description: "Impossibile eliminare il tag", | ||||
|         variant: "destructive", | ||||
|       }); | ||||
|     } finally { | ||||
|       setIsDeletingTag(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Dialog open={open} onOpenChange={onOpenChange}> | ||||
|         <DialogContent className="sm:max-w-md"> | ||||
|           <DialogHeader> | ||||
|             <DialogTitle className="flex items-center gap-2"> | ||||
|               <TagIcon className="w-5 h-5" /> | ||||
|               Gestisci Tag | ||||
|             </DialogTitle> | ||||
|             <DialogDescription> | ||||
|               Visualizza, crea ed elimina tag per organizzare l'inventario | ||||
|             </DialogDescription> | ||||
|           </DialogHeader> | ||||
|            | ||||
|           <div className="space-y-6"> | ||||
|             {/* Create New Tag */} | ||||
|             <div className="space-y-3"> | ||||
|               <h4 className="font-medium text-gray-900">Crea Nuovo Tag</h4> | ||||
|               <form onSubmit={handleCreateTag} className="space-y-3"> | ||||
|                 <div className="flex gap-2"> | ||||
|                   <Input | ||||
|                     value={newTagName} | ||||
|                     onChange={(e) => setNewTagName(e.target.value)} | ||||
|                     placeholder="Nome del tag..." | ||||
|                     className="flex-1" | ||||
|                   /> | ||||
|                   <input | ||||
|                     type="color" | ||||
|                     value={newTagColor} | ||||
|                     onChange={(e) => setNewTagColor(e.target.value)} | ||||
|                     className="w-10 h-10 p-0 border rounded" | ||||
|                     title="Seleziona colore" | ||||
|                   /> | ||||
|                   <Button | ||||
|                     type="submit" | ||||
|                     disabled={isLoading} | ||||
|                     className="bg-green-600 hover:bg-green-700" | ||||
|                   > | ||||
|                     <Plus className="w-4 h-4" /> | ||||
|                   </Button> | ||||
|                 </div> | ||||
|               </form> | ||||
|             </div> | ||||
| 
 | ||||
|             {/* Existing Tags */} | ||||
|             <div className="space-y-3"> | ||||
|               <h4 className="font-medium text-gray-900">Tag Esistenti ({tags.length})</h4> | ||||
|               <div className="p-3 border rounded-md bg-gray-50 min-h-[100px] max-h-[200px] overflow-y-auto"> | ||||
|                 {tags.map(tag => ( | ||||
|                   <div key={tag._id} className="flex items-center justify-between mb-2 p-2 bg-white rounded border"> | ||||
|                     <Badge | ||||
|                       variant="outline" | ||||
|                       style={{ backgroundColor: tag.color }} | ||||
|                       className="text-white" | ||||
|                     > | ||||
|                       {tag.name} | ||||
|                     </Badge> | ||||
|                     <Button | ||||
|                       variant="ghost" | ||||
|                       size="sm" | ||||
|                       onClick={() => setTagToDelete(tag)} | ||||
|                       className="text-red-600 hover:text-red-700 hover:bg-red-50" | ||||
|                     > | ||||
|                       <Trash2 className="w-4 h-4" /> | ||||
|                     </Button> | ||||
|                   </div> | ||||
|                 ))} | ||||
|                 {tags.length === 0 && ( | ||||
|                   <p className="text-gray-500 text-sm">Nessun tag disponibile</p> | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex justify-end pt-4"> | ||||
|             <Button variant="outline" onClick={() => onOpenChange(false)}> | ||||
|               Chiudi | ||||
|             </Button> | ||||
|           </div> | ||||
|         </DialogContent> | ||||
|       </Dialog> | ||||
| 
 | ||||
|       {/* Delete Confirmation Dialog */} | ||||
|       <AlertDialog open={!!tagToDelete} onOpenChange={() => setTagToDelete(null)}> | ||||
|         <AlertDialogContent> | ||||
|           <AlertDialogHeader> | ||||
|             <AlertDialogTitle>Conferma eliminazione</AlertDialogTitle> | ||||
|             <AlertDialogDescription> | ||||
|               Sei sicuro di voler eliminare il tag <strong>"{tagToDelete?.name}"</strong>? | ||||
|               <br /> | ||||
|               Questa azione non può essere annullata e rimuoverà il tag da tutti gli oggetti associati. | ||||
|             </AlertDialogDescription> | ||||
|           </AlertDialogHeader> | ||||
|           <AlertDialogFooter> | ||||
|             <AlertDialogCancel disabled={isDeletingTag}> | ||||
|               Annulla | ||||
|             </AlertDialogCancel> | ||||
|             <AlertDialogAction  | ||||
|               onClick={handleDeleteTag} | ||||
|               disabled={isDeletingTag} | ||||
|               className="bg-red-600 hover:bg-red-700" | ||||
|             > | ||||
|               {isDeletingTag ? "Eliminazione..." : "Elimina"} | ||||
|             </AlertDialogAction> | ||||
|           </AlertDialogFooter> | ||||
|         </AlertDialogContent> | ||||
|       </AlertDialog> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										56
									
								
								frontend/src/components/ui/accordion.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								frontend/src/components/ui/accordion.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<typeof AccordionPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AccordionPrimitive.Item | ||||
|     ref={ref} | ||||
|     className={cn("border-b", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AccordionItem.displayName = "AccordionItem" | ||||
| 
 | ||||
| const AccordionTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof AccordionPrimitive.Trigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <AccordionPrimitive.Header className="flex"> | ||||
|     <AccordionPrimitive.Trigger | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       {children} | ||||
|       <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" /> | ||||
|     </AccordionPrimitive.Trigger> | ||||
|   </AccordionPrimitive.Header> | ||||
| )) | ||||
| AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName | ||||
| 
 | ||||
| const AccordionContent = React.forwardRef< | ||||
|   React.ElementRef<typeof AccordionPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <AccordionPrimitive.Content | ||||
|     ref={ref} | ||||
|     className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" | ||||
|     {...props} | ||||
|   > | ||||
|     <div className={cn("pb-4 pt-0", className)}>{children}</div> | ||||
|   </AccordionPrimitive.Content> | ||||
| )) | ||||
| 
 | ||||
| AccordionContent.displayName = AccordionPrimitive.Content.displayName | ||||
| 
 | ||||
| export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } | ||||
							
								
								
									
										139
									
								
								frontend/src/components/ui/alert-dialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								frontend/src/components/ui/alert-dialog.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<typeof AlertDialogPrimitive.Overlay>, | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AlertDialogPrimitive.Overlay | ||||
|     className={cn( | ||||
|       "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|     ref={ref} | ||||
|   /> | ||||
| )) | ||||
| AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName | ||||
| 
 | ||||
| const AlertDialogContent = React.forwardRef< | ||||
|   React.ElementRef<typeof AlertDialogPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AlertDialogPortal> | ||||
|     <AlertDialogOverlay /> | ||||
|     <AlertDialogPrimitive.Content | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   </AlertDialogPortal> | ||||
| )) | ||||
| AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName | ||||
| 
 | ||||
| const AlertDialogHeader = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col space-y-2 text-center sm:text-left", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| AlertDialogHeader.displayName = "AlertDialogHeader" | ||||
| 
 | ||||
| const AlertDialogFooter = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| AlertDialogFooter.displayName = "AlertDialogFooter" | ||||
| 
 | ||||
| const AlertDialogTitle = React.forwardRef< | ||||
|   React.ElementRef<typeof AlertDialogPrimitive.Title>, | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AlertDialogPrimitive.Title | ||||
|     ref={ref} | ||||
|     className={cn("text-lg font-semibold", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName | ||||
| 
 | ||||
| const AlertDialogDescription = React.forwardRef< | ||||
|   React.ElementRef<typeof AlertDialogPrimitive.Description>, | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AlertDialogPrimitive.Description | ||||
|     ref={ref} | ||||
|     className={cn("text-sm text-muted-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AlertDialogDescription.displayName = | ||||
|   AlertDialogPrimitive.Description.displayName | ||||
| 
 | ||||
| const AlertDialogAction = React.forwardRef< | ||||
|   React.ElementRef<typeof AlertDialogPrimitive.Action>, | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AlertDialogPrimitive.Action | ||||
|     ref={ref} | ||||
|     className={cn(buttonVariants(), className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName | ||||
| 
 | ||||
| const AlertDialogCancel = React.forwardRef< | ||||
|   React.ElementRef<typeof AlertDialogPrimitive.Cancel>, | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AlertDialogPrimitive.Cancel | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       buttonVariants({ variant: "outline" }), | ||||
|       "mt-2 sm:mt-0", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName | ||||
| 
 | ||||
| export { | ||||
|   AlertDialog, | ||||
|   AlertDialogPortal, | ||||
|   AlertDialogOverlay, | ||||
|   AlertDialogTrigger, | ||||
|   AlertDialogContent, | ||||
|   AlertDialogHeader, | ||||
|   AlertDialogFooter, | ||||
|   AlertDialogTitle, | ||||
|   AlertDialogDescription, | ||||
|   AlertDialogAction, | ||||
|   AlertDialogCancel, | ||||
| } | ||||
							
								
								
									
										59
									
								
								frontend/src/components/ui/alert.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								frontend/src/components/ui/alert.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<HTMLDivElement> & VariantProps<typeof alertVariants> | ||||
| >(({ className, variant, ...props }, ref) => ( | ||||
|   <div | ||||
|     ref={ref} | ||||
|     role="alert" | ||||
|     className={cn(alertVariants({ variant }), className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| Alert.displayName = "Alert" | ||||
| 
 | ||||
| const AlertTitle = React.forwardRef< | ||||
|   HTMLParagraphElement, | ||||
|   React.HTMLAttributes<HTMLHeadingElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <h5 | ||||
|     ref={ref} | ||||
|     className={cn("mb-1 font-medium leading-none tracking-tight", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AlertTitle.displayName = "AlertTitle" | ||||
| 
 | ||||
| const AlertDescription = React.forwardRef< | ||||
|   HTMLParagraphElement, | ||||
|   React.HTMLAttributes<HTMLParagraphElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div | ||||
|     ref={ref} | ||||
|     className={cn("text-sm [&_p]:leading-relaxed", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AlertDescription.displayName = "AlertDescription" | ||||
| 
 | ||||
| export { Alert, AlertTitle, AlertDescription } | ||||
							
								
								
									
										5
									
								
								frontend/src/components/ui/aspect-ratio.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/components/ui/aspect-ratio.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" | ||||
| 
 | ||||
| const AspectRatio = AspectRatioPrimitive.Root | ||||
| 
 | ||||
| export { AspectRatio } | ||||
							
								
								
									
										48
									
								
								frontend/src/components/ui/avatar.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								frontend/src/components/ui/avatar.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<typeof AvatarPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AvatarPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| Avatar.displayName = AvatarPrimitive.Root.displayName | ||||
| 
 | ||||
| const AvatarImage = React.forwardRef< | ||||
|   React.ElementRef<typeof AvatarPrimitive.Image>, | ||||
|   React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AvatarPrimitive.Image | ||||
|     ref={ref} | ||||
|     className={cn("aspect-square h-full w-full", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AvatarImage.displayName = AvatarPrimitive.Image.displayName | ||||
| 
 | ||||
| const AvatarFallback = React.forwardRef< | ||||
|   React.ElementRef<typeof AvatarPrimitive.Fallback>, | ||||
|   React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AvatarPrimitive.Fallback | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex h-full w-full items-center justify-center rounded-full bg-muted", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName | ||||
| 
 | ||||
| export { Avatar, AvatarImage, AvatarFallback } | ||||
							
								
								
									
										36
									
								
								frontend/src/components/ui/badge.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/src/components/ui/badge.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<HTMLDivElement>, | ||||
|     VariantProps<typeof badgeVariants> {} | ||||
| 
 | ||||
| function Badge({ className, variant, ...props }: BadgeProps) { | ||||
|   return ( | ||||
|     <div className={cn(badgeVariants({ variant }), className)} {...props} /> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export { Badge, badgeVariants } | ||||
							
								
								
									
										115
									
								
								frontend/src/components/ui/breadcrumb.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								frontend/src/components/ui/breadcrumb.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) => <nav ref={ref} aria-label="breadcrumb" {...props} />) | ||||
| Breadcrumb.displayName = "Breadcrumb" | ||||
| 
 | ||||
| const BreadcrumbList = React.forwardRef< | ||||
|   HTMLOListElement, | ||||
|   React.ComponentPropsWithoutRef<"ol"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <ol | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| BreadcrumbList.displayName = "BreadcrumbList" | ||||
| 
 | ||||
| const BreadcrumbItem = React.forwardRef< | ||||
|   HTMLLIElement, | ||||
|   React.ComponentPropsWithoutRef<"li"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <li | ||||
|     ref={ref} | ||||
|     className={cn("inline-flex items-center gap-1.5", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| BreadcrumbItem.displayName = "BreadcrumbItem" | ||||
| 
 | ||||
| const BreadcrumbLink = React.forwardRef< | ||||
|   HTMLAnchorElement, | ||||
|   React.ComponentPropsWithoutRef<"a"> & { | ||||
|     asChild?: boolean | ||||
|   } | ||||
| >(({ asChild, className, ...props }, ref) => { | ||||
|   const Comp = asChild ? Slot : "a" | ||||
| 
 | ||||
|   return ( | ||||
|     <Comp | ||||
|       ref={ref} | ||||
|       className={cn("transition-colors hover:text-foreground", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| BreadcrumbLink.displayName = "BreadcrumbLink" | ||||
| 
 | ||||
| const BreadcrumbPage = React.forwardRef< | ||||
|   HTMLSpanElement, | ||||
|   React.ComponentPropsWithoutRef<"span"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <span | ||||
|     ref={ref} | ||||
|     role="link" | ||||
|     aria-disabled="true" | ||||
|     aria-current="page" | ||||
|     className={cn("font-normal text-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| BreadcrumbPage.displayName = "BreadcrumbPage" | ||||
| 
 | ||||
| const BreadcrumbSeparator = ({ | ||||
|   children, | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<"li">) => ( | ||||
|   <li | ||||
|     role="presentation" | ||||
|     aria-hidden="true" | ||||
|     className={cn("[&>svg]:size-3.5", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     {children ?? <ChevronRight />} | ||||
|   </li> | ||||
| ) | ||||
| BreadcrumbSeparator.displayName = "BreadcrumbSeparator" | ||||
| 
 | ||||
| const BreadcrumbEllipsis = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<"span">) => ( | ||||
|   <span | ||||
|     role="presentation" | ||||
|     aria-hidden="true" | ||||
|     className={cn("flex h-9 w-9 items-center justify-center", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     <MoreHorizontal className="h-4 w-4" /> | ||||
|     <span className="sr-only">More</span> | ||||
|   </span> | ||||
| ) | ||||
| BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" | ||||
| 
 | ||||
| export { | ||||
|   Breadcrumb, | ||||
|   BreadcrumbList, | ||||
|   BreadcrumbItem, | ||||
|   BreadcrumbLink, | ||||
|   BreadcrumbPage, | ||||
|   BreadcrumbSeparator, | ||||
|   BreadcrumbEllipsis, | ||||
| } | ||||
							
								
								
									
										56
									
								
								frontend/src/components/ui/button.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								frontend/src/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 } | ||||
							
								
								
									
										64
									
								
								frontend/src/components/ui/calendar.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								frontend/src/components/ui/calendar.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| import * as React from "react"; | ||||
| import { ChevronLeft, ChevronRight } from "lucide-react"; | ||||
| import { DayPicker } from "react-day-picker"; | ||||
| 
 | ||||
| import { cn } from "@/lib/utils"; | ||||
| import { buttonVariants } from "@/components/ui/button"; | ||||
| 
 | ||||
| export type CalendarProps = React.ComponentProps<typeof DayPicker>; | ||||
| 
 | ||||
| function Calendar({ | ||||
|   className, | ||||
|   classNames, | ||||
|   showOutsideDays = true, | ||||
|   ...props | ||||
| }: CalendarProps) { | ||||
|   return ( | ||||
|     <DayPicker | ||||
|       showOutsideDays={showOutsideDays} | ||||
|       className={cn("p-3", className)} | ||||
|       classNames={{ | ||||
|         months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", | ||||
|         month: "space-y-4", | ||||
|         caption: "flex justify-center pt-1 relative items-center", | ||||
|         caption_label: "text-sm font-medium", | ||||
|         nav: "space-x-1 flex items-center", | ||||
|         nav_button: cn( | ||||
|           buttonVariants({ variant: "outline" }), | ||||
|           "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" | ||||
|         ), | ||||
|         nav_button_previous: "absolute left-1", | ||||
|         nav_button_next: "absolute right-1", | ||||
|         table: "w-full border-collapse space-y-1", | ||||
|         head_row: "flex", | ||||
|         head_cell: | ||||
|           "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", | ||||
|         row: "flex w-full mt-2", | ||||
|         cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", | ||||
|         day: cn( | ||||
|           buttonVariants({ variant: "ghost" }), | ||||
|           "h-9 w-9 p-0 font-normal aria-selected:opacity-100" | ||||
|         ), | ||||
|         day_range_end: "day-range-end", | ||||
|         day_selected: | ||||
|           "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", | ||||
|         day_today: "bg-accent text-accent-foreground", | ||||
|         day_outside: | ||||
|           "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", | ||||
|         day_disabled: "text-muted-foreground opacity-50", | ||||
|         day_range_middle: | ||||
|           "aria-selected:bg-accent aria-selected:text-accent-foreground", | ||||
|         day_hidden: "invisible", | ||||
|         ...classNames, | ||||
|       }} | ||||
|       components={{ | ||||
|         IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />, | ||||
|         IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />, | ||||
|       }} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| Calendar.displayName = "Calendar"; | ||||
| 
 | ||||
| export { Calendar }; | ||||
							
								
								
									
										79
									
								
								frontend/src/components/ui/card.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								frontend/src/components/ui/card.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | |||
| import * as React from "react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Card = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "rounded-lg border bg-card text-card-foreground shadow-sm", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| Card.displayName = "Card" | ||||
| 
 | ||||
| const CardHeader = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div | ||||
|     ref={ref} | ||||
|     className={cn("flex flex-col space-y-1.5 p-6", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| CardHeader.displayName = "CardHeader" | ||||
| 
 | ||||
| const CardTitle = React.forwardRef< | ||||
|   HTMLParagraphElement, | ||||
|   React.HTMLAttributes<HTMLHeadingElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <h3 | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "text-2xl font-semibold leading-none tracking-tight", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| CardTitle.displayName = "CardTitle" | ||||
| 
 | ||||
| const CardDescription = React.forwardRef< | ||||
|   HTMLParagraphElement, | ||||
|   React.HTMLAttributes<HTMLParagraphElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <p | ||||
|     ref={ref} | ||||
|     className={cn("text-sm text-muted-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| CardDescription.displayName = "CardDescription" | ||||
| 
 | ||||
| const CardContent = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> | ||||
| )) | ||||
| CardContent.displayName = "CardContent" | ||||
| 
 | ||||
| const CardFooter = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div | ||||
|     ref={ref} | ||||
|     className={cn("flex items-center p-6 pt-0", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| CardFooter.displayName = "CardFooter" | ||||
| 
 | ||||
| export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } | ||||
							
								
								
									
										260
									
								
								frontend/src/components/ui/carousel.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								frontend/src/components/ui/carousel.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,260 @@ | |||
| import * as React from "react" | ||||
| import useEmblaCarousel, { | ||||
|   type UseEmblaCarouselType, | ||||
| } from "embla-carousel-react" | ||||
| import { ArrowLeft, ArrowRight } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| import { Button } from "@/components/ui/button" | ||||
| 
 | ||||
| type CarouselApi = UseEmblaCarouselType[1] | ||||
| type UseCarouselParameters = Parameters<typeof useEmblaCarousel> | ||||
| type CarouselOptions = UseCarouselParameters[0] | ||||
| type CarouselPlugin = UseCarouselParameters[1] | ||||
| 
 | ||||
| type CarouselProps = { | ||||
|   opts?: CarouselOptions | ||||
|   plugins?: CarouselPlugin | ||||
|   orientation?: "horizontal" | "vertical" | ||||
|   setApi?: (api: CarouselApi) => void | ||||
| } | ||||
| 
 | ||||
| type CarouselContextProps = { | ||||
|   carouselRef: ReturnType<typeof useEmblaCarousel>[0] | ||||
|   api: ReturnType<typeof useEmblaCarousel>[1] | ||||
|   scrollPrev: () => void | ||||
|   scrollNext: () => void | ||||
|   canScrollPrev: boolean | ||||
|   canScrollNext: boolean | ||||
| } & CarouselProps | ||||
| 
 | ||||
| const CarouselContext = React.createContext<CarouselContextProps | null>(null) | ||||
| 
 | ||||
| function useCarousel() { | ||||
|   const context = React.useContext(CarouselContext) | ||||
| 
 | ||||
|   if (!context) { | ||||
|     throw new Error("useCarousel must be used within a <Carousel />") | ||||
|   } | ||||
| 
 | ||||
|   return context | ||||
| } | ||||
| 
 | ||||
| const Carousel = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> & CarouselProps | ||||
| >( | ||||
|   ( | ||||
|     { | ||||
|       orientation = "horizontal", | ||||
|       opts, | ||||
|       setApi, | ||||
|       plugins, | ||||
|       className, | ||||
|       children, | ||||
|       ...props | ||||
|     }, | ||||
|     ref | ||||
|   ) => { | ||||
|     const [carouselRef, api] = useEmblaCarousel( | ||||
|       { | ||||
|         ...opts, | ||||
|         axis: orientation === "horizontal" ? "x" : "y", | ||||
|       }, | ||||
|       plugins | ||||
|     ) | ||||
|     const [canScrollPrev, setCanScrollPrev] = React.useState(false) | ||||
|     const [canScrollNext, setCanScrollNext] = React.useState(false) | ||||
| 
 | ||||
|     const onSelect = React.useCallback((api: CarouselApi) => { | ||||
|       if (!api) { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       setCanScrollPrev(api.canScrollPrev()) | ||||
|       setCanScrollNext(api.canScrollNext()) | ||||
|     }, []) | ||||
| 
 | ||||
|     const scrollPrev = React.useCallback(() => { | ||||
|       api?.scrollPrev() | ||||
|     }, [api]) | ||||
| 
 | ||||
|     const scrollNext = React.useCallback(() => { | ||||
|       api?.scrollNext() | ||||
|     }, [api]) | ||||
| 
 | ||||
|     const handleKeyDown = React.useCallback( | ||||
|       (event: React.KeyboardEvent<HTMLDivElement>) => { | ||||
|         if (event.key === "ArrowLeft") { | ||||
|           event.preventDefault() | ||||
|           scrollPrev() | ||||
|         } else if (event.key === "ArrowRight") { | ||||
|           event.preventDefault() | ||||
|           scrollNext() | ||||
|         } | ||||
|       }, | ||||
|       [scrollPrev, scrollNext] | ||||
|     ) | ||||
| 
 | ||||
|     React.useEffect(() => { | ||||
|       if (!api || !setApi) { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       setApi(api) | ||||
|     }, [api, setApi]) | ||||
| 
 | ||||
|     React.useEffect(() => { | ||||
|       if (!api) { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       onSelect(api) | ||||
|       api.on("reInit", onSelect) | ||||
|       api.on("select", onSelect) | ||||
| 
 | ||||
|       return () => { | ||||
|         api?.off("select", onSelect) | ||||
|       } | ||||
|     }, [api, onSelect]) | ||||
| 
 | ||||
|     return ( | ||||
|       <CarouselContext.Provider | ||||
|         value={{ | ||||
|           carouselRef, | ||||
|           api: api, | ||||
|           opts, | ||||
|           orientation: | ||||
|             orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), | ||||
|           scrollPrev, | ||||
|           scrollNext, | ||||
|           canScrollPrev, | ||||
|           canScrollNext, | ||||
|         }} | ||||
|       > | ||||
|         <div | ||||
|           ref={ref} | ||||
|           onKeyDownCapture={handleKeyDown} | ||||
|           className={cn("relative", className)} | ||||
|           role="region" | ||||
|           aria-roledescription="carousel" | ||||
|           {...props} | ||||
|         > | ||||
|           {children} | ||||
|         </div> | ||||
|       </CarouselContext.Provider> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| Carousel.displayName = "Carousel" | ||||
| 
 | ||||
| const CarouselContent = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   const { carouselRef, orientation } = useCarousel() | ||||
| 
 | ||||
|   return ( | ||||
|     <div ref={carouselRef} className="overflow-hidden"> | ||||
|       <div | ||||
|         ref={ref} | ||||
|         className={cn( | ||||
|           "flex", | ||||
|           orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", | ||||
|           className | ||||
|         )} | ||||
|         {...props} | ||||
|       /> | ||||
|     </div> | ||||
|   ) | ||||
| }) | ||||
| CarouselContent.displayName = "CarouselContent" | ||||
| 
 | ||||
| const CarouselItem = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   const { orientation } = useCarousel() | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       ref={ref} | ||||
|       role="group" | ||||
|       aria-roledescription="slide" | ||||
|       className={cn( | ||||
|         "min-w-0 shrink-0 grow-0 basis-full", | ||||
|         orientation === "horizontal" ? "pl-4" : "pt-4", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| CarouselItem.displayName = "CarouselItem" | ||||
| 
 | ||||
| const CarouselPrevious = React.forwardRef< | ||||
|   HTMLButtonElement, | ||||
|   React.ComponentProps<typeof Button> | ||||
| >(({ className, variant = "outline", size = "icon", ...props }, ref) => { | ||||
|   const { orientation, scrollPrev, canScrollPrev } = useCarousel() | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       ref={ref} | ||||
|       variant={variant} | ||||
|       size={size} | ||||
|       className={cn( | ||||
|         "absolute  h-8 w-8 rounded-full", | ||||
|         orientation === "horizontal" | ||||
|           ? "-left-12 top-1/2 -translate-y-1/2" | ||||
|           : "-top-12 left-1/2 -translate-x-1/2 rotate-90", | ||||
|         className | ||||
|       )} | ||||
|       disabled={!canScrollPrev} | ||||
|       onClick={scrollPrev} | ||||
|       {...props} | ||||
|     > | ||||
|       <ArrowLeft className="h-4 w-4" /> | ||||
|       <span className="sr-only">Previous slide</span> | ||||
|     </Button> | ||||
|   ) | ||||
| }) | ||||
| CarouselPrevious.displayName = "CarouselPrevious" | ||||
| 
 | ||||
| const CarouselNext = React.forwardRef< | ||||
|   HTMLButtonElement, | ||||
|   React.ComponentProps<typeof Button> | ||||
| >(({ className, variant = "outline", size = "icon", ...props }, ref) => { | ||||
|   const { orientation, scrollNext, canScrollNext } = useCarousel() | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       ref={ref} | ||||
|       variant={variant} | ||||
|       size={size} | ||||
|       className={cn( | ||||
|         "absolute h-8 w-8 rounded-full", | ||||
|         orientation === "horizontal" | ||||
|           ? "-right-12 top-1/2 -translate-y-1/2" | ||||
|           : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", | ||||
|         className | ||||
|       )} | ||||
|       disabled={!canScrollNext} | ||||
|       onClick={scrollNext} | ||||
|       {...props} | ||||
|     > | ||||
|       <ArrowRight className="h-4 w-4" /> | ||||
|       <span className="sr-only">Next slide</span> | ||||
|     </Button> | ||||
|   ) | ||||
| }) | ||||
| CarouselNext.displayName = "CarouselNext" | ||||
| 
 | ||||
| export { | ||||
|   type CarouselApi, | ||||
|   Carousel, | ||||
|   CarouselContent, | ||||
|   CarouselItem, | ||||
|   CarouselPrevious, | ||||
|   CarouselNext, | ||||
| } | ||||
							
								
								
									
										363
									
								
								frontend/src/components/ui/chart.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								frontend/src/components/ui/chart.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,363 @@ | |||
| import * as React from "react" | ||||
| import * as RechartsPrimitive from "recharts" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| // Format: { THEME_NAME: CSS_SELECTOR }
 | ||||
| const THEMES = { light: "", dark: ".dark" } as const | ||||
| 
 | ||||
| export type ChartConfig = { | ||||
|   [k in string]: { | ||||
|     label?: React.ReactNode | ||||
|     icon?: React.ComponentType | ||||
|   } & ( | ||||
|     | { color?: string; theme?: never } | ||||
|     | { color?: never; theme: Record<keyof typeof THEMES, string> } | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| type ChartContextProps = { | ||||
|   config: ChartConfig | ||||
| } | ||||
| 
 | ||||
| const ChartContext = React.createContext<ChartContextProps | null>(null) | ||||
| 
 | ||||
| function useChart() { | ||||
|   const context = React.useContext(ChartContext) | ||||
| 
 | ||||
|   if (!context) { | ||||
|     throw new Error("useChart must be used within a <ChartContainer />") | ||||
|   } | ||||
| 
 | ||||
|   return context | ||||
| } | ||||
| 
 | ||||
| const ChartContainer = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> & { | ||||
|     config: ChartConfig | ||||
|     children: React.ComponentProps< | ||||
|       typeof RechartsPrimitive.ResponsiveContainer | ||||
|     >["children"] | ||||
|   } | ||||
| >(({ id, className, children, config, ...props }, ref) => { | ||||
|   const uniqueId = React.useId() | ||||
|   const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` | ||||
| 
 | ||||
|   return ( | ||||
|     <ChartContext.Provider value={{ config }}> | ||||
|       <div | ||||
|         data-chart={chartId} | ||||
|         ref={ref} | ||||
|         className={cn( | ||||
|           "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", | ||||
|           className | ||||
|         )} | ||||
|         {...props} | ||||
|       > | ||||
|         <ChartStyle id={chartId} config={config} /> | ||||
|         <RechartsPrimitive.ResponsiveContainer> | ||||
|           {children} | ||||
|         </RechartsPrimitive.ResponsiveContainer> | ||||
|       </div> | ||||
|     </ChartContext.Provider> | ||||
|   ) | ||||
| }) | ||||
| ChartContainer.displayName = "Chart" | ||||
| 
 | ||||
| const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { | ||||
|   const colorConfig = Object.entries(config).filter( | ||||
|     ([_, config]) => config.theme || config.color | ||||
|   ) | ||||
| 
 | ||||
|   if (!colorConfig.length) { | ||||
|     return null | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <style | ||||
|       dangerouslySetInnerHTML={{ | ||||
|         __html: Object.entries(THEMES) | ||||
|           .map( | ||||
|             ([theme, prefix]) => ` | ||||
| ${prefix} [data-chart=${id}] { | ||||
| ${colorConfig | ||||
|   .map(([key, itemConfig]) => { | ||||
|     const color = | ||||
|       itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || | ||||
|       itemConfig.color | ||||
|     return color ? `  --color-${key}: ${color};` : null | ||||
|   }) | ||||
|   .join("\n")} | ||||
| } | ||||
| ` | ||||
|           ) | ||||
|           .join("\n"), | ||||
|       }} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| const ChartTooltip = RechartsPrimitive.Tooltip | ||||
| 
 | ||||
| const ChartTooltipContent = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<typeof RechartsPrimitive.Tooltip> & | ||||
|     React.ComponentProps<"div"> & { | ||||
|       hideLabel?: boolean | ||||
|       hideIndicator?: boolean | ||||
|       indicator?: "line" | "dot" | "dashed" | ||||
|       nameKey?: string | ||||
|       labelKey?: string | ||||
|     } | ||||
| >( | ||||
|   ( | ||||
|     { | ||||
|       active, | ||||
|       payload, | ||||
|       className, | ||||
|       indicator = "dot", | ||||
|       hideLabel = false, | ||||
|       hideIndicator = false, | ||||
|       label, | ||||
|       labelFormatter, | ||||
|       labelClassName, | ||||
|       formatter, | ||||
|       color, | ||||
|       nameKey, | ||||
|       labelKey, | ||||
|     }, | ||||
|     ref | ||||
|   ) => { | ||||
|     const { config } = useChart() | ||||
| 
 | ||||
|     const tooltipLabel = React.useMemo(() => { | ||||
|       if (hideLabel || !payload?.length) { | ||||
|         return null | ||||
|       } | ||||
| 
 | ||||
|       const [item] = payload | ||||
|       const key = `${labelKey || item.dataKey || item.name || "value"}` | ||||
|       const itemConfig = getPayloadConfigFromPayload(config, item, key) | ||||
|       const value = | ||||
|         !labelKey && typeof label === "string" | ||||
|           ? config[label as keyof typeof config]?.label || label | ||||
|           : itemConfig?.label | ||||
| 
 | ||||
|       if (labelFormatter) { | ||||
|         return ( | ||||
|           <div className={cn("font-medium", labelClassName)}> | ||||
|             {labelFormatter(value, payload)} | ||||
|           </div> | ||||
|         ) | ||||
|       } | ||||
| 
 | ||||
|       if (!value) { | ||||
|         return null | ||||
|       } | ||||
| 
 | ||||
|       return <div className={cn("font-medium", labelClassName)}>{value}</div> | ||||
|     }, [ | ||||
|       label, | ||||
|       labelFormatter, | ||||
|       payload, | ||||
|       hideLabel, | ||||
|       labelClassName, | ||||
|       config, | ||||
|       labelKey, | ||||
|     ]) | ||||
| 
 | ||||
|     if (!active || !payload?.length) { | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
|     const nestLabel = payload.length === 1 && indicator !== "dot" | ||||
| 
 | ||||
|     return ( | ||||
|       <div | ||||
|         ref={ref} | ||||
|         className={cn( | ||||
|           "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", | ||||
|           className | ||||
|         )} | ||||
|       > | ||||
|         {!nestLabel ? tooltipLabel : null} | ||||
|         <div className="grid gap-1.5"> | ||||
|           {payload.map((item, index) => { | ||||
|             const key = `${nameKey || item.name || item.dataKey || "value"}` | ||||
|             const itemConfig = getPayloadConfigFromPayload(config, item, key) | ||||
|             const indicatorColor = color || item.payload.fill || item.color | ||||
| 
 | ||||
|             return ( | ||||
|               <div | ||||
|                 key={item.dataKey} | ||||
|                 className={cn( | ||||
|                   "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", | ||||
|                   indicator === "dot" && "items-center" | ||||
|                 )} | ||||
|               > | ||||
|                 {formatter && item?.value !== undefined && item.name ? ( | ||||
|                   formatter(item.value, item.name, item, index, item.payload) | ||||
|                 ) : ( | ||||
|                   <> | ||||
|                     {itemConfig?.icon ? ( | ||||
|                       <itemConfig.icon /> | ||||
|                     ) : ( | ||||
|                       !hideIndicator && ( | ||||
|                         <div | ||||
|                           className={cn( | ||||
|                             "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", | ||||
|                             { | ||||
|                               "h-2.5 w-2.5": indicator === "dot", | ||||
|                               "w-1": indicator === "line", | ||||
|                               "w-0 border-[1.5px] border-dashed bg-transparent": | ||||
|                                 indicator === "dashed", | ||||
|                               "my-0.5": nestLabel && indicator === "dashed", | ||||
|                             } | ||||
|                           )} | ||||
|                           style={ | ||||
|                             { | ||||
|                               "--color-bg": indicatorColor, | ||||
|                               "--color-border": indicatorColor, | ||||
|                             } as React.CSSProperties | ||||
|                           } | ||||
|                         /> | ||||
|                       ) | ||||
|                     )} | ||||
|                     <div | ||||
|                       className={cn( | ||||
|                         "flex flex-1 justify-between leading-none", | ||||
|                         nestLabel ? "items-end" : "items-center" | ||||
|                       )} | ||||
|                     > | ||||
|                       <div className="grid gap-1.5"> | ||||
|                         {nestLabel ? tooltipLabel : null} | ||||
|                         <span className="text-muted-foreground"> | ||||
|                           {itemConfig?.label || item.name} | ||||
|                         </span> | ||||
|                       </div> | ||||
|                       {item.value && ( | ||||
|                         <span className="font-mono font-medium tabular-nums text-foreground"> | ||||
|                           {item.value.toLocaleString()} | ||||
|                         </span> | ||||
|                       )} | ||||
|                     </div> | ||||
|                   </> | ||||
|                 )} | ||||
|               </div> | ||||
|             ) | ||||
|           })} | ||||
|         </div> | ||||
|       </div> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| ChartTooltipContent.displayName = "ChartTooltip" | ||||
| 
 | ||||
| const ChartLegend = RechartsPrimitive.Legend | ||||
| 
 | ||||
| const ChartLegendContent = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> & | ||||
|     Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { | ||||
|       hideIcon?: boolean | ||||
|       nameKey?: string | ||||
|     } | ||||
| >( | ||||
|   ( | ||||
|     { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, | ||||
|     ref | ||||
|   ) => { | ||||
|     const { config } = useChart() | ||||
| 
 | ||||
|     if (!payload?.length) { | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div | ||||
|         ref={ref} | ||||
|         className={cn( | ||||
|           "flex items-center justify-center gap-4", | ||||
|           verticalAlign === "top" ? "pb-3" : "pt-3", | ||||
|           className | ||||
|         )} | ||||
|       > | ||||
|         {payload.map((item) => { | ||||
|           const key = `${nameKey || item.dataKey || "value"}` | ||||
|           const itemConfig = getPayloadConfigFromPayload(config, item, key) | ||||
| 
 | ||||
|           return ( | ||||
|             <div | ||||
|               key={item.value} | ||||
|               className={cn( | ||||
|                 "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" | ||||
|               )} | ||||
|             > | ||||
|               {itemConfig?.icon && !hideIcon ? ( | ||||
|                 <itemConfig.icon /> | ||||
|               ) : ( | ||||
|                 <div | ||||
|                   className="h-2 w-2 shrink-0 rounded-[2px]" | ||||
|                   style={{ | ||||
|                     backgroundColor: item.color, | ||||
|                   }} | ||||
|                 /> | ||||
|               )} | ||||
|               {itemConfig?.label} | ||||
|             </div> | ||||
|           ) | ||||
|         })} | ||||
|       </div> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| ChartLegendContent.displayName = "ChartLegend" | ||||
| 
 | ||||
| // Helper to extract item config from a payload.
 | ||||
| function getPayloadConfigFromPayload( | ||||
|   config: ChartConfig, | ||||
|   payload: unknown, | ||||
|   key: string | ||||
| ) { | ||||
|   if (typeof payload !== "object" || payload === null) { | ||||
|     return undefined | ||||
|   } | ||||
| 
 | ||||
|   const payloadPayload = | ||||
|     "payload" in payload && | ||||
|     typeof payload.payload === "object" && | ||||
|     payload.payload !== null | ||||
|       ? payload.payload | ||||
|       : undefined | ||||
| 
 | ||||
|   let configLabelKey: string = key | ||||
| 
 | ||||
|   if ( | ||||
|     key in payload && | ||||
|     typeof payload[key as keyof typeof payload] === "string" | ||||
|   ) { | ||||
|     configLabelKey = payload[key as keyof typeof payload] as string | ||||
|   } else if ( | ||||
|     payloadPayload && | ||||
|     key in payloadPayload && | ||||
|     typeof payloadPayload[key as keyof typeof payloadPayload] === "string" | ||||
|   ) { | ||||
|     configLabelKey = payloadPayload[ | ||||
|       key as keyof typeof payloadPayload | ||||
|     ] as string | ||||
|   } | ||||
| 
 | ||||
|   return configLabelKey in config | ||||
|     ? config[configLabelKey] | ||||
|     : config[key as keyof typeof config] | ||||
| } | ||||
| 
 | ||||
| export { | ||||
|   ChartContainer, | ||||
|   ChartTooltip, | ||||
|   ChartTooltipContent, | ||||
|   ChartLegend, | ||||
|   ChartLegendContent, | ||||
|   ChartStyle, | ||||
| } | ||||
							
								
								
									
										28
									
								
								frontend/src/components/ui/checkbox.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/components/ui/checkbox.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| import * as React from "react" | ||||
| import * as CheckboxPrimitive from "@radix-ui/react-checkbox" | ||||
| import { Check } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Checkbox = React.forwardRef< | ||||
|   React.ElementRef<typeof CheckboxPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <CheckboxPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <CheckboxPrimitive.Indicator | ||||
|       className={cn("flex items-center justify-center text-current")} | ||||
|     > | ||||
|       <Check className="h-4 w-4" /> | ||||
|     </CheckboxPrimitive.Indicator> | ||||
|   </CheckboxPrimitive.Root> | ||||
| )) | ||||
| Checkbox.displayName = CheckboxPrimitive.Root.displayName | ||||
| 
 | ||||
| export { Checkbox } | ||||
							
								
								
									
										9
									
								
								frontend/src/components/ui/collapsible.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/components/ui/collapsible.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" | ||||
| 
 | ||||
| const Collapsible = CollapsiblePrimitive.Root | ||||
| 
 | ||||
| const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger | ||||
| 
 | ||||
| const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent | ||||
| 
 | ||||
| export { Collapsible, CollapsibleTrigger, CollapsibleContent } | ||||
							
								
								
									
										153
									
								
								frontend/src/components/ui/command.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								frontend/src/components/ui/command.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,153 @@ | |||
| import * as React from "react" | ||||
| import { type DialogProps } from "@radix-ui/react-dialog" | ||||
| import { Command as CommandPrimitive } from "cmdk" | ||||
| import { Search } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| import { Dialog, DialogContent } from "@/components/ui/dialog" | ||||
| 
 | ||||
| const Command = React.forwardRef< | ||||
|   React.ElementRef<typeof CommandPrimitive>, | ||||
|   React.ComponentPropsWithoutRef<typeof CommandPrimitive> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <CommandPrimitive | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| Command.displayName = CommandPrimitive.displayName | ||||
| 
 | ||||
| interface CommandDialogProps extends DialogProps {} | ||||
| 
 | ||||
| const CommandDialog = ({ children, ...props }: CommandDialogProps) => { | ||||
|   return ( | ||||
|     <Dialog {...props}> | ||||
|       <DialogContent className="overflow-hidden p-0 shadow-lg"> | ||||
|         <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> | ||||
|           {children} | ||||
|         </Command> | ||||
|       </DialogContent> | ||||
|     </Dialog> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| const CommandInput = React.forwardRef< | ||||
|   React.ElementRef<typeof CommandPrimitive.Input>, | ||||
|   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div className="flex items-center border-b px-3" cmdk-input-wrapper=""> | ||||
|     <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> | ||||
|     <CommandPrimitive.Input | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   </div> | ||||
| )) | ||||
| 
 | ||||
| CommandInput.displayName = CommandPrimitive.Input.displayName | ||||
| 
 | ||||
| const CommandList = React.forwardRef< | ||||
|   React.ElementRef<typeof CommandPrimitive.List>, | ||||
|   React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <CommandPrimitive.List | ||||
|     ref={ref} | ||||
|     className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| 
 | ||||
| CommandList.displayName = CommandPrimitive.List.displayName | ||||
| 
 | ||||
| const CommandEmpty = React.forwardRef< | ||||
|   React.ElementRef<typeof CommandPrimitive.Empty>, | ||||
|   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> | ||||
| >((props, ref) => ( | ||||
|   <CommandPrimitive.Empty | ||||
|     ref={ref} | ||||
|     className="py-6 text-center text-sm" | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| 
 | ||||
| CommandEmpty.displayName = CommandPrimitive.Empty.displayName | ||||
| 
 | ||||
| const CommandGroup = React.forwardRef< | ||||
|   React.ElementRef<typeof CommandPrimitive.Group>, | ||||
|   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <CommandPrimitive.Group | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| 
 | ||||
| CommandGroup.displayName = CommandPrimitive.Group.displayName | ||||
| 
 | ||||
| const CommandSeparator = React.forwardRef< | ||||
|   React.ElementRef<typeof CommandPrimitive.Separator>, | ||||
|   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <CommandPrimitive.Separator | ||||
|     ref={ref} | ||||
|     className={cn("-mx-1 h-px bg-border", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| CommandSeparator.displayName = CommandPrimitive.Separator.displayName | ||||
| 
 | ||||
| const CommandItem = React.forwardRef< | ||||
|   React.ElementRef<typeof CommandPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <CommandPrimitive.Item | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| 
 | ||||
| CommandItem.displayName = CommandPrimitive.Item.displayName | ||||
| 
 | ||||
| const CommandShortcut = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLSpanElement>) => { | ||||
|   return ( | ||||
|     <span | ||||
|       className={cn( | ||||
|         "ml-auto text-xs tracking-widest text-muted-foreground", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| CommandShortcut.displayName = "CommandShortcut" | ||||
| 
 | ||||
| export { | ||||
|   Command, | ||||
|   CommandDialog, | ||||
|   CommandInput, | ||||
|   CommandList, | ||||
|   CommandEmpty, | ||||
|   CommandGroup, | ||||
|   CommandItem, | ||||
|   CommandShortcut, | ||||
|   CommandSeparator, | ||||
| } | ||||
							
								
								
									
										198
									
								
								frontend/src/components/ui/context-menu.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								frontend/src/components/ui/context-menu.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | |||
| import * as React from "react" | ||||
| import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" | ||||
| import { Check, ChevronRight, Circle } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const ContextMenu = ContextMenuPrimitive.Root | ||||
| 
 | ||||
| const ContextMenuTrigger = ContextMenuPrimitive.Trigger | ||||
| 
 | ||||
| const ContextMenuGroup = ContextMenuPrimitive.Group | ||||
| 
 | ||||
| const ContextMenuPortal = ContextMenuPrimitive.Portal | ||||
| 
 | ||||
| const ContextMenuSub = ContextMenuPrimitive.Sub | ||||
| 
 | ||||
| const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup | ||||
| 
 | ||||
| const ContextMenuSubTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, children, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.SubTrigger | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     {children} | ||||
|     <ChevronRight className="ml-auto h-4 w-4" /> | ||||
|   </ContextMenuPrimitive.SubTrigger> | ||||
| )) | ||||
| ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName | ||||
| 
 | ||||
| const ContextMenuSubContent = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.SubContent>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.SubContent | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName | ||||
| 
 | ||||
| const ContextMenuContent = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.Portal> | ||||
|     <ContextMenuPrimitive.Content | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   </ContextMenuPrimitive.Portal> | ||||
| )) | ||||
| ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName | ||||
| 
 | ||||
| const ContextMenuItem = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.Item | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName | ||||
| 
 | ||||
| const ContextMenuCheckboxItem = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> | ||||
| >(({ className, children, checked, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.CheckboxItem | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     checked={checked} | ||||
|     {...props} | ||||
|   > | ||||
|     <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||||
|       <ContextMenuPrimitive.ItemIndicator> | ||||
|         <Check className="h-4 w-4" /> | ||||
|       </ContextMenuPrimitive.ItemIndicator> | ||||
|     </span> | ||||
|     {children} | ||||
|   </ContextMenuPrimitive.CheckboxItem> | ||||
| )) | ||||
| ContextMenuCheckboxItem.displayName = | ||||
|   ContextMenuPrimitive.CheckboxItem.displayName | ||||
| 
 | ||||
| const ContextMenuRadioItem = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.RadioItem>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.RadioItem | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||||
|       <ContextMenuPrimitive.ItemIndicator> | ||||
|         <Circle className="h-2 w-2 fill-current" /> | ||||
|       </ContextMenuPrimitive.ItemIndicator> | ||||
|     </span> | ||||
|     {children} | ||||
|   </ContextMenuPrimitive.RadioItem> | ||||
| )) | ||||
| ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName | ||||
| 
 | ||||
| const ContextMenuLabel = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.Label>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.Label | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "px-2 py-1.5 text-sm font-semibold text-foreground", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName | ||||
| 
 | ||||
| const ContextMenuSeparator = React.forwardRef< | ||||
|   React.ElementRef<typeof ContextMenuPrimitive.Separator>, | ||||
|   React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <ContextMenuPrimitive.Separator | ||||
|     ref={ref} | ||||
|     className={cn("-mx-1 my-1 h-px bg-border", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName | ||||
| 
 | ||||
| const ContextMenuShortcut = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLSpanElement>) => { | ||||
|   return ( | ||||
|     <span | ||||
|       className={cn( | ||||
|         "ml-auto text-xs tracking-widest text-muted-foreground", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| ContextMenuShortcut.displayName = "ContextMenuShortcut" | ||||
| 
 | ||||
| export { | ||||
|   ContextMenu, | ||||
|   ContextMenuTrigger, | ||||
|   ContextMenuContent, | ||||
|   ContextMenuItem, | ||||
|   ContextMenuCheckboxItem, | ||||
|   ContextMenuRadioItem, | ||||
|   ContextMenuLabel, | ||||
|   ContextMenuSeparator, | ||||
|   ContextMenuShortcut, | ||||
|   ContextMenuGroup, | ||||
|   ContextMenuPortal, | ||||
|   ContextMenuSub, | ||||
|   ContextMenuSubContent, | ||||
|   ContextMenuSubTrigger, | ||||
|   ContextMenuRadioGroup, | ||||
| } | ||||
							
								
								
									
										120
									
								
								frontend/src/components/ui/dialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								frontend/src/components/ui/dialog.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,120 @@ | |||
| import * as React from "react" | ||||
| import * as DialogPrimitive from "@radix-ui/react-dialog" | ||||
| import { X } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Dialog = DialogPrimitive.Root | ||||
| 
 | ||||
| const DialogTrigger = DialogPrimitive.Trigger | ||||
| 
 | ||||
| const DialogPortal = DialogPrimitive.Portal | ||||
| 
 | ||||
| const DialogClose = DialogPrimitive.Close | ||||
| 
 | ||||
| const DialogOverlay = React.forwardRef< | ||||
|   React.ElementRef<typeof DialogPrimitive.Overlay>, | ||||
|   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DialogPrimitive.Overlay | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DialogOverlay.displayName = DialogPrimitive.Overlay.displayName | ||||
| 
 | ||||
| const DialogContent = React.forwardRef< | ||||
|   React.ElementRef<typeof DialogPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <DialogPortal> | ||||
|     <DialogOverlay /> | ||||
|     <DialogPrimitive.Content | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       {children} | ||||
|       <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> | ||||
|         <X className="h-4 w-4" /> | ||||
|         <span className="sr-only">Close</span> | ||||
|       </DialogPrimitive.Close> | ||||
|     </DialogPrimitive.Content> | ||||
|   </DialogPortal> | ||||
| )) | ||||
| DialogContent.displayName = DialogPrimitive.Content.displayName | ||||
| 
 | ||||
| const DialogHeader = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col space-y-1.5 text-center sm:text-left", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| DialogHeader.displayName = "DialogHeader" | ||||
| 
 | ||||
| const DialogFooter = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| DialogFooter.displayName = "DialogFooter" | ||||
| 
 | ||||
| const DialogTitle = React.forwardRef< | ||||
|   React.ElementRef<typeof DialogPrimitive.Title>, | ||||
|   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DialogPrimitive.Title | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "text-lg font-semibold leading-none tracking-tight", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DialogTitle.displayName = DialogPrimitive.Title.displayName | ||||
| 
 | ||||
| const DialogDescription = React.forwardRef< | ||||
|   React.ElementRef<typeof DialogPrimitive.Description>, | ||||
|   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DialogPrimitive.Description | ||||
|     ref={ref} | ||||
|     className={cn("text-sm text-muted-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DialogDescription.displayName = DialogPrimitive.Description.displayName | ||||
| 
 | ||||
| export { | ||||
|   Dialog, | ||||
|   DialogPortal, | ||||
|   DialogOverlay, | ||||
|   DialogClose, | ||||
|   DialogTrigger, | ||||
|   DialogContent, | ||||
|   DialogHeader, | ||||
|   DialogFooter, | ||||
|   DialogTitle, | ||||
|   DialogDescription, | ||||
| } | ||||
							
								
								
									
										116
									
								
								frontend/src/components/ui/drawer.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								frontend/src/components/ui/drawer.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,116 @@ | |||
| import * as React from "react" | ||||
| import { Drawer as DrawerPrimitive } from "vaul" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Drawer = ({ | ||||
|   shouldScaleBackground = true, | ||||
|   ...props | ||||
| }: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( | ||||
|   <DrawerPrimitive.Root | ||||
|     shouldScaleBackground={shouldScaleBackground} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| Drawer.displayName = "Drawer" | ||||
| 
 | ||||
| const DrawerTrigger = DrawerPrimitive.Trigger | ||||
| 
 | ||||
| const DrawerPortal = DrawerPrimitive.Portal | ||||
| 
 | ||||
| const DrawerClose = DrawerPrimitive.Close | ||||
| 
 | ||||
| const DrawerOverlay = React.forwardRef< | ||||
|   React.ElementRef<typeof DrawerPrimitive.Overlay>, | ||||
|   React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DrawerPrimitive.Overlay | ||||
|     ref={ref} | ||||
|     className={cn("fixed inset-0 z-50 bg-black/80", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName | ||||
| 
 | ||||
| const DrawerContent = React.forwardRef< | ||||
|   React.ElementRef<typeof DrawerPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <DrawerPortal> | ||||
|     <DrawerOverlay /> | ||||
|     <DrawerPrimitive.Content | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> | ||||
|       {children} | ||||
|     </DrawerPrimitive.Content> | ||||
|   </DrawerPortal> | ||||
| )) | ||||
| DrawerContent.displayName = "DrawerContent" | ||||
| 
 | ||||
| const DrawerHeader = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| DrawerHeader.displayName = "DrawerHeader" | ||||
| 
 | ||||
| const DrawerFooter = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn("mt-auto flex flex-col gap-2 p-4", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| DrawerFooter.displayName = "DrawerFooter" | ||||
| 
 | ||||
| const DrawerTitle = React.forwardRef< | ||||
|   React.ElementRef<typeof DrawerPrimitive.Title>, | ||||
|   React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DrawerPrimitive.Title | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "text-lg font-semibold leading-none tracking-tight", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DrawerTitle.displayName = DrawerPrimitive.Title.displayName | ||||
| 
 | ||||
| const DrawerDescription = React.forwardRef< | ||||
|   React.ElementRef<typeof DrawerPrimitive.Description>, | ||||
|   React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DrawerPrimitive.Description | ||||
|     ref={ref} | ||||
|     className={cn("text-sm text-muted-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DrawerDescription.displayName = DrawerPrimitive.Description.displayName | ||||
| 
 | ||||
| export { | ||||
|   Drawer, | ||||
|   DrawerPortal, | ||||
|   DrawerOverlay, | ||||
|   DrawerTrigger, | ||||
|   DrawerClose, | ||||
|   DrawerContent, | ||||
|   DrawerHeader, | ||||
|   DrawerFooter, | ||||
|   DrawerTitle, | ||||
|   DrawerDescription, | ||||
| } | ||||
							
								
								
									
										198
									
								
								frontend/src/components/ui/dropdown-menu.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								frontend/src/components/ui/dropdown-menu.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | |||
| import * as React from "react" | ||||
| import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" | ||||
| import { Check, ChevronRight, Circle } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const DropdownMenu = DropdownMenuPrimitive.Root | ||||
| 
 | ||||
| const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger | ||||
| 
 | ||||
| const DropdownMenuGroup = DropdownMenuPrimitive.Group | ||||
| 
 | ||||
| const DropdownMenuPortal = DropdownMenuPrimitive.Portal | ||||
| 
 | ||||
| const DropdownMenuSub = DropdownMenuPrimitive.Sub | ||||
| 
 | ||||
| const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup | ||||
| 
 | ||||
| const DropdownMenuSubTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, children, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.SubTrigger | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     {children} | ||||
|     <ChevronRight className="ml-auto h-4 w-4" /> | ||||
|   </DropdownMenuPrimitive.SubTrigger> | ||||
| )) | ||||
| DropdownMenuSubTrigger.displayName = | ||||
|   DropdownMenuPrimitive.SubTrigger.displayName | ||||
| 
 | ||||
| const DropdownMenuSubContent = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.SubContent | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DropdownMenuSubContent.displayName = | ||||
|   DropdownMenuPrimitive.SubContent.displayName | ||||
| 
 | ||||
| const DropdownMenuContent = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> | ||||
| >(({ className, sideOffset = 4, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.Portal> | ||||
|     <DropdownMenuPrimitive.Content | ||||
|       ref={ref} | ||||
|       sideOffset={sideOffset} | ||||
|       className={cn( | ||||
|         "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   </DropdownMenuPrimitive.Portal> | ||||
| )) | ||||
| DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName | ||||
| 
 | ||||
| const DropdownMenuItem = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.Item | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName | ||||
| 
 | ||||
| const DropdownMenuCheckboxItem = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> | ||||
| >(({ className, children, checked, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.CheckboxItem | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     checked={checked} | ||||
|     {...props} | ||||
|   > | ||||
|     <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||||
|       <DropdownMenuPrimitive.ItemIndicator> | ||||
|         <Check className="h-4 w-4" /> | ||||
|       </DropdownMenuPrimitive.ItemIndicator> | ||||
|     </span> | ||||
|     {children} | ||||
|   </DropdownMenuPrimitive.CheckboxItem> | ||||
| )) | ||||
| DropdownMenuCheckboxItem.displayName = | ||||
|   DropdownMenuPrimitive.CheckboxItem.displayName | ||||
| 
 | ||||
| const DropdownMenuRadioItem = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.RadioItem | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||||
|       <DropdownMenuPrimitive.ItemIndicator> | ||||
|         <Circle className="h-2 w-2 fill-current" /> | ||||
|       </DropdownMenuPrimitive.ItemIndicator> | ||||
|     </span> | ||||
|     {children} | ||||
|   </DropdownMenuPrimitive.RadioItem> | ||||
| )) | ||||
| DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName | ||||
| 
 | ||||
| const DropdownMenuLabel = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.Label>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.Label | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "px-2 py-1.5 text-sm font-semibold", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName | ||||
| 
 | ||||
| const DropdownMenuSeparator = React.forwardRef< | ||||
|   React.ElementRef<typeof DropdownMenuPrimitive.Separator>, | ||||
|   React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <DropdownMenuPrimitive.Separator | ||||
|     ref={ref} | ||||
|     className={cn("-mx-1 my-1 h-px bg-muted", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName | ||||
| 
 | ||||
| const DropdownMenuShortcut = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLSpanElement>) => { | ||||
|   return ( | ||||
|     <span | ||||
|       className={cn("ml-auto text-xs tracking-widest opacity-60", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| DropdownMenuShortcut.displayName = "DropdownMenuShortcut" | ||||
| 
 | ||||
| export { | ||||
|   DropdownMenu, | ||||
|   DropdownMenuTrigger, | ||||
|   DropdownMenuContent, | ||||
|   DropdownMenuItem, | ||||
|   DropdownMenuCheckboxItem, | ||||
|   DropdownMenuRadioItem, | ||||
|   DropdownMenuLabel, | ||||
|   DropdownMenuSeparator, | ||||
|   DropdownMenuShortcut, | ||||
|   DropdownMenuGroup, | ||||
|   DropdownMenuPortal, | ||||
|   DropdownMenuSub, | ||||
|   DropdownMenuSubContent, | ||||
|   DropdownMenuSubTrigger, | ||||
|   DropdownMenuRadioGroup, | ||||
| } | ||||
							
								
								
									
										176
									
								
								frontend/src/components/ui/form.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								frontend/src/components/ui/form.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,176 @@ | |||
| import * as React from "react" | ||||
| import * as LabelPrimitive from "@radix-ui/react-label" | ||||
| import { Slot } from "@radix-ui/react-slot" | ||||
| import { | ||||
|   Controller, | ||||
|   ControllerProps, | ||||
|   FieldPath, | ||||
|   FieldValues, | ||||
|   FormProvider, | ||||
|   useFormContext, | ||||
| } from "react-hook-form" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| import { Label } from "@/components/ui/label" | ||||
| 
 | ||||
| const Form = FormProvider | ||||
| 
 | ||||
| type FormFieldContextValue< | ||||
|   TFieldValues extends FieldValues = FieldValues, | ||||
|   TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> | ||||
| > = { | ||||
|   name: TName | ||||
| } | ||||
| 
 | ||||
| const FormFieldContext = React.createContext<FormFieldContextValue>( | ||||
|   {} as FormFieldContextValue | ||||
| ) | ||||
| 
 | ||||
| const FormField = < | ||||
|   TFieldValues extends FieldValues = FieldValues, | ||||
|   TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> | ||||
| >({ | ||||
|   ...props | ||||
| }: ControllerProps<TFieldValues, TName>) => { | ||||
|   return ( | ||||
|     <FormFieldContext.Provider value={{ name: props.name }}> | ||||
|       <Controller {...props} /> | ||||
|     </FormFieldContext.Provider> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| const useFormField = () => { | ||||
|   const fieldContext = React.useContext(FormFieldContext) | ||||
|   const itemContext = React.useContext(FormItemContext) | ||||
|   const { getFieldState, formState } = useFormContext() | ||||
| 
 | ||||
|   const fieldState = getFieldState(fieldContext.name, formState) | ||||
| 
 | ||||
|   if (!fieldContext) { | ||||
|     throw new Error("useFormField should be used within <FormField>") | ||||
|   } | ||||
| 
 | ||||
|   const { id } = itemContext | ||||
| 
 | ||||
|   return { | ||||
|     id, | ||||
|     name: fieldContext.name, | ||||
|     formItemId: `${id}-form-item`, | ||||
|     formDescriptionId: `${id}-form-item-description`, | ||||
|     formMessageId: `${id}-form-item-message`, | ||||
|     ...fieldState, | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| type FormItemContextValue = { | ||||
|   id: string | ||||
| } | ||||
| 
 | ||||
| const FormItemContext = React.createContext<FormItemContextValue>( | ||||
|   {} as FormItemContextValue | ||||
| ) | ||||
| 
 | ||||
| const FormItem = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.HTMLAttributes<HTMLDivElement> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   const id = React.useId() | ||||
| 
 | ||||
|   return ( | ||||
|     <FormItemContext.Provider value={{ id }}> | ||||
|       <div ref={ref} className={cn("space-y-2", className)} {...props} /> | ||||
|     </FormItemContext.Provider> | ||||
|   ) | ||||
| }) | ||||
| FormItem.displayName = "FormItem" | ||||
| 
 | ||||
| const FormLabel = React.forwardRef< | ||||
|   React.ElementRef<typeof LabelPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   const { error, formItemId } = useFormField() | ||||
| 
 | ||||
|   return ( | ||||
|     <Label | ||||
|       ref={ref} | ||||
|       className={cn(error && "text-destructive", className)} | ||||
|       htmlFor={formItemId} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| FormLabel.displayName = "FormLabel" | ||||
| 
 | ||||
| const FormControl = React.forwardRef< | ||||
|   React.ElementRef<typeof Slot>, | ||||
|   React.ComponentPropsWithoutRef<typeof Slot> | ||||
| >(({ ...props }, ref) => { | ||||
|   const { error, formItemId, formDescriptionId, formMessageId } = useFormField() | ||||
| 
 | ||||
|   return ( | ||||
|     <Slot | ||||
|       ref={ref} | ||||
|       id={formItemId} | ||||
|       aria-describedby={ | ||||
|         !error | ||||
|           ? `${formDescriptionId}` | ||||
|           : `${formDescriptionId} ${formMessageId}` | ||||
|       } | ||||
|       aria-invalid={!!error} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| FormControl.displayName = "FormControl" | ||||
| 
 | ||||
| const FormDescription = React.forwardRef< | ||||
|   HTMLParagraphElement, | ||||
|   React.HTMLAttributes<HTMLParagraphElement> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   const { formDescriptionId } = useFormField() | ||||
| 
 | ||||
|   return ( | ||||
|     <p | ||||
|       ref={ref} | ||||
|       id={formDescriptionId} | ||||
|       className={cn("text-sm text-muted-foreground", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| FormDescription.displayName = "FormDescription" | ||||
| 
 | ||||
| const FormMessage = React.forwardRef< | ||||
|   HTMLParagraphElement, | ||||
|   React.HTMLAttributes<HTMLParagraphElement> | ||||
| >(({ className, children, ...props }, ref) => { | ||||
|   const { error, formMessageId } = useFormField() | ||||
|   const body = error ? String(error?.message) : children | ||||
| 
 | ||||
|   if (!body) { | ||||
|     return null | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <p | ||||
|       ref={ref} | ||||
|       id={formMessageId} | ||||
|       className={cn("text-sm font-medium text-destructive", className)} | ||||
|       {...props} | ||||
|     > | ||||
|       {body} | ||||
|     </p> | ||||
|   ) | ||||
| }) | ||||
| FormMessage.displayName = "FormMessage" | ||||
| 
 | ||||
| export { | ||||
|   useFormField, | ||||
|   Form, | ||||
|   FormItem, | ||||
|   FormLabel, | ||||
|   FormControl, | ||||
|   FormDescription, | ||||
|   FormMessage, | ||||
|   FormField, | ||||
| } | ||||
							
								
								
									
										27
									
								
								frontend/src/components/ui/hover-card.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/components/ui/hover-card.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| 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<typeof HoverCardPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> | ||||
| >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( | ||||
|   <HoverCardPrimitive.Content | ||||
|     ref={ref} | ||||
|     align={align} | ||||
|     sideOffset={sideOffset} | ||||
|     className={cn( | ||||
|       "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| HoverCardContent.displayName = HoverCardPrimitive.Content.displayName | ||||
| 
 | ||||
| export { HoverCard, HoverCardTrigger, HoverCardContent } | ||||
							
								
								
									
										69
									
								
								frontend/src/components/ui/input-otp.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								frontend/src/components/ui/input-otp.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,69 @@ | |||
| import * as React from "react" | ||||
| import { OTPInput, OTPInputContext } from "input-otp" | ||||
| import { Dot } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const InputOTP = React.forwardRef< | ||||
|   React.ElementRef<typeof OTPInput>, | ||||
|   React.ComponentPropsWithoutRef<typeof OTPInput> | ||||
| >(({ className, containerClassName, ...props }, ref) => ( | ||||
|   <OTPInput | ||||
|     ref={ref} | ||||
|     containerClassName={cn( | ||||
|       "flex items-center gap-2 has-[:disabled]:opacity-50", | ||||
|       containerClassName | ||||
|     )} | ||||
|     className={cn("disabled:cursor-not-allowed", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| InputOTP.displayName = "InputOTP" | ||||
| 
 | ||||
| const InputOTPGroup = React.forwardRef< | ||||
|   React.ElementRef<"div">, | ||||
|   React.ComponentPropsWithoutRef<"div"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div ref={ref} className={cn("flex items-center", className)} {...props} /> | ||||
| )) | ||||
| InputOTPGroup.displayName = "InputOTPGroup" | ||||
| 
 | ||||
| const InputOTPSlot = React.forwardRef< | ||||
|   React.ElementRef<"div">, | ||||
|   React.ComponentPropsWithoutRef<"div"> & { index: number } | ||||
| >(({ index, className, ...props }, ref) => { | ||||
|   const inputOTPContext = React.useContext(OTPInputContext) | ||||
|   const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md", | ||||
|         isActive && "z-10 ring-2 ring-ring ring-offset-background", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       {char} | ||||
|       {hasFakeCaret && ( | ||||
|         <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> | ||||
|           <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ) | ||||
| }) | ||||
| InputOTPSlot.displayName = "InputOTPSlot" | ||||
| 
 | ||||
| const InputOTPSeparator = React.forwardRef< | ||||
|   React.ElementRef<"div">, | ||||
|   React.ComponentPropsWithoutRef<"div"> | ||||
| >(({ ...props }, ref) => ( | ||||
|   <div ref={ref} role="separator" {...props}> | ||||
|     <Dot /> | ||||
|   </div> | ||||
| )) | ||||
| InputOTPSeparator.displayName = "InputOTPSeparator" | ||||
| 
 | ||||
| export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } | ||||
							
								
								
									
										22
									
								
								frontend/src/components/ui/input.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/components/ui/input.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| import * as React from "react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( | ||||
|   ({ className, type, ...props }, ref) => { | ||||
|     return ( | ||||
|       <input | ||||
|         type={type} | ||||
|         className={cn( | ||||
|           "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", | ||||
|           className | ||||
|         )} | ||||
|         ref={ref} | ||||
|         {...props} | ||||
|       /> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| Input.displayName = "Input" | ||||
| 
 | ||||
| export { Input } | ||||
							
								
								
									
										24
									
								
								frontend/src/components/ui/label.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/components/ui/label.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| import * as React from "react" | ||||
| import * as LabelPrimitive from "@radix-ui/react-label" | ||||
| import { cva, type VariantProps } from "class-variance-authority" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const labelVariants = cva( | ||||
|   "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" | ||||
| ) | ||||
| 
 | ||||
| const Label = React.forwardRef< | ||||
|   React.ElementRef<typeof LabelPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & | ||||
|     VariantProps<typeof labelVariants> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <LabelPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn(labelVariants(), className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| Label.displayName = LabelPrimitive.Root.displayName | ||||
| 
 | ||||
| export { Label } | ||||
							
								
								
									
										234
									
								
								frontend/src/components/ui/menubar.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								frontend/src/components/ui/menubar.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,234 @@ | |||
| import * as React from "react" | ||||
| import * as MenubarPrimitive from "@radix-ui/react-menubar" | ||||
| import { Check, ChevronRight, Circle } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const MenubarMenu = MenubarPrimitive.Menu | ||||
| 
 | ||||
| const MenubarGroup = MenubarPrimitive.Group | ||||
| 
 | ||||
| const MenubarPortal = MenubarPrimitive.Portal | ||||
| 
 | ||||
| const MenubarSub = MenubarPrimitive.Sub | ||||
| 
 | ||||
| const MenubarRadioGroup = MenubarPrimitive.RadioGroup | ||||
| 
 | ||||
| const Menubar = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex h-10 items-center space-x-1 rounded-md border bg-background p-1", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| Menubar.displayName = MenubarPrimitive.Root.displayName | ||||
| 
 | ||||
| const MenubarTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.Trigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.Trigger | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName | ||||
| 
 | ||||
| const MenubarSubTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.SubTrigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, children, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.SubTrigger | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     {children} | ||||
|     <ChevronRight className="ml-auto h-4 w-4" /> | ||||
|   </MenubarPrimitive.SubTrigger> | ||||
| )) | ||||
| MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName | ||||
| 
 | ||||
| const MenubarSubContent = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.SubContent>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.SubContent | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName | ||||
| 
 | ||||
| const MenubarContent = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> | ||||
| >( | ||||
|   ( | ||||
|     { className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, | ||||
|     ref | ||||
|   ) => ( | ||||
|     <MenubarPrimitive.Portal> | ||||
|       <MenubarPrimitive.Content | ||||
|         ref={ref} | ||||
|         align={align} | ||||
|         alignOffset={alignOffset} | ||||
|         sideOffset={sideOffset} | ||||
|         className={cn( | ||||
|           "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|           className | ||||
|         )} | ||||
|         {...props} | ||||
|       /> | ||||
|     </MenubarPrimitive.Portal> | ||||
|   ) | ||||
| ) | ||||
| MenubarContent.displayName = MenubarPrimitive.Content.displayName | ||||
| 
 | ||||
| const MenubarItem = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.Item | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| MenubarItem.displayName = MenubarPrimitive.Item.displayName | ||||
| 
 | ||||
| const MenubarCheckboxItem = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.CheckboxItem>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> | ||||
| >(({ className, children, checked, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.CheckboxItem | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     checked={checked} | ||||
|     {...props} | ||||
|   > | ||||
|     <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||||
|       <MenubarPrimitive.ItemIndicator> | ||||
|         <Check className="h-4 w-4" /> | ||||
|       </MenubarPrimitive.ItemIndicator> | ||||
|     </span> | ||||
|     {children} | ||||
|   </MenubarPrimitive.CheckboxItem> | ||||
| )) | ||||
| MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName | ||||
| 
 | ||||
| const MenubarRadioItem = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.RadioItem>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.RadioItem | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||||
|       <MenubarPrimitive.ItemIndicator> | ||||
|         <Circle className="h-2 w-2 fill-current" /> | ||||
|       </MenubarPrimitive.ItemIndicator> | ||||
|     </span> | ||||
|     {children} | ||||
|   </MenubarPrimitive.RadioItem> | ||||
| )) | ||||
| MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName | ||||
| 
 | ||||
| const MenubarLabel = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.Label>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { | ||||
|     inset?: boolean | ||||
|   } | ||||
| >(({ className, inset, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.Label | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "px-2 py-1.5 text-sm font-semibold", | ||||
|       inset && "pl-8", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| MenubarLabel.displayName = MenubarPrimitive.Label.displayName | ||||
| 
 | ||||
| const MenubarSeparator = React.forwardRef< | ||||
|   React.ElementRef<typeof MenubarPrimitive.Separator>, | ||||
|   React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <MenubarPrimitive.Separator | ||||
|     ref={ref} | ||||
|     className={cn("-mx-1 my-1 h-px bg-muted", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName | ||||
| 
 | ||||
| const MenubarShortcut = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLSpanElement>) => { | ||||
|   return ( | ||||
|     <span | ||||
|       className={cn( | ||||
|         "ml-auto text-xs tracking-widest text-muted-foreground", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| MenubarShortcut.displayname = "MenubarShortcut" | ||||
| 
 | ||||
| export { | ||||
|   Menubar, | ||||
|   MenubarMenu, | ||||
|   MenubarTrigger, | ||||
|   MenubarContent, | ||||
|   MenubarItem, | ||||
|   MenubarSeparator, | ||||
|   MenubarLabel, | ||||
|   MenubarCheckboxItem, | ||||
|   MenubarRadioGroup, | ||||
|   MenubarRadioItem, | ||||
|   MenubarPortal, | ||||
|   MenubarSubContent, | ||||
|   MenubarSubTrigger, | ||||
|   MenubarGroup, | ||||
|   MenubarSub, | ||||
|   MenubarShortcut, | ||||
| } | ||||
							
								
								
									
										128
									
								
								frontend/src/components/ui/navigation-menu.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								frontend/src/components/ui/navigation-menu.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,128 @@ | |||
| import * as React from "react" | ||||
| import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" | ||||
| import { cva } from "class-variance-authority" | ||||
| import { ChevronDown } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const NavigationMenu = React.forwardRef< | ||||
|   React.ElementRef<typeof NavigationMenuPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <NavigationMenuPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative z-10 flex max-w-max flex-1 items-center justify-center", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     {children} | ||||
|     <NavigationMenuViewport /> | ||||
|   </NavigationMenuPrimitive.Root> | ||||
| )) | ||||
| NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName | ||||
| 
 | ||||
| const NavigationMenuList = React.forwardRef< | ||||
|   React.ElementRef<typeof NavigationMenuPrimitive.List>, | ||||
|   React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <NavigationMenuPrimitive.List | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "group flex flex-1 list-none items-center justify-center space-x-1", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName | ||||
| 
 | ||||
| const NavigationMenuItem = NavigationMenuPrimitive.Item | ||||
| 
 | ||||
| const navigationMenuTriggerStyle = cva( | ||||
|   "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" | ||||
| ) | ||||
| 
 | ||||
| const NavigationMenuTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <NavigationMenuPrimitive.Trigger | ||||
|     ref={ref} | ||||
|     className={cn(navigationMenuTriggerStyle(), "group", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     {children}{" "} | ||||
|     <ChevronDown | ||||
|       className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180" | ||||
|       aria-hidden="true" | ||||
|     /> | ||||
|   </NavigationMenuPrimitive.Trigger> | ||||
| )) | ||||
| NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName | ||||
| 
 | ||||
| const NavigationMenuContent = React.forwardRef< | ||||
|   React.ElementRef<typeof NavigationMenuPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <NavigationMenuPrimitive.Content | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName | ||||
| 
 | ||||
| const NavigationMenuLink = NavigationMenuPrimitive.Link | ||||
| 
 | ||||
| const NavigationMenuViewport = React.forwardRef< | ||||
|   React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, | ||||
|   React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div className={cn("absolute left-0 top-full flex justify-center")}> | ||||
|     <NavigationMenuPrimitive.Viewport | ||||
|       className={cn( | ||||
|         "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]", | ||||
|         className | ||||
|       )} | ||||
|       ref={ref} | ||||
|       {...props} | ||||
|     /> | ||||
|   </div> | ||||
| )) | ||||
| NavigationMenuViewport.displayName = | ||||
|   NavigationMenuPrimitive.Viewport.displayName | ||||
| 
 | ||||
| const NavigationMenuIndicator = React.forwardRef< | ||||
|   React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, | ||||
|   React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <NavigationMenuPrimitive.Indicator | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> | ||||
|   </NavigationMenuPrimitive.Indicator> | ||||
| )) | ||||
| NavigationMenuIndicator.displayName = | ||||
|   NavigationMenuPrimitive.Indicator.displayName | ||||
| 
 | ||||
| export { | ||||
|   navigationMenuTriggerStyle, | ||||
|   NavigationMenu, | ||||
|   NavigationMenuList, | ||||
|   NavigationMenuItem, | ||||
|   NavigationMenuContent, | ||||
|   NavigationMenuTrigger, | ||||
|   NavigationMenuLink, | ||||
|   NavigationMenuIndicator, | ||||
|   NavigationMenuViewport, | ||||
| } | ||||
							
								
								
									
										117
									
								
								frontend/src/components/ui/pagination.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								frontend/src/components/ui/pagination.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | |||
| import * as React from "react" | ||||
| import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| import { ButtonProps, buttonVariants } from "@/components/ui/button" | ||||
| 
 | ||||
| const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( | ||||
|   <nav | ||||
|     role="navigation" | ||||
|     aria-label="pagination" | ||||
|     className={cn("mx-auto flex w-full justify-center", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| Pagination.displayName = "Pagination" | ||||
| 
 | ||||
| const PaginationContent = React.forwardRef< | ||||
|   HTMLUListElement, | ||||
|   React.ComponentProps<"ul"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <ul | ||||
|     ref={ref} | ||||
|     className={cn("flex flex-row items-center gap-1", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| PaginationContent.displayName = "PaginationContent" | ||||
| 
 | ||||
| const PaginationItem = React.forwardRef< | ||||
|   HTMLLIElement, | ||||
|   React.ComponentProps<"li"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <li ref={ref} className={cn("", className)} {...props} /> | ||||
| )) | ||||
| PaginationItem.displayName = "PaginationItem" | ||||
| 
 | ||||
| type PaginationLinkProps = { | ||||
|   isActive?: boolean | ||||
| } & Pick<ButtonProps, "size"> & | ||||
|   React.ComponentProps<"a"> | ||||
| 
 | ||||
| const PaginationLink = ({ | ||||
|   className, | ||||
|   isActive, | ||||
|   size = "icon", | ||||
|   ...props | ||||
| }: PaginationLinkProps) => ( | ||||
|   <a | ||||
|     aria-current={isActive ? "page" : undefined} | ||||
|     className={cn( | ||||
|       buttonVariants({ | ||||
|         variant: isActive ? "outline" : "ghost", | ||||
|         size, | ||||
|       }), | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| PaginationLink.displayName = "PaginationLink" | ||||
| 
 | ||||
| const PaginationPrevious = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<typeof PaginationLink>) => ( | ||||
|   <PaginationLink | ||||
|     aria-label="Go to previous page" | ||||
|     size="default" | ||||
|     className={cn("gap-1 pl-2.5", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     <ChevronLeft className="h-4 w-4" /> | ||||
|     <span>Previous</span> | ||||
|   </PaginationLink> | ||||
| ) | ||||
| PaginationPrevious.displayName = "PaginationPrevious" | ||||
| 
 | ||||
| const PaginationNext = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<typeof PaginationLink>) => ( | ||||
|   <PaginationLink | ||||
|     aria-label="Go to next page" | ||||
|     size="default" | ||||
|     className={cn("gap-1 pr-2.5", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     <span>Next</span> | ||||
|     <ChevronRight className="h-4 w-4" /> | ||||
|   </PaginationLink> | ||||
| ) | ||||
| PaginationNext.displayName = "PaginationNext" | ||||
| 
 | ||||
| const PaginationEllipsis = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<"span">) => ( | ||||
|   <span | ||||
|     aria-hidden | ||||
|     className={cn("flex h-9 w-9 items-center justify-center", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     <MoreHorizontal className="h-4 w-4" /> | ||||
|     <span className="sr-only">More pages</span> | ||||
|   </span> | ||||
| ) | ||||
| PaginationEllipsis.displayName = "PaginationEllipsis" | ||||
| 
 | ||||
| export { | ||||
|   Pagination, | ||||
|   PaginationContent, | ||||
|   PaginationEllipsis, | ||||
|   PaginationItem, | ||||
|   PaginationLink, | ||||
|   PaginationNext, | ||||
|   PaginationPrevious, | ||||
| } | ||||
							
								
								
									
										29
									
								
								frontend/src/components/ui/popover.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/components/ui/popover.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| import * as React from "react" | ||||
| import * as PopoverPrimitive from "@radix-ui/react-popover" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Popover = PopoverPrimitive.Root | ||||
| 
 | ||||
| const PopoverTrigger = PopoverPrimitive.Trigger | ||||
| 
 | ||||
| const PopoverContent = React.forwardRef< | ||||
|   React.ElementRef<typeof PopoverPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> | ||||
| >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( | ||||
|   <PopoverPrimitive.Portal> | ||||
|     <PopoverPrimitive.Content | ||||
|       ref={ref} | ||||
|       align={align} | ||||
|       sideOffset={sideOffset} | ||||
|       className={cn( | ||||
|         "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   </PopoverPrimitive.Portal> | ||||
| )) | ||||
| PopoverContent.displayName = PopoverPrimitive.Content.displayName | ||||
| 
 | ||||
| export { Popover, PopoverTrigger, PopoverContent } | ||||
							
								
								
									
										26
									
								
								frontend/src/components/ui/progress.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/components/ui/progress.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| import * as React from "react" | ||||
| import * as ProgressPrimitive from "@radix-ui/react-progress" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Progress = React.forwardRef< | ||||
|   React.ElementRef<typeof ProgressPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> | ||||
| >(({ className, value, ...props }, ref) => ( | ||||
|   <ProgressPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative h-4 w-full overflow-hidden rounded-full bg-secondary", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <ProgressPrimitive.Indicator | ||||
|       className="h-full w-full flex-1 bg-primary transition-all" | ||||
|       style={{ transform: `translateX(-${100 - (value || 0)}%)` }} | ||||
|     /> | ||||
|   </ProgressPrimitive.Root> | ||||
| )) | ||||
| Progress.displayName = ProgressPrimitive.Root.displayName | ||||
| 
 | ||||
| export { Progress } | ||||
							
								
								
									
										42
									
								
								frontend/src/components/ui/radio-group.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								frontend/src/components/ui/radio-group.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| import * as React from "react" | ||||
| import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" | ||||
| import { Circle } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const RadioGroup = React.forwardRef< | ||||
|   React.ElementRef<typeof RadioGroupPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <RadioGroupPrimitive.Root | ||||
|       className={cn("grid gap-2", className)} | ||||
|       {...props} | ||||
|       ref={ref} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| RadioGroup.displayName = RadioGroupPrimitive.Root.displayName | ||||
| 
 | ||||
| const RadioGroupItem = React.forwardRef< | ||||
|   React.ElementRef<typeof RadioGroupPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <RadioGroupPrimitive.Item | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> | ||||
|         <Circle className="h-2.5 w-2.5 fill-current text-current" /> | ||||
|       </RadioGroupPrimitive.Indicator> | ||||
|     </RadioGroupPrimitive.Item> | ||||
|   ) | ||||
| }) | ||||
| RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName | ||||
| 
 | ||||
| export { RadioGroup, RadioGroupItem } | ||||
							
								
								
									
										43
									
								
								frontend/src/components/ui/resizable.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/src/components/ui/resizable.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import { GripVertical } from "lucide-react" | ||||
| import * as ResizablePrimitive from "react-resizable-panels" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const ResizablePanelGroup = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => ( | ||||
|   <ResizablePrimitive.PanelGroup | ||||
|     className={cn( | ||||
|       "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| 
 | ||||
| const ResizablePanel = ResizablePrimitive.Panel | ||||
| 
 | ||||
| const ResizableHandle = ({ | ||||
|   withHandle, | ||||
|   className, | ||||
|   ...props | ||||
| }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { | ||||
|   withHandle?: boolean | ||||
| }) => ( | ||||
|   <ResizablePrimitive.PanelResizeHandle | ||||
|     className={cn( | ||||
|       "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     {withHandle && ( | ||||
|       <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border"> | ||||
|         <GripVertical className="h-2.5 w-2.5" /> | ||||
|       </div> | ||||
|     )} | ||||
|   </ResizablePrimitive.PanelResizeHandle> | ||||
| ) | ||||
| 
 | ||||
| export { ResizablePanelGroup, ResizablePanel, ResizableHandle } | ||||
							
								
								
									
										46
									
								
								frontend/src/components/ui/scroll-area.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								frontend/src/components/ui/scroll-area.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| import * as React from "react" | ||||
| import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const ScrollArea = React.forwardRef< | ||||
|   React.ElementRef<typeof ScrollAreaPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <ScrollAreaPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn("relative overflow-hidden", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> | ||||
|       {children} | ||||
|     </ScrollAreaPrimitive.Viewport> | ||||
|     <ScrollBar /> | ||||
|     <ScrollAreaPrimitive.Corner /> | ||||
|   </ScrollAreaPrimitive.Root> | ||||
| )) | ||||
| ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName | ||||
| 
 | ||||
| const ScrollBar = React.forwardRef< | ||||
|   React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, | ||||
|   React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> | ||||
| >(({ className, orientation = "vertical", ...props }, ref) => ( | ||||
|   <ScrollAreaPrimitive.ScrollAreaScrollbar | ||||
|     ref={ref} | ||||
|     orientation={orientation} | ||||
|     className={cn( | ||||
|       "flex touch-none select-none transition-colors", | ||||
|       orientation === "vertical" && | ||||
|         "h-full w-2.5 border-l border-l-transparent p-[1px]", | ||||
|       orientation === "horizontal" && | ||||
|         "h-2.5 flex-col border-t border-t-transparent p-[1px]", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> | ||||
|   </ScrollAreaPrimitive.ScrollAreaScrollbar> | ||||
| )) | ||||
| ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName | ||||
| 
 | ||||
| export { ScrollArea, ScrollBar } | ||||
							
								
								
									
										158
									
								
								frontend/src/components/ui/select.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								frontend/src/components/ui/select.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,158 @@ | |||
| import * as React from "react" | ||||
| import * as SelectPrimitive from "@radix-ui/react-select" | ||||
| import { Check, ChevronDown, ChevronUp } from "lucide-react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Select = SelectPrimitive.Root | ||||
| 
 | ||||
| const SelectGroup = SelectPrimitive.Group | ||||
| 
 | ||||
| const SelectValue = SelectPrimitive.Value | ||||
| 
 | ||||
| const SelectTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof SelectPrimitive.Trigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <SelectPrimitive.Trigger | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     {children} | ||||
|     <SelectPrimitive.Icon asChild> | ||||
|       <ChevronDown className="h-4 w-4 opacity-50" /> | ||||
|     </SelectPrimitive.Icon> | ||||
|   </SelectPrimitive.Trigger> | ||||
| )) | ||||
| SelectTrigger.displayName = SelectPrimitive.Trigger.displayName | ||||
| 
 | ||||
| const SelectScrollUpButton = React.forwardRef< | ||||
|   React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, | ||||
|   React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SelectPrimitive.ScrollUpButton | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex cursor-default items-center justify-center py-1", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <ChevronUp className="h-4 w-4" /> | ||||
|   </SelectPrimitive.ScrollUpButton> | ||||
| )) | ||||
| SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName | ||||
| 
 | ||||
| const SelectScrollDownButton = React.forwardRef< | ||||
|   React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, | ||||
|   React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SelectPrimitive.ScrollDownButton | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "flex cursor-default items-center justify-center py-1", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <ChevronDown className="h-4 w-4" /> | ||||
|   </SelectPrimitive.ScrollDownButton> | ||||
| )) | ||||
| SelectScrollDownButton.displayName = | ||||
|   SelectPrimitive.ScrollDownButton.displayName | ||||
| 
 | ||||
| const SelectContent = React.forwardRef< | ||||
|   React.ElementRef<typeof SelectPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> | ||||
| >(({ className, children, position = "popper", ...props }, ref) => ( | ||||
|   <SelectPrimitive.Portal> | ||||
|     <SelectPrimitive.Content | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|         position === "popper" && | ||||
|           "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", | ||||
|         className | ||||
|       )} | ||||
|       position={position} | ||||
|       {...props} | ||||
|     > | ||||
|       <SelectScrollUpButton /> | ||||
|       <SelectPrimitive.Viewport | ||||
|         className={cn( | ||||
|           "p-1", | ||||
|           position === "popper" && | ||||
|             "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" | ||||
|         )} | ||||
|       > | ||||
|         {children} | ||||
|       </SelectPrimitive.Viewport> | ||||
|       <SelectScrollDownButton /> | ||||
|     </SelectPrimitive.Content> | ||||
|   </SelectPrimitive.Portal> | ||||
| )) | ||||
| SelectContent.displayName = SelectPrimitive.Content.displayName | ||||
| 
 | ||||
| const SelectLabel = React.forwardRef< | ||||
|   React.ElementRef<typeof SelectPrimitive.Label>, | ||||
|   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SelectPrimitive.Label | ||||
|     ref={ref} | ||||
|     className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SelectLabel.displayName = SelectPrimitive.Label.displayName | ||||
| 
 | ||||
| const SelectItem = React.forwardRef< | ||||
|   React.ElementRef<typeof SelectPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> | ||||
| >(({ className, children, ...props }, ref) => ( | ||||
|   <SelectPrimitive.Item | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||||
|       <SelectPrimitive.ItemIndicator> | ||||
|         <Check className="h-4 w-4" /> | ||||
|       </SelectPrimitive.ItemIndicator> | ||||
|     </span> | ||||
| 
 | ||||
|     <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> | ||||
|   </SelectPrimitive.Item> | ||||
| )) | ||||
| SelectItem.displayName = SelectPrimitive.Item.displayName | ||||
| 
 | ||||
| const SelectSeparator = React.forwardRef< | ||||
|   React.ElementRef<typeof SelectPrimitive.Separator>, | ||||
|   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SelectPrimitive.Separator | ||||
|     ref={ref} | ||||
|     className={cn("-mx-1 my-1 h-px bg-muted", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SelectSeparator.displayName = SelectPrimitive.Separator.displayName | ||||
| 
 | ||||
| export { | ||||
|   Select, | ||||
|   SelectGroup, | ||||
|   SelectValue, | ||||
|   SelectTrigger, | ||||
|   SelectContent, | ||||
|   SelectLabel, | ||||
|   SelectItem, | ||||
|   SelectSeparator, | ||||
|   SelectScrollUpButton, | ||||
|   SelectScrollDownButton, | ||||
| } | ||||
							
								
								
									
										29
									
								
								frontend/src/components/ui/separator.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/components/ui/separator.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| import * as React from "react" | ||||
| import * as SeparatorPrimitive from "@radix-ui/react-separator" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Separator = React.forwardRef< | ||||
|   React.ElementRef<typeof SeparatorPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> | ||||
| >( | ||||
|   ( | ||||
|     { className, orientation = "horizontal", decorative = true, ...props }, | ||||
|     ref | ||||
|   ) => ( | ||||
|     <SeparatorPrimitive.Root | ||||
|       ref={ref} | ||||
|       decorative={decorative} | ||||
|       orientation={orientation} | ||||
|       className={cn( | ||||
|         "shrink-0 bg-border", | ||||
|         orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| ) | ||||
| Separator.displayName = SeparatorPrimitive.Root.displayName | ||||
| 
 | ||||
| export { Separator } | ||||
							
								
								
									
										131
									
								
								frontend/src/components/ui/sheet.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								frontend/src/components/ui/sheet.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,131 @@ | |||
| import * as SheetPrimitive from "@radix-ui/react-dialog" | ||||
| import { cva, type VariantProps } from "class-variance-authority" | ||||
| import { X } from "lucide-react" | ||||
| import * as React from "react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Sheet = SheetPrimitive.Root | ||||
| 
 | ||||
| const SheetTrigger = SheetPrimitive.Trigger | ||||
| 
 | ||||
| const SheetClose = SheetPrimitive.Close | ||||
| 
 | ||||
| const SheetPortal = SheetPrimitive.Portal | ||||
| 
 | ||||
| const SheetOverlay = React.forwardRef< | ||||
|   React.ElementRef<typeof SheetPrimitive.Overlay>, | ||||
|   React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SheetPrimitive.Overlay | ||||
|     className={cn( | ||||
|       "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|     ref={ref} | ||||
|   /> | ||||
| )) | ||||
| SheetOverlay.displayName = SheetPrimitive.Overlay.displayName | ||||
| 
 | ||||
| const sheetVariants = cva( | ||||
|   "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", | ||||
|   { | ||||
|     variants: { | ||||
|       side: { | ||||
|         top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", | ||||
|         bottom: | ||||
|           "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", | ||||
|         left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", | ||||
|         right: | ||||
|           "inset-y-0 right-0 h-full w-3/4  border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", | ||||
|       }, | ||||
|     }, | ||||
|     defaultVariants: { | ||||
|       side: "right", | ||||
|     }, | ||||
|   } | ||||
| ) | ||||
| 
 | ||||
| interface SheetContentProps | ||||
|   extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, | ||||
|   VariantProps<typeof sheetVariants> { } | ||||
| 
 | ||||
| const SheetContent = React.forwardRef< | ||||
|   React.ElementRef<typeof SheetPrimitive.Content>, | ||||
|   SheetContentProps | ||||
| >(({ side = "right", className, children, ...props }, ref) => ( | ||||
|   <SheetPortal> | ||||
|     <SheetOverlay /> | ||||
|     <SheetPrimitive.Content | ||||
|       ref={ref} | ||||
|       className={cn(sheetVariants({ side }), className)} | ||||
|       {...props} | ||||
|     > | ||||
|       {children} | ||||
|       <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> | ||||
|         <X className="h-4 w-4" /> | ||||
|         <span className="sr-only">Close</span> | ||||
|       </SheetPrimitive.Close> | ||||
|     </SheetPrimitive.Content> | ||||
|   </SheetPortal> | ||||
| )) | ||||
| SheetContent.displayName = SheetPrimitive.Content.displayName | ||||
| 
 | ||||
| const SheetHeader = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col space-y-2 text-center sm:text-left", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| SheetHeader.displayName = "SheetHeader" | ||||
| 
 | ||||
| const SheetFooter = ({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) => ( | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| SheetFooter.displayName = "SheetFooter" | ||||
| 
 | ||||
| const SheetTitle = React.forwardRef< | ||||
|   React.ElementRef<typeof SheetPrimitive.Title>, | ||||
|   React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SheetPrimitive.Title | ||||
|     ref={ref} | ||||
|     className={cn("text-lg font-semibold text-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SheetTitle.displayName = SheetPrimitive.Title.displayName | ||||
| 
 | ||||
| const SheetDescription = React.forwardRef< | ||||
|   React.ElementRef<typeof SheetPrimitive.Description>, | ||||
|   React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SheetPrimitive.Description | ||||
|     ref={ref} | ||||
|     className={cn("text-sm text-muted-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SheetDescription.displayName = SheetPrimitive.Description.displayName | ||||
| 
 | ||||
| export { | ||||
|   Sheet, SheetClose, | ||||
|   SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										761
									
								
								frontend/src/components/ui/sidebar.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										761
									
								
								frontend/src/components/ui/sidebar.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,761 @@ | |||
| import * as React from "react" | ||||
| import { Slot } from "@radix-ui/react-slot" | ||||
| import { VariantProps, cva } from "class-variance-authority" | ||||
| import { PanelLeft } from "lucide-react" | ||||
| 
 | ||||
| import { useIsMobile } from "@/hooks/use-mobile" | ||||
| import { cn } from "@/lib/utils" | ||||
| import { Button } from "@/components/ui/button" | ||||
| import { Input } from "@/components/ui/input" | ||||
| import { Separator } from "@/components/ui/separator" | ||||
| import { Sheet, SheetContent } from "@/components/ui/sheet" | ||||
| import { Skeleton } from "@/components/ui/skeleton" | ||||
| import { | ||||
|   Tooltip, | ||||
|   TooltipContent, | ||||
|   TooltipProvider, | ||||
|   TooltipTrigger, | ||||
| } from "@/components/ui/tooltip" | ||||
| 
 | ||||
| const SIDEBAR_COOKIE_NAME = "sidebar:state" | ||||
| const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 | ||||
| const SIDEBAR_WIDTH = "16rem" | ||||
| const SIDEBAR_WIDTH_MOBILE = "18rem" | ||||
| const SIDEBAR_WIDTH_ICON = "3rem" | ||||
| const SIDEBAR_KEYBOARD_SHORTCUT = "b" | ||||
| 
 | ||||
| type SidebarContext = { | ||||
|   state: "expanded" | "collapsed" | ||||
|   open: boolean | ||||
|   setOpen: (open: boolean) => void | ||||
|   openMobile: boolean | ||||
|   setOpenMobile: (open: boolean) => void | ||||
|   isMobile: boolean | ||||
|   toggleSidebar: () => void | ||||
| } | ||||
| 
 | ||||
| const SidebarContext = React.createContext<SidebarContext | null>(null) | ||||
| 
 | ||||
| function useSidebar() { | ||||
|   const context = React.useContext(SidebarContext) | ||||
|   if (!context) { | ||||
|     throw new Error("useSidebar must be used within a SidebarProvider.") | ||||
|   } | ||||
| 
 | ||||
|   return context | ||||
| } | ||||
| 
 | ||||
| const SidebarProvider = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> & { | ||||
|     defaultOpen?: boolean | ||||
|     open?: boolean | ||||
|     onOpenChange?: (open: boolean) => void | ||||
|   } | ||||
| >( | ||||
|   ( | ||||
|     { | ||||
|       defaultOpen = true, | ||||
|       open: openProp, | ||||
|       onOpenChange: setOpenProp, | ||||
|       className, | ||||
|       style, | ||||
|       children, | ||||
|       ...props | ||||
|     }, | ||||
|     ref | ||||
|   ) => { | ||||
|     const isMobile = useIsMobile() | ||||
|     const [openMobile, setOpenMobile] = React.useState(false) | ||||
| 
 | ||||
|     // This is the internal state of the sidebar.
 | ||||
|     // We use openProp and setOpenProp for control from outside the component.
 | ||||
|     const [_open, _setOpen] = React.useState(defaultOpen) | ||||
|     const open = openProp ?? _open | ||||
|     const setOpen = React.useCallback( | ||||
|       (value: boolean | ((value: boolean) => boolean)) => { | ||||
|         const openState = typeof value === "function" ? value(open) : value | ||||
|         if (setOpenProp) { | ||||
|           setOpenProp(openState) | ||||
|         } else { | ||||
|           _setOpen(openState) | ||||
|         } | ||||
| 
 | ||||
|         // This sets the cookie to keep the sidebar state.
 | ||||
|         document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` | ||||
|       }, | ||||
|       [setOpenProp, open] | ||||
|     ) | ||||
| 
 | ||||
|     // Helper to toggle the sidebar.
 | ||||
|     const toggleSidebar = React.useCallback(() => { | ||||
|       return isMobile | ||||
|         ? setOpenMobile((open) => !open) | ||||
|         : setOpen((open) => !open) | ||||
|     }, [isMobile, setOpen, setOpenMobile]) | ||||
| 
 | ||||
|     // Adds a keyboard shortcut to toggle the sidebar.
 | ||||
|     React.useEffect(() => { | ||||
|       const handleKeyDown = (event: KeyboardEvent) => { | ||||
|         if ( | ||||
|           event.key === SIDEBAR_KEYBOARD_SHORTCUT && | ||||
|           (event.metaKey || event.ctrlKey) | ||||
|         ) { | ||||
|           event.preventDefault() | ||||
|           toggleSidebar() | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       window.addEventListener("keydown", handleKeyDown) | ||||
|       return () => window.removeEventListener("keydown", handleKeyDown) | ||||
|     }, [toggleSidebar]) | ||||
| 
 | ||||
|     // We add a state so that we can do data-state="expanded" or "collapsed".
 | ||||
|     // This makes it easier to style the sidebar with Tailwind classes.
 | ||||
|     const state = open ? "expanded" : "collapsed" | ||||
| 
 | ||||
|     const contextValue = React.useMemo<SidebarContext>( | ||||
|       () => ({ | ||||
|         state, | ||||
|         open, | ||||
|         setOpen, | ||||
|         isMobile, | ||||
|         openMobile, | ||||
|         setOpenMobile, | ||||
|         toggleSidebar, | ||||
|       }), | ||||
|       [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] | ||||
|     ) | ||||
| 
 | ||||
|     return ( | ||||
|       <SidebarContext.Provider value={contextValue}> | ||||
|         <TooltipProvider delayDuration={0}> | ||||
|           <div | ||||
|             style={ | ||||
|               { | ||||
|                 "--sidebar-width": SIDEBAR_WIDTH, | ||||
|                 "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, | ||||
|                 ...style, | ||||
|               } as React.CSSProperties | ||||
|             } | ||||
|             className={cn( | ||||
|               "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", | ||||
|               className | ||||
|             )} | ||||
|             ref={ref} | ||||
|             {...props} | ||||
|           > | ||||
|             {children} | ||||
|           </div> | ||||
|         </TooltipProvider> | ||||
|       </SidebarContext.Provider> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| SidebarProvider.displayName = "SidebarProvider" | ||||
| 
 | ||||
| const Sidebar = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> & { | ||||
|     side?: "left" | "right" | ||||
|     variant?: "sidebar" | "floating" | "inset" | ||||
|     collapsible?: "offcanvas" | "icon" | "none" | ||||
|   } | ||||
| >( | ||||
|   ( | ||||
|     { | ||||
|       side = "left", | ||||
|       variant = "sidebar", | ||||
|       collapsible = "offcanvas", | ||||
|       className, | ||||
|       children, | ||||
|       ...props | ||||
|     }, | ||||
|     ref | ||||
|   ) => { | ||||
|     const { isMobile, state, openMobile, setOpenMobile } = useSidebar() | ||||
| 
 | ||||
|     if (collapsible === "none") { | ||||
|       return ( | ||||
|         <div | ||||
|           className={cn( | ||||
|             "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", | ||||
|             className | ||||
|           )} | ||||
|           ref={ref} | ||||
|           {...props} | ||||
|         > | ||||
|           {children} | ||||
|         </div> | ||||
|       ) | ||||
|     } | ||||
| 
 | ||||
|     if (isMobile) { | ||||
|       return ( | ||||
|         <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> | ||||
|           <SheetContent | ||||
|             data-sidebar="sidebar" | ||||
|             data-mobile="true" | ||||
|             className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" | ||||
|             style={ | ||||
|               { | ||||
|                 "--sidebar-width": SIDEBAR_WIDTH_MOBILE, | ||||
|               } as React.CSSProperties | ||||
|             } | ||||
|             side={side} | ||||
|           > | ||||
|             <div className="flex h-full w-full flex-col">{children}</div> | ||||
|           </SheetContent> | ||||
|         </Sheet> | ||||
|       ) | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div | ||||
|         ref={ref} | ||||
|         className="group peer hidden md:block text-sidebar-foreground" | ||||
|         data-state={state} | ||||
|         data-collapsible={state === "collapsed" ? collapsible : ""} | ||||
|         data-variant={variant} | ||||
|         data-side={side} | ||||
|       > | ||||
|         {/* This is what handles the sidebar gap on desktop */} | ||||
|         <div | ||||
|           className={cn( | ||||
|             "duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear", | ||||
|             "group-data-[collapsible=offcanvas]:w-0", | ||||
|             "group-data-[side=right]:rotate-180", | ||||
|             variant === "floating" || variant === "inset" | ||||
|               ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]" | ||||
|               : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]" | ||||
|           )} | ||||
|         /> | ||||
|         <div | ||||
|           className={cn( | ||||
|             "duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex", | ||||
|             side === "left" | ||||
|               ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" | ||||
|               : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", | ||||
|             // Adjust the padding for floating and inset variants.
 | ||||
|             variant === "floating" || variant === "inset" | ||||
|               ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]" | ||||
|               : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l", | ||||
|             className | ||||
|           )} | ||||
|           {...props} | ||||
|         > | ||||
|           <div | ||||
|             data-sidebar="sidebar" | ||||
|             className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow" | ||||
|           > | ||||
|             {children} | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| Sidebar.displayName = "Sidebar" | ||||
| 
 | ||||
| const SidebarTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof Button>, | ||||
|   React.ComponentProps<typeof Button> | ||||
| >(({ className, onClick, ...props }, ref) => { | ||||
|   const { toggleSidebar } = useSidebar() | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       ref={ref} | ||||
|       data-sidebar="trigger" | ||||
|       variant="ghost" | ||||
|       size="icon" | ||||
|       className={cn("h-7 w-7", className)} | ||||
|       onClick={(event) => { | ||||
|         onClick?.(event) | ||||
|         toggleSidebar() | ||||
|       }} | ||||
|       {...props} | ||||
|     > | ||||
|       <PanelLeft /> | ||||
|       <span className="sr-only">Toggle Sidebar</span> | ||||
|     </Button> | ||||
|   ) | ||||
| }) | ||||
| SidebarTrigger.displayName = "SidebarTrigger" | ||||
| 
 | ||||
| const SidebarRail = React.forwardRef< | ||||
|   HTMLButtonElement, | ||||
|   React.ComponentProps<"button"> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   const { toggleSidebar } = useSidebar() | ||||
| 
 | ||||
|   return ( | ||||
|     <button | ||||
|       ref={ref} | ||||
|       data-sidebar="rail" | ||||
|       aria-label="Toggle Sidebar" | ||||
|       tabIndex={-1} | ||||
|       onClick={toggleSidebar} | ||||
|       title="Toggle Sidebar" | ||||
|       className={cn( | ||||
|         "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", | ||||
|         "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize", | ||||
|         "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", | ||||
|         "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar", | ||||
|         "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", | ||||
|         "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarRail.displayName = "SidebarRail" | ||||
| 
 | ||||
| const SidebarInset = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"main"> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <main | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         "relative flex min-h-svh flex-1 flex-col bg-background", | ||||
|         "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarInset.displayName = "SidebarInset" | ||||
| 
 | ||||
| const SidebarInput = React.forwardRef< | ||||
|   React.ElementRef<typeof Input>, | ||||
|   React.ComponentProps<typeof Input> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <Input | ||||
|       ref={ref} | ||||
|       data-sidebar="input" | ||||
|       className={cn( | ||||
|         "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarInput.displayName = "SidebarInput" | ||||
| 
 | ||||
| const SidebarHeader = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <div | ||||
|       ref={ref} | ||||
|       data-sidebar="header" | ||||
|       className={cn("flex flex-col gap-2 p-2", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarHeader.displayName = "SidebarHeader" | ||||
| 
 | ||||
| const SidebarFooter = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <div | ||||
|       ref={ref} | ||||
|       data-sidebar="footer" | ||||
|       className={cn("flex flex-col gap-2 p-2", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarFooter.displayName = "SidebarFooter" | ||||
| 
 | ||||
| const SidebarSeparator = React.forwardRef< | ||||
|   React.ElementRef<typeof Separator>, | ||||
|   React.ComponentProps<typeof Separator> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <Separator | ||||
|       ref={ref} | ||||
|       data-sidebar="separator" | ||||
|       className={cn("mx-2 w-auto bg-sidebar-border", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarSeparator.displayName = "SidebarSeparator" | ||||
| 
 | ||||
| const SidebarContent = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <div | ||||
|       ref={ref} | ||||
|       data-sidebar="content" | ||||
|       className={cn( | ||||
|         "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarContent.displayName = "SidebarContent" | ||||
| 
 | ||||
| const SidebarGroup = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> | ||||
| >(({ className, ...props }, ref) => { | ||||
|   return ( | ||||
|     <div | ||||
|       ref={ref} | ||||
|       data-sidebar="group" | ||||
|       className={cn("relative flex w-full min-w-0 flex-col p-2", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarGroup.displayName = "SidebarGroup" | ||||
| 
 | ||||
| const SidebarGroupLabel = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> & { asChild?: boolean } | ||||
| >(({ className, asChild = false, ...props }, ref) => { | ||||
|   const Comp = asChild ? Slot : "div" | ||||
| 
 | ||||
|   return ( | ||||
|     <Comp | ||||
|       ref={ref} | ||||
|       data-sidebar="group-label" | ||||
|       className={cn( | ||||
|         "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", | ||||
|         "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarGroupLabel.displayName = "SidebarGroupLabel" | ||||
| 
 | ||||
| const SidebarGroupAction = React.forwardRef< | ||||
|   HTMLButtonElement, | ||||
|   React.ComponentProps<"button"> & { asChild?: boolean } | ||||
| >(({ className, asChild = false, ...props }, ref) => { | ||||
|   const Comp = asChild ? Slot : "button" | ||||
| 
 | ||||
|   return ( | ||||
|     <Comp | ||||
|       ref={ref} | ||||
|       data-sidebar="group-action" | ||||
|       className={cn( | ||||
|         "absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", | ||||
|         // Increases the hit area of the button on mobile.
 | ||||
|         "after:absolute after:-inset-2 after:md:hidden", | ||||
|         "group-data-[collapsible=icon]:hidden", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarGroupAction.displayName = "SidebarGroupAction" | ||||
| 
 | ||||
| const SidebarGroupContent = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div | ||||
|     ref={ref} | ||||
|     data-sidebar="group-content" | ||||
|     className={cn("w-full text-sm", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SidebarGroupContent.displayName = "SidebarGroupContent" | ||||
| 
 | ||||
| const SidebarMenu = React.forwardRef< | ||||
|   HTMLUListElement, | ||||
|   React.ComponentProps<"ul"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <ul | ||||
|     ref={ref} | ||||
|     data-sidebar="menu" | ||||
|     className={cn("flex w-full min-w-0 flex-col gap-1", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SidebarMenu.displayName = "SidebarMenu" | ||||
| 
 | ||||
| const SidebarMenuItem = React.forwardRef< | ||||
|   HTMLLIElement, | ||||
|   React.ComponentProps<"li"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <li | ||||
|     ref={ref} | ||||
|     data-sidebar="menu-item" | ||||
|     className={cn("group/menu-item relative", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SidebarMenuItem.displayName = "SidebarMenuItem" | ||||
| 
 | ||||
| const sidebarMenuButtonVariants = cva( | ||||
|   "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", | ||||
|   { | ||||
|     variants: { | ||||
|       variant: { | ||||
|         default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", | ||||
|         outline: | ||||
|           "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", | ||||
|       }, | ||||
|       size: { | ||||
|         default: "h-8 text-sm", | ||||
|         sm: "h-7 text-xs", | ||||
|         lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", | ||||
|       }, | ||||
|     }, | ||||
|     defaultVariants: { | ||||
|       variant: "default", | ||||
|       size: "default", | ||||
|     }, | ||||
|   } | ||||
| ) | ||||
| 
 | ||||
| const SidebarMenuButton = React.forwardRef< | ||||
|   HTMLButtonElement, | ||||
|   React.ComponentProps<"button"> & { | ||||
|     asChild?: boolean | ||||
|     isActive?: boolean | ||||
|     tooltip?: string | React.ComponentProps<typeof TooltipContent> | ||||
|   } & VariantProps<typeof sidebarMenuButtonVariants> | ||||
| >( | ||||
|   ( | ||||
|     { | ||||
|       asChild = false, | ||||
|       isActive = false, | ||||
|       variant = "default", | ||||
|       size = "default", | ||||
|       tooltip, | ||||
|       className, | ||||
|       ...props | ||||
|     }, | ||||
|     ref | ||||
|   ) => { | ||||
|     const Comp = asChild ? Slot : "button" | ||||
|     const { isMobile, state } = useSidebar() | ||||
| 
 | ||||
|     const button = ( | ||||
|       <Comp | ||||
|         ref={ref} | ||||
|         data-sidebar="menu-button" | ||||
|         data-size={size} | ||||
|         data-active={isActive} | ||||
|         className={cn(sidebarMenuButtonVariants({ variant, size }), className)} | ||||
|         {...props} | ||||
|       /> | ||||
|     ) | ||||
| 
 | ||||
|     if (!tooltip) { | ||||
|       return button | ||||
|     } | ||||
| 
 | ||||
|     if (typeof tooltip === "string") { | ||||
|       tooltip = { | ||||
|         children: tooltip, | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Tooltip> | ||||
|         <TooltipTrigger asChild>{button}</TooltipTrigger> | ||||
|         <TooltipContent | ||||
|           side="right" | ||||
|           align="center" | ||||
|           hidden={state !== "collapsed" || isMobile} | ||||
|           {...tooltip} | ||||
|         /> | ||||
|       </Tooltip> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| SidebarMenuButton.displayName = "SidebarMenuButton" | ||||
| 
 | ||||
| const SidebarMenuAction = React.forwardRef< | ||||
|   HTMLButtonElement, | ||||
|   React.ComponentProps<"button"> & { | ||||
|     asChild?: boolean | ||||
|     showOnHover?: boolean | ||||
|   } | ||||
| >(({ className, asChild = false, showOnHover = false, ...props }, ref) => { | ||||
|   const Comp = asChild ? Slot : "button" | ||||
| 
 | ||||
|   return ( | ||||
|     <Comp | ||||
|       ref={ref} | ||||
|       data-sidebar="menu-action" | ||||
|       className={cn( | ||||
|         "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0", | ||||
|         // Increases the hit area of the button on mobile.
 | ||||
|         "after:absolute after:-inset-2 after:md:hidden", | ||||
|         "peer-data-[size=sm]/menu-button:top-1", | ||||
|         "peer-data-[size=default]/menu-button:top-1.5", | ||||
|         "peer-data-[size=lg]/menu-button:top-2.5", | ||||
|         "group-data-[collapsible=icon]:hidden", | ||||
|         showOnHover && | ||||
|           "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarMenuAction.displayName = "SidebarMenuAction" | ||||
| 
 | ||||
| const SidebarMenuBadge = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div | ||||
|     ref={ref} | ||||
|     data-sidebar="menu-badge" | ||||
|     className={cn( | ||||
|       "absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none", | ||||
|       "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", | ||||
|       "peer-data-[size=sm]/menu-button:top-1", | ||||
|       "peer-data-[size=default]/menu-button:top-1.5", | ||||
|       "peer-data-[size=lg]/menu-button:top-2.5", | ||||
|       "group-data-[collapsible=icon]:hidden", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SidebarMenuBadge.displayName = "SidebarMenuBadge" | ||||
| 
 | ||||
| const SidebarMenuSkeleton = React.forwardRef< | ||||
|   HTMLDivElement, | ||||
|   React.ComponentProps<"div"> & { | ||||
|     showIcon?: boolean | ||||
|   } | ||||
| >(({ className, showIcon = false, ...props }, ref) => { | ||||
|   // Random width between 50 to 90%.
 | ||||
|   const width = React.useMemo(() => { | ||||
|     return `${Math.floor(Math.random() * 40) + 50}%` | ||||
|   }, []) | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       ref={ref} | ||||
|       data-sidebar="menu-skeleton" | ||||
|       className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)} | ||||
|       {...props} | ||||
|     > | ||||
|       {showIcon && ( | ||||
|         <Skeleton | ||||
|           className="size-4 rounded-md" | ||||
|           data-sidebar="menu-skeleton-icon" | ||||
|         /> | ||||
|       )} | ||||
|       <Skeleton | ||||
|         className="h-4 flex-1 max-w-[--skeleton-width]" | ||||
|         data-sidebar="menu-skeleton-text" | ||||
|         style={ | ||||
|           { | ||||
|             "--skeleton-width": width, | ||||
|           } as React.CSSProperties | ||||
|         } | ||||
|       /> | ||||
|     </div> | ||||
|   ) | ||||
| }) | ||||
| SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton" | ||||
| 
 | ||||
| const SidebarMenuSub = React.forwardRef< | ||||
|   HTMLUListElement, | ||||
|   React.ComponentProps<"ul"> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <ul | ||||
|     ref={ref} | ||||
|     data-sidebar="menu-sub" | ||||
|     className={cn( | ||||
|       "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5", | ||||
|       "group-data-[collapsible=icon]:hidden", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| SidebarMenuSub.displayName = "SidebarMenuSub" | ||||
| 
 | ||||
| const SidebarMenuSubItem = React.forwardRef< | ||||
|   HTMLLIElement, | ||||
|   React.ComponentProps<"li"> | ||||
| >(({ ...props }, ref) => <li ref={ref} {...props} />) | ||||
| SidebarMenuSubItem.displayName = "SidebarMenuSubItem" | ||||
| 
 | ||||
| const SidebarMenuSubButton = React.forwardRef< | ||||
|   HTMLAnchorElement, | ||||
|   React.ComponentProps<"a"> & { | ||||
|     asChild?: boolean | ||||
|     size?: "sm" | "md" | ||||
|     isActive?: boolean | ||||
|   } | ||||
| >(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { | ||||
|   const Comp = asChild ? Slot : "a" | ||||
| 
 | ||||
|   return ( | ||||
|     <Comp | ||||
|       ref={ref} | ||||
|       data-sidebar="menu-sub-button" | ||||
|       data-size={size} | ||||
|       data-active={isActive} | ||||
|       className={cn( | ||||
|         "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", | ||||
|         "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", | ||||
|         size === "sm" && "text-xs", | ||||
|         size === "md" && "text-sm", | ||||
|         "group-data-[collapsible=icon]:hidden", | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| }) | ||||
| SidebarMenuSubButton.displayName = "SidebarMenuSubButton" | ||||
| 
 | ||||
| export { | ||||
|   Sidebar, | ||||
|   SidebarContent, | ||||
|   SidebarFooter, | ||||
|   SidebarGroup, | ||||
|   SidebarGroupAction, | ||||
|   SidebarGroupContent, | ||||
|   SidebarGroupLabel, | ||||
|   SidebarHeader, | ||||
|   SidebarInput, | ||||
|   SidebarInset, | ||||
|   SidebarMenu, | ||||
|   SidebarMenuAction, | ||||
|   SidebarMenuBadge, | ||||
|   SidebarMenuButton, | ||||
|   SidebarMenuItem, | ||||
|   SidebarMenuSkeleton, | ||||
|   SidebarMenuSub, | ||||
|   SidebarMenuSubButton, | ||||
|   SidebarMenuSubItem, | ||||
|   SidebarProvider, | ||||
|   SidebarRail, | ||||
|   SidebarSeparator, | ||||
|   SidebarTrigger, | ||||
|   useSidebar, | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/components/ui/skeleton.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/components/ui/skeleton.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| function Skeleton({ | ||||
|   className, | ||||
|   ...props | ||||
| }: React.HTMLAttributes<HTMLDivElement>) { | ||||
|   return ( | ||||
|     <div | ||||
|       className={cn("animate-pulse rounded-md bg-muted", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export { Skeleton } | ||||
							
								
								
									
										26
									
								
								frontend/src/components/ui/slider.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/components/ui/slider.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| import * as React from "react" | ||||
| import * as SliderPrimitive from "@radix-ui/react-slider" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Slider = React.forwardRef< | ||||
|   React.ElementRef<typeof SliderPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SliderPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "relative flex w-full touch-none select-none items-center", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   > | ||||
|     <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary"> | ||||
|       <SliderPrimitive.Range className="absolute h-full bg-primary" /> | ||||
|     </SliderPrimitive.Track> | ||||
|     <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background 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" /> | ||||
|   </SliderPrimitive.Root> | ||||
| )) | ||||
| Slider.displayName = SliderPrimitive.Root.displayName | ||||
| 
 | ||||
| export { Slider } | ||||
							
								
								
									
										29
									
								
								frontend/src/components/ui/sonner.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/components/ui/sonner.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| import { useTheme } from "next-themes" | ||||
| import { Toaster as Sonner, toast } from "sonner" | ||||
| 
 | ||||
| type ToasterProps = React.ComponentProps<typeof Sonner> | ||||
| 
 | ||||
| const Toaster = ({ ...props }: ToasterProps) => { | ||||
|   const { theme = "system" } = useTheme() | ||||
| 
 | ||||
|   return ( | ||||
|     <Sonner | ||||
|       theme={theme as ToasterProps["theme"]} | ||||
|       className="toaster group" | ||||
|       toastOptions={{ | ||||
|         classNames: { | ||||
|           toast: | ||||
|             "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", | ||||
|           description: "group-[.toast]:text-muted-foreground", | ||||
|           actionButton: | ||||
|             "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", | ||||
|           cancelButton: | ||||
|             "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", | ||||
|         }, | ||||
|       }} | ||||
|       {...props} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export { Toaster, toast } | ||||
							
								
								
									
										27
									
								
								frontend/src/components/ui/switch.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/components/ui/switch.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import * as React from "react" | ||||
| import * as SwitchPrimitives from "@radix-ui/react-switch" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Switch = React.forwardRef< | ||||
|   React.ElementRef<typeof SwitchPrimitives.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <SwitchPrimitives.Root | ||||
|     className={cn( | ||||
|       "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|     ref={ref} | ||||
|   > | ||||
|     <SwitchPrimitives.Thumb | ||||
|       className={cn( | ||||
|         "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" | ||||
|       )} | ||||
|     /> | ||||
|   </SwitchPrimitives.Root> | ||||
| )) | ||||
| Switch.displayName = SwitchPrimitives.Root.displayName | ||||
| 
 | ||||
| export { Switch } | ||||
							
								
								
									
										117
									
								
								frontend/src/components/ui/table.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								frontend/src/components/ui/table.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | |||
| import * as React from "react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Table = React.forwardRef< | ||||
|   HTMLTableElement, | ||||
|   React.HTMLAttributes<HTMLTableElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <div className="relative w-full overflow-auto"> | ||||
|     <table | ||||
|       ref={ref} | ||||
|       className={cn("w-full caption-bottom text-sm", className)} | ||||
|       {...props} | ||||
|     /> | ||||
|   </div> | ||||
| )) | ||||
| Table.displayName = "Table" | ||||
| 
 | ||||
| const TableHeader = React.forwardRef< | ||||
|   HTMLTableSectionElement, | ||||
|   React.HTMLAttributes<HTMLTableSectionElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> | ||||
| )) | ||||
| TableHeader.displayName = "TableHeader" | ||||
| 
 | ||||
| const TableBody = React.forwardRef< | ||||
|   HTMLTableSectionElement, | ||||
|   React.HTMLAttributes<HTMLTableSectionElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <tbody | ||||
|     ref={ref} | ||||
|     className={cn("[&_tr:last-child]:border-0", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TableBody.displayName = "TableBody" | ||||
| 
 | ||||
| const TableFooter = React.forwardRef< | ||||
|   HTMLTableSectionElement, | ||||
|   React.HTMLAttributes<HTMLTableSectionElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <tfoot | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TableFooter.displayName = "TableFooter" | ||||
| 
 | ||||
| const TableRow = React.forwardRef< | ||||
|   HTMLTableRowElement, | ||||
|   React.HTMLAttributes<HTMLTableRowElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <tr | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TableRow.displayName = "TableRow" | ||||
| 
 | ||||
| const TableHead = React.forwardRef< | ||||
|   HTMLTableCellElement, | ||||
|   React.ThHTMLAttributes<HTMLTableCellElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <th | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TableHead.displayName = "TableHead" | ||||
| 
 | ||||
| const TableCell = React.forwardRef< | ||||
|   HTMLTableCellElement, | ||||
|   React.TdHTMLAttributes<HTMLTableCellElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <td | ||||
|     ref={ref} | ||||
|     className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TableCell.displayName = "TableCell" | ||||
| 
 | ||||
| const TableCaption = React.forwardRef< | ||||
|   HTMLTableCaptionElement, | ||||
|   React.HTMLAttributes<HTMLTableCaptionElement> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <caption | ||||
|     ref={ref} | ||||
|     className={cn("mt-4 text-sm text-muted-foreground", className)} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TableCaption.displayName = "TableCaption" | ||||
| 
 | ||||
| export { | ||||
|   Table, | ||||
|   TableHeader, | ||||
|   TableBody, | ||||
|   TableFooter, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   TableCell, | ||||
|   TableCaption, | ||||
| } | ||||
							
								
								
									
										53
									
								
								frontend/src/components/ui/tabs.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								frontend/src/components/ui/tabs.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| import * as React from "react" | ||||
| import * as TabsPrimitive from "@radix-ui/react-tabs" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const Tabs = TabsPrimitive.Root | ||||
| 
 | ||||
| const TabsList = React.forwardRef< | ||||
|   React.ElementRef<typeof TabsPrimitive.List>, | ||||
|   React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <TabsPrimitive.List | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TabsList.displayName = TabsPrimitive.List.displayName | ||||
| 
 | ||||
| const TabsTrigger = React.forwardRef< | ||||
|   React.ElementRef<typeof TabsPrimitive.Trigger>, | ||||
|   React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <TabsPrimitive.Trigger | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TabsTrigger.displayName = TabsPrimitive.Trigger.displayName | ||||
| 
 | ||||
| const TabsContent = React.forwardRef< | ||||
|   React.ElementRef<typeof TabsPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <TabsPrimitive.Content | ||||
|     ref={ref} | ||||
|     className={cn( | ||||
|       "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TabsContent.displayName = TabsPrimitive.Content.displayName | ||||
| 
 | ||||
| export { Tabs, TabsList, TabsTrigger, TabsContent } | ||||
							
								
								
									
										24
									
								
								frontend/src/components/ui/textarea.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/components/ui/textarea.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| import * as React from "react" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| export interface TextareaProps | ||||
|   extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} | ||||
| 
 | ||||
| const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( | ||||
|   ({ className, ...props }, ref) => { | ||||
|     return ( | ||||
|       <textarea | ||||
|         className={cn( | ||||
|           "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", | ||||
|           className | ||||
|         )} | ||||
|         ref={ref} | ||||
|         {...props} | ||||
|       /> | ||||
|     ) | ||||
|   } | ||||
| ) | ||||
| Textarea.displayName = "Textarea" | ||||
| 
 | ||||
| export { Textarea } | ||||
							
								
								
									
										127
									
								
								frontend/src/components/ui/toast.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								frontend/src/components/ui/toast.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,127 @@ | |||
| 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, | ||||
| } | ||||
							
								
								
									
										33
									
								
								frontend/src/components/ui/toaster.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/components/ui/toaster.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| 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> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										59
									
								
								frontend/src/components/ui/toggle-group.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								frontend/src/components/ui/toggle-group.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | |||
| import * as React from "react" | ||||
| import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" | ||||
| import { type VariantProps } from "class-variance-authority" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| import { toggleVariants } from "@/components/ui/toggle" | ||||
| 
 | ||||
| const ToggleGroupContext = React.createContext< | ||||
|   VariantProps<typeof toggleVariants> | ||||
| >({ | ||||
|   size: "default", | ||||
|   variant: "default", | ||||
| }) | ||||
| 
 | ||||
| const ToggleGroup = React.forwardRef< | ||||
|   React.ElementRef<typeof ToggleGroupPrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & | ||||
|     VariantProps<typeof toggleVariants> | ||||
| >(({ className, variant, size, children, ...props }, ref) => ( | ||||
|   <ToggleGroupPrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn("flex items-center justify-center gap-1", className)} | ||||
|     {...props} | ||||
|   > | ||||
|     <ToggleGroupContext.Provider value={{ variant, size }}> | ||||
|       {children} | ||||
|     </ToggleGroupContext.Provider> | ||||
|   </ToggleGroupPrimitive.Root> | ||||
| )) | ||||
| 
 | ||||
| ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName | ||||
| 
 | ||||
| const ToggleGroupItem = React.forwardRef< | ||||
|   React.ElementRef<typeof ToggleGroupPrimitive.Item>, | ||||
|   React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & | ||||
|     VariantProps<typeof toggleVariants> | ||||
| >(({ className, children, variant, size, ...props }, ref) => { | ||||
|   const context = React.useContext(ToggleGroupContext) | ||||
| 
 | ||||
|   return ( | ||||
|     <ToggleGroupPrimitive.Item | ||||
|       ref={ref} | ||||
|       className={cn( | ||||
|         toggleVariants({ | ||||
|           variant: context.variant || variant, | ||||
|           size: context.size || size, | ||||
|         }), | ||||
|         className | ||||
|       )} | ||||
|       {...props} | ||||
|     > | ||||
|       {children} | ||||
|     </ToggleGroupPrimitive.Item> | ||||
|   ) | ||||
| }) | ||||
| 
 | ||||
| ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName | ||||
| 
 | ||||
| export { ToggleGroup, ToggleGroupItem } | ||||
							
								
								
									
										43
									
								
								frontend/src/components/ui/toggle.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/src/components/ui/toggle.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import * as React from "react" | ||||
| import * as TogglePrimitive from "@radix-ui/react-toggle" | ||||
| import { cva, type VariantProps } from "class-variance-authority" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const toggleVariants = cva( | ||||
|   "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground", | ||||
|   { | ||||
|     variants: { | ||||
|       variant: { | ||||
|         default: "bg-transparent", | ||||
|         outline: | ||||
|           "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", | ||||
|       }, | ||||
|       size: { | ||||
|         default: "h-10 px-3", | ||||
|         sm: "h-9 px-2.5", | ||||
|         lg: "h-11 px-5", | ||||
|       }, | ||||
|     }, | ||||
|     defaultVariants: { | ||||
|       variant: "default", | ||||
|       size: "default", | ||||
|     }, | ||||
|   } | ||||
| ) | ||||
| 
 | ||||
| const Toggle = React.forwardRef< | ||||
|   React.ElementRef<typeof TogglePrimitive.Root>, | ||||
|   React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & | ||||
|     VariantProps<typeof toggleVariants> | ||||
| >(({ className, variant, size, ...props }, ref) => ( | ||||
|   <TogglePrimitive.Root | ||||
|     ref={ref} | ||||
|     className={cn(toggleVariants({ variant, size, className }))} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| 
 | ||||
| Toggle.displayName = TogglePrimitive.Root.displayName | ||||
| 
 | ||||
| export { Toggle, toggleVariants } | ||||
							
								
								
									
										28
									
								
								frontend/src/components/ui/tooltip.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/components/ui/tooltip.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| import * as React from "react" | ||||
| import * as TooltipPrimitive from "@radix-ui/react-tooltip" | ||||
| 
 | ||||
| import { cn } from "@/lib/utils" | ||||
| 
 | ||||
| const TooltipProvider = TooltipPrimitive.Provider | ||||
| 
 | ||||
| const Tooltip = TooltipPrimitive.Root | ||||
| 
 | ||||
| const TooltipTrigger = TooltipPrimitive.Trigger | ||||
| 
 | ||||
| const TooltipContent = React.forwardRef< | ||||
|   React.ElementRef<typeof TooltipPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> | ||||
| >(({ className, sideOffset = 4, ...props }, ref) => ( | ||||
|   <TooltipPrimitive.Content | ||||
|     ref={ref} | ||||
|     sideOffset={sideOffset} | ||||
|     className={cn( | ||||
|       "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", | ||||
|       className | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| )) | ||||
| TooltipContent.displayName = TooltipPrimitive.Content.displayName | ||||
| 
 | ||||
| export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } | ||||
							
								
								
									
										3
									
								
								frontend/src/components/ui/use-toast.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/components/ui/use-toast.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| import { useToast, toast } from "@/hooks/use-toast"; | ||||
| 
 | ||||
| export { useToast, toast }; | ||||
							
								
								
									
										19
									
								
								frontend/src/hooks/use-mobile.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/hooks/use-mobile.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| import * as React from "react" | ||||
| 
 | ||||
| const MOBILE_BREAKPOINT = 768 | ||||
| 
 | ||||
| export function useIsMobile() { | ||||
|   const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) | ||||
|     const onChange = () => { | ||||
|       setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) | ||||
|     } | ||||
|     mql.addEventListener("change", onChange) | ||||
|     setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) | ||||
|     return () => mql.removeEventListener("change", onChange) | ||||
|   }, []) | ||||
| 
 | ||||
|   return !!isMobile | ||||
| } | ||||
							
								
								
									
										191
									
								
								frontend/src/hooks/use-toast.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								frontend/src/hooks/use-toast.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,191 @@ | |||
| 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 } | ||||
							
								
								
									
										94
									
								
								frontend/src/hooks/useAuth.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								frontend/src/hooks/useAuth.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | |||
| /*  Autenticazione (JWT)  */ | ||||
| 
 | ||||
| import { | ||||
|   createContext, | ||||
|   useContext, | ||||
|   useState, | ||||
|   useEffect, | ||||
|   ReactNode, | ||||
| } from 'react'; | ||||
| 
 | ||||
| interface User { | ||||
|   id: string; | ||||
|   email: string; | ||||
|   name: string; | ||||
| } | ||||
| 
 | ||||
| interface AuthContextType { | ||||
|   user: User | null; | ||||
|   loading: boolean; | ||||
|   login: (email: string, password: string) => Promise<void>; | ||||
|   register: (email: string, password: string, name: string) => Promise<void>; | ||||
|   logout: () => void; | ||||
| } | ||||
| 
 | ||||
| const AuthContext = createContext<AuthContextType | undefined>(undefined); | ||||
| 
 | ||||
| const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:6789/api'; | ||||
| 
 | ||||
| export const AuthProvider = ({ children }: { children: ReactNode }) => { | ||||
|   const [user, setUser]   = useState<User | null>(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
| 
 | ||||
|   /* al mount, prova a caricare user + token */ | ||||
|   useEffect(() => { | ||||
|     const savedUser  = localStorage.getItem('hackerspace_user'); | ||||
|     const savedToken = localStorage.getItem('hackerspace_token'); | ||||
|     if (savedUser && savedToken) setUser(JSON.parse(savedUser)); | ||||
|     setLoading(false); | ||||
|   }, []); | ||||
| 
 | ||||
|   const login = async (email: string, password: string) => { | ||||
|     const res = await fetch(`${API_URL}/auth/login`, { | ||||
|       method: 'POST', | ||||
|       headers: { 'Content-Type': 'application/json' }, | ||||
|       body: JSON.stringify({ email, password }), | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Login fallito'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
| 
 | ||||
|     const { token, user } = await res.json(); | ||||
|     localStorage.setItem('hackerspace_token', token); | ||||
|     localStorage.setItem('hackerspace_user', JSON.stringify(user)); | ||||
|     setUser(user); | ||||
|   }; | ||||
| 
 | ||||
|   const register = async (email: string, password: string, name: string) => { | ||||
|     const res = await fetch(`${API_URL}/auth/register`, { | ||||
|       method: 'POST', | ||||
|       headers: { 'Content-Type': 'application/json' }, | ||||
|       body: JSON.stringify({ email, password, name }), | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Registrazione fallita'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
| 
 | ||||
|     const { token, user } = await res.json(); | ||||
|     localStorage.setItem('hackerspace_token', token); | ||||
|     localStorage.setItem('hackerspace_user', JSON.stringify(user)); | ||||
|     setUser(user); | ||||
|   }; | ||||
| 
 | ||||
|   const logout = () => { | ||||
|     localStorage.removeItem('hackerspace_token'); | ||||
|     localStorage.removeItem('hackerspace_user'); | ||||
|     setUser(null); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <AuthContext.Provider value={{ user, loading, login, register, logout }}> | ||||
|       {children} | ||||
|     </AuthContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const useAuth = () => { | ||||
|   const ctx = useContext(AuthContext); | ||||
|   if (!ctx) throw new Error('useAuth must be used within AuthProvider'); | ||||
|   return ctx; | ||||
| }; | ||||
							
								
								
									
										105
									
								
								frontend/src/index.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								frontend/src/index.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | |||
| @tailwind base; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
| 
 | ||||
| /* Definition of the design system. All colors, gradients, fonts, etc should be defined here.  | ||||
| All colors MUST be HSL. | ||||
| */ | ||||
| 
 | ||||
| @layer base { | ||||
|   :root { | ||||
|     --background: 0 0% 100%; | ||||
|     --foreground: 222.2 84% 4.9%; | ||||
| 
 | ||||
|     --card: 0 0% 100%; | ||||
|     --card-foreground: 222.2 84% 4.9%; | ||||
| 
 | ||||
|     --popover: 0 0% 100%; | ||||
|     --popover-foreground: 222.2 84% 4.9%; | ||||
| 
 | ||||
|     --primary: 222.2 47.4% 11.2%; | ||||
|     --primary-foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --secondary: 210 40% 96.1%; | ||||
|     --secondary-foreground: 222.2 47.4% 11.2%; | ||||
| 
 | ||||
|     --muted: 210 40% 96.1%; | ||||
|     --muted-foreground: 215.4 16.3% 46.9%; | ||||
| 
 | ||||
|     --accent: 210 40% 96.1%; | ||||
|     --accent-foreground: 222.2 47.4% 11.2%; | ||||
| 
 | ||||
|     --destructive: 0 84.2% 60.2%; | ||||
|     --destructive-foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --border: 214.3 31.8% 91.4%; | ||||
|     --input: 214.3 31.8% 91.4%; | ||||
|     --ring: 222.2 84% 4.9%; | ||||
| 
 | ||||
|     --radius: 0.5rem; | ||||
| 
 | ||||
|     --sidebar-background: 0 0% 98%; | ||||
| 
 | ||||
|     --sidebar-foreground: 240 5.3% 26.1%; | ||||
| 
 | ||||
|     --sidebar-primary: 240 5.9% 10%; | ||||
| 
 | ||||
|     --sidebar-primary-foreground: 0 0% 98%; | ||||
| 
 | ||||
|     --sidebar-accent: 240 4.8% 95.9%; | ||||
| 
 | ||||
|     --sidebar-accent-foreground: 240 5.9% 10%; | ||||
| 
 | ||||
|     --sidebar-border: 220 13% 91%; | ||||
| 
 | ||||
|     --sidebar-ring: 217.2 91.2% 59.8%; | ||||
|   } | ||||
| 
 | ||||
|   .dark { | ||||
|     --background: 222.2 84% 4.9%; | ||||
|     --foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --card: 222.2 84% 4.9%; | ||||
|     --card-foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --popover: 222.2 84% 4.9%; | ||||
|     --popover-foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --primary: 210 40% 98%; | ||||
|     --primary-foreground: 222.2 47.4% 11.2%; | ||||
| 
 | ||||
|     --secondary: 217.2 32.6% 17.5%; | ||||
|     --secondary-foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --muted: 217.2 32.6% 17.5%; | ||||
|     --muted-foreground: 215 20.2% 65.1%; | ||||
| 
 | ||||
|     --accent: 217.2 32.6% 17.5%; | ||||
|     --accent-foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --destructive: 0 62.8% 30.6%; | ||||
|     --destructive-foreground: 210 40% 98%; | ||||
| 
 | ||||
|     --border: 217.2 32.6% 17.5%; | ||||
|     --input: 217.2 32.6% 17.5%; | ||||
|     --ring: 212.7 26.8% 83.9%; | ||||
|     --sidebar-background: 240 5.9% 10%; | ||||
|     --sidebar-foreground: 240 4.8% 95.9%; | ||||
|     --sidebar-primary: 224.3 76.3% 48%; | ||||
|     --sidebar-primary-foreground: 0 0% 100%; | ||||
|     --sidebar-accent: 240 3.7% 15.9%; | ||||
|     --sidebar-accent-foreground: 240 4.8% 95.9%; | ||||
|     --sidebar-border: 240 3.7% 15.9%; | ||||
|     --sidebar-ring: 217.2 91.2% 59.8%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @layer base { | ||||
|   * { | ||||
|     @apply border-border; | ||||
|   } | ||||
| 
 | ||||
|   body { | ||||
|     @apply bg-background text-foreground; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										6
									
								
								frontend/src/lib/utils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/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)) | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/main.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/main.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| import { createRoot } from 'react-dom/client' | ||||
| import App from './App.tsx' | ||||
| import './index.css' | ||||
| 
 | ||||
| createRoot(document.getElementById("root")!).render(<App />); | ||||
							
								
								
									
										27
									
								
								frontend/src/pages/Index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/pages/Index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| 
 | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { LoginForm } from '@/components/LoginForm'; | ||||
| import { InventoryDashboard } from '@/components/InventoryDashboard'; | ||||
| import { useAuth } from '@/hooks/useAuth'; | ||||
| 
 | ||||
| const Index = () => { | ||||
|   const { user, loading } = useAuth(); | ||||
| 
 | ||||
|   if (loading) { | ||||
|     return ( | ||||
|       <div className="min-h-screen flex items-center justify-center bg-slate-50"> | ||||
|         <div className="animate-pulse"> | ||||
|           <div className="w-8 h-8 bg-blue-600 rounded-full"></div> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   if (!user) { | ||||
|     return <LoginForm />; | ||||
|   } | ||||
| 
 | ||||
|   return <InventoryDashboard />; | ||||
| }; | ||||
| 
 | ||||
| export default Index; | ||||
							
								
								
									
										27
									
								
								frontend/src/pages/NotFound.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/pages/NotFound.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import { useLocation } from "react-router-dom"; | ||||
| import { useEffect } from "react"; | ||||
| 
 | ||||
| const NotFound = () => { | ||||
|   const location = useLocation(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     console.error( | ||||
|       "404 Error: User attempted to access non-existent route:", | ||||
|       location.pathname | ||||
|     ); | ||||
|   }, [location.pathname]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen flex items-center justify-center bg-gray-100"> | ||||
|       <div className="text-center"> | ||||
|         <h1 className="text-4xl font-bold mb-4">404</h1> | ||||
|         <p className="text-xl text-gray-600 mb-4">Oops! Page not found</p> | ||||
|         <a href="/" className="text-blue-500 hover:text-blue-700 underline"> | ||||
|           Return to Home | ||||
|         </a> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default NotFound; | ||||
							
								
								
									
										102
									
								
								frontend/src/services/inventoryService.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								frontend/src/services/inventoryService.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | |||
| 
 | ||||
| /*  Inventory API service  */ | ||||
| 
 | ||||
| import { | ||||
|   InventoryItem, | ||||
|   Tag, | ||||
|   CreateItemData, | ||||
| } from '@/types/inventory'; | ||||
| 
 | ||||
| const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:6789/api'; | ||||
| 
 | ||||
| // ⬇️ helper per recuperare il token JWT dal localStorage
 | ||||
| const getAuthHeader = () => { | ||||
|   const token = localStorage.getItem('hackerspace_token'); | ||||
|   return token ? { Authorization: `Bearer ${token}` } : {}; | ||||
| }; | ||||
| 
 | ||||
| export const inventoryService = { | ||||
|   async getItems(): Promise<InventoryItem[]> { | ||||
|     const res = await fetch(`${API_URL}/items`); | ||||
|     if (!res.ok) throw new Error('Impossibile scaricare gli item'); | ||||
|     return res.json(); | ||||
|   }, | ||||
| 
 | ||||
|   async getTags(): Promise<Tag[]> { | ||||
|     const res = await fetch(`${API_URL}/tags`); | ||||
|     if (!res.ok) throw new Error('Impossibile scaricare i tag'); | ||||
|     return res.json(); | ||||
|   }, | ||||
| 
 | ||||
|   async createItem(data: CreateItemData): Promise<InventoryItem> { | ||||
|     const res = await fetch(`${API_URL}/items`, { | ||||
|       method: 'POST', | ||||
|       headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         ...getAuthHeader(), | ||||
|       }, | ||||
|       body: JSON.stringify(data), | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Errore creazione item'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
|     return res.json(); | ||||
|   }, | ||||
| 
 | ||||
|   async deleteItem(itemId: string): Promise<void> { | ||||
|     const res = await fetch(`${API_URL}/items/${itemId}`, { | ||||
|       method: 'DELETE', | ||||
|       headers: { | ||||
|         ...getAuthHeader(), | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Errore eliminazione item'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   async createTag(name: string, color: string): Promise<Tag> { | ||||
|     const res = await fetch(`${API_URL}/tags`, { | ||||
|       method: 'POST', | ||||
|       headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         ...getAuthHeader(), | ||||
|       }, | ||||
|       body: JSON.stringify({ name, color }), | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Errore creazione tag'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
|     return res.json(); | ||||
|   }, | ||||
| 
 | ||||
|   async deleteTag(tagId: string): Promise<void> { | ||||
|     const res = await fetch(`${API_URL}/tags/${tagId}`, { | ||||
|       method: 'DELETE', | ||||
|       headers: { | ||||
|         ...getAuthHeader(), | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     if (!res.ok) { | ||||
|       const msg = (await res.json())?.message ?? 'Errore eliminazione tag'; | ||||
|       throw new Error(msg); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   async searchItems(query = '', tagIds: string[] = []): Promise<InventoryItem[]> { | ||||
|     const qs = new URLSearchParams({ | ||||
|       query, | ||||
|       tagIds: tagIds.join(','), | ||||
|     }); | ||||
|     const res = await fetch(`${API_URL}/items/search?${qs.toString()}`); | ||||
|     if (!res.ok) throw new Error('Errore ricerca item'); | ||||
|     return res.json(); | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										21
									
								
								frontend/src/types/inventory.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/src/types/inventory.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| 
 | ||||
| export interface Tag { | ||||
|   _id: string; | ||||
|   name: string; | ||||
|   color: string; | ||||
| } | ||||
| 
 | ||||
| export interface InventoryItem { | ||||
|   _id: string; | ||||
|   name: string; | ||||
|   description: string; | ||||
|   tags: Tag[]; | ||||
|   dateAdded: string; | ||||
|   addedBy: string; | ||||
| } | ||||
| 
 | ||||
| export interface CreateItemData { | ||||
|   name: string; | ||||
|   description: string; | ||||
|   tagIds: string[]; | ||||
| } | ||||
							
								
								
									
										1
									
								
								frontend/src/vite-env.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/vite-env.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| /// <reference types="vite/client" />
 | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 StefanoPutelli
						StefanoPutelli