-
-
-
-
-
- Where shadows dance and secrets nest, Silent Whisper serves as the dark sanctuary for those
- who value discretion above all. Born from ancient corvid traditions, this messenger’s haven ensures
- your
- whispers remain unheard by all but their intended recipients.
-
-
-
- Like the sacred ravens of old, your messages fly through the darkness, their contents sealed by shadows
- and
- protected by forgotten wards. Each member of our dark fellowship is known only by their chosen name, their
- true identity shrouded in mystery.
-
-
-
-
-
- setIsSearchExpanded(!isSearchExpanded)}
- >
-
-
-
- setInputValue(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- fetchUser(inputValue).then((res) => {
- console.log(res);
- })
- }
- }}
- />
-
-
-
-
-
-
-
-
- F.A.Q
-
-
-
- How does this works?
-
-
- Please, click here
-
-
-
-
- Why does this exists?
-
- I made this as a CS50X final project, hence why it is not intended for real usage. (Do not use it in a
- situation where you need real privacy.)
-
-
-
- Is this open-source?
-
- No, not yet (As of 11/12/2024)
-
-
-
-
+
+
-
+
>
- );
+ )
}
\ No newline at end of file
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
deleted file mode 100644
index 9379e13..0000000
--- a/src/app/settings/page.tsx
+++ /dev/null
@@ -1,287 +0,0 @@
-"use client"
-import {motion} from "framer-motion";
-import {useTheme} from "next-themes";
-import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
-import {Button} from "@/components/ui/button";
-import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
-import {Input} from "@/components/ui/input";
-import {Label} from "@/components/ui/label";
-import {Switch} from "@/components/ui/switch";
-import {Separator} from "@/components/ui/separator";
-import {useUser} from "@/contexts/user";
-import {useState} from "react";
-import {AlertTriangle, Copy, Download, Eye, EyeOff, Key, Lock, Save, User} from "lucide-react";
-import {CryptoManager} from "@/lib/crypto/keys";
-import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
-
-export default function SettingsPage() {
- const {theme, setTheme} = useTheme();
- const {user} = useUser();
- const [loading, setLoading] = useState(false);
- const [privateKeyVisible, setPrivateKeyVisible] = useState(false);
- const [privateKeyData, setPrivateKeyData] = useState<{ text: string; file: File } | null>(null);
- const [backupError, setBackupError] = useState("");
-
- const containerVariants = {
- hidden: {opacity: 0, y: 20},
- visible: {
- opacity: 1,
- y: 0,
- transition: {
- duration: 0.6,
- staggerChildren: 0.1
- }
- }
- };
-
- const itemVariants = {
- hidden: {opacity: 0, y: 20},
- visible: {opacity: 1, y: 0}
- };
-
- return (
-
-
-
-
Settings
-
- Manage your account settings and preferences
-
-
-
-
-
-
-
-
- Profile
-
-
-
- Privacy
-
-
-
-
-
-
-
- Profile Information
-
- Update your profile information and settings
-
-
-
-
- Username
-
-
-
-
Your SUUID
-
-
- {
- navigator.clipboard.writeText(user.suuid);
- }}
- variant="outline"
- >
- Copy
-
-
-
-
-
-
-
-
-
-
- Privacy Settings
-
- Manage your privacy and security preferences
-
-
-
-
-
-
Message Encryption
-
- End-to-end encryption is always enabled
-
-
-
-
-
-
-
-
-
Private Key Backup
-
- View and download your private key for backup
-
-
-
- {
- try {
- const data = await CryptoManager.exportPrivateKey();
- if (data) {
- setPrivateKeyData(data);
- setBackupError("");
- } else {
- setBackupError("Failed to export private key");
- }
- } catch (error) {
- setBackupError("Error accessing private key");
- }
- }}
- >
-
- View Key
-
- {
- try {
- const data = await CryptoManager.exportPrivateKey();
- if (data) {
- const url = URL.createObjectURL(data.file);
- const a = document.createElement('a');
- a.href = url;
- a.download = data.file.name;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- setBackupError("");
- } else {
- setBackupError("Failed to download private key");
- }
- } catch (error) {
- setBackupError("Error downloading private key");
- }
- }}
- >
-
- Download
-
-
-
-
- {backupError && (
-
-
- Error
- {backupError}
-
- )}
-
- {privateKeyData && (
-
-
-
-
Private Key
-
- {
- navigator.clipboard.writeText(privateKeyData.text);
- }}
- >
-
-
- {
- setPrivateKeyData(null);
- setPrivateKeyVisible(false);
- }}
- >
-
-
-
-
-
-
-
-
- {privateKeyData.text}
-
-
-
-
- )}
-
-
-
-
-
Allow Message Requests
-
- Receive message requests from other users
-
-
-
-
-
-
-
-
-
- Private Key Management
-
- Your private key is stored securely in your browser.
- Make sure to back it up to avoid losing access to your messages.
-
-
-
-
-
-
-
- {
- setLoading(true);
- // Simulate saving
- setTimeout(() => setLoading(false), 1000);
- }}
- >
- {loading ? (
-
-
-
- ) : (
- "Save Changes"
- )}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/components/home/index.tsx b/src/components/home/index.tsx
new file mode 100644
index 0000000..e7ce899
--- /dev/null
+++ b/src/components/home/index.tsx
@@ -0,0 +1,72 @@
+import LogoIcon from "@/components/ui/logo-icon";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarHeader,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuItem,
+ SidebarProvider,
+ SidebarTrigger
+} from "@/components/ui/sidebar";
+import { Button } from "../ui/button";
+import { Separator } from "../ui/separator";
+
+const SidebarItems = [
+ {
+ id: "home",
+ // The icon of the home item is the same as the logo
+ icon:
+ }
+]
+
+/**
+ * The main component for the homepage. This component is used to wrap all the components of any page.
+ * It also is the controller for everything on the app, including going to other pages, showing conversations and other.
+ * @param children - The children to be rendered in the sidebar inset
+ */
+export default function AppSidebar({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {SidebarItems.map((item) => (
+
+
+ {item.icon}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Your Header Title
+
{/* Spacer for centering on mobile */}
+
+
+
+ {children}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/main/realtime/index.tsx b/src/components/main/realtime/index.tsx
deleted file mode 100644
index ae11924..0000000
--- a/src/components/main/realtime/index.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-// hooks/useRealtime.ts
-import {Dispatch, SetStateAction, useCallback, useEffect} from 'react'
-import {createBrowserClient} from '@/lib/supabase/browser'
-import {useUser} from '@/contexts/user'
-
-interface UseRealtimeProps {
- setThreads: Dispatch
>;
-}
-
-export function useRealtime({setThreads}: UseRealtimeProps) {
- const supabase = createBrowserClient();
- const {user, updateUser} = useUser();
-
- const fetchAndUpdateThreads = useCallback(async () => {
- try {
- const response = await fetch("/api/user/get/threads");
- if (response.ok) {
- const {threads} = await response.json();
- setThreads(threads);
- }
- } catch (error) {
- console.error('Error fetching threads:', error);
- }
- }, [setThreads])
-
- useEffect(() => {
- if (!user) return;
-
- const userUpdate = supabase
- .channel("request updates")
- .on("postgres_changes", {
- event: "*",
- schema: 'public',
- table: 'users',
- filter: `uuid=eq.${user.uuid}`,
- }, async (payload) => {
- console.log(payload)
- if (payload.eventType === "UPDATE") {
- // This will also handle updates for the threads, but only for the user that accepted the request.
- // Why? Because the function that creates the thread will also update the current user request field and remove
- // the corresponding request.
- if (payload.new.requests !== payload.old.requests) {
- updateUser({
- ...user,
- requests: payload.new.requests
- })
- }
- } else if (payload.eventType === "DELETE") {
- console.log(`Payload from delete: \n${payload}`)
- updateUser({
- ...user,
- //@ts-expect-error
- requests: payload.new
- })
- }
- }).subscribe()
-
- const threadUpdate = supabase
- .channel("thread updates")
- .on("postgres_changes", {
- event: "*",
- schema: 'public',
- table: "thread_participants",
- filter: `user_uuid=eq.${user.uuid}`
- }, async (payload) => {
- if (payload.new !== payload.old) {
- await fetchAndUpdateThreads();
- }
- }).subscribe()
-
- return () => {
- threadUpdate.unsubscribe()
- userUpdate.unsubscribe()
- }
-
- }, [user?.uuid, fetchAndUpdateThreads, supabase, updateUser, user]);
-}
\ No newline at end of file
diff --git a/src/components/main/sidebar/mobile.tsx b/src/components/main/sidebar/mobile.tsx
deleted file mode 100644
index e69ccf7..0000000
--- a/src/components/main/sidebar/mobile.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-"use client"
-import React from 'react'
-import {Button} from "@/components/ui/button"
-import {HamburgerMenuIcon} from "@radix-ui/react-icons"
-import {useTheme} from "next-themes"
-import Image from "next/image"
-import {useUIState} from "@/hooks/shared-states"
-import Link from "next/link";
-
-const MobileHeader: React.FC = () => {
- const {setIsDrawerOpen} = useUIState()
- const {theme, systemTheme} = useTheme()
-
- const getTheme = () => {
- if (theme === "system") {
- switch (systemTheme) {
- case "dark":
- return "dark"
- default:
- return "light"
- }
- }
- return theme === "dark" ? "dark" : "light"
- }
-
- const logoSrc = getTheme() === 'dark' ? '/logos/logo-light.png' : '/logos/logo.png'
-
- return (
-
- )
-}
-
-export default MobileHeader
\ No newline at end of file
diff --git a/src/components/main/sidebar/rightsidebar.tsx b/src/components/main/sidebar/rightsidebar.tsx
deleted file mode 100644
index 75a3851..0000000
--- a/src/components/main/sidebar/rightsidebar.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-import React, {useCallback, useEffect, useState} from "react";
-import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
-import {Avatar, AvatarFallback} from "@/components/ui/avatar";
-import {Separator} from "@/components/ui/separator";
-import {ScrollArea} from "@/components/ui/scroll-area";
-import {DropdownMenu, DropdownMenuContent, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
-import {Check, LogOut, Mail, MailPlus, X} from "lucide-react";
-import {Button} from "@/components/ui/button";
-import {GearIcon} from "@radix-ui/react-icons";
-import Link from "next/link";
-import {useRealtime} from "@/components/main/realtime";
-import {useUser} from "@/contexts/user";
-import {usePathname} from "next/navigation";
-import {useSharedState} from "@/hooks/shared-states";
-
-interface RightSidebarContentProps {
- isDarkMode: boolean;
-}
-
-export default function RightSidebarContent(
- {
- isDarkMode,
- }: RightSidebarContentProps) {
-
- const [copied, setCopied] = useState(false);
-
- const {threads, setThreads} = useSharedState();
- useRealtime({setThreads});
-
- const {user} = useUser();
- const {username, suuid, requests = []} = user;
- const pathname = usePathname();
-
- const pendingRequests = requests?.length ?? 0;
-
- const fetchThreads = useCallback(async () => {
- try {
- const req = await fetch("/api/user/get/threads")
- if (req.ok) {
- const {threads} = await req.json() as { threads: SiPher.Thread[] | [] }
- setThreads(threads)
- } else {
- setThreads([])
- }
- } catch (error) {
- console.log(error);
- setThreads([])
- }
- }, [setThreads]);
-
- useEffect(() => {
- fetchThreads();
- }, [fetchThreads]);
-
- const handleAccept = async (request: string) => {
- try {
- const response = await fetch("/api/user/create/thread", {
- method: "POST",
- body: JSON.stringify({participant: request}),
- });
- if (response.ok) {
- fetchThreads();
- }
- } catch (error) {
- console.error('Error accepting request:', error);
- }
- }
-
- return (
- <>
-
-
-
-
-
- Copied SUUID to clipboard!
-
-
-
-
{
- setCopied(true)
- navigator.clipboard.writeText(suuid)
- }}
- className={`flex items-center p-3 m-2 ${isDarkMode ? "hover:bg-accent/90" : "hover:bg-secondary/20"} rounded-full transition-colors duration-200 cursor-pointer select-none`}>
-
- {username.charAt(0)}
-
-
-
{username}
-
${suuid}
-
-
-
-
-
-
-
-
-
- {
- (user.requests?.length ?? 0) > 0 ? (
-
- ) : (
-
- )}
- Requests
-
-
-
-
-
User
-
Decline | Accept
-
- {
- pendingRequests > 0 && requests!.map((request, item) => {
- return (
-
-
-
-
{request}
-
-
-
-
- {
- handleAccept(request)
- }} size={"icon"} className={"bg-green-500"}>
-
-
-
-
-
- )
- }) || (
- Nothing new here
- )
- }
-
-
-
- {threads && threads.length > 0 ? (
- threads.map((thread, index) => {
- // Gets the user's username instead of the SUUID to use as a recognizable user.
- const otherUser = thread.participants.filter((user) => user !== username)[0];
- return (
-
-
-
-
- {otherUser.charAt(0).toUpperCase()}
-
- {otherUser}
-
-
-
- )
- })
- ) : (
- No threads available
- )}
-
-
-
-
-
- window.location.href = "/settings"}
- >
-
- Settings
-
- {
- fetch("/api/auth/logout", {
- method: "GET",
- headers: {
- "Content-Type": "application/json"
- },
- }).then((response) => {
- if (response.ok) {
- window.location.href = "/auth/login"
- }
- })
- }} variant="outline" className="w-full justify-start text-[17px] py-2 text-destructive">
-
- Log Out
-
-
-
- >
- )
-}
\ No newline at end of file
diff --git a/src/components/main/sidebar/sidebar.tsx b/src/components/main/sidebar/sidebar.tsx
deleted file mode 100644
index 2843719..0000000
--- a/src/components/main/sidebar/sidebar.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-"use client"
-import React from "react"
-import Link from "next/link"
-import {AnimatePresence, motion} from "framer-motion"
-import {X} from "lucide-react"
-import {Button} from "@/components/ui/button"
-import Image from "next/image";
-import MobileHeader from "@/components/main/sidebar/mobile";
-import {useRefs, useUIState} from "@/hooks/shared-states";
-import {useTheme} from "next-themes";
-import RightSidebarContent from "@/components/main/sidebar/rightsidebar";
-
-type SidebarProps = {
- children?: React.ReactNode
-}
-
-function Sidebar(
- {
- children
- }: SidebarProps
-) {
- const {theme, systemTheme} = useTheme();
-
- const {isDrawerOpen, setIsDrawerOpen} = useUIState();
- const {drawerRef} = useRefs();
-
- const isDarkMode = theme === "system"
- ? systemTheme === "dark"
- : theme === "dark"
-
- return (
- <>
-
-
-
- {isDrawerOpen && (
-
-
- setIsDrawerOpen(false)}
- >
-
- Close menu
-
-
-
-
- )}
-
- {
- children ?? null
- }
-
- >
- )
-}
-
-export default Sidebar
\ No newline at end of file
diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx
new file mode 100644
index 0000000..a4776c9
--- /dev/null
+++ b/src/components/mode-toggle.tsx
@@ -0,0 +1,20 @@
+"use client"
+
+import * as React from "react"
+import { Moon, Sun } from "lucide-react"
+import { useTheme } from "next-themes"
+
+import { Button } from "@/components/ui/button"
+
+export function ModeToggle() {
+ const { theme, setTheme } = useTheme()
+
+ return (
+ setTheme(theme === "dark" ? "light" : "dark")}>
+
+
+ Toggle theme
+
+ )
+}
+
diff --git a/src/components/socket-test.tsx b/src/components/socket-test.tsx
new file mode 100644
index 0000000..b2c20ec
--- /dev/null
+++ b/src/components/socket-test.tsx
@@ -0,0 +1,93 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { io, Socket } from "socket.io-client"
+import { Button } from "./ui/button"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
+import { Input } from "./ui/input"
+
+export default function SocketTest() {
+ const [socket, setSocket] = useState(null)
+ const [isConnected, setIsConnected] = useState(false)
+ const [messages, setMessages] = useState([])
+ const [inputMessage, setInputMessage] = useState("")
+
+ useEffect(() => {
+ // Initialize Socket.IO client
+ const socketInstance = io()
+
+ socketInstance.on("connect", () => {
+ console.log("Connected to Socket.IO:", socketInstance.id)
+ setIsConnected(true)
+ setMessages(prev => [...prev, `âś… Connected: ${socketInstance.id}`])
+ })
+
+ socketInstance.on("disconnect", (reason) => {
+ console.log("Disconnected:", reason)
+ setIsConnected(false)
+ setMessages(prev => [...prev, `❌ Disconnected: ${reason}`])
+ })
+
+ socketInstance.on("message", (data) => {
+ console.log("Message received:", data)
+ setMessages(prev => [...prev, `đź“© Received: ${data}`])
+ })
+
+ setSocket(socketInstance)
+
+ return () => {
+ socketInstance.disconnect()
+ }
+ }, [])
+
+ const sendMessage = () => {
+ if (socket && inputMessage.trim()) {
+ socket.emit("message", inputMessage)
+ setMessages(prev => [...prev, `📤 Sent: ${inputMessage}`])
+ setInputMessage("")
+ }
+ }
+
+ return (
+
+
+ Socket.IO Test Client
+
+ Status: {isConnected ? (
+ 🟢 Connected
+ ) : (
+ đź”´ Disconnected
+ )}
+
+
+
+
+ setInputMessage(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && sendMessage()}
+ disabled={!isConnected}
+ />
+
+ Send
+
+
+
+
+
+ {messages.length === 0 ? (
+
No messages yet...
+ ) : (
+ messages.map((msg, idx) => (
+
{msg}
+ ))
+ )}
+
+
+
+
+ )
+}
+
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx
new file mode 100644
index 0000000..3f9d035
--- /dev/null
+++ b/src/components/theme-provider.tsx
@@ -0,0 +1,12 @@
+"use client"
+
+import { ThemeProvider as NextThemesProvider } from "next-themes"
+import * as React from "react"
+
+export function ThemeProvider({
+ children,
+ ...props
+}: React.ComponentProps) {
+ return {children}
+}
+
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
deleted file mode 100644
index b432017..0000000
--- a/src/components/ui/accordion.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AccordionPrimitive from "@radix-ui/react-accordion"
-import {ChevronDown} from "lucide-react"
-
-import {cn} from "@/lib/utils"
-
-const Accordion = AccordionPrimitive.Root
-
-const AccordionItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AccordionItem.displayName = "AccordionItem"
-
-const AccordionTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, children, ...props}, ref) => (
-
- svg]:rotate-180",
- className
- )}
- {...props}
- >
- {children}
-
-
-
-))
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
-
-const AccordionContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, children, ...props}, ref) => (
-
- {children}
-
-))
-AccordionContent.displayName = AccordionPrimitive.Content.displayName
-
-export {Accordion, AccordionItem, AccordionTrigger, AccordionContent}
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
deleted file mode 100644
index 461c340..0000000
--- a/src/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
-
-import {cn} from "@/lib/utils"
-import {buttonVariants} from "@/components/ui/button"
-
-const AlertDialog = AlertDialogPrimitive.Root
-
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
-
-const AlertDialogPortal = AlertDialogPrimitive.Portal
-
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-
-
-
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
-
-const AlertDialogHeader = ({
- className,
- ...props
- }: React.HTMLAttributes) => (
-
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
-
-const AlertDialogFooter = ({
- className,
- ...props
- }: React.HTMLAttributes) => (
-
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
-
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
-
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AlertDialogDescription.displayName =
- AlertDialogPrimitive.Description.displayName
-
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
-
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
-
-export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
-}
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
deleted file mode 100644
index c382e3a..0000000
--- a/src/components/ui/alert.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import * as React from "react"
-import {cva, type VariantProps} from "class-variance-authority"
-
-import {cn} from "@/lib/utils"
-
-const alertVariants = cva(
- "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
- {
- variants: {
- variant: {
- default: "bg-background text-foreground",
- destructive:
- "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-const Alert = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes & VariantProps
->(({className, variant, ...props}, ref) => (
-
-))
-Alert.displayName = "Alert"
-
-const AlertTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-AlertTitle.displayName = "AlertTitle"
-
-const AlertDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-AlertDescription.displayName = "AlertDescription"
-
-export {Alert, AlertTitle, AlertDescription}
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
deleted file mode 100644
index 027d499..0000000
--- a/src/components/ui/avatar.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
-
-import {cn} from "@/lib/utils"
-
-const Avatar = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
-
-const AvatarImage = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
-
-const AvatarFallback = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
-
-export {Avatar, AvatarImage, AvatarFallback}
diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx
new file mode 100644
index 0000000..8600af0
--- /dev/null
+++ b/src/components/ui/button-group.tsx
@@ -0,0 +1,83 @@
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { Separator } from "@/components/ui/separator"
+
+const buttonGroupVariants = cva(
+ "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
+ {
+ variants: {
+ orientation: {
+ horizontal:
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
+ vertical:
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
+ },
+ },
+ defaultVariants: {
+ orientation: "horizontal",
+ },
+ }
+)
+
+function ButtonGroup({
+ className,
+ orientation,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function ButtonGroupText({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+
+ )
+}
+
+function ButtonGroupSeparator({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ ButtonGroup,
+ ButtonGroupSeparator,
+ ButtonGroupText,
+ buttonGroupVariants,
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index e69ddcc..6810932 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -1,30 +1,33 @@
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
-import {Slot} from "@radix-ui/react-slot"
-import {cva, type VariantProps} from "class-variance-authority"
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
- default:
- "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
- "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
- "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
- default: "h-9 px-4 py-2",
- sm: "h-8 rounded-md px-3 text-xs",
- lg: "h-10 rounded-md px-8",
- icon: "h-9 w-9",
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ "icon-xl": "size-12",
},
},
defaultVariants: {
@@ -34,24 +37,25 @@ const buttonVariants = cva(
}
)
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
}
-const Button = React.forwardRef(
- ({className, variant, size, asChild = false, ...props}, ref) => {
- const Comp = asChild ? Slot : "button"
- return (
-
- )
- }
-)
-Button.displayName = "Button"
-
-export {Button, buttonVariants}
+export { Button, buttonVariants }
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..6f304b5
--- /dev/null
+++ b/src/components/ui/calendar.tsx
@@ -0,0 +1,216 @@
+"use client"
+
+import * as React from "react"
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from "lucide-react"
+import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { Button, buttonVariants } from "@/components/ui/button"
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = "label",
+ buttonVariant = "ghost",
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps["variant"]
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString("default", { month: "short" }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn("w-fit", defaultClassNames.root),
+ months: cn(
+ "flex gap-4 flex-col md:flex-row relative",
+ defaultClassNames.months
+ ),
+ month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
+ nav: cn(
+ "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
+ defaultClassNames.nav
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+ defaultClassNames.button_previous
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
+ defaultClassNames.button_next
+ ),
+ month_caption: cn(
+ "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
+ defaultClassNames.month_caption
+ ),
+ dropdowns: cn(
+ "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
+ defaultClassNames.dropdowns
+ ),
+ dropdown_root: cn(
+ "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
+ defaultClassNames.dropdown_root
+ ),
+ dropdown: cn(
+ "absolute bg-popover inset-0 opacity-0",
+ defaultClassNames.dropdown
+ ),
+ caption_label: cn(
+ "select-none font-medium",
+ captionLayout === "label"
+ ? "text-sm"
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
+ defaultClassNames.caption_label
+ ),
+ table: "w-full border-collapse",
+ weekdays: cn("flex", defaultClassNames.weekdays),
+ weekday: cn(
+ "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
+ defaultClassNames.weekday
+ ),
+ week: cn("flex w-full mt-2", defaultClassNames.week),
+ week_number_header: cn(
+ "select-none w-(--cell-size)",
+ defaultClassNames.week_number_header
+ ),
+ week_number: cn(
+ "text-[0.8rem] select-none text-muted-foreground",
+ defaultClassNames.week_number
+ ),
+ day: cn(
+ "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
+ props.showWeekNumber
+ ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
+ : "[&:first-child[data-selected=true]_button]:rounded-l-md",
+ defaultClassNames.day
+ ),
+ range_start: cn(
+ "rounded-l-md bg-accent",
+ defaultClassNames.range_start
+ ),
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
+ today: cn(
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
+ defaultClassNames.today
+ ),
+ outside: cn(
+ "text-muted-foreground aria-selected:text-muted-foreground",
+ defaultClassNames.outside
+ ),
+ disabled: cn(
+ "text-muted-foreground opacity-50",
+ defaultClassNames.disabled
+ ),
+ hidden: cn("invisible", defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === "left") {
+ return (
+
+ )
+ }
+
+ if (orientation === "right") {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+ span]:text-xs [&>span]:opacity-70",
+ defaultClassNames.day,
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Calendar, CalendarDayButton }
diff --git a/src/components/ui/captcha.tsx b/src/components/ui/captcha.tsx
new file mode 100644
index 0000000..0d14511
--- /dev/null
+++ b/src/components/ui/captcha.tsx
@@ -0,0 +1,24 @@
+import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
+import { forwardRef, useImperativeHandle, useRef } from 'react';
+
+export interface CaptchaRef {
+ reset: () => void;
+}
+
+const Captcha = forwardRef void }>(
+ ({ onSuccess }, ref) => {
+ const turnstileRef = useRef(null);
+
+ useImperativeHandle(ref, () => ({
+ reset: () => {
+ turnstileRef.current?.reset();
+ },
+ }));
+
+ return
+ }
+);
+
+Captcha.displayName = 'Captcha';
+
+export default Captcha;
\ No newline at end of file
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
index d15945d..681ad98 100644
--- a/src/components/ui/card.tsx
+++ b/src/components/ui/card.tsx
@@ -1,76 +1,92 @@
import * as React from "react"
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-Card.displayName = "Card"
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-CardHeader.displayName = "CardHeader"
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const CardTitle = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-CardTitle.displayName = "CardTitle"
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const CardDescription = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-CardDescription.displayName = "CardDescription"
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-CardContent.displayName = "CardContent"
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({className, ...props}, ref) => (
-
-))
-CardFooter.displayName = "CardFooter"
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-export {Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent}
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..cb0b07b
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { CheckIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Checkbox({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
new file mode 100644
index 0000000..8cb4ca7
--- /dev/null
+++ b/src/components/ui/command.tsx
@@ -0,0 +1,184 @@
+"use client"
+
+import * as React from "react"
+import { Command as CommandPrimitive } from "cmdk"
+import { SearchIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+
+function Command({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandDialog({
+ title = "Command Palette",
+ description = "Search for a command to run...",
+ children,
+ className,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ title?: string
+ description?: string
+ className?: string
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+ {title}
+ {description}
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function CommandInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function CommandList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandEmpty({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/src/components/ui/context-menu.tsx b/src/components/ui/context-menu.tsx
new file mode 100644
index 0000000..f024a9c
--- /dev/null
+++ b/src/components/ui/context-menu.tsx
@@ -0,0 +1,252 @@
+"use client"
+
+import * as React from "react"
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function ContextMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function ContextMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function ContextMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function ContextMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function ContextMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function ContextMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function ContextMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function ContextMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function ContextMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ContextMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup,
+}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..d9ccec9
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
deleted file mode 100644
index 94f9dc5..0000000
--- a/src/components/ui/dropdown-menu.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
-import {Check, ChevronRight, Circle} from "lucide-react"
-
-import {cn} from "@/lib/utils"
-
-const DropdownMenu = DropdownMenuPrimitive.Root
-
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
-
-const DropdownMenuGroup = DropdownMenuPrimitive.Group
-
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal
-
-const DropdownMenuSub = DropdownMenuPrimitive.Sub
-
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
-
-const DropdownMenuSubTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean
-}
->(({className, inset, children, ...props}, ref) => (
-
- {children}
-
-
-))
-DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName
-
-const DropdownMenuSubContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName
-
-const DropdownMenuContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, sideOffset = 4, ...props}, ref) => (
-
-
-
-))
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
-
-const DropdownMenuItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean
-}
->(({className, inset, ...props}, ref) => (
- svg]:size-4 [&>svg]:shrink-0",
- inset && "pl-8",
- className
- )}
- {...props}
- />
-))
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
-
-const DropdownMenuCheckboxItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, children, checked, ...props}, ref) => (
-
-
-
-
-
-
- {children}
-
-))
-DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName
-
-const DropdownMenuRadioItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, children, ...props}, ref) => (
-
-
-
-
-
-
- {children}
-
-))
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
-
-const DropdownMenuLabel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean
-}
->(({className, inset, ...props}, ref) => (
-
-))
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
-
-const DropdownMenuSeparator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
-
-const DropdownMenuShortcut = ({
- className,
- ...props
- }: React.HTMLAttributes) => {
- return (
-
- )
-}
-DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
-
-export {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuRadioGroup,
-}
diff --git a/src/components/ui/empty.tsx b/src/components/ui/empty.tsx
new file mode 100644
index 0000000..df91e9d
--- /dev/null
+++ b/src/components/ui/empty.tsx
@@ -0,0 +1,104 @@
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+function Empty({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+const emptyMediaVariants = cva(
+ "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function EmptyMedia({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+ a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Empty,
+ EmptyHeader,
+ EmptyTitle,
+ EmptyDescription,
+ EmptyContent,
+ EmptyMedia,
+}
diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx
new file mode 100644
index 0000000..235d00e
--- /dev/null
+++ b/src/components/ui/field.tsx
@@ -0,0 +1,248 @@
+"use client"
+
+import { useMemo } from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+import { Separator } from "@/components/ui/separator"
+
+function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
+ return (
+
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldLegend({
+ className,
+ variant = "legend",
+ ...props
+}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
+ return (
+
+ )
+}
+
+function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ [data-slot=field-group]]:gap-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+const fieldVariants = cva(
+ "group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
+ {
+ variants: {
+ orientation: {
+ vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
+ horizontal: [
+ "flex-row items-center",
+ "[&>[data-slot=field-label]]:flex-auto",
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
+ ],
+ responsive: [
+ "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
+ "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
+ "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
+ ],
+ },
+ },
+ defaultVariants: {
+ orientation: "vertical",
+ },
+ }
+)
+
+function Field({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps
) {
+ return (
+
+ )
+}
+
+function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function FieldLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+ [data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
+ "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+ a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"div"> & {
+ children?: React.ReactNode
+}) {
+ return (
+
+
+ {children && (
+
+ {children}
+
+ )}
+
+ )
+}
+
+function FieldError({
+ className,
+ children,
+ errors,
+ ...props
+}: React.ComponentProps<"div"> & {
+ errors?: Array<{ message?: string } | undefined>
+}) {
+ const content = useMemo(() => {
+ if (children) {
+ return children
+ }
+
+ if (!errors?.length) {
+ return null
+ }
+
+ const uniqueErrors = [
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
+ ]
+
+ if (uniqueErrors?.length == 1) {
+ return uniqueErrors[0]?.message
+ }
+
+ return (
+
+ {uniqueErrors.map(
+ (error, index) =>
+ error?.message && {error.message}
+ )}
+
+ )
+ }, [children, errors])
+
+ if (!content) {
+ return null
+ }
+
+ return (
+
+ {content}
+
+ )
+}
+
+export {
+ Field,
+ FieldLabel,
+ FieldDescription,
+ FieldError,
+ FieldGroup,
+ FieldLegend,
+ FieldSeparator,
+ FieldSet,
+ FieldContent,
+ FieldTitle,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 3a6716c..8916905 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.tsx
@@ -1,22 +1,21 @@
import * as React from "react"
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
-const Input = React.forwardRef>(
- ({className, type, ...props}, ref) => {
- return (
-
- )
- }
-)
-Input.displayName = "Input"
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
-export {Input}
+export { Input }
diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx
new file mode 100644
index 0000000..d97de21
--- /dev/null
+++ b/src/components/ui/item.tsx
@@ -0,0 +1,193 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { Separator } from "@/components/ui/separator"
+
+function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const itemVariants = cva(
+ "group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline: "border-border",
+ muted: "bg-muted/50",
+ },
+ size: {
+ default: "p-4 gap-4 ",
+ sm: "py-3 px-4 gap-2.5",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Item({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "div"
+ return (
+
+ )
+}
+
+const itemMediaVariants = cva(
+ "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
+ image:
+ "size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function ItemMedia({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+ a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Item,
+ ItemMedia,
+ ItemContent,
+ ItemActions,
+ ItemGroup,
+ ItemSeparator,
+ ItemTitle,
+ ItemDescription,
+ ItemHeader,
+ ItemFooter,
+}
diff --git a/src/components/ui/kbd.tsx b/src/components/ui/kbd.tsx
new file mode 100644
index 0000000..253c69f
--- /dev/null
+++ b/src/components/ui/kbd.tsx
@@ -0,0 +1,28 @@
+import { cn } from "@/lib/utils"
+
+function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
+ return (
+
+ )
+}
+
+function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Kbd, KbdGroup }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
index 2ce9295..fb5fbc3 100644
--- a/src/components/ui/label.tsx
+++ b/src/components/ui/label.tsx
@@ -2,25 +2,23 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
-import {cva, type VariantProps} from "class-variance-authority"
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
-const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-)
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const Label = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef &
- VariantProps
->(({className, ...props}, ref) => (
-
-))
-Label.displayName = LabelPrimitive.Root.displayName
-
-export {Label}
+export { Label }
diff --git a/src/components/ui/logo-icon.tsx b/src/components/ui/logo-icon.tsx
new file mode 100644
index 0000000..ba26f1e
--- /dev/null
+++ b/src/components/ui/logo-icon.tsx
@@ -0,0 +1,17 @@
+// Get the logo SVG and return it as a React component
+import logoDark from "@/assets/logo/logo-dark.svg";
+import logoLight from "@/assets/logo/logo-white.svg";
+import { cn } from "@/lib/utils";
+import { useTheme } from "next-themes";
+
+export default function LogoIcon(
+ {
+ className,
+ }: {
+ className?: string;
+ }
+) {
+ const { theme } = useTheme();
+
+ return theme === "dark" ? : ;
+}
\ No newline at end of file
diff --git a/src/components/ui/menubar.tsx b/src/components/ui/menubar.tsx
new file mode 100644
index 0000000..8480f0a
--- /dev/null
+++ b/src/components/ui/menubar.tsx
@@ -0,0 +1,276 @@
+"use client"
+
+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"
+
+function Menubar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function MenubarMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function MenubarGroup({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function MenubarPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function MenubarRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function MenubarTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function MenubarContent({
+ className,
+ align = "start",
+ alignOffset = -4,
+ sideOffset = 8,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function MenubarItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function MenubarCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function MenubarRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function MenubarLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function MenubarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function MenubarShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function MenubarSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function MenubarSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function MenubarSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Menubar,
+ MenubarPortal,
+ MenubarMenu,
+ MenubarTrigger,
+ MenubarContent,
+ MenubarGroup,
+ MenubarSeparator,
+ MenubarLabel,
+ MenubarItem,
+ MenubarShortcut,
+ MenubarCheckboxItem,
+ MenubarRadioGroup,
+ MenubarRadioItem,
+ MenubarSub,
+ MenubarSubTrigger,
+ MenubarSubContent,
+}
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
new file mode 100644
index 0000000..01e468b
--- /dev/null
+++ b/src/components/ui/popover.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+function Popover({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function PopoverTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function PopoverContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function PopoverAnchor({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
new file mode 100644
index 0000000..e7a416c
--- /dev/null
+++ b/src/components/ui/progress.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+function Progress({
+ className,
+ value,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Progress }
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
index 3d62166..8e4fa13 100644
--- a/src/components/ui/scroll-area.tsx
+++ b/src/components/ui/scroll-area.tsx
@@ -3,46 +3,56 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
-const ScrollArea = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, children, ...props}, ref) => (
-
-
- {children}
-
-
-
-
-))
-ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+function ScrollArea({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+
+ )
+}
-const ScrollBar = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, orientation = "vertical", ...props}, ref) => (
-
-
-
-))
-ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+function ScrollBar({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
-export {ScrollArea, ScrollBar}
+export { ScrollArea, ScrollBar }
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 321606d..275381c 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -3,29 +3,26 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
-const Separator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(
- (
- {className, orientation = "horizontal", decorative = true, ...props},
- ref
- ) => (
-
- )
-)
-Separator.displayName = SeparatorPrimitive.Root.displayName
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-export {Separator}
+export { Separator }
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..84649ad
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return
+}
+
+function SheetTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left"
+}) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..30638ac
--- /dev/null
+++ b/src/components/ui/sidebar.tsx
@@ -0,0 +1,726 @@
+"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"
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+function SidebarProvider({
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}) {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function Sidebar({
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+}
+
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+ return (
+
+ )
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: 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
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ 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
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+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",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+} & VariantProps) {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+}) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ 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",
+ 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
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showIcon?: boolean
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+}) {
+ 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
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
index f106a49..32ea0ef 100644
--- a/src/components/ui/skeleton.tsx
+++ b/src/components/ui/skeleton.tsx
@@ -1,15 +1,13 @@
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
-function Skeleton({
- className,
- ...props
- }: React.HTMLAttributes) {
- return (
-
- )
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
}
-export {Skeleton}
+export { Skeleton }
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..9b20afe
--- /dev/null
+++ b/src/components/ui/sonner.tsx
@@ -0,0 +1,40 @@
+"use client"
+
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+} from "lucide-react"
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, type ToasterProps } from "sonner"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
new file mode 100644
index 0000000..a70e713
--- /dev/null
+++ b/src/components/ui/spinner.tsx
@@ -0,0 +1,16 @@
+import { Loader2Icon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
+ return (
+
+ )
+}
+
+export { Spinner }
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
deleted file mode 100644
index 66b063c..0000000
--- a/src/components/ui/switch.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as SwitchPrimitives from "@radix-ui/react-switch"
-
-import {cn} from "@/lib/utils"
-
-const Switch = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-
-
-))
-Switch.displayName = SwitchPrimitives.Root.displayName
-
-export {Switch}
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
deleted file mode 100644
index 9134a12..0000000
--- a/src/components/ui/tabs.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as TabsPrimitive from "@radix-ui/react-tabs"
-
-import {cn} from "@/lib/utils"
-
-const Tabs = TabsPrimitive.Root
-
-const TabsList = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-TabsList.displayName = TabsPrimitive.List.displayName
-
-const TabsTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
-
-const TabsContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-TabsContent.displayName = TabsPrimitive.Content.displayName
-
-export {Tabs, TabsList, TabsTrigger, TabsContent}
diff --git a/src/components/ui/theme-provider.tsx b/src/components/ui/theme-provider.tsx
deleted file mode 100644
index 0e18587..0000000
--- a/src/components/ui/theme-provider.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-// components/providers/theme-provider.tsx
-'use client'
-
-import {ThemeProvider as NextThemesProvider, type ThemeProviderProps} from "next-themes"
-import {useEffect, useState} from "react"
-
-export default function ThemeProvider({children, ...props}: ThemeProviderProps) {
- const [mounted, setMounted] = useState(false)
-
- useEffect(() => {
- setMounted(true)
- }, [])
-
- if (!mounted) {
- return <>{children}>
- }
-
- return {children}
-}
\ No newline at end of file
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx
deleted file mode 100644
index eb92729..0000000
--- a/src/components/ui/toast.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as ToastPrimitives from "@radix-ui/react-toast"
-import {cva, type VariantProps} from "class-variance-authority"
-import {X} from "lucide-react"
-
-import {cn} from "@/lib/utils"
-
-const ToastProvider = ToastPrimitives.Provider
-
-const ToastViewport = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-ToastViewport.displayName = ToastPrimitives.Viewport.displayName
-
-const toastVariants = cva(
- "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
- {
- variants: {
- variant: {
- default: "border bg-background text-foreground",
- destructive:
- "destructive group border-destructive bg-destructive text-destructive-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-const Toast = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef &
- VariantProps
->(({className, variant, ...props}, ref) => {
- return (
-
- )
-})
-Toast.displayName = ToastPrimitives.Root.displayName
-
-const ToastAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-ToastAction.displayName = ToastPrimitives.Action.displayName
-
-const ToastClose = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-
-
-))
-ToastClose.displayName = ToastPrimitives.Close.displayName
-
-const ToastTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-ToastTitle.displayName = ToastPrimitives.Title.displayName
-
-const ToastDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, ...props}, ref) => (
-
-))
-ToastDescription.displayName = ToastPrimitives.Description.displayName
-
-type ToastProps = React.ComponentPropsWithoutRef
-
-type ToastActionElement = React.ReactElement
-
-export {
- type ToastProps,
- type ToastActionElement,
- ToastProvider,
- ToastViewport,
- Toast,
- ToastTitle,
- ToastDescription,
- ToastClose,
- ToastAction,
-}
diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx
deleted file mode 100644
index 8f78008..0000000
--- a/src/components/ui/toaster.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-"use client"
-
-import {useToast} from "@/hooks/use-toast"
-import {Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport,} from "@/components/ui/toast"
-
-export function Toaster() {
- const {toasts} = useToast()
-
- return (
-
- {toasts.map(function ({id, title, description, action, ...props}) {
- return (
-
-
- {title && {title} }
- {description && (
- {description}
- )}
-
- {action}
-
-
- )
- })}
-
-
- )
-}
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
index c373d98..a4e90d4 100644
--- a/src/components/ui/tooltip.tsx
+++ b/src/components/ui/tooltip.tsx
@@ -3,30 +3,59 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
-import {cn} from "@/lib/utils"
+import { cn } from "@/lib/utils"
-const TooltipProvider = TooltipPrimitive.Provider
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const Tooltip = TooltipPrimitive.Root
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
-const TooltipTrigger = TooltipPrimitive.Trigger
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
-const TooltipContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({className, sideOffset = 4, ...props}, ref) => (
-
-
-
-))
-TooltipContent.displayName = TooltipPrimitive.Content.displayName
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
-export {Tooltip, TooltipTrigger, TooltipContent, TooltipProvider}
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/contexts/user.tsx b/src/contexts/user.tsx
deleted file mode 100644
index 076530a..0000000
--- a/src/contexts/user.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-// contexts/user.tsx
-'use client';
-
-import {createContext, useContext, useState} from 'react';
-import {useRouter} from 'next/navigation';
-
-interface UserContextType {
- user: NonNullable;
- getUser: (context: string, userId?: string) => Promise>;
- updateUser: (newUserData: NonNullable) => void;
-}
-
-const UserContext = createContext(null);
-
-export function useUser() {
- const context = useContext(UserContext);
- const router = useRouter();
-
- if (!context) {
- throw new Error('useUser must be used within a UserProvider');
- }
-
- return {
- user: context.user,
- updateUser: context.updateUser,
- getUser: async (context: string, userId?: string, type: "suuid" | "uuid" = "uuid", detailed: boolean = false) => {
- if (process.env.NODE_ENV !== 'production') {
- console.log(`useUser().getUser(): Being called by ${context}`)
- }
-
- try {
- const response = await fetch(`/api/auth/get_user?${
- userId && `${type}=${
- encodeURIComponent(userId)
- }${
- detailed ? "&detailed=true" : ""
- }`
- }`);
-
- if (!response.ok) {
- const error = await response.json();
- if (error.message?.includes("Auth session missing!")) {
- throw new Error('No authenticated user');
- }
- throw new Error(error.message || 'Authentication failed');
- }
-
- return await response.json();
- } catch (error) {
- console.error('Failed to get user:', error);
- router.push('/auth/login');
- throw error;
- }
- },
- checkAuth: async (context: string) => {
- if (process.env.NODE_ENV !== 'production') {
- console.log(`useUser().checkAuth(): Being called by ${context}`)
- }
- try {
- const response = await fetch('/api/auth/get_user');
- return response.ok;
- } catch {
- return false;
- }
- }
- };
-}
-
-export function UserProvider(
- {
- children,
- initialUser
- }: {
- children: React.ReactNode;
- initialUser: NonNullable;
- }
-) {
- const [user, setUser] = useState>(initialUser);
-
- const updateUser = (newUserData: NonNullable) => {
- setUser(newUserData);
- };
-
- return (
- {
- const response = await fetch(`/api/auth/get_user?${
- userId && `uuid=${
- encodeURIComponent(userId)
- }`
- }`);
- if (!response.ok) {
- throw new Error('Failed to get user');
- }
- const {user} = await response.json();
- return user as NonNullable;
- }
- }}>
- {children}
-
- );
-}
\ No newline at end of file
diff --git a/src/hooks/shared-states.tsx b/src/hooks/shared-states.tsx
deleted file mode 100644
index 1adda83..0000000
--- a/src/hooks/shared-states.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-"use client";
-
-// src/hooks/useSharedState.tsx
-import React, {createContext, useContext, useRef, useState} from 'react'
-
-// Define the shape of our shared state
-interface SharedState {
- // UI States
- isDrawerOpen: boolean
- setIsDrawerOpen: React.Dispatch>
- threads: SiPher.Thread[],
- setThreads: React.Dispatch>,
- // Refs
- drawerRef: React.RefObject
-}
-
-// Create the context
-const SharedStateContext = createContext(undefined)
-
-// Create the provider component
-export function SharedStateProvider({children}: { children: React.ReactNode }) {
- // UI States
- const [isDrawerOpen, setIsDrawerOpen] = useState(false)
- const [threads, setThreads] = useState([]);
-
- // Refs
- const drawerRef = useRef(null)
-
- // Theme
-
- const value = {
- // UI States
- isDrawerOpen,
- setIsDrawerOpen,
- threads,
- setThreads,
- // Refs
- drawerRef,
- }
-
- return (
-
- {children}
-
- )
-}
-
-// Create the custom hook
-export function useSharedState() {
- const context = useContext(SharedStateContext)
- if (context === undefined) {
- throw new Error('useSharedState must be used within a SharedStateProvider')
- }
- return context
-}
-
-// Optional: Create specific hooks for different parts of the state
-export function useUIState() {
- const {
-
- isDrawerOpen,
- setIsDrawerOpen,
-
- } = useSharedState()
-
- return {
- isDrawerOpen,
- setIsDrawerOpen,
- }
-}
-
-export function useRefs() {
- const {
- drawerRef,
- } = useSharedState()
-
- return {
- drawerRef,
- }
-}
\ No newline at end of file
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts
deleted file mode 100644
index 6cdbdfa..0000000
--- a/src/hooks/use-toast.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-"use client"
-
-// Inspired by react-hot-toast library
-import * as React from "react"
-
-import type {ToastActionElement, ToastProps,} from "@/components/ui/toast"
-
-const TOAST_LIMIT = 1
-const TOAST_REMOVE_DELAY = 1000000
-
-type ToasterToast = ToastProps & {
- id: string
- title?: React.ReactNode
- description?: React.ReactNode
- action?: ToastActionElement
-}
-
-const actionTypes = {
- ADD_TOAST: "ADD_TOAST",
- UPDATE_TOAST: "UPDATE_TOAST",
- DISMISS_TOAST: "DISMISS_TOAST",
- REMOVE_TOAST: "REMOVE_TOAST",
-} as const
-
-let count = 0
-
-function genId() {
- count = (count + 1) % Number.MAX_SAFE_INTEGER
- return count.toString()
-}
-
-type ActionType = typeof actionTypes
-
-type Action =
- | {
- type: ActionType["ADD_TOAST"]
- toast: ToasterToast
-}
- | {
- type: ActionType["UPDATE_TOAST"]
- toast: Partial
-}
- | {
- type: ActionType["DISMISS_TOAST"]
- toastId?: ToasterToast["id"]
-}
- | {
- type: ActionType["REMOVE_TOAST"]
- toastId?: ToasterToast["id"]
-}
-
-interface State {
- toasts: ToasterToast[]
-}
-
-const toastTimeouts = new Map>()
-
-const addToRemoveQueue = (toastId: string) => {
- if (toastTimeouts.has(toastId)) {
- return
- }
-
- const timeout = setTimeout(() => {
- toastTimeouts.delete(toastId)
- dispatch({
- type: "REMOVE_TOAST",
- toastId: toastId,
- })
- }, TOAST_REMOVE_DELAY)
-
- toastTimeouts.set(toastId, timeout)
-}
-
-export const reducer = (state: State, action: Action): State => {
- switch (action.type) {
- case "ADD_TOAST":
- return {
- ...state,
- toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
- }
-
- case "UPDATE_TOAST":
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === action.toast.id ? {...t, ...action.toast} : t
- ),
- }
-
- case "DISMISS_TOAST": {
- const {toastId} = action
-
- // ! Side effects ! - This could be extracted into a dismissToast() action,
- // but I'll keep it here for simplicity
- if (toastId) {
- addToRemoveQueue(toastId)
- } else {
- state.toasts.forEach((toast) => {
- addToRemoveQueue(toast.id)
- })
- }
-
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === toastId || toastId === undefined
- ? {
- ...t,
- open: false,
- }
- : t
- ),
- }
- }
- case "REMOVE_TOAST":
- if (action.toastId === undefined) {
- return {
- ...state,
- toasts: [],
- }
- }
- return {
- ...state,
- toasts: state.toasts.filter((t) => t.id !== action.toastId),
- }
- }
-}
-
-const listeners: Array<(state: State) => void> = []
-
-let memoryState: State = {toasts: []}
-
-function dispatch(action: Action) {
- memoryState = reducer(memoryState, action)
- listeners.forEach((listener) => {
- listener(memoryState)
- })
-}
-
-type Toast = Omit
-
-function toast({...props}: Toast) {
- const id = genId()
-
- const update = (props: ToasterToast) =>
- dispatch({
- type: "UPDATE_TOAST",
- toast: {...props, id},
- })
- const dismiss = () => dispatch({type: "DISMISS_TOAST", toastId: id})
-
- dispatch({
- type: "ADD_TOAST",
- toast: {
- ...props,
- id,
- open: true,
- onOpenChange: (open) => {
- if (!open) dismiss()
- },
- },
- })
-
- return {
- id: id,
- dismiss,
- update,
- }
-}
-
-function useToast() {
- const [state, setState] = React.useState(memoryState)
-
- React.useEffect(() => {
- listeners.push(setState)
- return () => {
- const index = listeners.indexOf(setState)
- if (index > -1) {
- listeners.splice(index, 1)
- }
- }
- }, [state])
-
- return {
- ...state,
- toast,
- dismiss: (toastId?: string) => dispatch({type: "DISMISS_TOAST", toastId}),
- }
-}
-
-export {useToast, toast}
diff --git a/src/lib/api/helpers/getUserByUUID.ts b/src/lib/api/helpers/getUserByUUID.ts
deleted file mode 100644
index 3c6fee2..0000000
--- a/src/lib/api/helpers/getUserByUUID.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import {SupabaseClient} from "@supabase/supabase-js";
-
-export default async function getUserByUUID(supabase: SupabaseClient, uuid: string) {
- const {data: userData, error: userError} = await supabase
- .from('users')
- .select('*, public_key')
- .eq('uuid', uuid)
- .single()
-
- if (userError) throw userError;
- return userData;
-}
\ No newline at end of file
diff --git a/src/lib/api/helpers/updateUserRequests.ts b/src/lib/api/helpers/updateUserRequests.ts
deleted file mode 100644
index 2f2f8b8..0000000
--- a/src/lib/api/helpers/updateUserRequests.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {SupabaseClient} from "@supabase/supabase-js";
-
-export default async function updateUserRequests(searchTerm: string, requestSuuid: string, supabase: SupabaseClient) {
- try {
-
- const {data, error} = await supabase.rpc('update_user_requests', {
- search_term: searchTerm,
- new_request: requestSuuid
- });
-
- if (error) {
- throw error;
- }
-
- return {success: true, data};
- } catch (error) {
- console.error('Error updating user requests:', error);
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred'
- };
- }
-}
\ No newline at end of file
diff --git a/src/lib/auth/client.ts b/src/lib/auth/client.ts
new file mode 100644
index 0000000..4f93a9a
--- /dev/null
+++ b/src/lib/auth/client.ts
@@ -0,0 +1,10 @@
+import { convexClient } from "@convex-dev/better-auth/client/plugins";
+import { usernameClient } from "better-auth/client/plugins";
+import { createAuthClient } from "better-auth/react";
+
+export const authClient = createAuthClient({
+ plugins: [
+ convexClient(),
+ usernameClient()
+ ]
+});
\ No newline at end of file
diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts
deleted file mode 100644
index e7c1cae..0000000
--- a/src/lib/auth/index.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-"use server"
-// lib/auth/index.ts
-import {createClient} from '@/lib/supabase/server';
-import {headers} from 'next/headers';
-
-const PUBLIC_PATHS = [
- '/auth/login',
- '/auth/signup',
-];
-
-/**
- * Mostly used for getting the first user to prevent it being null
- */
-export async function getAuthenticatedUser() {
- const headersList = await headers();
- const path = headersList.get("x-invoke-path") || "";
-
- // If we're on a public path, don't require authentication
- if (PUBLIC_PATHS.some(publicPath => path.startsWith(publicPath))) {
- return null;
- }
-
- const supabase = await createClient();
-
- const {data: {user: session}, error: sessionError} = await supabase.auth.getUser();
-
- if (sessionError || !session) {
- return null;
- }
-
- const {data: profile, error: profileError} = await supabase
- .from('users')
- .select('*')
- .eq('uuid', session.id)
- .single();
-
- if (profileError || !profile) {
- return null;
- }
-
- return profile
-}
\ No newline at end of file
diff --git a/src/lib/crypto/helpers/updateKey.ts b/src/lib/crypto/helpers/updateKey.ts
deleted file mode 100644
index dcc03d8..0000000
--- a/src/lib/crypto/helpers/updateKey.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-"use client";
-
-import {CryptoManager} from "@/lib/crypto/keys";
-
-export default async function UpdateKey() {
- const keyPair = await CryptoManager.generateUserKeys();
- await CryptoManager.storePrivateKey(keyPair.privateKey);
- const exportedPublic = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
-
- const req = await fetch("/api/user/send/update/key", {
- method: "POST",
- body: JSON.stringify({publicKey: exportedPublic}),
- })
-
- if (req.status !== 200) {
- await CryptoManager.deletePrivateKey();
- return {
- status: req.status,
- message: "Failed to update public key",
- }
- }
-
- return {
- status: 200,
- message: "Successfully updated keys",
- }
-}
\ No newline at end of file
diff --git a/src/lib/crypto/keys.ts b/src/lib/crypto/keys.ts
deleted file mode 100644
index 90eb0a1..0000000
--- a/src/lib/crypto/keys.ts
+++ /dev/null
@@ -1,406 +0,0 @@
-"use client"
-
-/**
- * @filedoc: When creating this, I thought that using PBKDF2 would be the best choice, which it isn't since I would have
- * to share passwords between user, and to do that I would have to pass the password through the server, which would defeat
- * both PBKDF2 and E2EE methods.
- * So I went with a better approach: Using public/private keys and signing messages with the public user's key and my own
- * key
- */
-
-/**
- * A kinda-simple CryptoManager to handle keys and encrypt/decrypt messages.
- * Uses IndexedDB to store private keys securely.
- */
-export class CryptoManager {
- private static readonly DB_NAME = 'SipherKeyStore';
- private static readonly DB_VERSION = 1;
- private static readonly STORE_NAME = 'keys';
- private static readonly KEY_ID = 'private_key';
-
- /**
- * Generates a fresh RSA key pair. Yay, new keys!
- * @returns {Promise} The generated RSA key pair.
- */
- static async generateUserKeys(): Promise {
- return await crypto.subtle.generateKey(
- {
- name: "RSA-OAEP",
- modulusLength: 2048,
- publicExponent: new Uint8Array([1, 0, 1]),
- hash: "SHA-256",
- },
- true,
- ["encrypt", "decrypt"]
- );
- }
-
- /**
- * Stores the private key.
- * @param {CryptoKey} privateKey - The private key to store.
- * @returns {Promise}
- */
- static async storePrivateKey(privateKey: CryptoKey): Promise {
- const exportedPrivate = await crypto.subtle.exportKey('jwk', privateKey);
- const db = await this.openDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction(this.STORE_NAME, 'readwrite');
- const store = transaction.objectStore(this.STORE_NAME);
- const request = store.put(exportedPrivate, this.KEY_ID);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
-
- transaction.oncomplete = () => db.close();
- });
- }
-
- /**
- * Deletes the private key.
- * @param {CryptoKey} privateKey - The private key to store.
- * @returns {Promise}
- */
- static async deletePrivateKey(): Promise {
- const db = await this.openDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction(this.STORE_NAME, 'readwrite');
- const store = transaction.objectStore(this.STORE_NAME);
- const request = store.delete(this.KEY_ID);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve();
-
- transaction.oncomplete = () => db.close();
- });
- }
-
- /**
- * Gets the stored private key from IndexedDB. Might return `null` if nothing's there.
- * @returns {Promise} The private key or `null` if not found.
- */
- static async getPrivateKey(): Promise {
- try {
- const db = await this.openDB();
-
- return new Promise((resolve, reject) => {
- const transaction = db.transaction(this.STORE_NAME, 'readonly');
- const store = transaction.objectStore(this.STORE_NAME);
- const request = store.get(this.KEY_ID);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = async () => {
- if (!request.result) {
- resolve(null);
- return;
- }
-
- const privateKey = await crypto.subtle.importKey(
- 'jwk',
- request.result,
- {
- name: "RSA-OAEP",
- hash: "SHA-256",
- },
- true,
- ["decrypt"]
- );
- resolve(privateKey);
- };
-
- transaction.oncomplete = () => db.close();
- });
- } catch (error) {
- console.error('Oops! Error retrieving private key:', error);
- return null;
- }
- }
-
- static async prepareAndSendMessage(
- message: string,
- senderPublicKey: JsonWebKey, // Our own public key
- recipientPublicKey: JsonWebKey,
- threadId: string
- ): Promise {
- // Encrypt for ourselves
- const senderContent = await this.encryptMessage(message, senderPublicKey);
-
- // Encrypt for recipient
- const recipientContent = await this.encryptMessage(message, recipientPublicKey);
-
- // Send to server
- const response = await fetch('/api/user/send/message', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- threadId,
- senderContent,
- recipientContent
- })
- });
-
- if (!response.ok) {
- throw new Error('Failed to send message');
- }
-
- return await response.json();
- }
-
- static async decryptThreadMessages(messages: any[], userUuid: string): Promise {
- try {
- // Get our private key for decryption
- const privateKey = await this.getPrivateKey();
- if (!privateKey) {
- throw new Error("No private key found for decryption");
- }
-
- // Decrypt each message
- const decryptedMessages = await Promise.all(messages.map(async (message) => {
- // Determine if we're the sender
- const isSender = message.sender_uuid === userUuid;
-
- try {
- const decryptedContent = await this.decryptMessage(message.content);
-
- return {
- id: message.id,
- content: decryptedContent,
- sender_uuid: message.sender_uuid,
- created_at: message.created_at,
- isSender
- };
- } catch (error) {
- console.error('Failed to decrypt message:', message.id, error);
- return {
- id: message.id,
- content: "Failed to decrypt message",
- sender_uuid: message.sender_uuid,
- created_at: message.created_at,
- isSender,
- error: true
- };
- }
- }));
-
- return decryptedMessages;
- } catch (error) {
- console.error('Error decrypting messages:', error);
- throw error;
- }
- }
-
- /**
- * Encrypts a message using the recipient's public key.
- * @param {string} message - The message you wanna encrypt.
- * @param {JsonWebKey} recipientPublicKey - The recipient's public key in JWK format.
- * @returns {Promise} The encrypted message in base64 format.
- */
- static async encryptMessage(message: string, recipientPublicKey: JsonWebKey): Promise {
- const publicKey = await crypto.subtle.importKey(
- 'jwk',
- recipientPublicKey,
- {
- name: "RSA-OAEP",
- hash: "SHA-256", // This is important!
- },
- true,
- ["encrypt"]
- );
-
- const encoder = new TextEncoder();
- const encrypted = await crypto.subtle.encrypt(
- {
- name: "RSA-OAEP"
- },
- publicKey,
- encoder.encode(message)
- );
-
- return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
- }
-
- /**
- * Decrypts a message using your own private key.
- * @param {string} encryptedMessage - The encrypted message (base64 format).
- * @returns {Promise} The decrypted message.
- * @throws Will throw an error if no private key is found.
- */
- static async decryptMessage(encryptedMessage: string): Promise {
- const privateKey = await this.getPrivateKey();
- if (!privateKey) throw new Error("No private key found");
-
- const encrypted = new Uint8Array(
- atob(encryptedMessage).split('').map((char) => char.charCodeAt(0))
- );
-
- try {
- const decrypted = await crypto.subtle.decrypt(
- {
- name: "RSA-OAEP"
- },
- privateKey,
- encrypted
- );
-
- return new TextDecoder().decode(decrypted);
- } catch (e) {
- console.error(`Got an error while trying to decrypt the message: ${e}`);
- throw e;
- }
- }
-
- /**
- * Exports the private key as both a downloadable file and text content.
- * @param {string} filename - Name of the file to be downloaded (without extension)
- * @returns {Promise<{text: string, file: File} | null>} Object containing the text content and File object, or null if no key exists
- */
- static async exportPrivateKey(filename: string = 'private-key-backup'): Promise<{ text: string, file: File } | null> {
- try {
- const privateKey = await this.getPrivateKey();
- if (!privateKey) {
- throw new Error("No private key found to export");
- }
-
- // Export the private key to JWK format
- const exportedKey = await crypto.subtle.exportKey('jwk', privateKey);
-
- // Convert to formatted JSON string
- const keyString = JSON.stringify(exportedKey, null, 2);
-
- // Create file object
- const blob = new Blob([keyString], {type: 'application/json'});
- const file = new File([blob], `${filename}.json`, {type: 'application/json'});
-
- return {
- text: keyString,
- file: file
- };
-
- } catch (error) {
- console.error('Failed to export private key:', error);
- return null;
- }
- }
-
- /**
- * Validates if a provided private key matches the stored public key.
- * @param {JsonWebKey} privateKeyJwk - The private key in JWK format to validate
- * @param {JsonWebKey} publicKeyJwk - The public key in JWK format to validate against
- * @returns {Promise} True if the keys form a valid pair, false otherwise
- */
- static async validateKeyPair(privateKeyJwk: JsonWebKey, publicKeyJwk: JsonWebKey): Promise {
- try {
- // Import the private key
- const privateKey = await crypto.subtle.importKey(
- 'jwk',
- privateKeyJwk,
- {
- name: "RSA-OAEP",
- hash: "SHA-256",
- },
- true,
- ["decrypt"]
- );
-
- // Import the public key
- const publicKey = await crypto.subtle.importKey(
- 'jwk',
- publicKeyJwk,
- {
- name: "RSA-OAEP",
- hash: "SHA-256",
- },
- true,
- ["encrypt"]
- );
-
- // Create a test message
- const testMessage = "KeyValidationTest_" + new Date().getTime();
-
- // Encrypt with public key
- const encoder = new TextEncoder();
- const encrypted = await crypto.subtle.encrypt(
- {
- name: "RSA-OAEP",
- },
- publicKey,
- encoder.encode(testMessage)
- );
-
- // Decrypt with private key
- const decrypted = await crypto.subtle.decrypt(
- {
- name: "RSA-OAEP",
- },
- privateKey,
- encrypted
- );
-
- // Compare the result
- const decryptedText = new TextDecoder().decode(decrypted);
- return decryptedText === testMessage;
-
- } catch (error) {
- console.error('Key validation failed:', error);
- return false;
- }
- }
-
- /**
- * Restores a private key from a backup after validating it against a provided public key.
- * @param {JsonWebKey} privateKeyJwk - The private key in JWK format to restore
- * @param {JsonWebKey} publicKeyJwk - The public key in JWK format to validate against
- * @returns {Promise} True if restoration was successful, false otherwise
- */
- static async restoreFromBackup(privateKeyJwk: JsonWebKey, publicKeyJwk: JsonWebKey): Promise {
- try {
- // Validate the key pair
- const isValid = await this.validateKeyPair(privateKeyJwk, publicKeyJwk);
-
- if (!isValid) {
- throw new Error("Invalid key pair - backup key doesn't match public key");
- }
-
- // Import the private key
- const privateKey = await crypto.subtle.importKey(
- 'jwk',
- privateKeyJwk,
- {
- name: "RSA-OAEP",
- hash: "SHA-256",
- },
- true,
- ["decrypt"]
- );
-
- // Store the validated private key
- await this.storePrivateKey(privateKey);
- return true;
-
- } catch (error) {
- console.error('Backup restoration failed:', error);
- return false;
- }
- }
-
- /**
- * Opens db and creates the object store if needed.
- * @returns {Promise} A promise that resolves to the database instance.
- */
- private static async openDB(): Promise {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result);
-
- request.onupgradeneeded = (event) => {
- const db = (event.target as IDBOpenDBRequest).result;
- db.createObjectStore(this.STORE_NAME);
- };
- });
- }
-}
diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/providers/Convex.tsx b/src/lib/providers/Convex.tsx
new file mode 100644
index 0000000..ac1331d
--- /dev/null
+++ b/src/lib/providers/Convex.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { authClient } from "@/lib/auth/client";
+import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
+import { ConvexReactClient } from "convex/react";
+import { ReactNode } from "react";
+
+const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+
+export function ConvexClientProvider({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/lib/supabase/browser.tsx b/src/lib/supabase/browser.tsx
deleted file mode 100644
index 4dbe9de..0000000
--- a/src/lib/supabase/browser.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-"use client"
-import {createBrowserClient as browserClient} from '@supabase/ssr'
-
-export function createBrowserClient() {
- // Create a supabase client on the browser with project's credentials
- return browserClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
- )
-}
\ No newline at end of file
diff --git a/src/lib/supabase/server.tsx b/src/lib/supabase/server.tsx
deleted file mode 100644
index 19e6b66..0000000
--- a/src/lib/supabase/server.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-"use server"
-import {CookieOptions, createServerClient} from '@supabase/ssr';
-
-import {cookies} from 'next/headers';
-
-export async function createClient() {
- const cookieStore = await cookies();
-
- return createServerClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
- {
- cookies: {
- getAll() {
- return cookieStore.getAll().map(cookie => ({
- name: cookie.name,
- value: cookie.value,
- }))
- },
- setAll(cookiesList: { name: string; value: string; options?: CookieOptions }[]) {
- try {
- cookiesList.forEach(({name, value, options}) => {
- cookieStore.set({
- name,
- value,
- ...options,
- // Ensure cookies are secure in production
- secure: process.env.NODE_ENV === 'production',
- sameSite: 'lax'
- })
- })
- } catch (error) {
- console.error('Error setting cookies:', error)
- }
- }
- }
- }
- )
-}
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a50cb62..bd0c391 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import {type ClassValue, clsx} from "clsx"
-import {twMerge} from "tailwind-merge"
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs))
}
diff --git a/src/middleware.ts b/src/middleware.ts
deleted file mode 100644
index 2978ff9..0000000
--- a/src/middleware.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import {NextRequest, NextResponse} from "next/server";
-import {createClient} from "@/lib/supabase/server";
-
-const PUBLIC_ROUTES = [
- '/auth/login',
- '/auth/signup',
- '/api/auth',
- '/_next',
- '/favicon.ico',
- '/static',
- '/images',
-];
-
-const isPublicRoute = (path: string) => {
- return PUBLIC_ROUTES.some(route => path.startsWith(route));
-}
-
-export async function middleware(request: NextRequest) {
-
- const requestHeaders = new Headers(request.headers);
- requestHeaders.set('x-current-pathname', request.url)
- requestHeaders.set('x-next-pathname', request.nextUrl.pathname);
-
- let response = NextResponse.next({
- request: {
- headers: requestHeaders,
- },
- });
-
- try {
- const supabase = await createClient();
- const {data: {user}, error} = await supabase.auth.getUser();
-
- const path = request.nextUrl.pathname;
-
- if (!user && !isPublicRoute(path)) {
- const redirectUrl = new URL('/auth/login', request.url);
- if (request.nextUrl.search) {
- redirectUrl.search = request.nextUrl.search;
- }
- redirectUrl.searchParams.set('redirectTo', request.nextUrl.pathname);
- const redirect = NextResponse.redirect(redirectUrl);
- redirect.headers.set('x-current-pathname', path);
- return redirect;
- }
-
-
- if (user && path.startsWith('/auth/') && !path.includes("/auth/complete")) {
- return NextResponse.redirect(new URL('/', request.url));
- }
-
- if (user?.id) {
- response.headers.set('x-user-id', user.id);
- }
-
- return response;
- } catch (error) {
- console.error('Middleware error:', error);
- return response;
- }
-}
-
-export const config = {
- matcher: [
- '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
- "/api/preferences/language",
- ],
-}
\ No newline at end of file
diff --git a/src/server.ts b/src/server.ts
new file mode 100644
index 0000000..0ac3fc8
--- /dev/null
+++ b/src/server.ts
@@ -0,0 +1,20 @@
+import { createServer } from 'http'
+import next from 'next'
+import { parse } from 'url'
+
+const port = parseInt(process.env.PORT || '3000', 10)
+const dev = process.env.NODE_ENV !== 'production'
+const app = next({ dev })
+const handle = app.getRequestHandler()
+
+app.prepare().then(() => {
+ const nextServer = createServer((req, res) => {
+ const parsedUrl = parse(req.url!, true)
+ handle(req, res, parsedUrl)
+ }).listen(port)
+
+ console.log(
+ `> Server listening at http://localhost:${port} as ${dev ? 'development' : process.env.NODE_ENV
+ }`
+ )
+})
\ No newline at end of file
diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts
new file mode 100644
index 0000000..195a61f
--- /dev/null
+++ b/src/types/globals.d.ts
@@ -0,0 +1,104 @@
+import { Socket, Server as SocketIOServer } from "socket.io";
+
+declare global {
+ namespace SiPher {
+ type EventsType = {
+ name: string,
+ handler: (socket: Socket, io: SocketIOServer, ...args: any[]) => void
+ description: string
+ category: string
+ // Event type of socket.io
+ type: string
+ };
+
+ type SocketConnectionState = "connected" | "disconnected" | "connecting"
+
+ enum MessageType {
+ DM = "DM",
+ GROUP = "GROUP",
+ REGIONAL = "REGIONAL",
+ GLOBAL = "GLOBAL",
+ SERVER = "SERVER",
+ SYSTEM = "SYSTEM"
+ }
+
+ type SipherUser = {
+ id: string,
+ username: string,
+ displayUsername: string,
+ profile: {
+ avatar: string,
+ banner: string,
+ cover: string,
+ colors: {
+ primary: string,
+ accent: string,
+ }
+ }
+ metadata: {
+ description?: string,
+ pronouns?: string,
+ }
+ }
+
+ type Group = {
+ id: string,
+ }
+
+ type Regional = {
+ id: string,
+ }
+
+ type Global = {
+ id: string,
+ }
+
+ type Server = {
+ id: string,
+ }
+
+ type System = {
+ id: string,
+ }
+
+ type MessageRecipient = {
+ type: typeof MessageType.DM,
+ socketId: Socket["id"]
+ id: string,
+ user: SipherUser
+ } | {
+ type: typeof MessageType.GROUP,
+ id: string,
+ group: Group
+ } | {
+ type: typeof MessageType.REGIONAL,
+ id: string,
+ region: Regional
+ } | {
+ type: typeof MessageType.GLOBAL,
+ id: string,
+ global: Global
+ } | {
+ type: typeof MessageType.SERVER,
+ id: string,
+ server: Server
+ } | {
+ type: typeof MessageType.SYSTEM,
+ id: string,
+ system: System
+ }
+
+ type MessageEvent = {
+ message: {
+ /** Will either be a raw string or a encrypted blob, if it is a encrypted blob, the iv will be provided */
+ content: string,
+ iv?: string
+ },
+ from: SipherUser,
+ recipient: MessageRecipient
+ }
+ }
+}
+
+export { };
+
diff --git a/src/types/user.d.ts b/src/types/user.d.ts
deleted file mode 100644
index 83a814b..0000000
--- a/src/types/user.d.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-declare global {
- namespace SiPher {
- type Thread = {
- thread_id: string;
- participants: string[];
- participant_suuids: string[];
- messages: {
- error?: boolean;
- isSender: boolean;
- id: string; // UUID
- content: string; // The encrypted content (either sender_content or recipient_content)
- sender_uuid: string; // UUID of sender
- created_at: string; // ISO timestamp
- }[];
- }
-
- type User = {
- created_at: string
- indexable: boolean | null
- public_key: Json | null
- requests: string[] | null
- suuid: string
- username: string
- uuid: string
- }
-
- interface DecryptedMessage {
- id: string;
- content: string;
- sender_uuid: string;
- created_at: string;
- isSender: boolean;
- error?: boolean;
- }
-
- interface RealtimeMessageData {
- created_at: string;
- id: string;
- recipient_content: string;
- sender_content: string;
- sender_uuid: string;
- thread_id: string;
- }
- }
-}
-
-export {}
\ No newline at end of file
diff --git a/supabase/main.py b/supabase/main.py
deleted file mode 100644
index 00392f3..0000000
--- a/supabase/main.py
+++ /dev/null
@@ -1,116 +0,0 @@
-import requests
-import os
-from pathlib import Path
-from typing import Optional
-
-
-def sanitize_filename(filename: str) -> str:
- """
- Sanitize the filename while preserving as much of the original name as possible.
- """
- invalid_chars = '<>:"/\\|?*'
- for char in invalid_chars:
- filename = filename.replace(char, '_')
- return filename
-
-
-def download_sql_snippets(
- access_token: str,
- project_ref: Optional[str] = None,
- output_dir: Optional[str] = None
-) -> None:
- """
- Download SQL snippets from Supabase using the Management API.
- """
- headers = {
- "Authorization": f"Bearer {access_token}"
- }
-
- base_url = "https://api.supabase.com"
- params = {'project_ref': project_ref} if project_ref else {}
- snippets_url = f"{base_url}/v1/snippets"
-
- try:
- # Get list of all snippets
- response = requests.get(snippets_url, headers=headers, params=params)
- response.raise_for_status()
- snippets = response.json().get('data', [])
-
- if not snippets:
- print("No SQL snippets found")
- return
-
- output_dir = output_dir or "./sql_snippets"
- Path(output_dir).mkdir(parents=True, exist_ok=True)
- used_names = set()
-
- for i, snippet in enumerate(snippets, 1):
- snippet_id = snippet.get('id')
- name = snippet.get('name')
-
- if not snippet_id:
- continue
-
- snippet_url = f"{snippets_url}/{snippet_id}"
- print(f"Fetching snippet {i}/{len(snippets)}: {name or snippet_id}")
-
- # Get the detailed snippet
- snippet_response = requests.get(snippet_url, headers=headers, params=params)
- snippet_response.raise_for_status()
- full_snippet = snippet_response.json()
-
- # Use name from either response
- name = name or full_snippet.get('name')
- if not name:
- filename = f"snippet_{snippet_id}.sql"
- else:
- filename = name if name.lower().endswith('.sql') else f"{name}.sql"
- filename = sanitize_filename(filename)
-
- # Handle duplicate filenames
- base_filename = filename
- counter = 1
- while filename in used_names:
- name_parts = base_filename.rsplit('.', 1)
- filename = f"{name_parts[0]}_{counter}.{name_parts[1]}"
- counter += 1
- used_names.add(filename)
-
- filepath = Path(output_dir) / filename
-
- # Get the SQL content from the correct nested structure
- content_obj = full_snippet.get('content', {})
- sql_content = content_obj.get('sql', '')
-
- if not sql_content:
- print(f"Warning: No SQL content found for {filename}")
- continue
-
- with open(filepath, 'w', encoding='utf-8') as f:
- f.write(sql_content)
-
- print(f"Saved: {filename}")
-
- print(f"\nSuccessfully downloaded {len(snippets)} SQL snippets to {output_dir}")
-
- except requests.exceptions.RequestException as e:
- print(f"\nError accessing Supabase API:")
- print(f"- Error type: {type(e).__name__}")
- print(f"- Error message: {str(e)}")
- if hasattr(e, 'response') and e.response is not None:
- print(f"- Status code: {e.response.status_code}")
- print(f"- Response body: {e.response.text}")
- except Exception as e:
- print(f"Unexpected error: {e}")
-
-
-if __name__ == "__main__":
- print("Supabase SQL Snippet Downloader")
- print("-" * 30)
-
- access_token = os.getenv("SUPABASE_ACCESS_TOKEN") or input("Enter your Supabase access token (sbp_...): ").strip()
- project_ref = os.getenv("SUPABASE_PROJECT_REF") or input(
- "Enter your project reference ID (optional, press Enter to skip): ").strip() or None
- output_dir = input("Enter output directory (press Enter for default): ").strip() or None
-
- download_sql_snippets(access_token, project_ref, output_dir)
\ No newline at end of file
diff --git a/supabase/sql_snippets/Add public key to users.sql b/supabase/sql_snippets/Add public key to users.sql
deleted file mode 100644
index 16da8ba..0000000
--- a/supabase/sql_snippets/Add public key to users.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS public_key JSONB;
-- Function to update user's public key
CREATE
OR REPLACE FUNCTION public.update_user_public_key(
new_public_key JSONB
) RETURNS boolean AS $$
BEGIN
UPDATE public.users
SET public_key = new_public_key
WHERE uuid = auth.uid();
RETURN
FOUND;
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION public.update_user_public_key TO authenticated;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Check Realtime and Replica Identity for Messages.sql b/supabase/sql_snippets/Check Realtime and Replica Identity for Messages.sql
deleted file mode 100644
index 74e825a..0000000
--- a/supabase/sql_snippets/Check Realtime and Replica Identity for Messages.sql
+++ /dev/null
@@ -1 +0,0 @@
--- First, verify realtime is enabled
SELECT *
FROM pg_publication_tables
WHERE pubname = 'supabase_realtime'
AND tablename = 'messages';
-- Check REPLICA IDENTITY
SELECT relname, relreplident
FROM pg_class
WHERE oid = 'messages'::regclass;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Check and Set Replica Identity for Messages Table.sql b/supabase/sql_snippets/Check and Set Replica Identity for Messages Table.sql
deleted file mode 100644
index 307821b..0000000
--- a/supabase/sql_snippets/Check and Set Replica Identity for Messages Table.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE public.messages REPLICA IDENTITY FULL;
-- 2. Drop existing policies
DROP
POLICY IF EXISTS "messages_access" ON public.messages;
-- 3. Create one simple policy for messages
CREATE
POLICY "messages_realtime" ON public.messages
FOR ALL
USING (
sender_uuid = auth.uid() OR -- Either you sent it
thread_id IN ( -- Or you're in the thread
SELECT thread_id
FROM thread_participants
WHERE user_uuid = auth.uid()
)
);
\ No newline at end of file
diff --git a/supabase/sql_snippets/Create Private Thread Function.sql b/supabase/sql_snippets/Create Private Thread Function.sql
deleted file mode 100644
index a3dfc54..0000000
--- a/supabase/sql_snippets/Create Private Thread Function.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE OR REPLACE FUNCTION public.create_private_thread(
participant_suuid TEXT
) RETURNS UUID AS $$
DECLARE
current_user_uuid UUID;
current_user_suuid TEXT;
target_user_uuid UUID;
new_thread_id UUID;
existing_thread_id UUID;
BEGIN
-- Get current user's UUID and SUUID
SELECT uuid, suuid INTO STRICT current_user_uuid, current_user_suuid
FROM public.users
WHERE uuid = auth.uid();
-- Get target user's UUID
SELECT uuid INTO STRICT target_user_uuid
FROM public.users
WHERE suuid = participant_suuid;
-- Check if thread already exists between these users
SELECT tp1.thread_id INTO existing_thread_id
FROM thread_participants tp1
JOIN thread_participants tp2 ON tp1.thread_id = tp2.thread_id
WHERE tp1.user_uuid = current_user_uuid
AND tp2.user_uuid = target_user_uuid
AND (
SELECT COUNT(*)
FROM thread_participants tp3
WHERE tp3.thread_id = tp1.thread_id
) = 2;
-- If thread exists, return it
IF existing_thread_id IS NOT NULL THEN
RETURN existing_thread_id;
END IF;
-- Create new thread
INSERT INTO message_threads DEFAULT VALUES
RETURNING id INTO new_thread_id;
-- Add participants
INSERT INTO thread_participants (thread_id, user_uuid)
VALUES
(new_thread_id, current_user_uuid),
(new_thread_id, target_user_uuid);
-- Update users with both requests array and a timestamp update to force change detection
UPDATE users
SET
requests = array_remove(COALESCE(requests, ARRAY[]::text[]), participant_suuid),
created_at = created_at -- This forces a row update
WHERE uuid = current_user_uuid;
UPDATE users
SET
requests = array_remove(COALESCE(requests, ARRAY[]::text[]), current_user_suuid),
created_at = created_at -- This forces a row update
WHERE uuid = target_user_uuid;
RETURN new_thread_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Create Private Thread Function_1.sql b/supabase/sql_snippets/Create Private Thread Function_1.sql
deleted file mode 100644
index 011ac18..0000000
--- a/supabase/sql_snippets/Create Private Thread Function_1.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE
OR REPLACE FUNCTION public.create_private_thread(
participant_suuid TEXT
) RETURNS UUID AS $$
DECLARE
current_user_uuid UUID;
current_user_suuid
TEXT;
target_user_uuid
UUID;
new_thread_id
UUID;
existing_thread_id
UUID;
BEGIN
-- Get current user's UUID and SUUID
SELECT uuid, suuid
INTO STRICT current_user_uuid, current_user_suuid
FROM public.users
WHERE uuid = auth.uid();
-- Get target user's UUID
SELECT uuid
INTO STRICT target_user_uuid
FROM public.users
WHERE suuid = participant_suuid;
-- Check if thread already exists between these users
SELECT tp1.thread_id
INTO existing_thread_id
FROM thread_participants tp1
JOIN thread_participants tp2 ON tp1.thread_id = tp2.thread_id
WHERE tp1.user_uuid = current_user_uuid
AND tp2.user_uuid = target_user_uuid
AND (SELECT COUNT(*)
FROM thread_participants tp3
WHERE tp3.thread_id = tp1.thread_id) = 2;
-- If thread exists, return it
IF
existing_thread_id IS NOT NULL THEN
RETURN existing_thread_id;
END IF;
-- Create new thread
INSERT INTO message_threads DEFAULT
VALUES RETURNING id
INTO new_thread_id;
-- Add participants
INSERT INTO thread_participants (thread_id, user_uuid)
VALUES (new_thread_id, current_user_uuid),
(new_thread_id, target_user_uuid);
-- Update users with both requests array and a timestamp update to force change detection
UPDATE users
SET requests = array_remove(COALESCE(requests, ARRAY[]::text[]), participant_suuid),
created_at = created_at -- This forces a row update
WHERE uuid = current_user_uuid;
UPDATE users
SET requests = array_remove(COALESCE(requests, ARRAY[]::text[]), current_user_suuid),
created_at = created_at -- This forces a row update
WHERE uuid = target_user_uuid;
RETURN new_thread_id;
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Enable Full Replica Identity for Thread Participants.sql b/supabase/sql_snippets/Enable Full Replica Identity for Thread Participants.sql
deleted file mode 100644
index 4be42ef..0000000
--- a/supabase/sql_snippets/Enable Full Replica Identity for Thread Participants.sql
+++ /dev/null
@@ -1 +0,0 @@
--- Check and set the publication
SELECT *
FROM pg_publication;
-- If not set correctly, reset it:
DROP
PUBLICATION IF EXISTS supabase_realtime;
CREATE
PUBLICATION supabase_realtime FOR ALL TABLES;
-- Enable FULL replica identity for our tables
ALTER TABLE public.users REPLICA IDENTITY FULL;
ALTER TABLE public.messages REPLICA IDENTITY FULL;
ALTER TABLE public.thread_participants REPLICA IDENTITY FULL;
ALTER TABLE public.message_threads REPLICA IDENTITY FULL;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Enable Full Replica Identity for Users Table.sql b/supabase/sql_snippets/Enable Full Replica Identity for Users Table.sql
deleted file mode 100644
index d1d2525..0000000
--- a/supabase/sql_snippets/Enable Full Replica Identity for Users Table.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE public.users REPLICA IDENTITY FULL;
ALTER
PUBLICATION supabase_realtime ADD TABLE public.users;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Enable Replication for Messages Table.sql b/supabase/sql_snippets/Enable Replication for Messages Table.sql
deleted file mode 100644
index c8977b3..0000000
--- a/supabase/sql_snippets/Enable Replication for Messages Table.sql
+++ /dev/null
@@ -1 +0,0 @@
--- This snippet checks if the messages table is already part of the publication before attempting to add it.
-- Enable replication for the messages table
ALTER TABLE public.messages REPLICA IDENTITY FULL;
-- Check if the messages table is already part of the publication
DO
$$
BEGIN
IF
NOT EXISTS (
SELECT 1
FROM pg_publication_tables
WHERE pubname = 'supabase_realtime' AND schemaname = 'public' AND tablename = 'messages'
) THEN
ALTER
PUBLICATION supabase_realtime
ADD TABLE public.messages;
END IF;
END $$;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Get Thread Details.sql b/supabase/sql_snippets/Get Thread Details.sql
deleted file mode 100644
index 4f6599c..0000000
--- a/supabase/sql_snippets/Get Thread Details.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE OR REPLACE FUNCTION public.get_thread(thread_uuid UUID, user_id UUID)
RETURNS TABLE (
thread_id UUID,
participants TEXT[],
participant_suuids TEXT[],
messages JSON[]
) AS $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM public.thread_participants tp
WHERE tp.thread_id = thread_uuid
AND tp.user_uuid = user_id
) THEN
RETURN;
END IF;
RETURN QUERY
WITH thread_info AS (
-- Get thread participants info first
SELECT
mt.id as tid,
array_agg(DISTINCT u.username) as usernames,
array_agg(DISTINCT u.suuid::TEXT) as suuids
FROM public.message_threads mt
JOIN public.thread_participants tp ON mt.id = tp.thread_id
JOIN public.users u ON tp.user_uuid = u.uuid
WHERE mt.id = thread_uuid
GROUP BY mt.id
),
messages_info AS (
-- Get messages separately
SELECT
m.thread_id,
array_agg(
json_build_object(
'id', m.id,
'content', CASE
WHEN m.sender_uuid = user_id THEN m.sender_content
ELSE m.recipient_content
END,
'sender_uuid', m.sender_uuid,
'created_at', m.created_at
)
ORDER BY m.created_at ASC -- Add ordering here
) FILTER (WHERE m.id IS NOT NULL) as msg_array
FROM public.messages m
WHERE m.thread_id = thread_uuid
GROUP BY m.thread_id
)
SELECT
t.tid,
t.usernames,
t.suuids,
COALESCE(m.msg_array, ARRAY[]::JSON[])
FROM thread_info t
LEFT JOIN messages_info m ON t.tid = m.thread_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Get User Threads.sql b/supabase/sql_snippets/Get User Threads.sql
deleted file mode 100644
index eac02f4..0000000
--- a/supabase/sql_snippets/Get User Threads.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE OR REPLACE FUNCTION public.get_user_threads(user_id UUID)
RETURNS TABLE (
thread_id UUID,
participants TEXT[],
messages JSON[]
) AS $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM public.thread_participants
WHERE user_uuid = user_id
) THEN
-- Return empty result if user has no threads
RETURN;
END IF;
RETURN QUERY
SELECT
mt.id,
array_agg(DISTINCT u.username),
COALESCE(array_agg(
CASE WHEN m.id IS NOT NULL THEN
json_build_object(
'id', m.id,
'content', CASE
WHEN m.sender_uuid = user_id THEN m.sender_content
ELSE m.recipient_content
END,
'sender_uuid', m.sender_uuid,
'created_at', m.created_at
)
ELSE NULL END
) FILTER (WHERE m.id IS NOT NULL), ARRAY[]::JSON[])
FROM public.message_threads mt
JOIN public.thread_participants tp ON mt.id = tp.thread_id
JOIN public.users u ON tp.user_uuid = u.uuid
LEFT JOIN public.messages m ON mt.id = m.thread_id
WHERE mt.id IN (
SELECT tp2.thread_id
FROM public.thread_participants tp2
WHERE tp2.user_uuid = user_id
)
GROUP BY mt.id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Row Level Security Policies for Messages.sql b/supabase/sql_snippets/Row Level Security Policies for Messages.sql
deleted file mode 100644
index 4649fa0..0000000
--- a/supabase/sql_snippets/Row Level Security Policies for Messages.sql
+++ /dev/null
@@ -1 +0,0 @@
--- This snippet updates the policy to allow the sender of messages and participants in the thread to receive realtime events.
-- First, let's drop the existing policy
DROP
POLICY IF EXISTS "Thread participants access" ON public.messages;
-- 1. First ensure RLS is enabled
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
-- 2. Set REPLICA IDENTITY to FULL (required for realtime)
ALTER TABLE public.messages REPLICA IDENTITY FULL;
-- Check current publication configuration
SELECT *
FROM pg_publication_tables
WHERE pubname = 'supabase_realtime';
-- Just set these then:
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages REPLICA IDENTITY FULL;
GRANT
SELECT
,
INSERT ON public.messages TO authenticated;
GRANT
USAGE
ON
SCHEMA
public TO authenticated;
CREATE
POLICY "Thread participants access" ON public.messages FOR ALL USING (
auth.uid () IN (
SELECT
user_uuid
FROM
thread_participants
WHERE
thread_id = messages.thread_id
)
);
\ No newline at end of file
diff --git a/supabase/sql_snippets/Row Level Security Policies for Messaging App.sql b/supabase/sql_snippets/Row Level Security Policies for Messaging App.sql
deleted file mode 100644
index acb08d2..0000000
--- a/supabase/sql_snippets/Row Level Security Policies for Messaging App.sql
+++ /dev/null
@@ -1 +0,0 @@
--- RLS Policies
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE message_threads ENABLE ROW LEVEL SECURITY;
ALTER TABLE thread_participants ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Users policies
CREATE
POLICY "Users can view their own profile"
ON users FOR
SELECT
USING (auth.uid() = uuid);
CREATE
POLICY "Users can view indexable profiles"
ON users FOR
SELECT
USING (indexable = true);
CREATE
POLICY "Users can update their own profile"
ON users FOR
UPDATE
USING (auth.uid() = uuid);
-- Message threads policies
CREATE
POLICY "Users can view their threads"
ON message_threads FOR
SELECT
USING (is_thread_participant(id));
-- Thread participants policies
CREATE
POLICY "Users can view their thread participants"
ON thread_participants FOR
SELECT
USING (is_thread_participant(thread_id));
-- Messages policies
CREATE
POLICY "Users can view their messages"
ON messages FOR
SELECT
USING (is_thread_participant(thread_id));
CREATE
POLICY "Users can send messages"
ON messages FOR INSERT
WITH CHECK (is_thread_participant(thread_id));
\ No newline at end of file
diff --git a/supabase/sql_snippets/Send Message Function.sql b/supabase/sql_snippets/Send Message Function.sql
deleted file mode 100644
index d3bda1c..0000000
--- a/supabase/sql_snippets/Send Message Function.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP FUNCTION IF EXISTS send_message(uuid,text,text);
CREATE
OR REPLACE FUNCTION public.send_message(
thread_uuid UUID,
sender_content TEXT,
recipient_content TEXT
) RETURNS UUID AS $$
DECLARE
message_id UUID;
recipient_uuid
UUID;
BEGIN
IF
NOT EXISTS (
SELECT 1
FROM thread_participants tp
WHERE tp.thread_id = thread_uuid
AND tp.user_uuid = auth.uid()
) THEN
RAISE EXCEPTION 'User not authorized to send message in this thread';
END IF;
-- Get the recipient's UUID (the other participant)
SELECT tp.user_uuid
INTO recipient_uuid
FROM thread_participants tp
WHERE tp.thread_id = thread_uuid
AND tp.user_uuid != auth.uid()
LIMIT 1;
-- Insert message with both encrypted versions
INSERT INTO messages (thread_id,
sender_uuid,
sender_content,
recipient_content)
VALUES (thread_uuid,
auth.uid(),
sender_content,
recipient_content) RETURNING id
INTO message_id;
RETURN message_id;
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;
\ No newline at end of file
diff --git a/supabase/sql_snippets/Update User Requests Function.sql b/supabase/sql_snippets/Update User Requests Function.sql
deleted file mode 100644
index 78a7ebd..0000000
--- a/supabase/sql_snippets/Update User Requests Function.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP FUNCTION update_user_requests(uuid,text[]);
-- Create function to update user requests
CREATE
OR REPLACE FUNCTION public.update_user_requests(
search_term TEXT,
new_request TEXT -- Single SUUID to add/remove
) RETURNS boolean AS $$
DECLARE
target_user_uuid UUID;
current_requests
TEXT[];
BEGIN
-- First, find the target user based on SUUID or username (if indexable)
SELECT uuid, requests
INTO target_user_uuid, current_requests
FROM public.users
WHERE suuid = search_term
OR (
username = search_term
AND indexable = true
)
LIMIT 1;
IF
target_user_uuid IS NULL THEN
RETURN false;
END IF;
-- Update the requests array
-- Add if not exists, remove if exists
IF
new_request = ANY(current_requests) THEN
-- Remove the request
UPDATE public.users
SET requests = array_remove(requests, new_request)
WHERE uuid = target_user_uuid;
ELSE
-- Add the request
UPDATE public.users
SET requests = array_append(requests, new_request)
WHERE uuid = target_user_uuid;
END IF;
RETURN
FOUND;
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;
-- Grant access to authenticated users
GRANT EXECUTE ON FUNCTION public.update_user_requests TO authenticated;
\ No newline at end of file
diff --git a/supabase/sql_snippets/User Access Policy for Search Function.sql b/supabase/sql_snippets/User Access Policy for Search Function.sql
deleted file mode 100644
index c6fdca0..0000000
--- a/supabase/sql_snippets/User Access Policy for Search Function.sql
+++ /dev/null
@@ -1 +0,0 @@
--- Drop existing policies and function
DROP
POLICY IF EXISTS "Allow SUUID searches" ON public.users;
DROP
POLICY IF EXISTS "Allow SUUID searches - Exact Match" ON public.users;
DROP
POLICY IF EXISTS "Allow SUUID searches - Permissive" ON public.users;
DROP FUNCTION IF EXISTS search_users(text);
-- Create a new policy to explicitly allow SUUID searches
CREATE
POLICY "Allow SUUID searches - Exact Match" ON public.users
FOR
SELECT
USING (
suuid = current_setting('request.jwt.claims')::json->>'search_term'
OR indexable = true
);
-- Create an alternative approach: more permissive policy for SUUID searches
CREATE
POLICY "Allow SUUID searches - Permissive" ON public.users
FOR
SELECT
USING (
suuid = ANY (
ARRAY (
SELECT
unnest(
regexp_split_to_array(
current_setting('request.jwt.claims')::json->>'search_term', ','
)
)
)
)
OR indexable = true
);
-- Create or replace the search_users function
CREATE
OR REPLACE FUNCTION public.search_users (search_term TEXT)
RETURNS TABLE (
uuid UUID,
suuid TEXT,
username TEXT,
indexable BOOLEAN,
public_key JSONB
) AS $$
BEGIN
-- Set the search term in the current transaction
PERFORM
set_config('request.jwt.claims', json_build_object('search_term', search_term)::text, true);
RETURN QUERY
SELECT u.uuid,
u.suuid::TEXT, CASE
WHEN u.suuid = search_term OR u.indexable THEN u.username
ELSE NULL
END,
u.indexable,
u.public_key
FROM public.users u
WHERE u.suuid = search_term
OR (
u.indexable = true AND
u.username ILIKE '%' || search_term || '%'
);
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;
\ No newline at end of file
diff --git a/supabase/sql_snippets/User Management Functions.sql b/supabase/sql_snippets/User Management Functions.sql
deleted file mode 100644
index c19e3e8..0000000
--- a/supabase/sql_snippets/User Management Functions.sql
+++ /dev/null
@@ -1 +0,0 @@
--- For generate_short_uuid
CREATE
OR REPLACE FUNCTION public.generate_short_uuid () RETURNS TEXT AS $$
DECLARE
chars TEXT := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
result
TEXT := '';
i
INTEGER := 0;
max_attempts
INTEGER := 10;
current_attempt
INTEGER := 0;
is_unique
BOOLEAN := false;
BEGIN
WHILE
NOT is_unique AND current_attempt < max_attempts LOOP
result := '';
FOR i IN 1..8 LOOP
result := result || substr(chars, floor(random() * length(chars) + 1)::integer, 1);
END LOOP;
SELECT COUNT(*) = 0
INTO is_unique
FROM public.users
WHERE suuid = result;
current_attempt
:= current_attempt + 1;
END LOOP;
IF
NOT is_unique THEN
RAISE EXCEPTION 'Could not generate unique short UUID after % attempts', max_attempts;
END IF;
RETURN result;
END;
$$
LANGUAGE plpgsql;
CREATE
OR REPLACE FUNCTION public.search_users(search_term TEXT)
RETURNS TABLE (
uuid UUID,
suuid TEXT,
username TEXT,
indexable BOOLEAN
) AS $$
BEGIN
RETURN QUERY
SELECT u.uuid,
u.suuid::TEXT,
-- Simplified CASE logic: show username if SUUID match OR (username match AND indexable) CASE
WHEN u.suuid = search_term OR u.indexable THEN u.username
ELSE NULL
END,
u.indexable
FROM public.users u
WHERE u.suuid = search_term -- Case 1: SUUID match (always show)
OR (
u.indexable = true AND -- Case 2: Username match + indexable
u.username ILIKE '%' || search_term || '%'
);
END;
$$
LANGUAGE plpgsql;
-- For is_thread_participant
CREATE
OR REPLACE FUNCTION public.is_thread_participant (thread_uuid UUID) RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (SELECT 1
FROM public.thread_participants
WHERE thread_id = thread_uuid
AND user_uuid = auth.uid());
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;
-- For get_user_threads
CREATE
OR REPLACE FUNCTION public.get_user_threads (user_id UUID) RETURNS TABLE (
thread_id UUID,
participants TEXT[],
messages JSON[]
) AS $$
BEGIN
IF
NOT EXISTS (
SELECT 1
FROM public.thread_participants
WHERE user_uuid = user_id
) THEN
-- Return empty result if user has no threads
RETURN;
END IF;
RETURN QUERY
SELECT mt.id,
array_agg(DISTINCT u.username),
COALESCE(array_agg(
CASE
WHEN m.id IS NOT NULL THEN
json_build_object(
'id', m.id,
'content', m.content,
'created_at', m.created_at
)
ELSE NULL END
) FILTER(WHERE m.id IS NOT NULL), ARRAY[] ::JSON[])
FROM public.message_threads mt
JOIN public.thread_participants tp ON mt.id = tp.thread_id
JOIN public.users u ON tp.user_uuid = u.uuid
LEFT JOIN public.messages m ON mt.id = m.thread_id
WHERE mt.id IN (SELECT thread_id
FROM public.thread_participants
WHERE user_uuid = user_id)
GROUP BY mt.id;
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;
\ No newline at end of file
diff --git a/supabase/sql_snippets/User Management Table.sql b/supabase/sql_snippets/User Management Table.sql
deleted file mode 100644
index 9951c9f..0000000
--- a/supabase/sql_snippets/User Management Table.sql
+++ /dev/null
@@ -1 +0,0 @@
--- Drop everything related to users
DROP TABLE IF EXISTS public.users CASCADE;
-- Create new users table
CREATE TABLE
public.users
(
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
suuid CHAR(8) UNIQUE NOT NULL,
username TEXT UNIQUE NOT NULL CHECK (
length(username) >= 3
AND username ~ '^[a-zA-Z0-9_-]+$'
) ,
indexable BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE ('utc'::text, NOW()) NOT NULL
);
-- Create trigger function for SUUID generation
CREATE
OR REPLACE FUNCTION public.handle_new_user () RETURNS TRIGGER AS $$
BEGIN
NEW.suuid
:= public.generate_short_uuid();
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
-- Create the trigger
CREATE TRIGGER on_user_created
BEFORE INSERT
ON public.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user ();
-- Add policies
CREATE
POLICY "Users can view their own profile" ON public.users FOR
SELECT
USING (auth.uid () = uuid);
CREATE
POLICY "Users can view indexable profiles" ON public.users FOR
SELECT
USING (indexable = true);
CREATE
POLICY "Allow user registration" ON public.users FOR INSERT
WITH
CHECK (true);
CREATE
POLICY "Users can update their own profile" ON public.users
FOR
UPDATE
USING (auth.uid () = uuid);
-- Enable RLS
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
-- Create index for better performance
CREATE INDEX idx_users_suuid ON public.users (suuid);
CREATE INDEX idx_users_indexable ON public.users (indexable) WHERE
indexable = true;
\ No newline at end of file
diff --git a/supabase/sql_snippets/User Registration Policy.sql b/supabase/sql_snippets/User Registration Policy.sql
deleted file mode 100644
index 0118a0e..0000000
--- a/supabase/sql_snippets/User Registration Policy.sql
+++ /dev/null
@@ -1 +0,0 @@
--- Drop everything related to users
DROP TABLE IF EXISTS public.users CASCADE;
-- Create new users table
CREATE TABLE
public.users
(
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
suuid CHAR(8) UNIQUE NOT NULL,
username TEXT UNIQUE NOT NULL CHECK (
length(username) >= 3
AND username ~ '^[a-zA-Z0-9_-]+$'
) ,
password TEXT NOT NULL CHECK (length(password) >= 8),
indexable BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE ('utc'::text, NOW()) NOT NULL
);
-- Create trigger function for SUUID generation
CREATE
OR REPLACE FUNCTION public.handle_new_user () RETURNS TRIGGER AS $$
BEGIN
NEW.suuid
:= public.generate_short_uuid();
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
-- Create the trigger
CREATE TRIGGER on_user_created
BEFORE INSERT
ON public.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user ();
-- Add policies
CREATE
POLICY "Users can view their own profile" ON public.users FOR
SELECT
USING (auth.uid () = uuid);
CREATE
POLICY "Users can view indexable profiles" ON public.users FOR
SELECT
USING (indexable = true);
CREATE
POLICY "Allow user registration" ON public.users FOR INSERT
WITH
CHECK (true);
CREATE
POLICY "Users can update their own profile" ON public.users
FOR
UPDATE
USING (auth.uid () = uuid);
-- Enable RLS
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
-- Create index for better performance
CREATE INDEX idx_users_suuid ON public.users (suuid);
CREATE INDEX idx_users_indexable ON public.users (indexable) WHERE
indexable = true;
\ No newline at end of file
diff --git a/supabase/sql_snippets/User Requests and Messages Management.sql b/supabase/sql_snippets/User Requests and Messages Management.sql
deleted file mode 100644
index 076632b..0000000
--- a/supabase/sql_snippets/User Requests and Messages Management.sql
+++ /dev/null
@@ -1 +0,0 @@
--- Add requests array to users table
ALTER TABLE public.users
ADD COLUMN requests TEXT[] DEFAULT ARRAY[]::TEXT[];
-- Create index for the requests array for better performance
CREATE INDEX idx_users_requests ON public.users USING GIN (requests);
-- Add policy for requests field
CREATE
POLICY "Users can only see their own requests" ON public.users FOR
SELECT
USING (auth.uid () = uuid);
\ No newline at end of file
diff --git a/supabase/sql_snippets/User and Message Indexes.sql b/supabase/sql_snippets/User and Message Indexes.sql
deleted file mode 100644
index 7d79f91..0000000
--- a/supabase/sql_snippets/User and Message Indexes.sql
+++ /dev/null
@@ -1 +0,0 @@
-CREATE INDEX idx_users_suuid ON users (suuid);
CREATE INDEX idx_users_indexable ON users (indexable) WHERE indexable = true;
CREATE INDEX idx_thread_participants_user ON thread_participants (user_uuid);
CREATE INDEX idx_messages_thread ON messages (thread_id);
\ No newline at end of file
diff --git a/supabase/sql_snippets/Users Table.sql b/supabase/sql_snippets/Users Table.sql
deleted file mode 100644
index 0327a90..0000000
--- a/supabase/sql_snippets/Users Table.sql
+++ /dev/null
@@ -1 +0,0 @@
--- Base Tables
CREATE TABLE
users
(
uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
suuid CHAR(8) UNIQUE NOT NULL,
username TEXT UNIQUE NOT NULL CHECK (
length(username) >= 3
AND username ~ '^[a-zA-Z0-9_-]+$'
) ,
password TEXT NOT NULL CHECK (length(password) >= 8),
indexable BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE ('utc'::text, NOW()) NOT NULL
);
CREATE TABLE
message_threads
(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);
CREATE TABLE
thread_participants
(
thread_id UUID REFERENCES message_threads (id) ON DELETE CASCADE,
user_uuid UUID REFERENCES users (uuid) ON DELETE CASCADE,
PRIMARY KEY (thread_id, user_uuid)
);
CREATE TABLE
messages
(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
thread_id UUID REFERENCES message_threads (id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);
\ No newline at end of file
diff --git a/tailwind.config.ts b/tailwind.config.ts
deleted file mode 100644
index 9a58f7a..0000000
--- a/tailwind.config.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import type { Config } from "tailwindcss";
-
-export default {
- darkMode: ["class"],
- content: [
- "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
- "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
- "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
- ],
- theme: {
- extend: {
- colors: {
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
- card: {
- DEFAULT: 'hsl(var(--card))',
- foreground: 'hsl(var(--card-foreground))'
- },
- popover: {
- DEFAULT: 'hsl(var(--popover))',
- foreground: 'hsl(var(--popover-foreground))'
- },
- primary: {
- DEFAULT: 'hsl(var(--primary))',
- foreground: 'hsl(var(--primary-foreground))'
- },
- secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))'
- },
- muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))'
- },
- accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))'
- },
- destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))'
- },
- border: 'hsl(var(--border))',
- input: 'hsl(var(--input))',
- ring: 'hsl(var(--ring))',
- chart: {
- '1': 'hsl(var(--chart-1))',
- '2': 'hsl(var(--chart-2))',
- '3': 'hsl(var(--chart-3))',
- '4': 'hsl(var(--chart-4))',
- '5': 'hsl(var(--chart-5))'
- }
- },
- borderRadius: {
- lg: 'var(--radius)',
- md: 'calc(var(--radius) - 2px)',
- sm: 'calc(var(--radius) - 4px)'
- },
- keyframes: {
- 'accordion-down': {
- from: {
- height: '0'
- },
- to: {
- height: 'var(--radix-accordion-content-height)'
- }
- },
- 'accordion-up': {
- from: {
- height: 'var(--radix-accordion-content-height)'
- },
- to: {
- height: '0'
- }
- }
- },
- animation: {
- 'accordion-down': 'accordion-down 0.2s ease-out',
- 'accordion-up': 'accordion-up 0.2s ease-out'
- }
- }
- },
- plugins: [require("tailwindcss-animate")],
-} satisfies Config;
diff --git a/tsconfig.json b/tsconfig.json
index c133409..f25e28f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,27 +1,45 @@
{
- "compilerOptions": {
- "target": "ES2017",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
-}
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ],
+ "@/assets/*": [
+ "./public/assets/*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file