diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5ecd514322b0ce49ba4d15cfcde9ab538f5e55fc Binary files /dev/null and b/.DS_Store differ diff --git a/bun.lockb b/bun.lockb index b33671b65c63c2754e5c32f91b13133ef2d5ea11..2e2a2da73856d2a9eaabddd9b603b1aac8da6a70 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/demo.tsx b/demo.tsx deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fb3a0bf7aed9b59d0c1c5677240e644e814c8e64 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/jsc.js b/public/jsc.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..89e22ee99cb0965d775b3940b90c91c6b9e65385 Binary files /dev/null and b/public/og-image.png differ diff --git a/public/placeholder.svg b/public/placeholder.svg new file mode 100644 index 0000000000000000000000000000000000000000..e763910b27fdd9ac872f56baede51bc839402347 --- /dev/null +++ b/public/placeholder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a54e3c365d026f29000f68ca6d41233dc259ebef Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..b9d355df2a5956b526c004531b7b0ffe412461e0 --- /dev/null +++ b/src/App.css @@ -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; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..69cd0411231b7854b4decdd61916b922eadf95b9 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,29 @@ + +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 Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; +import Calendar from "./pages/Calendar"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + } /> + } /> + } /> + + + + +); + +export default App; diff --git a/src/components/ConferenceCalendar.tsx b/src/components/ConferenceCalendar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d9bff116678a59a5f7f90093135c2b8fc6ccded9 --- /dev/null +++ b/src/components/ConferenceCalendar.tsx @@ -0,0 +1,156 @@ +import { useState } from "react"; +import { Calendar } from "@/components/ui/calendar"; +import { Conference } from "@/types/conference"; +import { parseISO, format, parse, startOfMonth } from "date-fns"; + +interface ConferenceCalendarProps { + conferences: Conference[]; +} + +const ConferenceCalendar = ({ conferences }: ConferenceCalendarProps) => { + const [selectedDate, setSelectedDate] = useState(undefined); + const [currentMonth, setCurrentMonth] = useState(new Date()); + + // Handle month change + const handleMonthChange = (month: Date) => { + setCurrentMonth(month); + setSelectedDate(undefined); // Clear selected date when changing months + }; + + // Convert conference dates to calendar events + const conferenceEvents = conferences.map(conf => { + let startDate: Date | null = null; + let endDate: Date | null = null; + + try { + // Parse both start and end dates + if (conf.start && conf.end) { + startDate = parseISO(conf.start); + endDate = parseISO(conf.end); + } + // If no start/end fields, try to parse from date field + else if (conf.date) { + const [startStr, endStr] = conf.date.split(/[-–]/).map(d => d.trim()); + + try { + // Try parsing start date + startDate = parse(startStr, 'MMM d, yyyy', new Date()) || + parse(startStr, 'MMMM d, yyyy', new Date()) || + parseISO(startStr); + + // Try parsing end date if it exists + if (endStr) { + endDate = parse(endStr, 'MMM d, yyyy', new Date()) || + parse(endStr, 'MMMM d, yyyy', new Date()) || + parseISO(endStr); + } else { + // If no end date, use start date + endDate = startDate; + } + } catch (error) { + console.warn(`Failed to parse date range for conference ${conf.title}:`, error); + } + } + + // Only return event if we successfully parsed both dates + if (startDate && endDate && isValidDate(startDate) && isValidDate(endDate)) { + return { + startDate, + endDate, + title: conf.title, + conference: conf + }; + } + return null; + } catch (error) { + console.warn(`Failed to parse dates for conference ${conf.title}:`, error); + return null; + } + }).filter(event => event !== null); + + // Helper function to check if date is valid + function isValidDate(date: Date) { + return date instanceof Date && !isNaN(date.getTime()); + } + + // Get events for the selected date + const getEventsForDate = (date: Date) => { + if (!date || !isValidDate(date)) return []; + return conferenceEvents.filter(event => + event && event.startDate && event.endDate && + date >= event.startDate && date <= event.endDate + ); + }; + + // Get events for the current month + const getEventsForMonth = (date: Date) => { + const monthStart = startOfMonth(date); + const nextMonthStart = new Date(date.getFullYear(), date.getMonth() + 1, 1); + + return conferenceEvents.filter(event => + event && event.startDate && event.endDate && + ((event.startDate >= monthStart && event.startDate < nextMonthStart) || + (event.endDate >= monthStart && event.endDate < nextMonthStart) || + (event.startDate <= monthStart && event.endDate >= nextMonthStart)) + ); + }; + + // Create footer content + const footer = ( +
+

+ Events in {format(currentMonth, 'MMMM yyyy')}: +

+ {getEventsForMonth(currentMonth).length > 0 ? ( +
    + {getEventsForMonth(currentMonth).map((event, index) => ( +
  • + {event.title} ({format(event.startDate, 'MMM d')}-{format(event.endDate, 'MMM d')}) - {event.conference.place} +
  • + ))} +
+ ) : ( +

No events this month

+ )} + {selectedDate && ( +
+

+ Events on {format(selectedDate, 'MMMM d, yyyy')}: +

+ {getEventsForDate(selectedDate).length > 0 ? ( +
    + {getEventsForDate(selectedDate).map((event, index) => ( +
  • + {event.title} - {event.conference.place} +
  • + ))} +
+ ) : ( +

No events on this date

+ )} +
+ )} +
+ ); + + return ( +
+ getEventsForDate(date).length > 0 + }} + modifiersStyles={{ + event: { fontWeight: 'bold', textDecoration: 'underline' } + }} + /> +
+ ); +}; + +export default ConferenceCalendar; \ No newline at end of file diff --git a/src/components/ConferenceCard.tsx b/src/components/ConferenceCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..534fe439e2e0dda8e02527a10b8c6c67caf9a7cd --- /dev/null +++ b/src/components/ConferenceCard.tsx @@ -0,0 +1,132 @@ +import { CalendarDays, Globe, Tag, Clock, AlarmClock } from "lucide-react"; +import { Conference } from "@/types/conference"; +import { formatDistanceToNow, parseISO, isValid } from "date-fns"; +import ConferenceDialog from "./ConferenceDialog"; +import { useState } from "react"; + +const ConferenceCard = ({ + title, + full_name, + date, + place, + deadline, + timezone, + tags = [], + link, + note, + abstract_deadline, + ...conferenceProps +}: Conference) => { + const [dialogOpen, setDialogOpen] = useState(false); + const deadlineDate = deadline && deadline !== 'TBD' ? parseISO(deadline) : null; + const daysLeft = deadlineDate && isValid(deadlineDate) ? formatDistanceToNow(deadlineDate, { addSuffix: true }) : 'TBD'; + + // Determine countdown color based on days remaining + const getCountdownColor = () => { + if (!deadlineDate || !isValid(deadlineDate)) return "text-neutral-600"; + const daysRemaining = Math.ceil((deadlineDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)); + if (daysRemaining <= 7) return "text-red-600"; + if (daysRemaining <= 30) return "text-orange-600"; + return "text-green-600"; + }; + + const handleCardClick = (e: React.MouseEvent) => { + if (!(e.target as HTMLElement).closest('a') && + !(e.target as HTMLElement).closest('.tag-button')) { + setDialogOpen(true); + } + }; + + const handleTagClick = (e: React.MouseEvent, tag: string) => { + e.stopPropagation(); + const searchParams = new URLSearchParams(window.location.search); + const currentTags = searchParams.get('tags')?.split(',') || []; + + let newTags; + if (currentTags.includes(tag)) { + newTags = currentTags.filter(t => t !== tag); + } else { + newTags = [...currentTags, tag]; + } + + if (newTags.length > 0) { + searchParams.set('tags', newTags.join(',')); + } else { + searchParams.delete('tags'); + } + + const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; + window.history.pushState({}, '', newUrl); + window.dispatchEvent(new CustomEvent('urlchange', { detail: { tag } })); + }; + + return ( + <> +
+
+

{title}

+ {link && ( + e.stopPropagation()} + > + + + )} +
+ +
+
+ + {date} +
+
+ + {place} +
+
+ + + {deadline === 'TBD' ? 'TBD' : deadline} + +
+
+ + + {daysLeft} + +
+
+ + {Array.isArray(tags) && tags.length > 0 && ( +
+ {tags.map((tag) => ( + + ))} +
+ )} +
+ + + + ); +}; + +export default ConferenceCard; diff --git a/src/components/ConferenceDialog.tsx b/src/components/ConferenceDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b7037fef974551db8fa330180e788eea313321e8 --- /dev/null +++ b/src/components/ConferenceDialog.tsx @@ -0,0 +1,330 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { CalendarDays, Globe, Tag, Clock, AlarmClock, CalendarPlus } from "lucide-react"; +import { Conference } from "@/types/conference"; +import { formatDistanceToNow, parseISO, isValid, format, parse, addDays } from "date-fns"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useState, useEffect } from "react"; + +interface ConferenceDialogProps { + conference: Conference; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogProps) => { + const deadlineDate = conference.deadline && conference.deadline !== 'TBD' ? parseISO(conference.deadline) : null; + const [countdown, setCountdown] = useState(''); + + useEffect(() => { + const calculateTimeLeft = () => { + if (!deadlineDate || !isValid(deadlineDate)) { + setCountdown('TBD'); + return; + } + + const now = new Date().getTime(); + const difference = deadlineDate.getTime() - now; + + if (difference <= 0) { + setCountdown('Deadline passed'); + return; + } + + const days = Math.floor(difference / (1000 * 60 * 60 * 24)); + const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((difference % (1000 * 60)) / 1000); + + setCountdown(`${days}d ${hours}h ${minutes}m ${seconds}s`); + }; + + // Calculate immediately + calculateTimeLeft(); + + // Update every second + const timer = setInterval(calculateTimeLeft, 1000); + + // Cleanup interval on component unmount + return () => clearInterval(timer); + }, [deadlineDate]); + + const getCountdownColor = () => { + if (!deadlineDate || !isValid(deadlineDate)) return "text-neutral-600"; + const daysRemaining = Math.ceil((deadlineDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)); + if (daysRemaining <= 7) return "text-red-600"; + if (daysRemaining <= 30) return "text-orange-600"; + return "text-green-600"; + }; + + const parseDateFromString = (dateStr: string) => { + try { + // Handle formats like "October 19-25, 2025" or "Sept 9-12, 2025" + const [monthDay, year] = dateStr.split(", "); + const [month, dayRange] = monthDay.split(" "); + const [startDay] = dayRange.split("-"); + + // Construct a date string in a format that can be parsed + const dateString = `${month} ${startDay} ${year}`; + const date = parse(dateString, 'MMMM d yyyy', new Date()); + + if (!isValid(date)) { + // Try alternative format for abbreviated months + return parse(dateString, 'MMM d yyyy', new Date()); + } + + return date; + } catch (error) { + console.error("Error parsing date:", error); + return new Date(); + } + }; + + const createCalendarEvent = (type: 'google' | 'apple') => { + try { + if (!conference.deadline || conference.deadline === 'TBD') { + throw new Error('No valid deadline found'); + } + + // Parse the deadline date + const deadlineDate = parseISO(conference.deadline); + if (!isValid(deadlineDate)) { + throw new Error('Invalid deadline date'); + } + + // Create an end date 1 hour after the deadline + const endDate = new Date(deadlineDate.getTime() + (60 * 60 * 1000)); + + const formatDateForGoogle = (date: Date) => format(date, "yyyyMMdd'T'HHmmss'Z'"); + const formatDateForApple = (date: Date) => format(date, "yyyyMMdd'T'HHmmss'Z'"); + + const title = encodeURIComponent(`${conference.title} deadline`); + const location = encodeURIComponent(conference.place); + const description = encodeURIComponent( + `Paper Submission Deadline for ${conference.full_name || conference.title}\n` + + (conference.abstract_deadline ? `Abstract Deadline: ${conference.abstract_deadline}\n` : '') + + `Dates: ${conference.date}\n` + + `Location: ${conference.place}\n` + + (conference.link ? `Website: ${conference.link}` : '') + ); + + if (type === 'google') { + const url = `https://calendar.google.com/calendar/render?action=TEMPLATE` + + `&text=${title}` + + `&dates=${formatDateForGoogle(deadlineDate)}/${formatDateForGoogle(endDate)}` + + `&details=${description}` + + `&location=${location}` + + `&sprop=website:${encodeURIComponent(conference.link || '')}`; + window.open(url, '_blank'); + } else { + const url = `data:text/calendar;charset=utf8,BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +URL:${conference.link || ''} +DTSTART:${formatDateForApple(deadlineDate)} +DTEND:${formatDateForApple(endDate)} +SUMMARY:${title} +DESCRIPTION:${description} +LOCATION:${location} +END:VEVENT +END:VCALENDAR`; + + const link = document.createElement('a'); + link.href = url; + link.download = `${conference.title.toLowerCase().replace(/\s+/g, '-')}-deadline.ics`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } catch (error) { + console.error("Error creating calendar event:", error); + alert("Sorry, there was an error creating the calendar event. Please try again."); + } + }; + + const generateGoogleMapsUrl = (venue: string | undefined, place: string): string => { + return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(venue || place)}`; + }; + + return ( + + + + {conference.title} {conference.year} + + {conference.full_name} + + + +
+
+ +
+

Dates

+

{conference.date}

+
+
+ +
+ +
+

Important Deadlines

+
+ {conference.abstract_deadline && ( +
+

Abstract: {parseISO(conference.abstract_deadline) && isValid(parseISO(conference.abstract_deadline)) + ? format(parseISO(conference.abstract_deadline), "MMMM d, yyyy") + : conference.abstract_deadline} +

+
+ )} +
+

Submission: {conference.deadline && conference.deadline !== 'TBD' && isValid(parseISO(conference.deadline)) + ? format(parseISO(conference.deadline), "MMMM d, yyyy") + : conference.deadline} +

+
+ {conference.commitment_deadline && ( +
+

Commitment: {isValid(parseISO(conference.commitment_deadline)) + ? format(parseISO(conference.commitment_deadline), "MMMM d, yyyy") + : conference.commitment_deadline} +

+
+ )} + {conference.review_release_date && ( +
+

Reviews Released: {isValid(parseISO(conference.review_release_date)) + ? format(parseISO(conference.review_release_date), "MMMM d, yyyy") + : conference.review_release_date} +

+
+ )} + {(conference.rebuttal_period_start || conference.rebuttal_period_end) && ( +
+

Rebuttal Period: {conference.rebuttal_period_start && isValid(parseISO(conference.rebuttal_period_start)) + ? format(parseISO(conference.rebuttal_period_start), "MMMM d, yyyy") + : conference.rebuttal_period_start} - {conference.rebuttal_period_end && isValid(parseISO(conference.rebuttal_period_end)) + ? format(parseISO(conference.rebuttal_period_end), "MMMM d, yyyy") + : conference.rebuttal_period_end} +

+
+ )} + {conference.final_decision_date && ( +
+

Final Decision: {isValid(parseISO(conference.final_decision_date)) + ? format(parseISO(conference.final_decision_date), "MMMM d, yyyy") + : conference.final_decision_date} +

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

Location

+

{conference.place}

+ {conference.venue && ( +

{conference.venue}

+ )} +
+
+ +
+ +
+ + {countdown} + + {deadlineDate && isValid(deadlineDate) && ( +
+ {format(deadlineDate, "MMMM d, yyyy 'at' HH:mm:ss")} {conference.timezone} +
+ )} +
+
+ + {Array.isArray(conference.tags) && conference.tags.length > 0 && ( +
+ {conference.tags.map((tag) => ( + + + {tag} + + ))} +
+ )} + + {conference.note && ( +
+ )} + +
+ {conference.link && ( + + )} + + + + + + + createCalendarEvent('google')} + > + Add to Google Calendar + + createCalendarEvent('apple')} + > + Add to Apple Calendar + + + +
+
+ +
+ ); +}; + +export default ConferenceDialog; diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a90260f6da0dacb7ea9ad85b9a6589abeb1e35cf --- /dev/null +++ b/src/components/FilterBar.tsx @@ -0,0 +1,80 @@ +import { useMemo } from "react"; +import conferencesData from "@/data/conferences.yml"; +import { X } from "lucide-react"; + +interface FilterBarProps { + selectedTags: Set; + onTagSelect: (tags: Set) => void; +} + +const FilterBar = ({ selectedTags = new Set(), onTagSelect }: FilterBarProps) => { + const uniqueTags = useMemo(() => { + const tags = new Set(); + if (Array.isArray(conferencesData)) { + conferencesData.forEach(conf => { + if (Array.isArray(conf.tags)) { + conf.tags.forEach(tag => tags.add(tag)); + } + }); + } + return Array.from(tags).map(tag => ({ + id: tag, + label: tag.split("-").map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(" "), + description: `${tag} Conferences` + })); + }, []); + + const isTagSelected = (tagId: string) => { + return selectedTags?.has(tagId) ?? false; + }; + + return ( +
+
+
+ {uniqueTags.map((filter) => ( + + ))} + + {selectedTags?.size > 0 && ( + + )} +
+
+
+ ); +}; + +export default FilterBar; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9c2a394965aceaf6d3a90a15cf29b52b93d4b160 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,122 @@ +import { Search } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Link } from "react-router-dom"; +import { CalendarDays } from "lucide-react"; + +interface HeaderProps { + onSearch: (query: string) => void; + showEmptyMessage?: boolean; +} + +const Header = ({ onSearch, showEmptyMessage = false }: HeaderProps) => { + return ( +
+
+
+
+ + Hugging Face Logo + + AI Conference Deadlines + + + +
+
+
+
+ +
+ onSearch(e.target.value)} + /> +
+
+
+ {showEmptyMessage && ( +
+

+ There are no upcoming conferences for the selected categories - enable "Show past conferences" to see previous ones +

+
+ )} +
+

+ Countdowns to top CV/NLP/ML/Robotics/AI conference deadlines. To add/edit a conference, send in a{' '} + + pull request + . +
+ P.S. Is your paper already on Arxiv? Feel free to{' '} + + submit + + {' '}it to{' '} + + hf.co/papers + + {' '}and upload your artifacts such as{' '} + + models + + {', '} + + datasets + + {' '}and{' '} + + demos + +

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