) {
)
}
-function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
)
}
-function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
- const body = error ? String(error?.message ?? '') : props.children
+ const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
@@ -147,7 +132,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
{body}
diff --git a/components/ui/hover-card.tsx b/components/ui/hover-card.tsx
index 55d6f76..59a1298 100644
--- a/components/ui/hover-card.tsx
+++ b/components/ui/hover-card.tsx
@@ -1,27 +1,21 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
+import * as React from "react"
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function HoverCard({
- ...props
-}: React.ComponentProps) {
+function HoverCard({ ...props }: React.ComponentProps) {
return
}
-function HoverCardTrigger({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
+function HoverCardTrigger({ ...props }: React.ComponentProps) {
+ return
}
function HoverCardContent({
className,
- align = 'center',
+ align = "center",
sideOffset = 4,
...props
}: React.ComponentProps) {
@@ -32,8 +26,8 @@ function HoverCardContent({
align={align}
sideOffset={sideOffset}
className={cn(
- 'bg-popover 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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
- className,
+ "bg-popover 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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+ className
)}
{...props}
/>
diff --git a/components/ui/image-lightbox.tsx b/components/ui/image-lightbox.tsx
index 5d617d7..2a89252 100644
--- a/components/ui/image-lightbox.tsx
+++ b/components/ui/image-lightbox.tsx
@@ -1,15 +1,21 @@
"use client"
-import type React from "react"
+import React from "react"
-import { useState, useEffect, useCallback, useRef } from "react"
+import { useState, useEffect, useCallback, useRef, useMemo } from "react"
import Image from "next/image"
-import { Dialog, DialogContent } from "@/components/ui/dialog"
+import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { X, ChevronLeft, ChevronRight, Download, Maximize, Loader2 } from "lucide-react"
import { cn } from "@/lib/utils"
+declare global {
+ interface Window {
+ gtag?: (...args: unknown[]) => void
+ }
+}
+
interface ImageLightboxProps {
images: string[]
initialIndex?: number
@@ -19,28 +25,41 @@ interface ImageLightboxProps {
}
export function useImageLightboxAnalytics() {
- const trackEvent = useCallback((eventName: string, properties?: Record) => {
- if (typeof window !== "undefined" && window.gtag) {
- window.gtag("event", eventName, properties)
- }
- }, [])
+ const trackEvent = useCallback(
+ (eventName: string, properties?: Record) => {
+ if (typeof window !== "undefined" && window.gtag) {
+ window.gtag("event", eventName, properties)
+ }
+ },
+ []
+ )
- return {
- trackImageOpen: (projectTitle: string, imageIndex: number) =>
- trackEvent("image_open", { project: projectTitle, index: imageIndex }),
- trackImageClose: (projectTitle: string) => trackEvent("image_close", { project: projectTitle }),
- trackImageNavigate: (projectTitle: string, imageIndex: number) =>
- trackEvent("image_navigate", { project: projectTitle, index: imageIndex }),
- trackImageDownload: (projectTitle: string, imageIndex: number) =>
- trackEvent("image_download", { project: projectTitle, index: imageIndex }),
- }
+ return useMemo(
+ () => ({
+ trackImageOpen: (projectTitle: string, imageIndex: number) =>
+ trackEvent("image_open", { project: projectTitle, index: imageIndex }),
+ trackImageClose: (projectTitle: string) =>
+ trackEvent("image_close", { project: projectTitle }),
+ trackImageNavigate: (projectTitle: string, imageIndex: number) =>
+ trackEvent("image_navigate", { project: projectTitle, index: imageIndex }),
+ trackImageDownload: (projectTitle: string, imageIndex: number) =>
+ trackEvent("image_download", { project: projectTitle, index: imageIndex }),
+ }),
+ [trackEvent]
+ )
}
const MIN_SCALE = 1
const MAX_SCALE = 3
const SCALE_STEP = 0.5
-export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, projectTitle }: ImageLightboxProps) {
+export function ImageLightbox({
+ images,
+ initialIndex = 0,
+ isOpen,
+ onClose,
+ projectTitle,
+}: ImageLightboxProps) {
const [currentIndex, setCurrentIndex] = useState(initialIndex)
const [isLoading, setIsLoading] = useState(true)
const dialogRef = useRef(null)
@@ -58,8 +77,34 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
const pinchStartScale = useRef(MIN_SCALE)
const resetTransform = useCallback(() => {
- setScale(MIN_SCALE)
- setTranslate({ x: 0, y: 0 })
+ setScale((s) => (s === MIN_SCALE ? s : MIN_SCALE))
+ setTranslate((t) => (t.x === 0 && t.y === 0 ? t : { x: 0, y: 0 }))
+ }, [])
+
+ const navigateToNext = useCallback(() => {
+ if (currentIndex < images.length - 1) {
+ setCurrentIndex((prev) => prev + 1)
+ setIsLoading(true)
+ resetTransform()
+ if (projectTitle) analytics.trackImageNavigate(projectTitle, currentIndex + 1)
+ }
+ }, [currentIndex, images.length, projectTitle, analytics, resetTransform])
+
+ const navigateToPrevious = useCallback(() => {
+ if (currentIndex > 0) {
+ setCurrentIndex((prev) => prev - 1)
+ setIsLoading(true)
+ resetTransform()
+ if (projectTitle) analytics.trackImageNavigate(projectTitle, currentIndex - 1)
+ }
+ }, [currentIndex, projectTitle, analytics, resetTransform])
+
+ const toggleFullscreen = useCallback(() => {
+ if (!document.fullscreenElement) {
+ dialogRef.current?.requestFullscreen?.()
+ } else {
+ document.exitFullscreen?.()
+ }
}, [])
// Reset state when dialog opens/closes or image changes
@@ -124,39 +169,13 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
- }, [isOpen, currentIndex, images.length, scale])
+ }, [isOpen, scale, navigateToNext, navigateToPrevious, onClose, resetTransform, toggleFullscreen])
// Focus management
useEffect(() => {
if (isOpen && dialogRef.current) dialogRef.current.focus()
}, [isOpen])
- const navigateToNext = useCallback(() => {
- if (currentIndex < images.length - 1) {
- setCurrentIndex((prev) => prev + 1)
- setIsLoading(true)
- resetTransform()
- if (projectTitle) analytics.trackImageNavigate(projectTitle, currentIndex + 1)
- }
- }, [currentIndex, images.length, projectTitle, analytics, resetTransform])
-
- const navigateToPrevious = useCallback(() => {
- if (currentIndex > 0) {
- setCurrentIndex((prev) => prev - 1)
- setIsLoading(true)
- resetTransform()
- if (projectTitle) analytics.trackImageNavigate(projectTitle, currentIndex - 1)
- }
- }, [currentIndex, projectTitle, analytics, resetTransform])
-
- const toggleFullscreen = useCallback(() => {
- if (!document.fullscreenElement) {
- dialogRef.current?.requestFullscreen?.()
- } else {
- document.exitFullscreen?.()
- }
- }, [])
-
const handleDownload = useCallback(async () => {
const currentImage = images[currentIndex]
try {
@@ -187,18 +206,15 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
}, [scale, resetTransform])
// Wheel to zoom
- const handleWheel = useCallback(
- (e: React.WheelEvent) => {
- e.preventDefault()
- const delta = e.deltaY > 0 ? -SCALE_STEP : SCALE_STEP
- setScale((prev) => {
- const next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, prev + delta))
- if (next === MIN_SCALE) setTranslate({ x: 0, y: 0 })
- return next
- })
- },
- [],
- )
+ const handleWheel = useCallback((e: React.WheelEvent) => {
+ e.preventDefault()
+ const delta = e.deltaY > 0 ? -SCALE_STEP : SCALE_STEP
+ setScale((prev) => {
+ const next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, prev + delta))
+ if (next === MIN_SCALE) setTranslate({ x: 0, y: 0 })
+ return next
+ })
+ }, [])
// Pointer drag for panning
const handlePointerDown = useCallback(
@@ -209,7 +225,7 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
translateOrigin.current = { ...translate }
;(e.target as HTMLElement).setPointerCapture(e.pointerId)
},
- [scale, translate],
+ [scale, translate]
)
const handlePointerMove = useCallback(
@@ -224,7 +240,7 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
y: translateOrigin.current.y + dy / scale,
})
},
- [scale],
+ [scale]
)
const handlePointerUp = useCallback((e: React.PointerEvent) => {
@@ -245,23 +261,20 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
pinchStartScale.current = scale
}
},
- [scale],
+ [scale]
)
- const handleTouchMove = useCallback(
- (e: React.TouchEvent) => {
- if (e.touches.length === 2 && pinchStartDist.current !== null) {
- const dx = e.touches[0].clientX - e.touches[1].clientX
- const dy = e.touches[0].clientY - e.touches[1].clientY
- const dist = Math.hypot(dx, dy)
- const ratio = dist / pinchStartDist.current
- const next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinchStartScale.current * ratio))
- setScale(next)
- if (next === MIN_SCALE) setTranslate({ x: 0, y: 0 })
- }
- },
- [],
- )
+ const handleTouchMove = useCallback((e: React.TouchEvent) => {
+ if (e.touches.length === 2 && pinchStartDist.current !== null) {
+ const dx = e.touches[0].clientX - e.touches[1].clientX
+ const dy = e.touches[0].clientY - e.touches[1].clientY
+ const dist = Math.hypot(dx, dy)
+ const ratio = dist / pinchStartDist.current
+ const next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinchStartScale.current * ratio))
+ setScale(next)
+ if (next === MIN_SCALE) setTranslate({ x: 0, y: 0 })
+ }
+ }, [])
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
@@ -273,7 +286,7 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
if (scale > MIN_SCALE || e.changedTouches.length === 0) return
// Swipe detection handled via pointer events on single touch
},
- [scale],
+ [scale]
)
if (!isOpen) return null
@@ -287,14 +300,19 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
+
+ {projectTitle ? `${projectTitle} — Image lightbox` : "Image lightbox"}
+
+
+ Image {currentIndex + 1}
+ {images.length > 1 ? ` of ${images.length}` : ""}. Use arrow keys to navigate.
+
{/* Header */}
@@ -389,7 +407,7 @@ export function ImageLightbox({ images, initialIndex = 0, isOpen, onClose, proje
diff --git a/components/ui/input-otp.tsx b/components/ui/input-otp.tsx
index 3f6c477..3c5b003 100644
--- a/components/ui/input-otp.tsx
+++ b/components/ui/input-otp.tsx
@@ -1,10 +1,10 @@
-'use client'
+"use client"
-import * as React from 'react'
-import { OTPInput, OTPInputContext } from 'input-otp'
-import { MinusIcon } from 'lucide-react'
+import * as React from "react"
+import { OTPInput, OTPInputContext } from "input-otp"
+import { MinusIcon } from "lucide-react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
function InputOTP({
className,
@@ -16,23 +16,16 @@ function InputOTP({
return (
)
}
-function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
+function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
-
+
)
}
@@ -40,7 +33,7 @@ function InputOTPSlot({
index,
className,
...props
-}: React.ComponentProps<'div'> & {
+}: React.ComponentProps<"div"> & {
index: number
}) {
const inputOTPContext = React.useContext(OTPInputContext)
@@ -51,8 +44,8 @@ function InputOTPSlot({
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
- 'data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
- className,
+ "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
+ className
)}
{...props}
>
@@ -66,7 +59,7 @@ function InputOTPSlot({
)
}
-function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
+function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
index 75fee7d..03295ca 100644
--- a/components/ui/input.tsx
+++ b/components/ui/input.tsx
@@ -1,17 +1,17 @@
-import * as React from 'react'
+import * as React from "react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
index 5d66da2..7d50284 100644
--- a/components/ui/label.tsx
+++ b/components/ui/label.tsx
@@ -1,20 +1,17 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as LabelPrimitive from '@radix-ui/react-label'
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Label({
- className,
- ...props
-}: React.ComponentProps
) {
+function Label({ className, ...props }: React.ComponentProps) {
return (
diff --git a/components/ui/menubar.tsx b/components/ui/menubar.tsx
index 791360c..cc1d53f 100644
--- a/components/ui/menubar.tsx
+++ b/components/ui/menubar.tsx
@@ -1,51 +1,38 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as MenubarPrimitive from '@radix-ui/react-menubar'
-import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
+import * as React from "react"
+import * as MenubarPrimitive from "@radix-ui/react-menubar"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Menubar({
- className,
- ...props
-}: React.ComponentProps) {
+function Menubar({ className, ...props }: React.ComponentProps) {
return (
)
}
-function MenubarMenu({
- ...props
-}: React.ComponentProps) {
+function MenubarMenu({ ...props }: React.ComponentProps) {
return
}
-function MenubarGroup({
- ...props
-}: React.ComponentProps) {
+function MenubarGroup({ ...props }: React.ComponentProps) {
return
}
-function MenubarPortal({
- ...props
-}: React.ComponentProps) {
+function MenubarPortal({ ...props }: React.ComponentProps) {
return
}
-function MenubarRadioGroup({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
+function MenubarRadioGroup({ ...props }: React.ComponentProps) {
+ return
}
function MenubarTrigger({
@@ -56,8 +43,8 @@ function MenubarTrigger({
@@ -66,7 +53,7 @@ function MenubarTrigger({
function MenubarContent({
className,
- align = 'start',
+ align = "start",
alignOffset = -4,
sideOffset = 8,
...props
@@ -79,8 +66,8 @@ function MenubarContent({
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
- 'bg-popover text-popover-foreground 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md',
- className,
+ "bg-popover text-popover-foreground 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
+ className
)}
{...props}
/>
@@ -91,11 +78,11 @@ function MenubarContent({
function MenubarItem({
className,
inset,
- variant = 'default',
+ variant = "default",
...props
}: React.ComponentProps & {
inset?: boolean
- variant?: 'default' | 'destructive'
+ variant?: "default" | "destructive"
}) {
return (
@@ -122,7 +109,7 @@ function MenubarCheckboxItem({
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- className,
+ className
)}
checked={checked}
{...props}
@@ -147,7 +134,7 @@ function MenubarRadioItem({
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- className,
+ className
)}
{...props}
>
@@ -172,10 +159,7 @@ function MenubarLabel({
)
@@ -188,31 +172,23 @@ function MenubarSeparator({
return (
)
}
-function MenubarShortcut({
- className,
- ...props
-}: React.ComponentProps<'span'>) {
+function MenubarShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
)
}
-function MenubarSub({
- ...props
-}: React.ComponentProps) {
+function MenubarSub({ ...props }: React.ComponentProps) {
return
}
@@ -229,8 +205,8 @@ function MenubarSubTrigger({
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
- 'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8',
- className,
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
+ className
)}
{...props}
>
@@ -248,8 +224,8 @@ function MenubarSubContent({
diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx
index 53e7543..a641622 100644
--- a/components/ui/navigation-menu.tsx
+++ b/components/ui/navigation-menu.tsx
@@ -1,9 +1,9 @@
-import * as React from 'react'
-import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
-import { cva } from 'class-variance-authority'
-import { ChevronDownIcon } from 'lucide-react'
+import * as React from "react"
+import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
+import { cva } from "class-variance-authority"
+import { ChevronDownIcon } from "lucide-react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
function NavigationMenu({
className,
@@ -18,8 +18,8 @@ function NavigationMenu({
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
- 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
- className,
+ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
+ className
)}
{...props}
>
@@ -36,10 +36,7 @@ function NavigationMenuList({
return (
)
@@ -52,14 +49,14 @@ function NavigationMenuItem({
return (
)
}
const navigationMenuTriggerStyle = cva(
- 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1',
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
@@ -70,10 +67,10 @@ function NavigationMenuTrigger({
return (
- {children}{' '}
+ {children}{" "}
@@ -104,14 +101,12 @@ function NavigationMenuViewport({
...props
}: React.ComponentProps) {
return (
-
+
@@ -128,7 +123,7 @@ function NavigationMenuLink({
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
- className,
+ className
)}
{...props}
/>
@@ -143,8 +138,8 @@ function NavigationMenuIndicator({
diff --git a/components/ui/pagination.tsx b/components/ui/pagination.tsx
index ce8f25d..f3031f7 100644
--- a/components/ui/pagination.tsx
+++ b/components/ui/pagination.tsx
@@ -1,79 +1,64 @@
-import * as React from 'react'
-import {
- ChevronLeftIcon,
- ChevronRightIcon,
- MoreHorizontalIcon,
-} from 'lucide-react'
+import * as React from "react"
+import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
-import { cn } from '@/lib/utils'
-import { Button, buttonVariants } from '@/components/ui/button'
+import { cn } from "@/lib/utils"
+import { type Button, buttonVariants } from "@/components/ui/button"
-function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
+function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
)
}
-function PaginationContent({
- className,
- ...props
-}: React.ComponentProps<'ul'>) {
+function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) {
return (
)
}
-function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
+function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return
}
type PaginationLinkProps = {
isActive?: boolean
-} & Pick, 'size'> &
- React.ComponentProps<'a'>
+} & Pick, "size"> &
+ React.ComponentProps<"a">
-function PaginationLink({
- className,
- isActive,
- size = 'icon',
- ...props
-}: PaginationLinkProps) {
+function PaginationLink({ className, isActive, size = "icon", ...props }: PaginationLinkProps) {
return (
)
}
-function PaginationPrevious({
- className,
- ...props
-}: React.ComponentProps) {
+function PaginationPrevious({ className, ...props }: React.ComponentProps) {
return (
@@ -82,15 +67,12 @@ function PaginationPrevious({
)
}
-function PaginationNext({
- className,
- ...props
-}: React.ComponentProps) {
+function PaginationNext({ className, ...props }: React.ComponentProps) {
return (
Next
@@ -99,15 +81,12 @@ function PaginationNext({
)
}
-function PaginationEllipsis({
- className,
- ...props
-}: React.ComponentProps<'span'>) {
+function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) {
return (
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
index b4fc827..4eb2ae5 100644
--- a/components/ui/popover.tsx
+++ b/components/ui/popover.tsx
@@ -1,25 +1,21 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as PopoverPrimitive from '@radix-ui/react-popover'
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Popover({
- ...props
-}: React.ComponentProps) {
+function Popover({ ...props }: React.ComponentProps) {
return
}
-function PopoverTrigger({
- ...props
-}: React.ComponentProps) {
+function PopoverTrigger({ ...props }: React.ComponentProps) {
return
}
function PopoverContent({
className,
- align = 'center',
+ align = "center",
sideOffset = 4,
...props
}: React.ComponentProps) {
@@ -30,8 +26,8 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
className={cn(
- 'bg-popover 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
- className,
+ "bg-popover 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+ className
)}
{...props}
/>
@@ -39,9 +35,7 @@ function PopoverContent({
)
}
-function PopoverAnchor({
- ...props
-}: React.ComponentProps) {
+function PopoverAnchor({ ...props }: React.ComponentProps) {
return
}
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx
index ab38305..a87c1db 100644
--- a/components/ui/progress.tsx
+++ b/components/ui/progress.tsx
@@ -1,9 +1,9 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as ProgressPrimitive from '@radix-ui/react-progress'
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
function Progress({
className,
@@ -13,10 +13,7 @@ function Progress({
return (
)
@@ -27,8 +27,8 @@ function RadioGroupItem({
diff --git a/components/ui/resizable.tsx b/components/ui/resizable.tsx
index 5c2f91d..ac76bc9 100644
--- a/components/ui/resizable.tsx
+++ b/components/ui/resizable.tsx
@@ -1,46 +1,41 @@
-'use client'
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+// @ts-nocheck
+/* eslint-enable @typescript-eslint/ban-ts-comment */
+"use client"
-import * as React from 'react'
-import { GripVerticalIcon } from 'lucide-react'
-import * as ResizablePrimitive from 'react-resizable-panels'
+import * as React from "react"
+import { GripVerticalIcon } from "lucide-react"
+import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function ResizablePanelGroup({
- className,
- ...props
-}: React.ComponentProps) {
+function ResizablePanelGroup({ className, ...props }: React.ComponentProps) {
return (
-
)
}
-function ResizablePanel({
- ...props
-}: React.ComponentProps) {
- return
+function ResizablePanel({ ...props }: React.ComponentProps) {
+ return
}
function ResizableHandle({
withHandle,
className,
...props
-}: React.ComponentProps & {
+}: React.ComponentProps & {
withHandle?: boolean
}) {
return (
- div]:rotate-90',
- className,
+ "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden 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-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
+ className
)}
{...props}
>
@@ -49,7 +44,7 @@ function ResizableHandle({
)}
-
+
)
}
diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx
index dc98eb0..2edf7b6 100644
--- a/components/ui/scroll-area.tsx
+++ b/components/ui/scroll-area.tsx
@@ -1,9 +1,9 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
function ScrollArea({
className,
@@ -13,7 +13,7 @@ function ScrollArea({
return (
) {
return (
@@ -38,12 +38,10 @@ function ScrollBar({
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
- 'flex touch-none p-px transition-colors select-none',
- orientation === 'vertical' &&
- 'h-full w-2.5 border-l border-l-transparent',
- orientation === 'horizontal' &&
- 'h-2.5 flex-col border-t border-t-transparent',
- className,
+ "flex touch-none p-px transition-colors select-none",
+ orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
+ orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
+ className
)}
{...props}
>
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
index 5db0bca..080d023 100644
--- a/components/ui/select.tsx
+++ b/components/ui/select.tsx
@@ -1,36 +1,30 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SelectPrimitive from '@radix-ui/react-select'
-import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Select({
- ...props
-}: React.ComponentProps) {
+function Select({ ...props }: React.ComponentProps) {
return
}
-function SelectGroup({
- ...props
-}: React.ComponentProps) {
+function SelectGroup({ ...props }: React.ComponentProps) {
return
}
-function SelectValue({
- ...props
-}: React.ComponentProps) {
+function SelectValue({ ...props }: React.ComponentProps) {
return
}
function SelectTrigger({
className,
- size = 'default',
+ size = "default",
children,
...props
}: React.ComponentProps & {
- size?: 'sm' | 'default'
+ size?: "sm" | "default"
}) {
return (
@@ -53,7 +47,7 @@ function SelectTrigger({
function SelectContent({
className,
children,
- position = 'popper',
+ position = "popper",
...props
}: React.ComponentProps) {
return (
@@ -61,10 +55,10 @@ function SelectContent({
{children}
@@ -85,14 +79,11 @@ function SelectContent({
)
}
-function SelectLabel({
- className,
- ...props
-}: React.ComponentProps) {
+function SelectLabel({ className, ...props }: React.ComponentProps) {
return (
)
@@ -108,7 +99,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
- className,
+ className
)}
{...props}
>
@@ -129,7 +120,7 @@ function SelectSeparator({
return (
)
@@ -142,10 +133,7 @@ function SelectScrollUpButton({
return (
@@ -160,10 +148,7 @@ function SelectScrollDownButton({
return (
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
index f43aaf5..275381c 100644
--- a/components/ui/separator.tsx
+++ b/components/ui/separator.tsx
@@ -1,13 +1,13 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SeparatorPrimitive from '@radix-ui/react-separator'
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
function Separator({
className,
- orientation = 'horizontal',
+ orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps) {
@@ -17,8 +17,8 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
- 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
- className,
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
+ className
)}
{...props}
/>
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx
index 51041fa..e2121c0 100644
--- a/components/ui/sheet.tsx
+++ b/components/ui/sheet.tsx
@@ -1,30 +1,24 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SheetPrimitive from '@radix-ui/react-dialog'
-import { XIcon } from 'lucide-react'
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps) {
return
}
-function SheetTrigger({
- ...props
-}: React.ComponentProps) {
+function SheetTrigger({ ...props }: React.ComponentProps) {
return
}
-function SheetClose({
- ...props
-}: React.ComponentProps) {
+function SheetClose({ ...props }: React.ComponentProps) {
return
}
-function SheetPortal({
- ...props
-}: React.ComponentProps) {
+function SheetPortal({ ...props }: React.ComponentProps) {
return
}
@@ -36,8 +30,8 @@ function SheetOverlay({
@@ -47,10 +41,10 @@ function SheetOverlay({
function SheetContent({
className,
children,
- side = 'right',
+ side = "right",
...props
}: React.ComponentProps & {
- side?: 'top' | 'right' | 'bottom' | 'left'
+ side?: "top" | "right" | "bottom" | "left"
}) {
return (
@@ -58,16 +52,16 @@ function SheetContent({
@@ -81,34 +75,31 @@ function SheetContent({
)
}
-function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
-function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
-function SheetTitle({
- className,
- ...props
-}: React.ComponentProps) {
+function SheetTitle({ className, ...props }: React.ComponentProps) {
return (
)
@@ -121,7 +112,7 @@ function SheetDescription({
return (
)
diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx
index ee74343..ebf0661 100644
--- a/components/ui/sidebar.tsx
+++ b/components/ui/sidebar.tsx
@@ -1,39 +1,34 @@
-'use client'
-
-import * as React from 'react'
-import { Slot } from '@radix-ui/react-slot'
-import { cva, VariantProps } from 'class-variance-authority'
-import { PanelLeftIcon } 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'
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+import { PanelLeftIcon } 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,
SheetDescription,
SheetHeader,
SheetTitle,
-} from '@/components/ui/sheet'
-import { Skeleton } from '@/components/ui/skeleton'
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@/components/ui/tooltip'
+} 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_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'
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
- state: 'expanded' | 'collapsed'
+ state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
@@ -47,7 +42,7 @@ const SidebarContext = React.createContext(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
- throw new Error('useSidebar must be used within a SidebarProvider.')
+ throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
@@ -61,7 +56,7 @@ function SidebarProvider({
style,
children,
...props
-}: React.ComponentProps<'div'> & {
+}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
@@ -75,7 +70,7 @@ function SidebarProvider({
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
- const openState = typeof value === 'function' ? value(open) : value
+ const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
@@ -85,7 +80,7 @@ function SidebarProvider({
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
- [setOpenProp, open],
+ [setOpenProp, open]
)
// Helper to toggle the sidebar.
@@ -96,22 +91,19 @@ function SidebarProvider({
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
- if (
- event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
- (event.metaKey || event.ctrlKey)
- ) {
+ if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
toggleSidebar()
}
}
- window.addEventListener('keydown', handleKeyDown)
- return () => window.removeEventListener('keydown', handleKeyDown)
+ 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 state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo(
() => ({
@@ -123,7 +115,7 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
- [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
@@ -133,14 +125,14 @@ function SidebarProvider({
data-slot="sidebar-wrapper"
style={
{
- '--sidebar-width': SIDEBAR_WIDTH,
- '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
+ "--sidebar-width": SIDEBAR_WIDTH,
+ "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
- 'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
- className,
+ "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
+ className
)}
{...props}
>
@@ -152,26 +144,26 @@ function SidebarProvider({
}
function Sidebar({
- side = 'left',
- variant = 'sidebar',
- collapsible = 'offcanvas',
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
className,
children,
...props
-}: React.ComponentProps<'div'> & {
- side?: 'left' | 'right'
- variant?: 'sidebar' | 'floating' | 'inset'
- collapsible?: 'offcanvas' | 'icon' | 'none'
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
- if (collapsible === 'none') {
+ if (collapsible === "none") {
return (
@@ -190,7 +182,7 @@ function Sidebar({
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
- '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
+ "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
@@ -209,7 +201,7 @@ function Sidebar({
@@ -253,11 +245,7 @@ function Sidebar({
)
}
-function SidebarTrigger({
- className,
- onClick,
- ...props
-}: React.ComponentProps
) {
+function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) {
const { toggleSidebar } = useSidebar()
return (
@@ -266,7 +254,7 @@ function SidebarTrigger({
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
- className={cn('size-7', className)}
+ className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
@@ -279,7 +267,7 @@ function SidebarTrigger({
)
}
-function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
@@ -291,103 +279,97 @@ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
- 'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex',
- 'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
- '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
- 'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
- '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
- '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
- className,
+ "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
+ "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
+ "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
+ "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
+ "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
+ className
)}
{...props}
/>
)
}
-function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
)
}
-function SidebarInput({
- className,
- ...props
-}: React.ComponentProps) {
+function SidebarInput({ className, ...props }: React.ComponentProps) {
return (
)
}
-function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
-function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
-function SidebarSeparator({
- className,
- ...props
-}: React.ComponentProps) {
+function SidebarSeparator({ className, ...props }: React.ComponentProps) {
return (
)
}
-function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
-function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
)
@@ -397,17 +379,17 @@ function SidebarGroupLabel({
className,
asChild = false,
...props
-}: React.ComponentProps<'div'> & { asChild?: boolean }) {
- const Comp = asChild ? Slot : 'div'
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "div"
return (
svg]:size-4 [&>svg]:shrink-0',
- 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
- className,
+ "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 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}
/>
@@ -418,97 +400,94 @@ function SidebarGroupAction({
className,
asChild = false,
...props
-}: React.ComponentProps<'button'> & { asChild?: boolean }) {
- const Comp = asChild ? Slot : 'button'
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "button"
return (
svg]:size-4 [&>svg]:shrink-0',
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
- 'after:absolute after:-inset-2 md:after:hidden',
- 'group-data-[collapsible=icon]:hidden',
- className,
+ "after:absolute after:-inset-2 md:after:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
)}
{...props}
/>
)
}
-function SidebarGroupContent({
- className,
- ...props
-}: React.ComponentProps<'div'>) {
+function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
-function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
)
}
-function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
)
}
const sidebarMenuButtonVariants = cva(
- 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden 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',
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden 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',
+ 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))]',
+ "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!',
+ 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',
+ variant: "default",
+ size: "default",
},
- },
+ }
)
function SidebarMenuButton({
asChild = false,
isActive = false,
- variant = 'default',
- size = 'default',
+ variant = "default",
+ size = "default",
tooltip,
className,
...props
-}: React.ComponentProps<'button'> & {
+}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps
} & VariantProps) {
- const Comp = asChild ? Slot : 'button'
+ const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
@@ -526,7 +505,7 @@ function SidebarMenuButton({
return button
}
- if (typeof tooltip === 'string') {
+ if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
@@ -538,7 +517,7 @@ function SidebarMenuButton({
@@ -550,49 +529,46 @@ function SidebarMenuAction({
asChild = false,
showOnHover = false,
...props
-}: React.ComponentProps<'button'> & {
+}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
- const Comp = asChild ? Slot : 'button'
+ const Comp = asChild ? Slot : "button"
return (
svg]:size-4 [&>svg]:shrink-0',
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
- 'after:absolute after:-inset-2 md:after: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',
+ "after:absolute after:-inset-2 md:after: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 &&
- 'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
- className,
+ "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+ className
)}
{...props}
/>
)
}
-function SidebarMenuBadge({
- className,
- ...props
-}: React.ComponentProps<'div'>) {
+function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -603,7 +579,7 @@ function SidebarMenuSkeleton({
className,
showIcon = false,
...props
-}: React.ComponentProps<'div'> & {
+}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
@@ -615,21 +591,16 @@ function SidebarMenuSkeleton({
- {showIcon && (
-
- )}
+ {showIcon &&
}
@@ -637,30 +608,27 @@ function SidebarMenuSkeleton({
)
}
-function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
)
}
-function SidebarMenuSubItem({
- className,
- ...props
-}: React.ComponentProps<'li'>) {
+function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
return (
)
@@ -668,16 +636,16 @@ function SidebarMenuSubItem({
function SidebarMenuSubButton({
asChild = false,
- size = 'md',
+ size = "md",
isActive = false,
className,
...props
-}: React.ComponentProps<'a'> & {
+}: React.ComponentProps<"a"> & {
asChild?: boolean
- size?: 'sm' | 'md'
+ size?: "sm" | "md"
isActive?: boolean
}) {
- const Comp = asChild ? Slot : 'a'
+ const Comp = asChild ? Slot : "a"
return (
svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 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',
- '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,
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 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",
+ "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}
/>
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
index e3beb90..32ea0ef 100644
--- a/components/ui/skeleton.tsx
+++ b/components/ui/skeleton.tsx
@@ -1,10 +1,10 @@
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
)
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx
index 5821325..faa8918 100644
--- a/components/ui/slider.tsx
+++ b/components/ui/slider.tsx
@@ -1,9 +1,9 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SliderPrimitive from '@radix-ui/react-slider'
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
function Slider({
className,
@@ -14,13 +14,8 @@ function Slider({
...props
}: React.ComponentProps) {
const _values = React.useMemo(
- () =>
- Array.isArray(value)
- ? value
- : Array.isArray(defaultValue)
- ? defaultValue
- : [min, max],
- [value, defaultValue, min, max],
+ () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),
+ [value, defaultValue, min, max]
)
return (
@@ -31,21 +26,21 @@ function Slider({
min={min}
max={max}
className={cn(
- 'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
- className,
+ "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
+ className
)}
{...props}
>
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
index 42135ba..1b74ec0 100644
--- a/components/ui/sonner.tsx
+++ b/components/ui/sonner.tsx
@@ -1,7 +1,6 @@
"use client"
-import type React from "react"
-
+import { type CSSProperties } from "react"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
@@ -14,7 +13,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
- } as React.CSSProperties
+ } as CSSProperties
}
{...props}
/>
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
index 3c4cfa3..348741c 100644
--- a/components/ui/switch.tsx
+++ b/components/ui/switch.tsx
@@ -1,27 +1,24 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SwitchPrimitive from '@radix-ui/react-switch'
+import * as React from "react"
+import * as SwitchPrimitive from "@radix-ui/react-switch"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Switch({
- className,
- ...props
-}: React.ComponentProps) {
+function Switch({ className, ...props }: React.ComponentProps) {
return (
diff --git a/components/ui/table.tsx b/components/ui/table.tsx
index fcdd10c..6a54b8a 100644
--- a/components/ui/table.tsx
+++ b/components/ui/table.tsx
@@ -1,116 +1,92 @@
-'use client'
+"use client"
-import * as React from 'react'
+import * as React from "react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Table({ className, ...props }: React.ComponentProps<'table'>) {
+function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
-
+
)
}
-function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
- return (
-
- )
+function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
+ return
}
-function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
+function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
)
}
-function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
+function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
tr]:last:border-b-0',
- className,
- )}
+ className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
)
}
-function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
)
}
-function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
[role=checkbox]]:translate-y-[2px]',
- className,
+ "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+ className
)}
{...props}
/>
)
}
-function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
| [role=checkbox]]:translate-y-[2px]',
- className,
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
+ className
)}
{...props}
/>
)
}
-function TableCaption({
- className,
- ...props
-}: React.ComponentProps<'caption'>) {
+function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
return (
)
}
-export {
- Table,
- TableHeader,
- TableBody,
- TableFooter,
- TableHead,
- TableRow,
- TableCell,
- TableCaption,
-}
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
index ff67104..9abd7fe 100644
--- a/components/ui/tabs.tsx
+++ b/components/ui/tabs.tsx
@@ -1,63 +1,51 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as TabsPrimitive from '@radix-ui/react-tabs'
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Tabs({
- className,
- ...props
-}: React.ComponentProps) {
+function Tabs({ className, ...props }: React.ComponentProps) {
return (
)
}
-function TabsList({
- className,
- ...props
-}: React.ComponentProps) {
+function TabsList({ className, ...props }: React.ComponentProps) {
return (
)
}
-function TabsTrigger({
- className,
- ...props
-}: React.ComponentProps) {
+function TabsTrigger({ className, ...props }: React.ComponentProps) {
return (
)
}
-function TabsContent({
- className,
- ...props
-}: React.ComponentProps) {
+function TabsContent({ className, ...props }: React.ComponentProps) {
return (
)
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
index 3809775..7f21b5e 100644
--- a/components/ui/textarea.tsx
+++ b/components/ui/textarea.tsx
@@ -1,14 +1,14 @@
-import * as React from 'react'
+import * as React from "react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
-function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx
index 40eedf5..3c61932 100644
--- a/components/ui/toast.tsx
+++ b/components/ui/toast.tsx
@@ -1,11 +1,11 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as ToastPrimitives from '@radix-ui/react-toast'
-import { cva, type VariantProps } from 'class-variance-authority'
-import { X } from 'lucide-react'
+import * 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'
+import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
@@ -16,8 +16,8 @@ const ToastViewport = React.forwardRef<
@@ -25,25 +25,24 @@ const ToastViewport = React.forwardRef<
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',
+ "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',
+ default: "border bg-background text-foreground",
destructive:
- 'destructive group border-destructive bg-destructive text-destructive-foreground',
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
- variant: 'default',
+ variant: "default",
},
- },
+ }
)
const Toast = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef &
- VariantProps
+ React.ComponentPropsWithoutRef & VariantProps
>(({ className, variant, ...props }, ref) => {
return (
@@ -77,8 +76,8 @@ const ToastClose = React.forwardRef<
,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
+
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
@@ -106,7 +101,7 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
))
diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx
index 3b91885..d957061 100644
--- a/components/ui/toaster.tsx
+++ b/components/ui/toaster.tsx
@@ -1,6 +1,6 @@
-'use client'
+"use client"
-import { useToast } from '@/hooks/use-toast'
+import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
@@ -8,7 +8,7 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
-} from '@/components/ui/toast'
+} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
@@ -20,9 +20,7 @@ export function Toaster() {
{title && {title}}
- {description && (
- {description}
- )}
+ {description && {description}}
{action}
diff --git a/components/ui/toggle-group.tsx b/components/ui/toggle-group.tsx
index 0ab9971..a456721 100644
--- a/components/ui/toggle-group.tsx
+++ b/components/ui/toggle-group.tsx
@@ -1,17 +1,15 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
-import { type VariantProps } from 'class-variance-authority'
+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'
+import { cn } from "@/lib/utils"
+import { toggleVariants } from "@/components/ui/toggle"
-const ToggleGroupContext = React.createContext<
- VariantProps
->({
- size: 'default',
- variant: 'default',
+const ToggleGroupContext = React.createContext>({
+ size: "default",
+ variant: "default",
})
function ToggleGroup({
@@ -20,16 +18,15 @@ function ToggleGroup({
size,
children,
...props
-}: React.ComponentProps &
- VariantProps) {
+}: React.ComponentProps & VariantProps) {
return (
@@ -46,8 +43,7 @@ function ToggleGroupItem({
variant,
size,
...props
-}: React.ComponentProps &
- VariantProps) {
+}: React.ComponentProps & VariantProps) {
const context = React.useContext(ToggleGroupContext)
return (
@@ -60,8 +56,8 @@ function ToggleGroupItem({
variant: context.variant || variant,
size: context.size || size,
}),
- 'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
- className,
+ "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
+ className
)}
{...props}
>
diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx
index ad6b286..0b27843 100644
--- a/components/ui/toggle.tsx
+++ b/components/ui/toggle.tsx
@@ -1,31 +1,31 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as TogglePrimitive from '@radix-ui/react-toggle'
-import { cva, type VariantProps } from 'class-variance-authority'
+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'
+import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
- default: 'bg-transparent',
+ default: "bg-transparent",
outline:
- 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
- default: 'h-9 px-2 min-w-9',
- sm: 'h-8 px-1.5 min-w-8',
- lg: 'h-10 px-2.5 min-w-10',
+ default: "h-9 px-2 min-w-9",
+ sm: "h-8 px-1.5 min-w-8",
+ lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
- variant: 'default',
- size: 'default',
+ variant: "default",
+ size: "default",
},
- },
+ }
)
function Toggle({
@@ -33,8 +33,7 @@ function Toggle({
variant,
size,
...props
-}: React.ComponentProps &
- VariantProps) {
+}: React.ComponentProps & VariantProps) {
return (
) {
+function Tooltip({ ...props }: React.ComponentProps) {
return (
@@ -28,9 +26,7 @@ function Tooltip({
)
}
-function TooltipTrigger({
- ...props
-}: React.ComponentProps) {
+function TooltipTrigger({ ...props }: React.ComponentProps) {
return
}
@@ -46,8 +42,8 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
- 'bg-primary text-primary-foreground 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
- className,
+ "bg-primary text-primary-foreground 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+ className
)}
{...props}
>
diff --git a/components/ui/use-mobile.tsx b/components/ui/use-mobile.tsx
index 4331d5c..2b0fe1d 100644
--- a/components/ui/use-mobile.tsx
+++ b/components/ui/use-mobile.tsx
@@ -1,4 +1,4 @@
-import * as React from 'react'
+import * as React from "react"
const MOBILE_BREAKPOINT = 768
@@ -10,9 +10,9 @@ export function useIsMobile() {
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
- mql.addEventListener('change', onChange)
+ mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener('change', onChange)
+ return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
diff --git a/components/ui/use-toast.ts b/components/ui/use-toast.ts
index 8932bc5..2e4c6ea 100644
--- a/components/ui/use-toast.ts
+++ b/components/ui/use-toast.ts
@@ -1,9 +1,9 @@
-'use client'
+"use client"
// Inspired by react-hot-toast library
-import * as React from 'react'
+import * as React from "react"
-import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
+import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
@@ -16,10 +16,10 @@ type ToasterToast = ToastProps & {
}
const actionTypes = {
- ADD_TOAST: 'ADD_TOAST',
- UPDATE_TOAST: 'UPDATE_TOAST',
- DISMISS_TOAST: 'DISMISS_TOAST',
- REMOVE_TOAST: 'REMOVE_TOAST',
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
@@ -33,20 +33,20 @@ type ActionType = typeof actionTypes
type Action =
| {
- type: ActionType['ADD_TOAST']
+ type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
- type: ActionType['UPDATE_TOAST']
+ type: ActionType["UPDATE_TOAST"]
toast: Partial
}
| {
- type: ActionType['DISMISS_TOAST']
- toastId?: ToasterToast['id']
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
}
| {
- type: ActionType['REMOVE_TOAST']
- toastId?: ToasterToast['id']
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
}
interface State {
@@ -63,7 +63,7 @@ const addToRemoveQueue = (toastId: string) => {
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
- type: 'REMOVE_TOAST',
+ type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
@@ -73,21 +73,19 @@ const addToRemoveQueue = (toastId: string) => {
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
- case 'ADD_TOAST':
+ case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
- case 'UPDATE_TOAST':
+ case "UPDATE_TOAST":
return {
...state,
- toasts: state.toasts.map((t) =>
- t.id === action.toast.id ? { ...t, ...action.toast } : t,
- ),
+ toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
}
- case 'DISMISS_TOAST': {
+ case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
@@ -108,11 +106,11 @@ export const reducer = (state: State, action: Action): State => {
...t,
open: false,
}
- : t,
+ : t
),
}
}
- case 'REMOVE_TOAST':
+ case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
@@ -137,20 +135,20 @@ function dispatch(action: Action) {
})
}
-type Toast = Omit
+type Toast = Omit
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
- type: 'UPDATE_TOAST',
+ type: "UPDATE_TOAST",
toast: { ...props, id },
})
- const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
- type: 'ADD_TOAST',
+ type: "ADD_TOAST",
toast: {
...props,
id,
@@ -184,7 +182,7 @@ function useToast() {
return {
...state,
toast,
- dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
diff --git a/content/projects.json b/content/projects.json
index 701825b..e91e9f6 100644
--- a/content/projects.json
+++ b/content/projects.json
@@ -1,284 +1,338 @@
-{
- "projects": [
- {
- "id": "farmhero",
- "locales": {
- "en": {
- "title": "FarmHero",
- "short": "Educational simulator using real NASA/CONAE data to teach sustainable agriculture. Regional Winner and Global Nominee at NASA Space Apps 2025.",
- "description": "Educational simulator that uses real NASA and CONAE satellite data to teach resource-management and sustainable agriculture decisions. Players restore degraded land by managing limited capital and choosing interventions (fertilizers, pesticides and irrigation), whose short- and long-term effects the game simulates. Regional Winner and Global Nominee at NASA Space Apps 2025.",
- "features": [
- "Simulation of agricultural interventions (irrigation, fertilization, pesticides) with short and long-term effects",
- "Real data ingestion and normalization: NASA and CONAE satellite layers",
- "Educational UI with step-by-step guidance widgets",
- "Agricultural rules and heuristics engine (extensible with ML)",
- "Simulation of climatic/statistical events (droughts, heat waves, floods) with probabilistic impacts on crops and resources to evaluate mitigation strategies",
- "Prototype developed in 48 hours; continuous post-hackathon iteration"
- ]
- },
- "es": {
- "title": "FarmHero",
- "short": "Simulador educativo que utiliza datos satelitales de NASA y CONAE para enseñar agricultura sostenible. Ganador Regional y Nominado Global por la NASA.",
- "description": "Simulador educativo que utiliza datos reales de satélites de NASA y CONAE para enseñar gestión de recursos y decisiones de agricultura sostenible. Los jugadores restauran tierras degradadas gestionando capital limitado y eligiendo intervenciones (fertilizantes, plaguicidas e irrigación), cuyos efectos a corto y largo plazo simula el juego. Ganador Regional y Nominado Global en NASA Space Apps 2025.",
- "features": [
- "Simulación de intervenciones agrícolas (irrigación, fertilización, plaguicidas) con efectos a corto y largo plazo",
- "Ingesta y normalización de datos reales: capas satelitales de NASA y CONAE",
- "Interfaz educativa con widgets de orientación paso a paso",
- "Motor de reglas y heurísticas agrícolas (extensible con ML)",
- "Simulación de eventos climáticos/estadísticos (sequías, olas de calor, inundaciones) con impactos probabilísticos en cultivos y recursos para evaluar estrategias de mitigación",
- "Prototipo desarrollado en 48 horas; iteración continua post-hackathon"
- ]
- }
- },
- "status": "production",
- "techStack": ["Next.js", "React", "TypeScript", "Tailwind CSS", "NASA & CONAE Data"],
- "githubUrl": "https://github.com/Repetto-A/NASA-FarmHero",
- "demoUrl": "https://nasa-farm-hero.vercel.app/",
- "images": [
- "/projects/farmhero-1.jpg",
- "/projects/farmhero-2.jpg",
- "/projects/farmhero-3.jpg",
- "/projects/farmhero-4.jpg",
- "/projects/farmhero-5.jpg"
- ],
- "category": "Game / AgTech"
- },
- {
- "id": "nasa-spaceapps",
- "locales": {
- "en": {
- "title": "Lunar & Martian Seismic Detection",
- "short": "On-board LSTM model trained on NASA Apollo & InSight data to filter seismic signals from noise in deep space. 2nd place NASA Space Apps 2024.",
- "description": "Seismic detection system trained on real data from NASA's Apollo (Moon) and InSight (Mars) missions using LSTM (Long Short-Term Memory) neural networks. Transmitting data from distant bodies requires energy that scales with distance, and seismic events are rare, so most transmitted data is noise. Our solution runs classification algorithms on-board to separate seismic signals from noise in situ, sending only useful data back to Earth. The required software and hardware are low-cost in terms of development, operation, energy, and footprint, maximizing the scientific value of every transmitted byte.",
- "features": [
- "Machine learning model for seismic pattern recognition",
- "Data preprocessing pipeline for space seismic data",
- "Real-time seismic event detection",
- "Visualization dashboard for seismic activity",
- "Scientific data analysis and reporting"
- ]
- },
- "es": {
- "title": "Detección Sísmica en la Luna y Marte",
- "short": "Modelo LSTM entrenado con datos de misiones Apollo e InSight para filtrar in situ señales sísmicas del ruido en el espacio profundo. 2do puesto NASA 2024.",
- "description": "Sistema de detección sísmica entrenado con datos reales de las misiones Apollo (Luna) e InSight (Marte) de la NASA usando redes neuronales LSTM (Long Short-Term Memory). Transmitir datos desde cuerpos distantes requiere un consumo energético que escala con la distancia, y los eventos sísmicos son raros, por lo que la mayor parte de los datos transmitidos es ruido. Nuestra solución ejecuta algoritmos de clasificación a bordo para separar las señales sísmicas del ruido in situ, enviando solo datos útiles a la Tierra. El software y hardware necesarios son de bajo costo en desarrollo, operación, energía y espacio, maximizando el valor científico de cada byte transmitido.",
- "features": [
- "Modelo de aprendizaje automático para reconocimiento de patrones sísmicos",
- "Pipeline de preprocesamiento de datos sísmicos espaciales",
- "Detección de eventos sísmicos en tiempo real",
- "Panel de visualización de actividad sísmica",
- "Análisis y reportes de datos científicos"
- ]
- }
- },
- "status": "production",
- "techStack": ["Python", "TensorFlow", "Keras", "Scikit-learn", "Pandas", "NumPy", "Matplotlib"],
- "githubUrl": "https://github.com/Repetto-A/AI-Seismic-Detection",
- "demoUrl": null,
- "images": ["/projects/nasa-1.png", "/projects/nasa-2.png", "/projects/nasa-3.png"],
- "category": "AI/ML Research"
- },
- {
- "id": "braintumorai-web",
- "locales": {
- "en": {
- "title": "Brain Tumor Classifier",
- "short": "Deep learning brain tumor classifier running in your browser for full privacy. ~95% accuracy.",
- "description": "AI-powered brain tumor classification running entirely in your browser with no server required, ensuring complete privacy. Transform PyTorch deep learning models into real-time web applications with ONNX Runtime Web. Features 95.4% test accuracy on 4 tumor classes (Glioma, Meningioma, Pituitary, No Tumor) with blazing fast 200-400ms predictions after warmup.",
- "features": [
- "100% client-side inference - images never leave your device",
- "95.4% test accuracy on 7,000+ brain MRI images",
- "WebGL-accelerated computation with 200-400ms predictions",
- "4 tumor classes: Glioma, Meningioma, Pituitary, No Tumor",
- "Offline-capable after first model load",
- "Responsive design - works on any modern browser"
- ]
- },
- "es": {
- "title": "Clasificador de Tumores Cerebrales",
- "short": "Clasificador de tumores cerebrales con deep learning ejecutándose en tu navegador para lograr privacidad total. ~95% de precisión.",
- "description": "Clasificación de tumores cerebrales potenciada por IA que se ejecuta completamente en tu navegador sin necesidad de servidor, garantizando privacidad total. Transforma modelos de deep learning de PyTorch en aplicaciones web en tiempo real con ONNX Runtime Web. Presenta 95.4% de precisión en 4 clases de tumores (Glioma, Meningioma, Pituitaria, Sin Tumor) con predicciones ultra rápidas de 200-400ms después del calentamiento.",
- "features": [
- "Inferencia 100% del lado del cliente - las imágenes nunca salen de tu dispositivo",
- "95.4% de precisión en más de 7,000 imágenes de resonancia magnética cerebral",
- "Cómputo acelerado por WebGL con predicciones de 200-400ms",
- "4 clases de tumores: Glioma, Meningioma, Pituitaria, Sin Tumor",
- "Funciona sin conexión después de la primera carga del modelo",
- "Diseño responsivo - funciona en cualquier navegador moderno"
- ]
- }
- },
- "status": "production",
- "techStack": ["Python", "Next.js", "React", "TypeScript", "Tailwind CSS", "PyTorch", "Matplotlib", "Pillow", "NumPy", "Scikit-learn"],
- "githubUrl": "https://github.com/Repetto-A/BrainTumorAI-Web",
- "demoUrl": "https://brain-tumor-ai-web.vercel.app",
- "images": [
- "/projects/braintumorai-1.jpg",
- "/projects/braintumorai-2.jpg",
- "/projects/braintumorai-3.jpg"
- ],
- "category": "AI/ML Research"
- },
- {
- "id": "zorrito-finance",
- "locales": {
- "en": {
- "title": "Zorrito Finance",
- "short": "No-loss lottery where you save, win, and raise your own virtual Zorrito. 2nd Best dApp Using Filecoin at ETH Global 2025.",
- "description": "A decentralized finance platform that enables users to earn yield on their savings without risking their principal. Built during ETH Global 2025, awarded 2nd Best dApp Using Filecoin. Users deposit funds into smart contracts that automatically allocate capital to yield-generating protocols, while maintaining full liquidity and zero loss of principal.",
- "features": [
- "No-loss savings mechanism with yield generation",
- "Smart contract-based fund allocation",
- "Integration with Filecoin for decentralized storage",
- "Real-time yield tracking dashboard",
- "Instant withdrawals with no penalties",
- "Multi-protocol yield optimization"
- ]
- },
- "es": {
- "title": "Zorrito Finance",
- "short": "Lotería sin pérdidas en la que ahorrás y ganás mientras cuidás a tu propio Zorrito virtual. 2da Mejor dApp Usando Filecoin en ETH Global 2025.",
- "description": "Una plataforma de finanzas descentralizadas que permite a los usuarios ganar rendimientos sobre sus ahorros sin arriesgar su capital principal. Construida durante ETH Global 2025, premiada como 2do Mejor dApp Usando Filecoin. Los usuarios depositan fondos en contratos inteligentes que asignan automáticamente capital a protocolos generadores de rendimiento, manteniendo liquidez total y cero pérdida de principal.",
- "features": [
- "Mecanismo de ahorro sin pérdidas con generación de rendimientos",
- "Asignación de fondos basada en contratos inteligentes",
- "Integración con Filecoin para almacenamiento descentralizado",
- "Panel de seguimiento de rendimientos en tiempo real",
- "Retiros instantáneos sin penalizaciones",
- "Optimización de rendimientos multi-protocolo"
- ]
- }
- },
- "status": "in-progress",
- "techStack": ["Solidity", "Filecoin", "IPFS", "Self Protocol", "Next.js", "React", "TypeScript", "AI SDK", "Tailwind CSS"],
- "githubUrl": "https://github.com/Repetto-A/zorrito-finance",
- "demoUrl": "https://zorrito.vercel.app/",
- "images": [
- "/projects/zorrito-1.jpg",
- "/projects/zorrito-2.jpg",
- "/projects/zorrito-3.jpg",
- "/projects/zorrito-4.jpeg"
- ],
- "category": "Blockchain/DeFi"
- },
- {
- "id": "roxium-dao-ops",
- "locales": {
- "en": {
- "title": "Roxium DAO Ops",
- "short": "DAO management system that brings transparency and visibility over the status of proposals and tasks. 1st place PowerHouse Ecosystem prize at ETH Argentina.",
- "description": "DAO operations platform built for ETH Argentina. Roxium brings transparency and visibility to DAOs by tracking proposals and tasks with full status history, dashboards and analytics, and AI-powered daily digests that summarize key factors and overall progress contributors need to pay attention to.",
- "features": [
- "Proposal and task tracking with full status history",
- "Dashboards and analytics for DAO activity",
- "AI-powered daily digest with key factor summaries",
- "Overall progress overview for contributors",
- "Bottleneck identification through status trends"
- ]
- },
- "es": {
- "title": "Roxium DAO Ops",
- "short": "Sistema de gestión de DAOs que brinda transparencia y visibilidad sobre el estado de proposals y tasks. 1er premio del ecosistema PowerHouse en ETH Argentina.",
- "description": "Plataforma de operaciones para DAOs construida para ETH Argentina. Roxium brinda transparencia y visibilidad al trackear proposals y tasks con historial de estados completo, dashboards, analytics y resúmenes diarios potenciados con IA que sintetizan los factores clave y el estado general al que los contribuidores deben prestarle atención.",
- "features": [
- "Tracking de proposals y tasks con historial de estados completo",
- "Dashboards y analytics de actividad del DAO",
- "Resumen diario potenciado con IA con factores clave",
- "Vista general del progreso para contribuidores",
- "Identificación de cuellos de botella a través de tendencias de estado"
- ]
- }
- },
- "status": "production",
- "techStack": ["TypeScript", "Next.js", "React", "PowerHouse", "Vetra", "LangChain", "GraphQL", "Tailwind CSS"],
- "githubUrl": "https://github.com/Repetto-A/roxium-dao-ops",
- "demoUrl": "https://roxium-dao-ops.up.railway.app/",
- "images": ["/projects/roxium-1.jpg", "/projects/roxium-2.jpg", "/projects/roxium-3.jpg"],
- "category": "Blockchain/AI"
- },
- {
- "id": "real-estate-system",
- "locales": {
- "en": {
- "title": "Real Estate Management System",
- "short": "Property management system with visit calendars, booking notifications for agents, SEO blog, and more.",
- "description": "A comprehensive property management platform featuring calendar-based booking system, automated agent email confirmations, internal blog for SEO optimization and content generation, newsletter system.",
- "features": [
- "Calendar booking system for property viewings",
- "Automated email confirmations for agents",
- "Internal blog for property updates",
- "Newsletter subscription system",
- "Property listing management"
- ]
- },
- "es": {
- "title": "Sistema de Gestión Inmobiliaria",
- "short": "Sistema de gestión de propiedades que incluye calendarios de visita, reservas con notificaciones a los agentes, newsletter, blog para mejorar el SEO, etc.",
- "description": "Una plataforma integral de gestión de propiedades que incluye sistema de reservas basado en calendario, confirmaciones automáticas por email para agentes, blog interno para actualizaciones de propiedades y sistema de newsletter.",
- "features": [
- "Sistema de reservas por calendario para visitas a propiedades",
- "Confirmaciones automáticas por email para agentes",
- "Blog interno para actualizaciones de propiedades",
- "Sistema de suscripción a newsletter",
- "Gestión de listados de propiedades"
- ]
- }
- },
- "status": "production",
- "techStack": ["Python", "Django", "LangChain", "MySQL", "Bootstrap 5", "Tailwind CSS", "Pillow"],
- "githubUrl": "https://github.com/Repetto-A/Real_Estate_System",
- "demoUrl": "https://realestatesystem-production.up.railway.app/",
- "images": [
- "/projects/realestate-1.png",
- "/projects/realestate-2.png",
- "/projects/realestate-3.png",
- "/projects/realestate-4.png",
- "/projects/realestate-5.png",
- "/projects/realestate-6.png",
- "/projects/realestate-7.png",
- "/projects/realestate-8.png"
- ],
- "category": "Management System"
- },
- {
- "id": "cotibot",
- "locales": {
- "en": {
- "title": "Cotibot",
- "short": "Custom-built system for an agricultural machinery manufacturer: PDF quotes, AFIP CUIT lookup, WhatsApp bot, and more.",
- "description": "An intelligent factory quoting system that streamlines the quotation process for manufacturing businesses. Features automated PDF generation, AFIP CUIT lookup integration for Argentine tax validation, and a comprehensive admin pricing panel.",
- "features": [
- "Automated PDF quote generation",
- "AFIP CUIT lookup integration",
- "Admin pricing management panel",
- "Client quote history tracking",
- "Real-time pricing calculations"
- ]
- },
- "es": {
- "title": "Cotibot",
- "short": "Sistema a medida para un fabricante de maquinaria agrícola: cotizaciones PDF, búsqueda de información en AFIP a partir del CUIT, bot de WhatsApp, etc.",
- "description": "Un sistema inteligente de cotización para fábricas que agiliza el proceso de cotización para empresas manufactureras. Incluye generación automática de PDF, integración de búsqueda CUIT AFIP para validación fiscal argentina, y un panel integral de precios para administradores.",
- "features": [
- "Generación automática de cotizaciones en PDF",
- "Integración con búsqueda de CUIT en AFIP",
- "Panel de gestión de precios para administradores",
- "Seguimiento del historial de cotizaciones de clientes",
- "Cálculos de precios en tiempo real"
- ]
- }
- },
- "status": "production",
- "techStack": ["TypeScript", "Python", "React", "FastAPI", "PostgreSQL", "Tailwind CSS", "Pillow"],
- "githubUrl": "https://github.com/Repetto-A/cotibot",
- "demoUrl": "https://cotibot-demo.vercel.app",
- "images": [
- "/projects/cotibot-1.png",
- "/projects/cotibot-2.png",
- "/projects/cotibot-3.png",
- "/projects/cotibot-4.png"
- ],
- "category": "Business Automation"
- }
- ]
-}
+{
+ "projects": [
+ {
+ "id": "farmhero",
+ "locales": {
+ "en": {
+ "title": "FarmHero",
+ "short": "Educational simulator using real NASA/CONAE data to teach sustainable agriculture. Regional Winner and Global Nominee at NASA Space Apps 2025.",
+ "description": "Educational simulator that uses real NASA and CONAE satellite data to teach resource-management and sustainable agriculture decisions. Players restore degraded land by managing limited capital and choosing interventions (fertilizers, pesticides and irrigation), whose short- and long-term effects the game simulates. Regional Winner and Global Nominee at NASA Space Apps 2025.",
+ "features": [
+ "Simulation of agricultural interventions (irrigation, fertilization, pesticides) with short and long-term effects",
+ "Real data ingestion and normalization: NASA and CONAE satellite layers",
+ "Educational UI with step-by-step guidance widgets",
+ "Agricultural rules and heuristics engine (extensible with ML)",
+ "Simulation of climatic/statistical events (droughts, heat waves, floods) with probabilistic impacts on crops and resources to evaluate mitigation strategies",
+ "Prototype developed in 48 hours; continuous post-hackathon iteration"
+ ]
+ },
+ "es": {
+ "title": "FarmHero",
+ "short": "Simulador educativo que utiliza datos satelitales de NASA y CONAE para enseñar agricultura sostenible. Ganador Regional y Nominado Global por la NASA.",
+ "description": "Simulador educativo que utiliza datos reales de satélites de NASA y CONAE para enseñar gestión de recursos y decisiones de agricultura sostenible. Los jugadores restauran tierras degradadas gestionando capital limitado y eligiendo intervenciones (fertilizantes, plaguicidas e irrigación), cuyos efectos a corto y largo plazo simula el juego. Ganador Regional y Nominado Global en NASA Space Apps 2025.",
+ "features": [
+ "Simulación de intervenciones agrícolas (irrigación, fertilización, plaguicidas) con efectos a corto y largo plazo",
+ "Ingesta y normalización de datos reales: capas satelitales de NASA y CONAE",
+ "Interfaz educativa con widgets de orientación paso a paso",
+ "Motor de reglas y heurísticas agrícolas (extensible con ML)",
+ "Simulación de eventos climáticos/estadísticos (sequías, olas de calor, inundaciones) con impactos probabilísticos en cultivos y recursos para evaluar estrategias de mitigación",
+ "Prototipo desarrollado en 48 horas; iteración continua post-hackathon"
+ ]
+ }
+ },
+ "status": "production",
+ "techStack": ["Next.js", "React", "TypeScript", "Tailwind CSS", "NASA & CONAE Data"],
+ "githubUrl": "https://github.com/Repetto-A/NASA-FarmHero",
+ "demoUrl": "https://nasa-farm-hero.vercel.app/",
+ "images": [
+ "/projects/farmhero-1.jpg",
+ "/projects/farmhero-2.jpg",
+ "/projects/farmhero-3.jpg",
+ "/projects/farmhero-4.jpg",
+ "/projects/farmhero-5.jpg"
+ ],
+ "category": "Game / AgTech"
+ },
+ {
+ "id": "nasa-spaceapps",
+ "locales": {
+ "en": {
+ "title": "Lunar & Martian Seismic Detection",
+ "short": "On-board LSTM model trained on NASA Apollo & InSight data to filter seismic signals from noise in deep space. 2nd place NASA Space Apps 2024.",
+ "description": "Seismic detection system trained on real data from NASA's Apollo (Moon) and InSight (Mars) missions using LSTM (Long Short-Term Memory) neural networks. Transmitting data from distant bodies requires energy that scales with distance, and seismic events are rare, so most transmitted data is noise. Our solution runs classification algorithms on-board to separate seismic signals from noise in situ, sending only useful data back to Earth. The required software and hardware are low-cost in terms of development, operation, energy, and footprint, maximizing the scientific value of every transmitted byte.",
+ "features": [
+ "Machine learning model for seismic pattern recognition",
+ "Data preprocessing pipeline for space seismic data",
+ "Real-time seismic event detection",
+ "Visualization dashboard for seismic activity",
+ "Scientific data analysis and reporting"
+ ]
+ },
+ "es": {
+ "title": "Detección Sísmica en la Luna y Marte",
+ "short": "Modelo LSTM entrenado con datos de misiones Apollo e InSight para filtrar in situ señales sísmicas del ruido en el espacio profundo. 2do puesto NASA 2024.",
+ "description": "Sistema de detección sísmica entrenado con datos reales de las misiones Apollo (Luna) e InSight (Marte) de la NASA usando redes neuronales LSTM (Long Short-Term Memory). Transmitir datos desde cuerpos distantes requiere un consumo energético que escala con la distancia, y los eventos sísmicos son raros, por lo que la mayor parte de los datos transmitidos es ruido. Nuestra solución ejecuta algoritmos de clasificación a bordo para separar las señales sísmicas del ruido in situ, enviando solo datos útiles a la Tierra. El software y hardware necesarios son de bajo costo en desarrollo, operación, energía y espacio, maximizando el valor científico de cada byte transmitido.",
+ "features": [
+ "Modelo de aprendizaje automático para reconocimiento de patrones sísmicos",
+ "Pipeline de preprocesamiento de datos sísmicos espaciales",
+ "Detección de eventos sísmicos en tiempo real",
+ "Panel de visualización de actividad sísmica",
+ "Análisis y reportes de datos científicos"
+ ]
+ }
+ },
+ "status": "production",
+ "techStack": [
+ "Python",
+ "TensorFlow",
+ "Keras",
+ "Scikit-learn",
+ "Pandas",
+ "NumPy",
+ "Matplotlib"
+ ],
+ "githubUrl": "https://github.com/Repetto-A/AI-Seismic-Detection",
+ "demoUrl": null,
+ "images": ["/projects/nasa-1.png", "/projects/nasa-2.png", "/projects/nasa-3.png"],
+ "category": "AI/ML Research"
+ },
+ {
+ "id": "braintumorai-web",
+ "locales": {
+ "en": {
+ "title": "Brain Tumor Classifier",
+ "short": "Deep learning brain tumor classifier running in your browser for full privacy. ~95% accuracy.",
+ "description": "AI-powered brain tumor classification running entirely in your browser with no server required, ensuring complete privacy. Transform PyTorch deep learning models into real-time web applications with ONNX Runtime Web. Features 95.4% test accuracy on 4 tumor classes (Glioma, Meningioma, Pituitary, No Tumor) with blazing fast 200-400ms predictions after warmup.",
+ "features": [
+ "100% client-side inference - images never leave your device",
+ "95.4% test accuracy on 7,000+ brain MRI images",
+ "WebGL-accelerated computation with 200-400ms predictions",
+ "4 tumor classes: Glioma, Meningioma, Pituitary, No Tumor",
+ "Offline-capable after first model load",
+ "Responsive design - works on any modern browser"
+ ]
+ },
+ "es": {
+ "title": "Clasificador de Tumores Cerebrales",
+ "short": "Clasificador de tumores cerebrales con deep learning ejecutándose en tu navegador para lograr privacidad total. ~95% de precisión.",
+ "description": "Clasificación de tumores cerebrales potenciada por IA que se ejecuta completamente en tu navegador sin necesidad de servidor, garantizando privacidad total. Transforma modelos de deep learning de PyTorch en aplicaciones web en tiempo real con ONNX Runtime Web. Presenta 95.4% de precisión en 4 clases de tumores (Glioma, Meningioma, Pituitaria, Sin Tumor) con predicciones ultra rápidas de 200-400ms después del calentamiento.",
+ "features": [
+ "Inferencia 100% del lado del cliente - las imágenes nunca salen de tu dispositivo",
+ "95.4% de precisión en más de 7,000 imágenes de resonancia magnética cerebral",
+ "Cómputo acelerado por WebGL con predicciones de 200-400ms",
+ "4 clases de tumores: Glioma, Meningioma, Pituitaria, Sin Tumor",
+ "Funciona sin conexión después de la primera carga del modelo",
+ "Diseño responsivo - funciona en cualquier navegador moderno"
+ ]
+ }
+ },
+ "status": "production",
+ "techStack": [
+ "Python",
+ "Next.js",
+ "React",
+ "TypeScript",
+ "Tailwind CSS",
+ "PyTorch",
+ "Matplotlib",
+ "Pillow",
+ "NumPy",
+ "Scikit-learn"
+ ],
+ "githubUrl": "https://github.com/Repetto-A/BrainTumorAI-Web",
+ "demoUrl": "https://brain-tumor-ai-web.vercel.app",
+ "images": [
+ "/projects/braintumorai-1.jpg",
+ "/projects/braintumorai-2.jpg",
+ "/projects/braintumorai-3.jpg"
+ ],
+ "category": "AI/ML Research"
+ },
+ {
+ "id": "zorrito-finance",
+ "locales": {
+ "en": {
+ "title": "Zorrito Finance",
+ "short": "No-loss lottery where you save, win, and raise your own virtual Zorrito. 2nd Best dApp Using Filecoin at ETH Global 2025.",
+ "description": "A decentralized finance platform that enables users to earn yield on their savings without risking their principal. Built during ETH Global 2025, awarded 2nd Best dApp Using Filecoin. Users deposit funds into smart contracts that automatically allocate capital to yield-generating protocols, while maintaining full liquidity and zero loss of principal.",
+ "features": [
+ "No-loss savings mechanism with yield generation",
+ "Smart contract-based fund allocation",
+ "Integration with Filecoin for decentralized storage",
+ "Real-time yield tracking dashboard",
+ "Instant withdrawals with no penalties",
+ "Multi-protocol yield optimization"
+ ]
+ },
+ "es": {
+ "title": "Zorrito Finance",
+ "short": "Lotería sin pérdidas en la que ahorrás y ganás mientras cuidás a tu propio Zorrito virtual. 2da Mejor dApp Usando Filecoin en ETH Global 2025.",
+ "description": "Una plataforma de finanzas descentralizadas que permite a los usuarios ganar rendimientos sobre sus ahorros sin arriesgar su capital principal. Construida durante ETH Global 2025, premiada como 2do Mejor dApp Usando Filecoin. Los usuarios depositan fondos en contratos inteligentes que asignan automáticamente capital a protocolos generadores de rendimiento, manteniendo liquidez total y cero pérdida de principal.",
+ "features": [
+ "Mecanismo de ahorro sin pérdidas con generación de rendimientos",
+ "Asignación de fondos basada en contratos inteligentes",
+ "Integración con Filecoin para almacenamiento descentralizado",
+ "Panel de seguimiento de rendimientos en tiempo real",
+ "Retiros instantáneos sin penalizaciones",
+ "Optimización de rendimientos multi-protocolo"
+ ]
+ }
+ },
+ "status": "in-progress",
+ "techStack": [
+ "Solidity",
+ "Filecoin",
+ "IPFS",
+ "Self Protocol",
+ "Next.js",
+ "React",
+ "TypeScript",
+ "AI SDK",
+ "Tailwind CSS"
+ ],
+ "githubUrl": "https://github.com/Repetto-A/zorrito-finance",
+ "demoUrl": "https://zorrito.vercel.app/",
+ "images": [
+ "/projects/zorrito-1.jpg",
+ "/projects/zorrito-2.jpg",
+ "/projects/zorrito-3.jpg",
+ "/projects/zorrito-4.jpeg"
+ ],
+ "category": "Blockchain/DeFi"
+ },
+ {
+ "id": "roxium-dao-ops",
+ "locales": {
+ "en": {
+ "title": "Roxium DAO Ops",
+ "short": "DAO management system that brings transparency and visibility over the status of proposals and tasks. 1st place PowerHouse Ecosystem prize at ETH Argentina.",
+ "description": "DAO operations platform built for ETH Argentina. Roxium brings transparency and visibility to DAOs by tracking proposals and tasks with full status history, dashboards and analytics, and AI-powered daily digests that summarize key factors and overall progress contributors need to pay attention to.",
+ "features": [
+ "Proposal and task tracking with full status history",
+ "Dashboards and analytics for DAO activity",
+ "AI-powered daily digest with key factor summaries",
+ "Overall progress overview for contributors",
+ "Bottleneck identification through status trends"
+ ]
+ },
+ "es": {
+ "title": "Roxium DAO Ops",
+ "short": "Sistema de gestión de DAOs que brinda transparencia y visibilidad sobre el estado de proposals y tasks. 1er premio del ecosistema PowerHouse en ETH Argentina.",
+ "description": "Plataforma de operaciones para DAOs construida para ETH Argentina. Roxium brinda transparencia y visibilidad al trackear proposals y tasks con historial de estados completo, dashboards, analytics y resúmenes diarios potenciados con IA que sintetizan los factores clave y el estado general al que los contribuidores deben prestarle atención.",
+ "features": [
+ "Tracking de proposals y tasks con historial de estados completo",
+ "Dashboards y analytics de actividad del DAO",
+ "Resumen diario potenciado con IA con factores clave",
+ "Vista general del progreso para contribuidores",
+ "Identificación de cuellos de botella a través de tendencias de estado"
+ ]
+ }
+ },
+ "status": "production",
+ "techStack": [
+ "TypeScript",
+ "Next.js",
+ "React",
+ "PowerHouse",
+ "Vetra",
+ "LangChain",
+ "GraphQL",
+ "Tailwind CSS"
+ ],
+ "githubUrl": "https://github.com/Repetto-A/roxium-dao-ops",
+ "demoUrl": "https://roxium-dao-ops.up.railway.app/",
+ "images": ["/projects/roxium-1.jpg", "/projects/roxium-2.jpg", "/projects/roxium-3.jpg"],
+ "category": "Blockchain/AI"
+ },
+ {
+ "id": "real-estate-system",
+ "locales": {
+ "en": {
+ "title": "Real Estate Management System",
+ "short": "Property management system with visit calendars, booking notifications for agents, SEO blog, and more.",
+ "description": "A comprehensive property management platform featuring calendar-based booking system, automated agent email confirmations, internal blog for SEO optimization and content generation, newsletter system.",
+ "features": [
+ "Calendar booking system for property viewings",
+ "Automated email confirmations for agents",
+ "Internal blog for property updates",
+ "Newsletter subscription system",
+ "Property listing management"
+ ]
+ },
+ "es": {
+ "title": "Sistema de Gestión Inmobiliaria",
+ "short": "Sistema de gestión de propiedades que incluye calendarios de visita, reservas con notificaciones a los agentes, newsletter, blog para mejorar el SEO, etc.",
+ "description": "Una plataforma integral de gestión de propiedades que incluye sistema de reservas basado en calendario, confirmaciones automáticas por email para agentes, blog interno para actualizaciones de propiedades y sistema de newsletter.",
+ "features": [
+ "Sistema de reservas por calendario para visitas a propiedades",
+ "Confirmaciones automáticas por email para agentes",
+ "Blog interno para actualizaciones de propiedades",
+ "Sistema de suscripción a newsletter",
+ "Gestión de listados de propiedades"
+ ]
+ }
+ },
+ "status": "production",
+ "techStack": [
+ "Python",
+ "Django",
+ "LangChain",
+ "MySQL",
+ "Bootstrap 5",
+ "Tailwind CSS",
+ "Pillow"
+ ],
+ "githubUrl": "https://github.com/Repetto-A/Real_Estate_System",
+ "demoUrl": "https://realestatesystem-production.up.railway.app/",
+ "images": [
+ "/projects/realestate-1.png",
+ "/projects/realestate-2.png",
+ "/projects/realestate-3.png",
+ "/projects/realestate-4.png",
+ "/projects/realestate-5.png",
+ "/projects/realestate-6.png",
+ "/projects/realestate-7.png",
+ "/projects/realestate-8.png"
+ ],
+ "category": "Management System"
+ },
+ {
+ "id": "cotibot",
+ "locales": {
+ "en": {
+ "title": "Cotibot",
+ "short": "Custom-built system for an agricultural machinery manufacturer: PDF quotes, AFIP CUIT lookup, WhatsApp bot, and more.",
+ "description": "An intelligent factory quoting system that streamlines the quotation process for manufacturing businesses. Features automated PDF generation, AFIP CUIT lookup integration for Argentine tax validation, and a comprehensive admin pricing panel.",
+ "features": [
+ "Automated PDF quote generation",
+ "AFIP CUIT lookup integration",
+ "Admin pricing management panel",
+ "Client quote history tracking",
+ "Real-time pricing calculations"
+ ]
+ },
+ "es": {
+ "title": "Cotibot",
+ "short": "Sistema a medida para un fabricante de maquinaria agrícola: cotizaciones PDF, búsqueda de información en AFIP a partir del CUIT, bot de WhatsApp, etc.",
+ "description": "Un sistema inteligente de cotización para fábricas que agiliza el proceso de cotización para empresas manufactureras. Incluye generación automática de PDF, integración de búsqueda CUIT AFIP para validación fiscal argentina, y un panel integral de precios para administradores.",
+ "features": [
+ "Generación automática de cotizaciones en PDF",
+ "Integración con búsqueda de CUIT en AFIP",
+ "Panel de gestión de precios para administradores",
+ "Seguimiento del historial de cotizaciones de clientes",
+ "Cálculos de precios en tiempo real"
+ ]
+ }
+ },
+ "status": "production",
+ "techStack": [
+ "TypeScript",
+ "Python",
+ "React",
+ "FastAPI",
+ "PostgreSQL",
+ "Tailwind CSS",
+ "Pillow"
+ ],
+ "githubUrl": "https://github.com/Repetto-A/cotibot",
+ "demoUrl": "https://cotibot-demo.vercel.app",
+ "images": [
+ "/projects/cotibot-1.png",
+ "/projects/cotibot-2.png",
+ "/projects/cotibot-3.png",
+ "/projects/cotibot-4.png"
+ ],
+ "category": "Business Automation"
+ }
+ ]
+}
diff --git a/data/awards.ts b/data/awards.ts
index e740db4..1ba97d0 100644
--- a/data/awards.ts
+++ b/data/awards.ts
@@ -67,7 +67,16 @@ export const awards: Award[] = [
es: "Reconocido por la mejor implementacion de PowerHouse, demostrando ejecucion tecnica excepcional y prototipado rapido en tecnologia blockchain.",
},
proofUrl: null,
- tags: ["TypeScript", "Next.js", "React", "PowerHouse", "Vetra", "LangChain", "GraphQL", "Tailwind CSS"],
+ tags: [
+ "TypeScript",
+ "Next.js",
+ "React",
+ "PowerHouse",
+ "Vetra",
+ "LangChain",
+ "GraphQL",
+ "Tailwind CSS",
+ ],
icon: "award",
color: "from-purple-500/20 to-pink-500/20",
borderColor: "border-purple-500/30",
@@ -267,7 +276,17 @@ export const awards: Award[] = [
es: "Plataforma DeFi gamificada de ahorro con modelo de loteria sin perdida y verificacion de edad con Self Protocol.",
},
proofUrl: null,
- tags: ["Solidity", "Filecoin", "IPFS", "Self Protocol", "Next.js", "React", "TypeScript", "AI SDK", "Tailwind CSS"],
+ tags: [
+ "Solidity",
+ "Filecoin",
+ "IPFS",
+ "Self Protocol",
+ "Next.js",
+ "React",
+ "TypeScript",
+ "AI SDK",
+ "Tailwind CSS",
+ ],
icon: "trophy",
color: "from-amber-500/20 to-orange-500/20",
borderColor: "border-amber-500/30",
diff --git a/hooks/use-mobile.ts b/hooks/use-mobile.ts
index 4331d5c..2b0fe1d 100644
--- a/hooks/use-mobile.ts
+++ b/hooks/use-mobile.ts
@@ -1,4 +1,4 @@
-import * as React from 'react'
+import * as React from "react"
const MOBILE_BREAKPOINT = 768
@@ -10,9 +10,9 @@ export function useIsMobile() {
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
- mql.addEventListener('change', onChange)
+ mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener('change', onChange)
+ return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
diff --git a/hooks/use-toast.ts b/hooks/use-toast.ts
index 8932bc5..2e4c6ea 100644
--- a/hooks/use-toast.ts
+++ b/hooks/use-toast.ts
@@ -1,9 +1,9 @@
-'use client'
+"use client"
// Inspired by react-hot-toast library
-import * as React from 'react'
+import * as React from "react"
-import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
+import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
@@ -16,10 +16,10 @@ type ToasterToast = ToastProps & {
}
const actionTypes = {
- ADD_TOAST: 'ADD_TOAST',
- UPDATE_TOAST: 'UPDATE_TOAST',
- DISMISS_TOAST: 'DISMISS_TOAST',
- REMOVE_TOAST: 'REMOVE_TOAST',
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
@@ -33,20 +33,20 @@ type ActionType = typeof actionTypes
type Action =
| {
- type: ActionType['ADD_TOAST']
+ type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
- type: ActionType['UPDATE_TOAST']
+ type: ActionType["UPDATE_TOAST"]
toast: Partial |