diff --git a/app/.prettierrc b/app/.prettierrc new file mode 100644 index 0000000..0a02bce --- /dev/null +++ b/app/.prettierrc @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/app/app.css b/app/app.css index 9a144fc..724d84f 100644 --- a/app/app.css +++ b/app/app.css @@ -4,194 +4,204 @@ @custom-variant dark (&:is(.dark *)); @theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans: + "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } :root { - --background: oklch(1.00 0 0); - --foreground: oklch(0.32 0 0); - --card: oklch(1.00 0 0); - --card-foreground: oklch(0.32 0 0); - --popover: oklch(1.00 0 0); - --popover-foreground: oklch(0.32 0 0); - --primary: oklch(0.62 0.19 259.81); - --primary-foreground: oklch(1.00 0 0); - --secondary: oklch(0.97 0.00 264.54); - --secondary-foreground: oklch(0.45 0.03 256.80); - --muted: oklch(0.98 0.00 247.84); - --muted-foreground: oklch(0.55 0.02 264.36); - --accent: oklch(0.95 0.03 236.82); - --accent-foreground: oklch(0.38 0.14 265.52); - --destructive: oklch(0.64 0.21 25.33); - --destructive-foreground: oklch(1.00 0 0); - --border: oklch(0.93 0.01 264.53); - --input: oklch(0.93 0.01 264.53); - --ring: oklch(0.62 0.19 259.81); - --chart-1: oklch(0.62 0.19 259.81); - --chart-2: oklch(0.55 0.22 262.88); - --chart-3: oklch(0.49 0.22 264.38); - --chart-4: oklch(0.42 0.18 265.64); - --chart-5: oklch(0.38 0.14 265.52); - --sidebar: oklch(0.98 0.00 247.84); - --sidebar-foreground: oklch(0.32 0 0); - --sidebar-primary: oklch(0.62 0.19 259.81); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.95 0.03 236.82); - --sidebar-accent-foreground: oklch(0.38 0.14 265.52); - --sidebar-border: oklch(0.93 0.01 264.53); - --sidebar-ring: oklch(0.62 0.19 259.81); - --font-sans: Inter, sans-serif; - --font-serif: Source Serif 4, serif; - --font-mono: JetBrains Mono, monospace; - --radius: 0.375rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(1 0 0); + --foreground: oklch(0.32 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.32 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.32 0 0); + --primary: oklch(0.62 0.19 259.81); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.97 0 264.54); + --secondary-foreground: oklch(0.45 0.03 256.8); + --muted: oklch(0.98 0 247.84); + --muted-foreground: oklch(0.55 0.02 264.36); + --accent: oklch(0.95 0.03 236.82); + --accent-foreground: oklch(0.38 0.14 265.52); + --destructive: oklch(0.64 0.21 25.33); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.93 0.01 264.53); + --input: oklch(0.93 0.01 264.53); + --ring: oklch(0.62 0.19 259.81); + --chart-1: oklch(0.62 0.19 259.81); + --chart-2: oklch(0.55 0.22 262.88); + --chart-3: oklch(0.49 0.22 264.38); + --chart-4: oklch(0.42 0.18 265.64); + --chart-5: oklch(0.38 0.14 265.52); + --sidebar: oklch(0.98 0 247.84); + --sidebar-foreground: oklch(0.32 0 0); + --sidebar-primary: oklch(0.62 0.19 259.81); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.95 0.03 236.82); + --sidebar-accent-foreground: oklch(0.38 0.14 265.52); + --sidebar-border: oklch(0.93 0.01 264.53); + --sidebar-ring: oklch(0.62 0.19 259.81); + --font-sans: Inter, sans-serif; + --font-serif: Source Serif 4, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.375rem; + --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-sm: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow-md: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1); + --shadow-lg: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1); + --shadow-xl: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1); + --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); } .dark { - --background: oklch(0.20 0 0); - --foreground: oklch(0.92 0 0); - --card: oklch(0.27 0 0); - --card-foreground: oklch(0.92 0 0); - --popover: oklch(0.27 0 0); - --popover-foreground: oklch(0.92 0 0); - --primary: oklch(0.62 0.19 259.81); - --primary-foreground: oklch(1.00 0 0); - --secondary: oklch(0.27 0 0); - --secondary-foreground: oklch(0.92 0 0); - --muted: oklch(0.27 0 0); - --muted-foreground: oklch(0.72 0 0); - --accent: oklch(0.38 0.14 265.52); - --accent-foreground: oklch(0.88 0.06 254.13); - --destructive: oklch(0.64 0.21 25.33); - --destructive-foreground: oklch(1.00 0 0); - --border: oklch(0.37 0 0); - --input: oklch(0.37 0 0); - --ring: oklch(0.62 0.19 259.81); - --chart-1: oklch(0.71 0.14 254.62); - --chart-2: oklch(0.62 0.19 259.81); - --chart-3: oklch(0.55 0.22 262.88); - --chart-4: oklch(0.49 0.22 264.38); - --chart-5: oklch(0.42 0.18 265.64); - --sidebar: oklch(0.20 0 0); - --sidebar-foreground: oklch(0.92 0 0); - --sidebar-primary: oklch(0.62 0.19 259.81); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.38 0.14 265.52); - --sidebar-accent-foreground: oklch(0.88 0.06 254.13); - --sidebar-border: oklch(0.37 0 0); - --sidebar-ring: oklch(0.62 0.19 259.81); - --font-sans: Inter, sans-serif; - --font-serif: Source Serif 4, serif; - --font-mono: JetBrains Mono, monospace; - --radius: 0.375rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(0.2 0 0); + --foreground: oklch(0.92 0 0); + --card: oklch(0.27 0 0); + --card-foreground: oklch(0.92 0 0); + --popover: oklch(0.27 0 0); + --popover-foreground: oklch(0.92 0 0); + --primary: oklch(0.62 0.19 259.81); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.27 0 0); + --secondary-foreground: oklch(0.92 0 0); + --muted: oklch(0.27 0 0); + --muted-foreground: oklch(0.72 0 0); + --accent: oklch(0.38 0.14 265.52); + --accent-foreground: oklch(0.88 0.06 254.13); + --destructive: oklch(0.64 0.21 25.33); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.37 0 0); + --input: oklch(0.37 0 0); + --ring: oklch(0.62 0.19 259.81); + --chart-1: oklch(0.71 0.14 254.62); + --chart-2: oklch(0.62 0.19 259.81); + --chart-3: oklch(0.55 0.22 262.88); + --chart-4: oklch(0.49 0.22 264.38); + --chart-5: oklch(0.42 0.18 265.64); + --sidebar: oklch(0.2 0 0); + --sidebar-foreground: oklch(0.92 0 0); + --sidebar-primary: oklch(0.62 0.19 259.81); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.38 0.14 265.52); + --sidebar-accent-foreground: oklch(0.88 0.06 254.13); + --sidebar-border: oklch(0.37 0 0); + --sidebar-ring: oklch(0.62 0.19 259.81); + --font-sans: Inter, sans-serif; + --font-serif: Source Serif 4, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.375rem; + --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-sm: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow-md: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1); + --shadow-lg: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1); + --shadow-xl: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1); + --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); - --font-sans: var(--font-sans); - --font-mono: var(--font-mono); - --font-serif: var(--font-serif); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); - --shadow-2xs: var(--shadow-2xs); - --shadow-xs: var(--shadow-xs); - --shadow-sm: var(--shadow-sm); - --shadow: var(--shadow); - --shadow-md: var(--shadow-md); - --shadow-lg: var(--shadow-lg); - --shadow-xl: var(--shadow-xl); - --shadow-2xl: var(--shadow-2xl); + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); } @layer base { - /* width */ - ::-webkit-scrollbar { - @apply w-1 - } + /* width */ + ::-webkit-scrollbar { + @apply w-1; + } - /* Handle */ - ::-webkit-scrollbar-thumb { - @apply bg-border rounded-full mr-0.5 - } + /* Handle */ + ::-webkit-scrollbar-thumb { + @apply bg-border rounded-full mr-0.5; + } - * { - @apply border-border outline-ring/50; - } + * { + @apply border-border outline-ring/50; + } - body { - @apply bg-background text-foreground; - } + body { + @apply bg-background text-foreground; + } - button, - [role="button"] { - cursor: pointer; - } + button, + [role="button"] { + cursor: pointer; + } } @layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } - /* Hide scrollbar for Chrome, Safari and Opera */ - .no-scrollbar::-webkit-scrollbar { - display: none; - } - - /* Hide scrollbar for IE, Edge and Firefox */ - .no-scrollbar { - /* IE and Edge */ - -ms-overflow-style: none; - /* Firefox */ - scrollbar-width: none; - } -} \ No newline at end of file + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + /* IE and Edge */ + -ms-overflow-style: none; + /* Firefox */ + scrollbar-width: none; + } +} diff --git a/app/components/app-layout.tsx b/app/components/app-layout.tsx index 5a49859..bc44d4b 100644 --- a/app/components/app-layout.tsx +++ b/app/components/app-layout.tsx @@ -14,31 +14,45 @@ interface AppLayoutProps { } export default function AppLayout({ children }: AppLayoutProps) { - let servers = useServerListStore(useShallow((state) => Object.values(state.servers))) + let servers = Object.values( + useServerListStore(useShallow((state) => state.servers)), + ); const matches = useMatches(); let list = React.useMemo(() => { - return matches.map(match => (match.handle as { - listComponent?: React.ReactNode - })?.listComponent).reverse().find(component => !!component) - }, [matches]) + return matches + .map( + (match) => + ( + match.handle as { + listComponent?: React.ReactNode; + } + )?.listComponent, + ) + .reverse() + .find((component) => !!component); + }, [matches]); return (
@@ -54,7 +55,12 @@ function GenericFileAttachment({ file }: ChatMessageAttachmentProps) { - + Download @@ -65,9 +71,15 @@ function GenericFileAttachment({ file }: ChatMessageAttachmentProps) { - + - Open in new tab + + Open in new tab + @@ -85,7 +97,10 @@ function ImageAttachment({ file }: ChatMessageAttachmentProps) { - + - {file.filename} + + {file.filename} + - Open original + Open + original @@ -133,4 +151,4 @@ function ImageAttachment({ file }: ChatMessageAttachmentProps) { ); -} \ No newline at end of file +} diff --git a/app/components/chat-message.tsx b/app/components/chat-message.tsx index 7ba9a4e..3498dec 100644 --- a/app/components/chat-message.tsx +++ b/app/components/chat-message.tsx @@ -1,86 +1,112 @@ -import { Clock } from "lucide-react" -import React from "react" -import { useShallow } from "zustand/react/shallow" -import type { Message } from "~/lib/api/types" -import { useUsersStore } from "~/stores/users-store" -import ChatMessageAttachment from "./chat-message-attachment" -import UserAvatar from "./user-avatar" +import { Clock } from "lucide-react"; +import React from "react"; +import { useShallow } from "zustand/react/shallow"; +import type { Message } from "~/lib/api/types"; +import { useUsersStore } from "~/stores/users-store"; +import ChatMessageAttachment from "./chat-message-attachment"; +import UserAvatar from "./user-avatar"; +import UserContextMenu from "./user-context-menu"; interface ChatMessageProps { - message: Message + message: Message; } -export default function ChatMessage( - { message }: ChatMessageProps -) { - const { user, fetchUsersIfNotPresent } = useUsersStore(useShallow(state => ({ - user: state.users[message.authorId], - fetchUsersIfNotPresent: state.fetchUsersIfNotPresent - }))) +export default function ChatMessage({ message }: ChatMessageProps) { + const { user, fetchUsersIfNotPresent } = useUsersStore( + useShallow((state) => ({ + user: state.users[message.authorId], + fetchUsersIfNotPresent: state.fetchUsersIfNotPresent, + })), + ); React.useEffect(() => { - fetchUsersIfNotPresent([message.authorId]) - }, []) + fetchUsersIfNotPresent([message.authorId]); + }, []); const formatMessageDate = (date: Date) => { - const now = new Date() - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const yesterday = new Date(today) - yesterday.setDate(yesterday.getDate() - 1) + const now = new Date(); + const today = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); - const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + const messageDate = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + ); // Get localized time string - const timeString = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false }) - const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1) + const timeString = date.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + const capitalize = (str: string) => + str.charAt(0).toUpperCase() + str.slice(1); if (messageDate.getTime() === today.getTime()) { // Use Intl.RelativeTimeFormat for localized "Today" - const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) - return `${(capitalize(rtf.format(0, 'day')))}, ${timeString}` + const rtf = new Intl.RelativeTimeFormat(undefined, { + numeric: "auto", + }); + return `${capitalize(rtf.format(0, "day"))}, ${timeString}`; } else if (messageDate.getTime() === yesterday.getTime()) { // Use Intl.RelativeTimeFormat for localized "Yesterday" - const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) - return `${capitalize(rtf.format(-1, 'day'))}, ${timeString}` + const rtf = new Intl.RelativeTimeFormat(undefined, { + numeric: "auto", + }); + return `${capitalize(rtf.format(-1, "day"))}, ${timeString}`; } else { return date.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false - }) + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); } - } + }; return ( - + - + + + - {user?.displayName || user?.username} + + {user?.displayName || user?.username} + - - {formatMessageDate(message.createdAt)} - + {formatMessageDate(message.createdAt)} {message.content} - - { - message.attachments.map((file, i) => ( + + {message.attachments.map((file, i) => ( + - )) - } + + ))} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/channel-list-item.tsx b/app/components/custom-ui/channel-list-item.tsx index 4836611..f23b272 100644 --- a/app/components/custom-ui/channel-list-item.tsx +++ b/app/components/custom-ui/channel-list-item.tsx @@ -1,19 +1,31 @@ -import { ChevronDown, Hash, Volume2 } from "lucide-react" -import React from "react" -import { NavLink } from "react-router" -import { useShallow } from "zustand/react/shallow" -import { ChannelType, type ServerChannel } from "~/lib/api/types" -import { cn } from "~/lib/utils" -import { useChannelsVoiceStateStore } from "~/stores/channels-voice-state" -import { useGatewayStore } from "~/stores/gateway-store" -import { useUsersStore } from "~/stores/users-store" -import { Button } from "../ui/button" -import UserAvatar from "../user-avatar" +import { ChevronDown, Hash, Volume2 } from "lucide-react"; +import React from "react"; +import { NavLink } from "react-router"; +import { useShallow } from "zustand/react/shallow"; +import { deleteChannel } from "~/lib/api/client/server"; +import { ChannelType, type ServerChannel } from "~/lib/api/types"; +import { cn } from "~/lib/utils"; +import { useChannelsVoiceStateStore } from "~/stores/channels-voice-state"; +import { useGatewayStore } from "~/stores/gateway-store"; +import { useUsersStore } from "~/stores/users-store"; +import { useVoiceStateStore } from "~/stores/voice-state-store"; +import { Button } from "../ui/button"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "../ui/context-menu"; +import UserAvatar from "../user-avatar"; interface ChannelListItemProps { - channel: ServerChannel + channel: ServerChannel; } +const onDeleteChannel = async (channel: ServerChannel) => { + const response = await deleteChannel(channel.serverId, channel.id); +}; + function ServerCategory({ channel }: ChannelListItemProps) { return ( @@ -24,91 +36,140 @@ function ServerCategory({ channel }: ChannelListItemProps) { - ) + ); } function ServerVoice({ channel }: ChannelListItemProps) { - const updateVoiceState = useGatewayStore(state => state.updateVoiceState) - const channelVoiceState = useChannelsVoiceStateStore(state => state.channels[channel.id]) || {} - const userIds = Object.keys(channelVoiceState.users ?? {}) + const updateVoiceState = useGatewayStore((state) => state.updateVoiceState); + const voiceStateChannel = useVoiceStateStore( + (state) => state.activeChannel, + ); + const channelVoiceState = + useChannelsVoiceStateStore((state) => state.channels[channel.id]) || {}; + const userIds = Object.keys(channelVoiceState.users ?? {}); - const { users, fetchUsersIfNotPresent } = useUsersStore(useShallow(state => ({ - users: state.users, - fetchUsersIfNotPresent: state.fetchUsersIfNotPresent - }))) + const { users, fetchUsersIfNotPresent } = useUsersStore( + useShallow((state) => ({ + users: state.users, + fetchUsersIfNotPresent: state.fetchUsersIfNotPresent, + })), + ); - const channelUsers = React.useMemo(() => userIds.map(userId => users[userId]).filter(Boolean), [userIds, users]) + const channelUsers = React.useMemo( + () => userIds.map((userId) => users[userId]).filter(Boolean), + [userIds, users], + ); React.useEffect(() => { - fetchUsersIfNotPresent(userIds) - }, [userIds]) + fetchUsersIfNotPresent(userIds); + }, [userIds]); const onClick = () => { - updateVoiceState(channel.serverId, channel.id) - } + if ( + voiceStateChannel?.serverId === channel.serverId && + voiceStateChannel.channelId === channel.id + ) + return; + + updateVoiceState(channel.serverId, channel.id); + }; return ( <> - - - - - - - {channel.name} - - - - {channelUsers.length > 0 && + + + + + + + + {channel.name} + + + + + onDeleteChannel(channel)} + > + Delete + + + + {channelUsers.length > 0 && ( - { - channelUsers - .map(user => ( - - - {user.displayName || user.username} - - )) - } - } + {channelUsers.map((user) => ( + + + {user.displayName || user.username} + + ))} + + )} > - ) + ); } function ServerText({ channel }: ChannelListItemProps) { return ( - + {({ isActive }) => ( - - - - - - - {channel.name} - - - - ) - } + + + + + + + + {channel.name} + + + + + onDeleteChannel(channel)} + > + Delete + + + + )} - ) + ); } -export default function ServerChannelListItem({ channel }: ChannelListItemProps) { +export default function ServerChannelListItem({ + channel, +}: ChannelListItemProps) { switch (channel.type) { case ChannelType.SERVER_CATEGORY: - return + return ; case ChannelType.SERVER_VOICE: - return + return ; case ChannelType.SERVER_TEXT: - return + return ; default: - return null + return null; } } diff --git a/app/components/custom-ui/create-server-button.tsx b/app/components/custom-ui/create-server-button.tsx index 7dba44c..c77d156 100644 --- a/app/components/custom-ui/create-server-button.tsx +++ b/app/components/custom-ui/create-server-button.tsx @@ -1,15 +1,18 @@ import { CirclePlus } from "lucide-react"; - import { Button } from "~/components/ui/button"; import { ModalType, useModalStore } from "~/stores/modal-store"; export function CreateServerButton() { - const onOpen = useModalStore(state => state.onOpen) + const onOpen = useModalStore((state) => state.onOpen); return ( - onOpen(ModalType.CREATE_SERVER)}> + onOpen(ModalType.CREATE_SERVER)} + > - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/home-button.tsx b/app/components/custom-ui/home-button.tsx index bec5cc0..4725a38 100644 --- a/app/components/custom-ui/home-button.tsx +++ b/app/components/custom-ui/home-button.tsx @@ -5,17 +5,18 @@ import { Button } from "../ui/button"; export function HomeButton() { return ( - { - ({ isActive }) => ( - - - - - - ) - } + {({ isActive }) => ( + + + + + + )} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/online-status.tsx b/app/components/custom-ui/online-status.tsx index d2d9040..1539bc2 100644 --- a/app/components/custom-ui/online-status.tsx +++ b/app/components/custom-ui/online-status.tsx @@ -10,11 +10,19 @@ export function OnlineStatus({ - {status === "online" && } - {status === "dnd" && } - {status === "idle" && } - {status === "offline" && } + {status === "online" && ( + + )} + {status === "dnd" && ( + + )} + {status === "idle" && ( + + )} + {status === "offline" && ( + + )} ); -} \ No newline at end of file +} diff --git a/app/components/custom-ui/password-input.tsx b/app/components/custom-ui/password-input.tsx index b4c6235..e05511a 100644 --- a/app/components/custom-ui/password-input.tsx +++ b/app/components/custom-ui/password-input.tsx @@ -1,11 +1,12 @@ -import { EyeIcon, EyeOffIcon } from "lucide-react" -import React from "react" -import { Button } from "../ui/button" -import { Input } from "../ui/input" +import { EyeIcon, EyeOffIcon } from "lucide-react"; +import React from "react"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; export function PasswordInput(props: React.ComponentProps<"input">) { - const [showPassword, setShowPassword] = React.useState(false) - const disabled = props.value === '' || props.value === undefined || props.disabled + const [showPassword, setShowPassword] = React.useState(false); + const disabled = + props.value === "" || props.value === undefined || props.disabled; return ( @@ -23,7 +24,9 @@ export function PasswordInput(props: React.ComponentProps<"input">) { ) : ( )} - {showPassword ? 'Hide password' : 'Show password'} + + {showPassword ? "Hide password" : "Show password"} + - ) + ); } diff --git a/app/components/custom-ui/private-channel-list-item.tsx b/app/components/custom-ui/private-channel-list-item.tsx index b4e60e3..43c5fcd 100644 --- a/app/components/custom-ui/private-channel-list-item.tsx +++ b/app/components/custom-ui/private-channel-list-item.tsx @@ -1,48 +1,71 @@ -import { Check } from "lucide-react" -import { NavLink } from "react-router" -import type { RecipientChannel } from "~/lib/api/types" -import { cn } from "~/lib/utils" -import { useUsersStore } from "~/stores/users-store" -import { Badge } from "../ui/badge" -import { Button } from "../ui/button" -import UserAvatar from "../user-avatar" -import { OnlineStatus } from "./online-status" +import { Check } from "lucide-react"; +import { NavLink } from "react-router"; +import type { RecipientChannel } from "~/lib/api/types"; +import { cn } from "~/lib/utils"; +import { useUsersStore } from "~/stores/users-store"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import UserAvatar from "../user-avatar"; +import { OnlineStatus } from "./online-status"; interface PrivateChannelListItemProps { - channel: RecipientChannel + channel: RecipientChannel; } -export default function PrivateChannelListItem({ channel }: PrivateChannelListItemProps) { - const currentUserId = useUsersStore(state => state.currentUserId) - const recipients = channel.recipients.filter(recipient => recipient.id !== currentUserId); - const renderSystemBadge = recipients.some(recipient => recipient.system) && recipients.length === 1 +export default function PrivateChannelListItem({ + channel, +}: PrivateChannelListItemProps) { + const currentUserId = useUsersStore((state) => state.currentUserId); + const recipients = channel.recipients.filter( + (recipient) => recipient.id !== currentUserId, + ); + const renderSystemBadge = + recipients.some((recipient) => recipient.system) && + recipients.length === 1; return ( <> - { - ({ isActive }) => ( - - - - - recipient.id !== currentUserId)} /> - - - - {recipients.map(recipient => recipient.displayName || recipient.username).join(", ")} - - {renderSystemBadge && System} + {({ isActive }) => ( + + + + + + recipient.id !== currentUserId, + )} + /> + - - ) - } + + {recipients + .map( + (recipient) => + recipient.displayName || + recipient.username, + ) + .join(", ")} + + {renderSystemBadge && ( + + {" "} + System + + )} + + + )} > - ) + ); } diff --git a/app/components/custom-ui/server-button.tsx b/app/components/custom-ui/server-button.tsx index e081d70..30d08f6 100644 --- a/app/components/custom-ui/server-button.tsx +++ b/app/components/custom-ui/server-button.tsx @@ -1,36 +1,36 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar" -import { NavLink } from "react-router" -import type { Server } from "~/lib/api/types" -import { getFirstLetters } from "~/lib/utils" -import { Button } from "../ui/button" +import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"; +import { NavLink } from "react-router"; +import type { Server } from "~/lib/api/types"; +import { getFirstLetters } from "~/lib/utils"; +import { Button } from "../ui/button"; export interface ServerButtonProps { - server: Server + server: Server; } -export function ServerButton( - { server }: ServerButtonProps -) { +export function ServerButton({ server }: ServerButtonProps) { return ( - { - ({ isActive }) => ( - - - - - - - {getFirstLetters(server.name, 4)} - - - - - - ) - } + {({ isActive }) => ( + + + + + + {getFirstLetters(server.name, 4)} + + + + + )} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/settings-button.tsx b/app/components/custom-ui/settings-button.tsx index 5fdb142..019b1db 100644 --- a/app/components/custom-ui/settings-button.tsx +++ b/app/components/custom-ui/settings-button.tsx @@ -1,21 +1,33 @@ import { Settings } from "lucide-react"; +import { useNavigate } from "react-router"; import { ModalType, useModalStore } from "~/stores/modal-store"; import { useTokenStore } from "~/stores/token-store"; import { Button } from "../ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; export function SettingsButton() { - const setToken = useTokenStore(state => state.setToken) - const onOpen = useModalStore(state => state.onOpen) + const setToken = useTokenStore((state) => state.setToken); + const onOpen = useModalStore((state) => state.onOpen); + const navigate = useNavigate(); const onUpdateProfile = () => { - onOpen(ModalType.UPDATE_PROFILE) - } + onOpen(ModalType.UPDATE_PROFILE); + }; + + const onOpenSettings = () => { + navigate("/app/settings"); + }; const onLogout = () => { - setToken(undefined) - window.location.reload() - } + setToken(undefined); + window.location.reload(); + }; return ( @@ -25,8 +37,8 @@ export function SettingsButton() { - - Update profile + + Settings @@ -34,5 +46,5 @@ export function SettingsButton() { - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/text-box.tsx b/app/components/custom-ui/text-box.tsx index c8115fa..af4f9c0 100644 --- a/app/components/custom-ui/text-box.tsx +++ b/app/components/custom-ui/text-box.tsx @@ -1,7 +1,13 @@ -import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; -import { cn } from '~/lib/utils'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef, +} from "react"; +import { cn } from "~/lib/utils"; -export interface TextBoxProps extends Omit, 'onChange' | 'value'> { +export interface TextBoxProps + extends Omit, "onChange" | "value"> { value: string; onChange: (value: string) => void; placeholder?: string; @@ -28,7 +34,7 @@ export const TextBox = forwardRef( onFocus, ...rest }, - ref + ref, ) => { const localRef = useRef(null); useImperativeHandle(ref, () => localRef.current as HTMLDivElement); @@ -39,10 +45,13 @@ export const TextBox = forwardRef( // Only update if different to avoid selection issues if (localRef.current.textContent !== newValue) { localRef.current.textContent = newValue; - + // Clear any elements if the content is empty - if (!newValue && localRef.current.innerHTML.includes('')) { - localRef.current.innerHTML = ''; + if ( + !newValue && + localRef.current.innerHTML.includes("") + ) { + localRef.current.innerHTML = ""; } } } @@ -60,34 +69,34 @@ export const TextBox = forwardRef( }, [autoFocus]); const handleInput = (event: React.FormEvent) => { - const newValue = event.currentTarget.textContent || ''; - + const newValue = event.currentTarget.textContent || ""; + // Handle the case where the content is empty but contains a - if (!newValue && event.currentTarget.innerHTML.includes('')) { - event.currentTarget.innerHTML = ''; + if (!newValue && event.currentTarget.innerHTML.includes("")) { + event.currentTarget.innerHTML = ""; } - + onChange(newValue); onInput?.(event); }; const handlePaste = (event: React.ClipboardEvent) => { event.preventDefault(); - const text = event.clipboardData.getData('text/plain'); - + const text = event.clipboardData.getData("text/plain"); + // Use document.execCommand to maintain undo stack - document.execCommand('insertText', false, text); - + document.execCommand("insertText", false, text); + // Manually trigger input event - const inputEvent = new Event('input', { bubbles: true }); + const inputEvent = new Event("input", { bubbles: true }); event.currentTarget.dispatchEvent(inputEvent); }; return ( localRef.current?.focus()} > @@ -99,10 +108,10 @@ export const TextBox = forwardRef( onBlur={onBlur} onFocus={onFocus} className={cn( - "break-words whitespace-pre-wrap outline-none w-full", + "outline-none break-all", "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground empty:before:cursor-text", disabled && "cursor-not-allowed opacity-50", - inputClassName + inputClassName, )} data-placeholder={placeholder} role="textbox" @@ -116,9 +125,9 @@ export const TextBox = forwardRef( /> ); - } + }, ); -TextBox.displayName = 'TextBox'; +TextBox.displayName = "TextBox"; -export default TextBox; \ No newline at end of file +export default TextBox; diff --git a/app/components/custom-ui/user-status.tsx b/app/components/custom-ui/user-status.tsx index d3ad5ee..ca6c456 100644 --- a/app/components/custom-ui/user-status.tsx +++ b/app/components/custom-ui/user-status.tsx @@ -11,16 +11,22 @@ import { OnlineStatus } from "./online-status"; import { SettingsButton } from "./settings-button"; function VoiceStatus({ - voiceState -}: { voiceState: { serverId: string; channelId: string } }) { + voiceState, +}: { + voiceState: { serverId: string; channelId: string }; +}) { // const webrtcState = useWebRTCStore(state => state.status) const leaveVoiceChannel = () => { - useVoiceStateStore.getState().leaveVoiceChannel() - } + useVoiceStateStore.getState().leaveVoiceChannel(); + }; - const channel = useServerChannelsStore(state => state.channels[voiceState.serverId]?.[voiceState.channelId]) - const server = useServerListStore(state => state.servers[voiceState.serverId]) + const channel = useServerChannelsStore( + (state) => state.channels[voiceState.serverId]?.[voiceState.channelId], + ); + const server = useServerListStore( + (state) => state.servers[voiceState.serverId], + ); return ( @@ -35,20 +41,21 @@ function VoiceStatus({ - ) + ); } export default function UserStatus() { - const user = useUsersStore(state => state.getCurrentUser()!) - const voiceState = useVoiceStateStore(state => state.activeChannel) + const user = useUsersStore((state) => state.getCurrentUser()!); + const voiceState = useVoiceStateStore((state) => state.activeChannel); return ( - - {voiceState && <> - - - > - } + + {voiceState && ( + <> + + + > + )} @@ -57,9 +64,13 @@ export default function UserStatus() { - {user?.displayName || user?.username || "Unknown user"} + {user?.displayName || + user?.username || + "Unknown user"} - @{user?.username} + + @{user?.username} + @@ -76,5 +87,5 @@ export default function UserStatus() { - ) -} \ No newline at end of file + ); +} diff --git a/app/components/file-icon.tsx b/app/components/file-icon.tsx index 9b7d522..317303a 100644 --- a/app/components/file-icon.tsx +++ b/app/components/file-icon.tsx @@ -1,10 +1,11 @@ import { FileArchive, FileAudio, + FileImage, FileQuestion, + FileSpreadsheet, FileText, FileVideo, - ImageIcon, type LucideProps, } from "lucide-react"; @@ -17,7 +18,7 @@ export function FileIcon({ contentType, className, ...props }: FileIconProps) { const commonProps = { className: className ?? "h-5 w-5", ...props }; if (contentType.startsWith("image/")) { - return ; + return ; } if (contentType.startsWith("audio/")) { return ; @@ -30,19 +31,25 @@ export function FileIcon({ contentType, className, ...props }: FileIconProps) { } if ( contentType.startsWith("application/vnd.ms-excel") || - contentType.startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml") + contentType.startsWith( + "application/vnd.openxmlformats-officedocument.spreadsheetml", + ) ) { - return ; // Could use a specific Excel icon if available/desired + return ; // Could use a specific Excel icon if available/desired } if ( contentType.startsWith("application/msword") || - contentType.startsWith("application/vnd.openxmlformats-officedocument.wordprocessingml") + contentType.startsWith( + "application/vnd.openxmlformats-officedocument.wordprocessingml", + ) ) { return ; } if ( contentType.startsWith("application/vnd.ms-powerpoint") || - contentType.startsWith("application/vnd.openxmlformats-officedocument.presentationml") + contentType.startsWith( + "application/vnd.openxmlformats-officedocument.presentationml", + ) ) { return ; } @@ -59,4 +66,4 @@ export function FileIcon({ contentType, className, ...props }: FileIconProps) { return ; } return ; // Default for unknown types -} \ No newline at end of file +} diff --git a/app/components/icons/Discord.tsx b/app/components/icons/Discord.tsx index 618b300..a85da0a 100644 --- a/app/components/icons/Discord.tsx +++ b/app/components/icons/Discord.tsx @@ -1,5 +1,19 @@ import type { SVGProps } from "react"; -const Discord = (props: SVGProps) => ; +const Discord = (props: SVGProps) => ( + + + +); export default Discord; diff --git a/app/components/manager/gateway-websocket-connection-manager.tsx b/app/components/manager/gateway-websocket-connection-manager.tsx index 8ed9860..917df63 100644 --- a/app/components/manager/gateway-websocket-connection-manager.tsx +++ b/app/components/manager/gateway-websocket-connection-manager.tsx @@ -1,20 +1,18 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; -import { ConnectionState } from '~/lib/websocket/gateway/types'; -import { useGatewayStore } from '~/stores/gateway-store'; -import { useTokenStore } from '~/stores/token-store'; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect } from "react"; +import { ConnectionState } from "~/lib/websocket/gateway/types"; +import { useGatewayStore } from "~/stores/gateway-store"; +import { useTokenStore } from "~/stores/token-store"; export function GatewayWebSocketConnectionManager() { - const token = useTokenStore((state) => - state.token, - ); + const token = useTokenStore((state) => state.token); const { setQueryClient } = useGatewayStore(); const queryClient = useQueryClient(); useEffect(() => { setQueryClient(queryClient); - }, [queryClient]) + }, [queryClient]); useEffect(() => { const { status, connect, disconnect } = useGatewayStore.getState(); @@ -34,9 +32,5 @@ export function GatewayWebSocketConnectionManager() { }; }, [token]); - return ( - <> - {null} - > - ); -} \ No newline at end of file + return <>{null}>; +} diff --git a/app/components/manager/webrtc-connection-manager.tsx b/app/components/manager/webrtc-connection-manager.tsx index 489275d..ebd607d 100644 --- a/app/components/manager/webrtc-connection-manager.tsx +++ b/app/components/manager/webrtc-connection-manager.tsx @@ -9,11 +9,11 @@ export function WebRTCConnectionManager() { const voiceState = useVoiceStateStore(); const webrtc = useWebRTCStore(); - const remoteStream = useWebRTCStore(state => state.remoteStream); - const audioRef = useRef(null) + const remoteStream = useWebRTCStore((state) => state.remoteStream); + const audioRef = useRef(null); if (audioRef.current) { - audioRef.current.srcObject = remoteStream + audioRef.current.srcObject = remoteStream; } useEffect(() => { @@ -25,7 +25,7 @@ export function WebRTCConnectionManager() { audio: { noiseSuppression: false, }, - video: false + video: false, }); webrtc.createOffer(stream); @@ -41,7 +41,6 @@ export function WebRTCConnectionManager() { if (webrtc.status === ConnectionState.DISCONNECTED) { voiceState.leaveVoiceChannel(); } - }, [webrtc.status]); return ( @@ -49,4 +48,4 @@ export function WebRTCConnectionManager() { > ); -} \ No newline at end of file +} diff --git a/app/components/message-box.tsx b/app/components/message-box.tsx index 4502b95..520c2ff 100644 --- a/app/components/message-box.tsx +++ b/app/components/message-box.tsx @@ -1,103 +1,220 @@ -import { Plus, Send, Trash } from "lucide-react" -import React from "react" -import { sendMessage } from "~/lib/api/client/channel" -import { uploadFiles } from "~/lib/api/client/file" -import TextBox from "./custom-ui/text-box" -import { Button } from "./ui/button" +import { Loader2, Paperclip, Send, X } from "lucide-react"; +import React from "react"; +import { FileIcon } from "~/components/file-icon"; // Adjust path +import { sendMessage } from "~/lib/api/client/channel"; // Adjust path +import { uploadFiles } from "~/lib/api/client/file"; // Adjust path +import type { Uuid } from "~/lib/api/types"; // Adjust path +import { cn, formatFileSize } from "~/lib/utils"; // Adjust path +import TextBox from "./custom-ui/text-box"; // Adjust path, assuming TextBox is in ./custom-ui/ +import { Button } from "./ui/button"; // Adjust path +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./ui/tooltip"; // Adjust path export interface MessageBoxProps { - channelId: string + channelId: string; } -export default function MessageBox( - { channelId }: MessageBoxProps -) { - const [text, setText] = React.useState("") - const [attachments, setAttachments] = React.useState([]) +export default function MessageBox({ channelId }: MessageBoxProps) { + const [text, setText] = React.useState(""); + const [attachments, setAttachments] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(false); - const fileInputRef = React.useRef(null) + const fileInputRef = React.useRef(null); + const textBoxRef = React.useRef(null); - const onSendMessage = async () => { - const content = text.trim() - if (!content && !attachments.length) - return + const handleSendMessage = async () => { + const content = text.trim(); + if ((!content && attachments.length === 0) || isLoading) return; - const uploadedAttachments = await uploadFiles(attachments) + setIsLoading(true); + try { + let uploadedAttachments: Uuid[] = []; + if (attachments.length > 0) { + uploadedAttachments = await uploadFiles(attachments); + } + await sendMessage(channelId, text, uploadedAttachments); + setText(""); + setAttachments([]); - await sendMessage(channelId, text, uploadedAttachments) - setText("") - setAttachments([]) - } + setTimeout(() => textBoxRef.current?.focus(), 0); + } catch (error) { + console.error("Failed to send message:", error); + } finally { + setIsLoading(false); + } + }; - const addAttachment = async () => { - if (!fileInputRef.current) - return - - fileInputRef.current.click() - } + const addAttachment = () => { + if (attachments.length >= 10 || isLoading) return; + fileInputRef.current?.click(); + }; const onFileChange = (e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []) - const newAttachments = [...attachments, ...files] - setAttachments(newAttachments.slice(0, 10)) + const files = Array.from(e.target.files || []); + if (files.length === 0) return; - if (!fileInputRef.current) - return + setAttachments((prev) => { + const newAttachments = [...prev, ...files]; + return newAttachments.slice(0, 10); + }); - fileInputRef.current.value = "" - } + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setTimeout(() => textBoxRef.current?.focus(), 0); + }; + + const removeAttachment = (indexToRemove: number) => { + setAttachments((prev) => prev.filter((_, i) => i !== indexToRemove)); + setTimeout(() => textBoxRef.current?.focus(), 0); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; React.useEffect(() => { - setText("") - setAttachments([]) - }, [channelId]) + setText(""); + setAttachments([]); + setIsLoading(false); + setTimeout(() => textBoxRef.current?.focus(), 0); + }, [channelId]); + + const canSend = + (text.trim().length > 0 || attachments.length > 0) && !isLoading; + const attachmentsRemaining = 10 - attachments.length; return ( - - - - = 10}> - - - - - - - - - - - - 0 ? "pt-2" : "hidden"}`}> - - { - attachments.map((file, i) => ( - - - {file.name} - - - setAttachments(attachments.filter((_, j) => i !== j))}> - - - - - )) - } + + {attachments.length > 0 && ( + + {attachments.map((file, i) => ( + + + + + {file.name} + + + {formatFileSize(file.size)} + + + + + + removeAttachment(i)} + disabled={isLoading} + aria-label={`Remove ${file.name}`} + > + + + + + Remove attachment + + + - + ))} - - - + )} + + + + + + + + = 10 || isLoading + } + aria-label="Add attachment" + > + + + + + + {attachments.length >= 10 ? ( + Maximum 10 attachments + ) : ( + + Add attachment ({attachmentsRemaining}{" "} + remaining) + + )} + + + + + + + + + + + {isLoading ? ( + + ) : ( + + )} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/modals/create-server-channel-modal.tsx b/app/components/modals/create-server-channel-modal.tsx index f3f225a..918dafd 100644 --- a/app/components/modals/create-server-channel-modal.tsx +++ b/app/components/modals/create-server-channel-modal.tsx @@ -2,42 +2,71 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { ChannelType } from "~/lib/api/types"; -import { ModalType, useModalStore, type CreateServerChannelModalData } from "~/stores/modal-store"; +import { + ModalType, + useModalStore, + type CreateServerChannelModalData, +} from "~/stores/modal-store"; import { Button } from "../ui/button"; -import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; import { Input } from "../ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; const schema = z.object({ name: z.string().min(1).max(32), type: z.nativeEnum(ChannelType), }); - export default function CreateServerChannelModal() { const { type, data, isOpen, onClose } = useModalStore(); - const isModalOpen = type === ModalType.CREATE_SERVER_CHANNEL && isOpen + const isModalOpen = type === ModalType.CREATE_SERVER_CHANNEL && isOpen; let form = useForm>({ resolver: zodResolver(schema), defaultValues: { - type: ChannelType.SERVER_TEXT - } + type: ChannelType.SERVER_TEXT, + }, }); const onOpenChange = () => { - form.reset() - onClose() - } + form.reset(); + onClose(); + }; const onSubmit = async (values: z.infer) => { - const response = await import("~/lib/api/client/server").then(m => m.default.createChannel((data as CreateServerChannelModalData['data']).serverId, values)) + const response = await import("~/lib/api/client/server").then((m) => + m.default.createChannel( + (data as CreateServerChannelModalData["data"]).serverId, + values, + ), + ); - form.reset() - onClose() - } + form.reset(); + onClose(); + }; return ( @@ -50,13 +79,22 @@ export default function CreateServerChannelModal() { - + ( - Name + + Name + @@ -69,8 +107,17 @@ export default function CreateServerChannelModal() { name="type" render={({ field }) => ( - Type - + + Type + + @@ -78,10 +125,15 @@ export default function CreateServerChannelModal() { {Object.entries({ - [ChannelType.SERVER_TEXT]: "Text", - [ChannelType.SERVER_VOICE]: "Voice", + [ChannelType.SERVER_TEXT]: + "Text", + [ChannelType.SERVER_VOICE]: + "Voice", }).map(([type, label]) => ( - + {label} ))} @@ -97,13 +149,18 @@ export default function CreateServerChannelModal() { Close - - {form.formState.isSubmitting ? "Creating..." : "Create"} + + {form.formState.isSubmitting + ? "Creating..." + : "Create"} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/modals/create-server-invite-modal.tsx b/app/components/modals/create-server-invite-modal.tsx index 482c796..f673454 100644 --- a/app/components/modals/create-server-invite-modal.tsx +++ b/app/components/modals/create-server-invite-modal.tsx @@ -3,42 +3,63 @@ import React from "react"; import { useOrigin } from "~/hooks/use-origin"; import { ModalType, useModalStore } from "~/stores/modal-store"; import { Button } from "../ui/button"; -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; export default function CreateServerInviteModal() { const { type, data, isOpen, onClose } = useModalStore(); - const [inviteCode, setInviteCode] = React.useState(undefined) - const origin = useOrigin() - const [isCopied, setCopied] = React.useState(false) + const [inviteCode, setInviteCode] = React.useState( + undefined, + ); + const origin = useOrigin(); + const [isCopied, setCopied] = React.useState(false); - const isModalOpen = type === ModalType.CREATE_SERVER_INVITE && isOpen - const inviteLink = `${origin}/app/invite/${inviteCode}` + const isModalOpen = type === ModalType.CREATE_SERVER_INVITE && isOpen; + const inviteLink = `${origin}/app/invite/${inviteCode}`; const onOpenChange = (openState: boolean) => { - onClose() - } + onClose(); + }; const regenerateInviteCode = () => { - import("~/lib/api/client/server").then(m => m.default.createInvite((data as { serverId: string }).serverId)).then(invite => { setInviteCode(invite.code) }) - } + import("~/lib/api/client/server") + .then((m) => + m.default.createInvite((data as { serverId: string }).serverId), + ) + .then((invite) => { + setInviteCode(invite.code); + }); + }; const onCopy = () => { - navigator.clipboard.writeText(inviteLink) - setCopied(true) + navigator.clipboard.writeText(inviteLink); + setCopied(true); - setTimeout(() => setCopied(false), 1000) - } + setTimeout(() => setCopied(false), 1000); + }; React.useEffect(() => { if (isModalOpen) { - import("~/lib/api/client/server").then(m => m.default.createInvite((data as { serverId: string }).serverId)).then(invite => { setInviteCode(invite.code) }) + import("~/lib/api/client/server") + .then((m) => + m.default.createInvite( + (data as { serverId: string }).serverId, + ), + ) + .then((invite) => { + setInviteCode(invite.code); + }); } else { - setInviteCode(undefined) + setInviteCode(undefined); } - }, [isModalOpen]) - + }, [isModalOpen]); return ( @@ -51,16 +72,25 @@ export default function CreateServerInviteModal() { - {isCopied ? : } + {isCopied ? ( + + ) : ( + + )} - + Generate a new invite - ) -} \ No newline at end of file + ); +} diff --git a/app/components/modals/create-server-modal.tsx b/app/components/modals/create-server-modal.tsx index e7d266f..b5096fc 100644 --- a/app/components/modals/create-server-modal.tsx +++ b/app/components/modals/create-server-modal.tsx @@ -5,8 +5,23 @@ import file from "~/lib/api/client/file"; import server from "~/lib/api/client/server"; import { ModalType, useModalStore } from "~/stores/modal-store"; import { Button } from "../ui/button"; -import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; import { IconUploadField } from "../ui/icon-upload-field"; import { Input } from "../ui/input"; @@ -15,37 +30,34 @@ const schema = z.object({ icon: z.instanceof(File).optional(), }); - export default function CreateServerModal() { const { type, isOpen, onClose } = useModalStore(); - const isModalOpen = type === ModalType.CREATE_SERVER && isOpen + const isModalOpen = type === ModalType.CREATE_SERVER && isOpen; let form = useForm>({ resolver: zodResolver(schema), }); const onOpenChange = (openState: boolean) => { - form.reset() - onClose() - } + form.reset(); + onClose(); + }; const onSubmit = async (values: z.infer) => { - let iconId = undefined + let iconId = undefined; if (values.icon) { - iconId = (await file.uploadFile(values.icon))[0] + iconId = (await file.uploadFile(values.icon))[0]; } const response = await server.create({ name: values.name, - iconId - }) - - form.reset() - onClose() - } - + iconId, + }); + form.reset(); + onClose(); + }; return ( @@ -58,13 +70,22 @@ export default function CreateServerModal() { - + ( - Icon + + Icon + ( - Name + + Name + @@ -96,13 +123,18 @@ export default function CreateServerModal() { Close - - {form.formState.isSubmitting ? "Creating..." : "Create"} + + {form.formState.isSubmitting + ? "Creating..." + : "Create"} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/modals/delete-server-confirm-modal.tsx b/app/components/modals/delete-server-confirm-modal.tsx index c82d094..eef944d 100644 --- a/app/components/modals/delete-server-confirm-modal.tsx +++ b/app/components/modals/delete-server-confirm-modal.tsx @@ -1,20 +1,29 @@ import { ModalType, useModalStore } from "~/stores/modal-store"; import { Button } from "../ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; export default function DeleteServerConfirmModal() { const { type, data, isOpen, onClose } = useModalStore(); - const isModalOpen = type === ModalType.DELETE_SERVER_CONFIRM && isOpen + const isModalOpen = type === ModalType.DELETE_SERVER_CONFIRM && isOpen; const onOpenChange = () => { - onClose() - } + onClose(); + }; const onConfirm = async () => { - await import("~/lib/api/client/server").then(m => m.default.delet((data as { serverId: string }).serverId)) - onClose() - } + await import("~/lib/api/client/server").then((m) => + m.default.delet((data as { serverId: string }).serverId), + ); + onClose(); + }; return ( @@ -36,5 +45,5 @@ export default function DeleteServerConfirmModal() { - ) -} \ No newline at end of file + ); +} diff --git a/app/components/modals/update-profile-modal.tsx b/app/components/modals/update-profile-modal.tsx index b0952aa..16bca0a 100644 --- a/app/components/modals/update-profile-modal.tsx +++ b/app/components/modals/update-profile-modal.tsx @@ -7,8 +7,23 @@ import { patchUser } from "~/lib/api/client/user"; import { ModalType, useModalStore } from "~/stores/modal-store"; import { useUsersStore } from "~/stores/users-store"; import { Button } from "../ui/button"; -import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; import { IconUploadField } from "../ui/icon-upload-field"; import { Input } from "../ui/input"; @@ -17,61 +32,73 @@ const schema = z.object({ avatar: z.instanceof(File).optional().nullable(), }); - export default function UpdateProfileModal() { const { type, isOpen, onClose } = useModalStore(); - const user = useUsersStore(useShallow(state => state.getCurrentUser())) + const user = useUsersStore(useShallow((state) => state.getCurrentUser())); - const isModalOpen = type === ModalType.UPDATE_PROFILE && isOpen + const isModalOpen = type === ModalType.UPDATE_PROFILE && isOpen; let form = useForm>({ resolver: zodResolver(schema), }); const onOpenChange = () => { - form.reset() - onClose() - } + form.reset(); + onClose(); + }; const onSubmit = async (values: z.infer) => { - if (!values) - return + console.log("values", values); - let avatarId = undefined + if (!values) return; + + let avatarId: string | null | undefined = + values.avatar === null ? null : undefined; if (values.avatar) { - avatarId = (await file.uploadFile(values.avatar))[0] + avatarId = (await file.uploadFile(values.avatar))[0]; } const response = await patchUser({ displayName: values.displayName, - avatarId - }) + avatarId, + }); - form.reset() - onClose() - } + form.reset(); + onClose(); + }; return ( Update profile - - Update your profile. - + Update your profile. - + ( - Avatar + + Avatar + @@ -86,9 +113,18 @@ export default function UpdateProfileModal() { name="displayName" render={({ field }) => ( - Display Name + + Display Name + - + @@ -100,13 +136,18 @@ export default function UpdateProfileModal() { Close - - {form.formState.isSubmitting ? "Updating..." : "Update"} + + {form.formState.isSubmitting + ? "Updating..." + : "Update"} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/providers/modal-provider.tsx b/app/components/providers/modal-provider.tsx index f8ec373..3787957 100644 --- a/app/components/providers/modal-provider.tsx +++ b/app/components/providers/modal-provider.tsx @@ -25,4 +25,4 @@ export default function ModalProvider() { > ); -} \ No newline at end of file +} diff --git a/app/components/theme/theme-provider.tsx b/app/components/theme/theme-provider.tsx index bf06446..7c2ed05 100644 --- a/app/components/theme/theme-provider.tsx +++ b/app/components/theme/theme-provider.tsx @@ -1,73 +1,74 @@ -import { createContext, useContext, useEffect, useState } from "react" +import { createContext, useContext, useEffect, useState } from "react"; -export type Theme = "dark" | "light" | "system" +export type Theme = "dark" | "light" | "system"; type ThemeProviderProps = { - children: React.ReactNode - defaultTheme?: Theme - storageKey?: string -} + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; type ThemeProviderState = { - theme: Theme - setTheme: (theme: Theme) => void -} + theme: Theme; + setTheme: (theme: Theme) => void; +}; const initialState: ThemeProviderState = { - theme: "system", - setTheme: () => null, -} + theme: "system", + setTheme: () => null, +}; -const ThemeProviderContext = createContext(initialState) +const ThemeProviderContext = createContext(initialState); export function ThemeProvider({ - children, - defaultTheme = "system", - storageKey = "ui-theme", - ...props + children, + defaultTheme = "system", + storageKey = "ui-theme", + ...props }: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ) + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, + ); - useEffect(() => { - const root = window.document.documentElement + useEffect(() => { + const root = window.document.documentElement; - root.classList.remove("light", "dark") + root.classList.remove("light", "dark"); - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light" + if (theme === "system") { + const systemTheme = window.matchMedia( + "(prefers-color-scheme: dark)", + ).matches + ? "dark" + : "light"; - root.classList.add(systemTheme) - return - } + root.classList.add(systemTheme); + return; + } - root.classList.add(theme) - }, [theme]) + root.classList.add(theme); + }, [theme]); - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme) - setTheme(theme) - }, - } + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; - return ( - - {children} - - ) + return ( + + {children} + + ); } export const useTheme = () => { - const context = useContext(ThemeProviderContext) + const context = useContext(ThemeProviderContext); - if (context === undefined) - throw new Error("useTheme must be used within a ThemeProvider") + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); - return context -} + return context; +}; diff --git a/app/components/theme/theme-toggle.tsx b/app/components/theme/theme-toggle.tsx index 1bcd63b..2a69039 100644 --- a/app/components/theme/theme-toggle.tsx +++ b/app/components/theme/theme-toggle.tsx @@ -1,40 +1,43 @@ -import { Moon, Sun } from "lucide-react" +import { Moon, Sun } from "lucide-react"; -import { useTheme, type Theme } from "~/components/theme/theme-provider" -import { Button } from "~/components/ui/button" +import { useTheme, type Theme } from "~/components/theme/theme-provider"; +import { Button } from "~/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuTrigger -} from "~/components/ui/dropdown-menu" + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; export function ThemeToggle() { - const { theme, setTheme } = useTheme() + const { theme, setTheme } = useTheme(); - return ( - - - - - - Toggle theme - - - - setTheme(value as Theme)}> - - Light - - - Dark - - - System - - - - - ) -} \ No newline at end of file + return ( + + + + + + Toggle theme + + + + setTheme(value as Theme)} + > + + Light + + + Dark + + + System + + + + + ); +} diff --git a/app/components/ui/aspect-ratio.tsx b/app/components/ui/aspect-ratio.tsx index 9b491fb..f7f14f8 100644 --- a/app/components/ui/aspect-ratio.tsx +++ b/app/components/ui/aspect-ratio.tsx @@ -1,9 +1,9 @@ -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; function AspectRatio({ - ...props + ...props }: React.ComponentProps) { - return + return ; } -export { AspectRatio } +export { AspectRatio }; diff --git a/app/components/ui/avatar.tsx b/app/components/ui/avatar.tsx index 28026bd..1f2b9af 100644 --- a/app/components/ui/avatar.tsx +++ b/app/components/ui/avatar.tsx @@ -1,52 +1,51 @@ -import * as AvatarPrimitive from "@radix-ui/react-avatar" -import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Avatar({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AvatarImage({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AvatarFallback({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Avatar, AvatarFallback, AvatarImage } - +export { Avatar, AvatarFallback, AvatarImage }; diff --git a/app/components/ui/badge.tsx b/app/components/ui/badge.tsx index fc40406..985303d 100644 --- a/app/components/ui/badge.tsx +++ b/app/components/ui/badge.tsx @@ -1,46 +1,46 @@ -import * as React from "react" -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 badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, }, - defaultVariants: { - variant: "default", - }, - } -) +); function Badge({ - className, - variant, - asChild = false, - ...props + className, + variant, + asChild = false, + ...props }: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; - return ( - - ) + return ( + + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index d33d6bc..cfbe96b 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -1,60 +1,59 @@ -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 * as React from "react"; -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-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-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "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-xs 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 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", - none: "" - }, + "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-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "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-xs 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 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", + none: "", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) +); function Button({ - className, - variant, - size, - asChild = false, - ...props + className, + variant, + size, + asChild = false, + ...props }: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "button"; - return ( - - ) + return ( + + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/app/components/ui/card.tsx b/app/components/ui/card.tsx index 921d88e..b32095d 100644 --- a/app/components/ui/card.tsx +++ b/app/components/ui/card.tsx @@ -1,87 +1,92 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } export { - Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle -} - + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +}; diff --git a/app/components/ui/context-menu.tsx b/app/components/ui/context-menu.tsx new file mode 100644 index 0000000..722dff7 --- /dev/null +++ b/app/components/ui/context-menu.tsx @@ -0,0 +1,256 @@ +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/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx index bb20726..92aa3b8 100644 --- a/app/components/ui/dialog.tsx +++ b/app/components/ui/dialog.tsx @@ -1,134 +1,133 @@ -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" -import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Dialog({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogTrigger({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogPortal({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogClose({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogOverlay({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DialogContent({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - - - {children} - - - Close - - - - ) + return ( + + + + {children} + + + Close + + + + ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ) + return ( + + ); } function DialogTitle({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DialogDescription({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger -} - + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/app/components/ui/dropdown-menu.tsx b/app/components/ui/dropdown-menu.tsx index db60c7a..8995238 100644 --- a/app/components/ui/dropdown-menu.tsx +++ b/app/components/ui/dropdown-menu.tsx @@ -1,247 +1,263 @@ -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" -import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function DropdownMenu({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DropdownMenuPortal({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DropdownMenuTrigger({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DropdownMenuContent({ - className, - sideOffset = 4, - ...props + className, + sideOffset = 4, + ...props }: React.ComponentProps) { - return ( - - - - ) + return ( + + + + ); } function DropdownMenuGroup({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DropdownMenuItem({ - className, - inset, - variant = "default", - ...props + className, + inset, + variant = "default", + ...props }: React.ComponentProps & { - inset?: boolean - variant?: "default" | "destructive" + inset?: boolean; + variant?: "default" | "destructive"; }) { - return ( - - ) + return ( + + ); } function DropdownMenuCheckboxItem({ - className, - children, - checked, - ...props + className, + children, + checked, + ...props }: React.ComponentProps) { - return ( - - - - - - - {children} - - ) + return ( + + + + + + + {children} + + ); } function DropdownMenuRadioGroup({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DropdownMenuRadioItem({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - - - - - - {children} - - ) + return ( + + + + + + + {children} + + ); } function DropdownMenuLabel({ - className, - inset, - ...props + className, + inset, + ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { - return ( - - ) + return ( + + ); } function DropdownMenuSeparator({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DropdownMenuShortcut({ - className, - ...props + className, + ...props }: React.ComponentProps<"span">) { - return ( - - ) + return ( + + ); } function DropdownMenuSub({ - ...props + ...props }: React.ComponentProps) { - return + return ( + + ); } function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props + className, + inset, + children, + ...props }: React.ComponentProps & { - inset?: boolean + inset?: boolean; }) { - return ( - - {children} - - - ) + return ( + + {children} + + + ); } function DropdownMenuSubContent({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } export { - DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, - DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger -} - + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/app/components/ui/form.tsx b/app/components/ui/form.tsx index 5c02ed8..408e2b6 100644 --- a/app/components/ui/form.tsx +++ b/app/components/ui/form.tsx @@ -1,165 +1,170 @@ -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" -import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import * as React from "react"; import { - Controller, - FormProvider, - useFormContext, - useFormState, - type ControllerProps, - type FieldPath, - type FieldValues, -} from "react-hook-form" + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form"; -import { Label } from "~/components/ui/label" -import { cn } from "~/lib/utils" +import { Label } from "~/components/ui/label"; +import { cn } from "~/lib/utils"; -const Form = FormProvider +const Form = FormProvider; type FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, > = { - name: TName, -} + name: TName; +}; const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) + {} as FormFieldContextValue, +); const FormField = < - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, >({ - ...props + ...props }: ControllerProps) => { - return ( - - - - ) -} + return ( + + + + ); +}; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState } = useFormContext() - const formState = useFormState({ name: fieldContext.name }) - const fieldState = getFieldState(fieldContext.name, formState) + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); - if (!fieldContext) { - throw new Error("useFormField should be used within ") - } + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } - const { id } = itemContext + const { id } = itemContext; - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - } -} + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; type FormItemContextValue = { - id: string, -} + id: string; +}; const FormItemContext = React.createContext( - {} as FormItemContextValue -) + {} as FormItemContextValue, +); function FormItem({ className, ...props }: React.ComponentProps<"div">) { - const id = React.useId() + const id = React.useId(); - return ( - - - - ) + return ( + + + + ); } function FormLabel({ - className, - required, - ...props + className, + required, + ...props }: React.ComponentProps & { required?: boolean }) { - const { error, formItemId } = useFormField() + const { error, formItemId } = useFormField(); - return ( - - - {required && *} - - ) + return ( + + + {required && *} + + ); } function FormControl({ ...props }: React.ComponentProps) { - const { error, formItemId, formDescriptionId, formMessageId } = useFormField() + const { error, formItemId, formDescriptionId, formMessageId } = + useFormField(); - return ( - - ) + return ( + + ); } function FormDescription({ className, ...props }: React.ComponentProps<"p">) { - const { formDescriptionId } = useFormField() + const { formDescriptionId } = useFormField(); - return ( - - ) + return ( + + ); } function FormMessage({ className, ...props }: React.ComponentProps<"p">) { - const { error, formMessageId } = useFormField() - const body = error ? String(error?.message ?? "") : props.children + const { error, formMessageId } = useFormField(); + const body = error ? String(error?.message ?? "") : props.children; - if (!body) { - return null - } + if (!body) { + return null; + } - return ( - - {body} - - ) + return ( + + {body} + + ); } export { - Form, FormControl, - FormDescription, FormField, FormItem, - FormLabel, FormMessage, useFormField -} - + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + useFormField, +}; diff --git a/app/components/ui/icon-upload-field.tsx b/app/components/ui/icon-upload-field.tsx index d1998c0..0fbc248 100644 --- a/app/components/ui/icon-upload-field.tsx +++ b/app/components/ui/icon-upload-field.tsx @@ -1,43 +1,52 @@ -import { ImagePlus, XCircle } from "lucide-react"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import { ImagePlus, RotateCcw, XCircle } from "lucide-react"; +import React, { useCallback } from "react"; import type { ControllerRenderProps, FieldError } from "react-hook-form"; -import { Button } from "./button"; // Your existing Button component +import { Button } from "./button"; // Props for our custom field component interface IconUploadFieldProps { - field: ControllerRenderProps; // Provided by RHF's FormField render prop - error?: FieldError; // Optional: if you want to pass error for internal styling - accept?: string; // e.g., "image/png, image/jpeg" - previewContainerClassName?: string; // Style for the preview box itself - // Add any other props you might want to customize its appearance/behavior + field: ControllerRenderProps; // field.value can be File | string (URL) | null | undefined + error?: FieldError; + accept?: string; + previewContainerClassName?: string; + defaultPreview?: string; // Visual fallback URL if field.value is initially undefined + formDefaultValue?: string | null | undefined; // The actual RHF default value for this field } export function IconUploadField({ field, error, accept = "image/*", - previewContainerClassName = "w-24 h-24 rounded-full", // Default circular preview + previewContainerClassName = "w-24 h-24 rounded-full", + defaultPreview, + formDefaultValue, // New prop }: IconUploadFieldProps) { - const [previewUrl, setPreviewUrl] = useState(null); - const fileInputRef = useRef(null); + const [previewUrl, setPreviewUrl] = React.useState(null); + const fileInputRef = React.useRef(null); - const currentFileValue = field.value as File | undefined | null; + React.useEffect(() => { + let objectUrlToRevoke: string | null = null; - useEffect(() => { - let objectUrl: string | null = null; - if (currentFileValue && currentFileValue instanceof File) { - objectUrl = URL.createObjectURL(currentFileValue); - setPreviewUrl(objectUrl); + if (field.value instanceof File) { + objectUrlToRevoke = URL.createObjectURL(field.value); + setPreviewUrl(objectUrlToRevoke); + } else if (typeof field.value === "string" && field.value) { + setPreviewUrl(field.value); + } else if (field.value === null) { + setPreviewUrl(null); + } else if (field.value === undefined && defaultPreview) { + // Show visual default prop if field value is undefined + setPreviewUrl(defaultPreview); } else { setPreviewUrl(null); } return () => { - if (objectUrl) { - URL.revokeObjectURL(objectUrl); + if (objectUrlToRevoke) { + URL.revokeObjectURL(objectUrlToRevoke); } }; - }, [currentFileValue]); + }, [field.value, defaultPreview]); const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -47,25 +56,60 @@ export function IconUploadField({ } }; - const handleRemoveImage = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - field.onChange(undefined); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }, [field]); + const handleRemoveImage = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + field.onChange(null); // Set RHF value to null + if (fileInputRef.current) { + fileInputRef.current.value = ""; // Clear the native file input + } + }, + [field], + ); + + const handleResetToDefault = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + // The button's visibility logic ensures formDefaultValue is relevant. + // This will set RHF value to string (URL), null, or undefined, as per formDefaultValue. + field.onChange(undefined); + + if (fileInputRef.current) { + fileInputRef.current.value = ""; // Clear the native file input + } + }, + [field, formDefaultValue], + ); const triggerFileInput = useCallback(() => { fileInputRef.current?.click(); }, []); + // Visibility for the "Remove" button: show if there's a File or a URL string in field.value + const showRemoveButton = + (!!field.value && + (field.value instanceof File || typeof field.value === "string")) || + (formDefaultValue && field.value === undefined); + + // Visibility for the "Reset to Default" button + const canReset = typeof formDefaultValue !== "undefined"; // Was a formDefaultValue prop provided? + // Is current value different from the formDefaultValue? + // (A File object is always different from a URL string/null/undefined default) + const isDifferentFromDefault = + (field.value instanceof File || + (field.value !== formDefaultValue && field.value !== undefined)) && + formDefaultValue; + const showResetButton = canReset && isDifferentFromDefault; + return ( - {/* Main clickable area for upload, also receives RHF's ref */} { if (e.key === "Enter" || e.key === " ") { @@ -86,36 +130,51 @@ export function IconUploadField({ className="w-full h-full object-cover" /> ) : ( - + )} + {/* Optional: A separate button to trigger file input, if needed. */} {/* {previewUrl ? "Change Icon" : "Upload Icon"} */} - {currentFileValue && ( // Show remove button only if a file is selected + + {showRemoveButton && ( - + Remove )} + + {showResetButton && ( + + + Reset to Default + + )} ); -} \ No newline at end of file +} diff --git a/app/components/ui/input.tsx b/app/components/ui/input.tsx index c9add61..0ecca87 100644 --- a/app/components/ui/input.tsx +++ b/app/components/ui/input.tsx @@ -1,21 +1,21 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ) + return ( + + ); } -export { Input } +export { Input }; diff --git a/app/components/ui/label.tsx b/app/components/ui/label.tsx index e15fb65..05a0570 100644 --- a/app/components/ui/label.tsx +++ b/app/components/ui/label.tsx @@ -1,24 +1,24 @@ -"use client" +"use client"; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Label({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Label } +export { Label }; diff --git a/app/components/ui/scroll-area.tsx b/app/components/ui/scroll-area.tsx index dcbfe6a..06ea54d 100644 --- a/app/components/ui/scroll-area.tsx +++ b/app/components/ui/scroll-area.tsx @@ -1,82 +1,82 @@ -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" -import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function ScrollArea({ - className, - children, - scrollbarSize, - viewportRef, - ...props + className, + children, + scrollbarSize, + viewportRef, + ...props }: React.ComponentProps & { - scrollbarSize?: "default" | "narrow" | "none", - viewportRef?: React.Ref + scrollbarSize?: "default" | "narrow" | "none"; + viewportRef?: React.Ref; }) { - return ( - - - {children} - - - - - ) + return ( + + + {children} + + + + + ); } function ScrollBar({ - className, - orientation = "vertical", - size = "default", - ...props + className, + orientation = "vertical", + size = "default", + ...props }: React.ComponentProps & { - size?: "default" | "narrow" | "none" + size?: "default" | "narrow" | "none"; }) { - const classes = { - vertical: { - className: "h-full border-l border-l-transparent", - size: { - default: "w-2.5", - narrow: "w-1.5", - none: "hidden", - }, - }, - horizontal: { - className: "flex-col border-t border-t-transparent", - size: { - default: "h-2.5", - narrow: "h-1.5", - none: "hidden", - }, - }, - } + const classes = { + vertical: { + className: "h-full border-l border-l-transparent", + size: { + default: "w-2.5", + narrow: "w-1.5", + none: "hidden", + }, + }, + horizontal: { + className: "flex-col border-t border-t-transparent", + size: { + default: "h-2.5", + narrow: "h-1.5", + none: "hidden", + }, + }, + }; - return ( - - - - ) + return ( + + + + ); } -export { ScrollArea, ScrollBar } +export { ScrollArea, ScrollBar }; diff --git a/app/components/ui/select.tsx b/app/components/ui/select.tsx index 4eee4a2..b928223 100644 --- a/app/components/ui/select.tsx +++ b/app/components/ui/select.tsx @@ -1,183 +1,189 @@ -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Select({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function SelectGroup({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function SelectValue({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function SelectTrigger({ - className, - size = "default", - children, - ...props + className, + size = "default", + children, + ...props }: React.ComponentProps & { - size?: "sm" | "default" + size?: "sm" | "default"; }) { - return ( - - {children} - - - - - ) + return ( + + {children} + + + + + ); } function SelectContent({ - className, - children, - position = "popper", - ...props + className, + children, + position = "popper", + ...props }: React.ComponentProps) { - return ( - - - - - {children} - - - - - ) + return ( + + + + + {children} + + + + + ); } function SelectLabel({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function SelectItem({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - - - - - - {children} - - ) + return ( + + + + + + + {children} + + ); } function SelectSeparator({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function SelectScrollUpButton({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - - - ) + return ( + + + + ); } function SelectScrollDownButton({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - - - ) + return ( + + + + ); } export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/app/components/ui/separator.tsx b/app/components/ui/separator.tsx index f197595..4659c93 100644 --- a/app/components/ui/separator.tsx +++ b/app/components/ui/separator.tsx @@ -1,26 +1,26 @@ -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Separator({ - className, - orientation = "horizontal", - decorative = true, - ...props + className, + orientation = "horizontal", + decorative = true, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Separator } +export { Separator }; diff --git a/app/components/ui/tabs.tsx b/app/components/ui/tabs.tsx index ac2c3f0..bf9d9e2 100644 --- a/app/components/ui/tabs.tsx +++ b/app/components/ui/tabs.tsx @@ -1,64 +1,64 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Tabs({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function TabsList({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function TabsTrigger({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function TabsContent({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/app/components/ui/textarea.tsx b/app/components/ui/textarea.tsx index 1c7ebff..6fb5668 100644 --- a/app/components/ui/textarea.tsx +++ b/app/components/ui/textarea.tsx @@ -1,18 +1,18 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( - - ) + return ( + + ); } -export { Textarea } +export { Textarea }; diff --git a/app/components/ui/tooltip.tsx b/app/components/ui/tooltip.tsx index cd42777..bddcd3a 100644 --- a/app/components/ui/tooltip.tsx +++ b/app/components/ui/tooltip.tsx @@ -1,59 +1,59 @@ -import * as React from "react" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function TooltipProvider({ - delayDuration = 0, - ...props + delayDuration = 0, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function Tooltip({ - ...props + ...props }: React.ComponentProps) { - return ( - - - - ) + return ( + + + + ); } function TooltipTrigger({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function TooltipContent({ - className, - sideOffset = 0, - children, - ...props + className, + sideOffset = 0, + children, + ...props }: React.ComponentProps) { - return ( - - - {children} - - - - ) + return ( + + + {children} + + + + ); } -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/app/components/user-avatar.tsx b/app/components/user-avatar.tsx index 7ef1e50..fc80070 100644 --- a/app/components/user-avatar.tsx +++ b/app/components/user-avatar.tsx @@ -1,13 +1,14 @@ -import type { PartialUser } from "~/lib/api/types" -import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar" +import type { PartialUser } from "~/lib/api/types"; +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; interface UserAvatarProps { - user: PartialUser | undefined + user: PartialUser | undefined; } -export default function UserAvatar( - { user, ...props }: UserAvatarProps & React.ComponentProps -) { +export default function UserAvatar({ + user, + ...props +}: UserAvatarProps & React.ComponentProps) { return ( @@ -15,5 +16,5 @@ export default function UserAvatar( {user?.username?.[0]} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/user-context-menu.tsx b/app/components/user-context-menu.tsx new file mode 100644 index 0000000..1223989 --- /dev/null +++ b/app/components/user-context-menu.tsx @@ -0,0 +1,62 @@ +import { IdCard, Mail } from "lucide-react"; +import { useNavigate } from "react-router"; +import { createChannel } from "~/lib/api/client/user"; +import type { UserId } from "~/lib/api/types"; +import { useUsersStore } from "~/stores/users-store"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "./ui/context-menu"; + +interface UserContextMenuProps { + userId: UserId; + children: React.ReactNode; +} + +export default function UserContextMenu({ + userId, + children, +}: UserContextMenuProps) { + const currentUser = useUsersStore((state) => state.getCurrentUser()); + const navigate = useNavigate(); + + const onMessage = async () => { + const reponse = await createChannel([userId]); + + navigate(`/app/@me/channels/${reponse.id}`); + }; + + const onCopyId = () => { + navigator.clipboard.writeText(userId); + }; + + return ( + <> + + {children} + + {currentUser?.id !== userId && ( + <> + + + Message + + + + + > + )} + + + Copy ID + + + + + + > + ); +} diff --git a/app/components/visible-trigger.tsx b/app/components/visible-trigger.tsx index 647e049..f1ef9e1 100644 --- a/app/components/visible-trigger.tsx +++ b/app/components/visible-trigger.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef } from "react"; interface VisibleTriggerProps { onVisible: () => void | Promise; @@ -20,7 +20,7 @@ export default function VisibleTrigger({ children, style, ...props -}: VisibleTriggerProps & React.ComponentProps<'div'>) { +}: VisibleTriggerProps & React.ComponentProps<"div">) { const elementRef = useRef(null); // Ref to attach to the DOM element we want to observe useEffect(() => { @@ -34,7 +34,7 @@ export default function VisibleTrigger({ // Default IntersectionObserver options const defaultOptions = { root: null, // default is the viewport - rootMargin: '0px', // No margin by default + rootMargin: "0px", // No margin by default threshold: 0, // Trigger as soon as any part of the element is visible }; @@ -62,7 +62,7 @@ export default function VisibleTrigger({ // console.log('VisibleTrigger: Element is NOT intersecting.', entry); } }, - observerOptions // Pass the options to the observer + observerOptions, // Pass the options to the observer ); // Start observing the element @@ -93,10 +93,10 @@ export default function VisibleTrigger({ return ( {children} {/* Render any children passed to the component */} ); -}; \ No newline at end of file +} diff --git a/app/hooks/use-origin.ts b/app/hooks/use-origin.ts index f68e8ba..2045c40 100644 --- a/app/hooks/use-origin.ts +++ b/app/hooks/use-origin.ts @@ -7,11 +7,14 @@ export const useOrigin = () => { setIsMounted(true); }, []); - const origin = typeof window !== 'undefined' && window.location.origin ? window.location.origin : ''; + const origin = + typeof window !== "undefined" && window.location.origin + ? window.location.origin + : ""; if (!isMounted) { - return ''; + return ""; } return origin; -} \ No newline at end of file +}; diff --git a/app/lib/api/client/auth.ts b/app/lib/api/client/auth.ts index 57291d6..a8f5cc1 100644 --- a/app/lib/api/client/auth.ts +++ b/app/lib/api/client/auth.ts @@ -1,34 +1,34 @@ -import axios from "../http-client" -import type { FullUser } from "../types" +import axios from "../http-client"; +import type { FullUser } from "../types"; interface RegisterRequest { - email: string - username: string - displayName?: string - password: string + email: string; + username: string; + displayName?: string; + password: string; } interface LoginRequest { - username: string - password: string + username: string; + password: string; } interface LoginResponse { - user: FullUser - token: string + user: FullUser; + token: string; } export async function register(request: RegisterRequest) { - await axios.post("/auth/register", request) + await axios.post("/auth/register", request); } export async function login(request: LoginRequest) { - const response = await axios.post("/auth/login", request) + const response = await axios.post("/auth/login", request); - return response.data as LoginResponse + return response.data as LoginResponse; } export default { register, login, -} \ No newline at end of file +}; diff --git a/app/lib/api/client/channel.ts b/app/lib/api/client/channel.ts index 0728452..64600f5 100644 --- a/app/lib/api/client/channel.ts +++ b/app/lib/api/client/channel.ts @@ -10,26 +10,28 @@ export async function paginatedMessages( params: { limit, before, - } - }) + }, + }); - return (response.data as any[]).map((value, _) => messageSchema.parse(value)) + return (response.data as any[]).map((value, _) => + messageSchema.parse(value), + ); } export async function sendMessage( channelId: ChannelId, content: string, - attachments?: Uuid[] + attachments?: Uuid[], ) { const response = await axios.post(`/channels/${channelId}/messages`, { content, attachments, - }) + }); - return messageSchema.parse(response.data) + return messageSchema.parse(response.data); } export default { paginatedMessages, sendMessage, -} \ No newline at end of file +}; diff --git a/app/lib/api/client/file.ts b/app/lib/api/client/file.ts index d1b4bbc..946d2cc 100644 --- a/app/lib/api/client/file.ts +++ b/app/lib/api/client/file.ts @@ -1,26 +1,26 @@ -import axios from "../http-client" +import axios from "../http-client"; export async function uploadFile(file: File) { - const formData = new FormData() - formData.append("files", file) + const formData = new FormData(); + formData.append("files", file); - const response = await axios.postForm(`/files`, formData) + const response = await axios.postForm(`/files`, formData); - return response.data as string[] + return response.data as string[]; } export async function uploadFiles(file: File[]) { - const formData = new FormData() + const formData = new FormData(); for (const f of file) { - formData.append("files", f) + formData.append("files", f); } - const response = await axios.postForm(`/files`, formData) + const response = await axios.postForm(`/files`, formData); - return response.data as string[] + return response.data as string[]; } export default { uploadFile, - uploadFiles -} \ No newline at end of file + uploadFiles, +}; diff --git a/app/lib/api/client/server.ts b/app/lib/api/client/server.ts index be7d580..19d6ac1 100644 --- a/app/lib/api/client/server.ts +++ b/app/lib/api/client/server.ts @@ -1,74 +1,88 @@ -import axios from "../http-client" -import type { ChannelId, ChannelType, Server, ServerChannel, ServerId, ServerInvite } from "../types" +import axios from "../http-client"; +import type { + ChannelId, + ChannelType, + Server, + ServerChannel, + ServerId, + ServerInvite, +} from "../types"; interface CreateServerRequest { - name: string - iconId?: string + name: string; + iconId?: string; } interface CreateServerChannelRequest { - name: string - type: ChannelType + name: string; + type: ChannelType; } export async function list() { - const response = await axios.get("/servers") + const response = await axios.get("/servers"); - return response.data as Server[] + return response.data as Server[]; } export async function create(request: CreateServerRequest) { - const response = await axios.post("/servers", request) + const response = await axios.post("/servers", request); - return response.data as Server + return response.data as Server; } export async function get(serverId: ServerId) { - const response = await axios.get(`/servers/${serverId}`) + const response = await axios.get(`/servers/${serverId}`); - return response.data as Server + return response.data as Server; } export async function delet(serverId: ServerId) { - const response = await axios.delete(`/servers/${serverId}`) + const response = await axios.delete(`/servers/${serverId}`); - return response.data as Server + return response.data as Server; } export async function listChannels(serverId: ServerId) { - const response = await axios.get(`/servers/${serverId}/channels`) + const response = await axios.get(`/servers/${serverId}/channels`); - return response.data as ServerChannel[] + return response.data as ServerChannel[]; } -export async function createChannel(serverId: ServerId, request: CreateServerChannelRequest) { - const response = await axios.post(`/servers/${serverId}/channels`, request) +export async function createChannel( + serverId: ServerId, + request: CreateServerChannelRequest, +) { + const response = await axios.post(`/servers/${serverId}/channels`, request); - return response.data as ServerChannel + return response.data as ServerChannel; } export async function getChannel(serverId: ServerId, channelId: ChannelId) { - const response = await axios.get(`/servers/${serverId}/channels/${channelId}`) + const response = await axios.get( + `/servers/${serverId}/channels/${channelId}`, + ); - return response.data as ServerChannel + return response.data as ServerChannel; } export async function deleteChannel(serverId: ServerId, channelId: ChannelId) { - const response = await axios.delete(`/servers/${serverId}/channels/${channelId}`) + const response = await axios.delete( + `/servers/${serverId}/channels/${channelId}`, + ); - return response.data as ServerChannel + return response.data as ServerChannel; } export async function createInvite(serverId: ServerId) { - const response = await axios.post(`/servers/${serverId}/invites`) + const response = await axios.post(`/servers/${serverId}/invites`); - return response.data as ServerInvite + return response.data as ServerInvite; } export async function getInvite(inviteCode: string) { - const response = await axios.get(`/invites/${inviteCode}`) + const response = await axios.get(`/invites/${inviteCode}`); - return response.data as Server + return response.data as Server; } export default { @@ -81,5 +95,5 @@ export default { getChannel, deleteChannel, createInvite, - getInvite -} \ No newline at end of file + getInvite, +}; diff --git a/app/lib/api/client/user.ts b/app/lib/api/client/user.ts index ef555b3..878630e 100644 --- a/app/lib/api/client/user.ts +++ b/app/lib/api/client/user.ts @@ -1,38 +1,53 @@ -import axios from "../http-client" -import type { FullUser, PartialUser, RecipientChannel, UserId, Uuid } from "../types" +import axios from "../http-client"; +import type { + FullUser, + PartialUser, + RecipientChannel, + UserId, + Uuid, +} from "../types"; export async function me() { - const response = await axios.get("/users/@me") + const response = await axios.get("/users/@me"); - return response.data as FullUser + return response.data as FullUser; } export async function getUser(userId: UserId) { - const response = await axios.get(`/users/${userId}`) + const response = await axios.get(`/users/${userId}`); - return response.data as PartialUser + return response.data as PartialUser; } export async function channels() { - const response = await axios.get("/users/@me/channels") + const response = await axios.get("/users/@me/channels"); - return response.data as RecipientChannel[] + return response.data as RecipientChannel[]; +} + +export async function createChannel(recipients: UserId[]) { + const response = await axios.post("/users/@me/channels", { + recipients, + }); + + return response.data as RecipientChannel; } interface PatchUserRequest { - displayName?: string | null - avatarId?: Uuid | null + displayName?: string | null; + avatarId?: Uuid | null; } export async function patchUser(request: PatchUserRequest) { - const response = await axios.patch(`/users/@me`, request) + const response = await axios.patch(`/users/@me`, request); - return response.data as FullUser + return response.data as FullUser; } export default { me, channels, + createChannel, getUser, - patchUser -} + patchUser, +}; diff --git a/app/lib/api/http-client.ts b/app/lib/api/http-client.ts index 7f70cdc..ad5bf6f 100644 --- a/app/lib/api/http-client.ts +++ b/app/lib/api/http-client.ts @@ -1,23 +1,23 @@ -import axios from "axios" -import { useTokenStore } from "~/stores/token-store" -import { API_URL } from "../consts" +import axios from "axios"; +import { useTokenStore } from "~/stores/token-store"; +import { API_URL } from "../consts"; axios.interceptors.request.use( (config) => { - const token = useTokenStore.getState().token + const token = useTokenStore.getState().token; if (token) { - config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}`; } - return config + return config; }, (error) => { - return Promise.reject(error) - } -) + return Promise.reject(error); + }, +); -axios.defaults.baseURL = API_URL -axios.defaults.headers.common["Content-Type"] = "application/json" +axios.defaults.baseURL = API_URL; +axios.defaults.headers.common["Content-Type"] = "application/json"; -export default axios \ No newline at end of file +export default axios; diff --git a/app/lib/api/types.ts b/app/lib/api/types.ts index 6e9fb20..da4f570 100644 --- a/app/lib/api/types.ts +++ b/app/lib/api/types.ts @@ -1,85 +1,86 @@ import { z } from "zod"; export type TypeToZod = { - [K in keyof T]: - // 1. Handle Arrays (including arrays of objects, optional or required) + [K in keyof T]: // 1. Handle Arrays (including arrays of objects, optional or required) T[K] extends ReadonlyArray | undefined - ? undefined extends T[K] - ? E extends object - ? z.ZodOptional>>> - : z.ZodOptional>>> - : E extends object - ? z.ZodArray>> - : z.ZodArray>> - // 2. Handle Primitives - : T[K] extends string | number | boolean | Date | null | undefined - ? undefined extends T[K] - ? z.ZodOptional>> - : z.ZodType - // 3. Handle Objects (required or optional, but not arrays) - : T[K] extends object | undefined - ? undefined extends T[K] - ? z.ZodOptional>>> - : T[K] extends object - ? z.ZodObject> - : z.ZodUnknown // Fallback for unexpected required non-object/non-primitive types - // 4. Fallback for any other types - : z.ZodUnknown; + ? undefined extends T[K] + ? E extends object + ? z.ZodOptional>>> + : z.ZodOptional< + z.ZodArray>> + > + : E extends object + ? z.ZodArray>> + : z.ZodArray>> + : // 2. Handle Primitives + T[K] extends string | number | boolean | Date | null | undefined + ? undefined extends T[K] + ? z.ZodOptional>> + : z.ZodType + : // 3. Handle Objects (required or optional, but not arrays) + T[K] extends object | undefined + ? undefined extends T[K] + ? z.ZodOptional>>> + : T[K] extends object + ? z.ZodObject> + : z.ZodUnknown // Fallback for unexpected required non-object/non-primitive types + : // 4. Fallback for any other types + z.ZodUnknown; }; export const createZodObject = (obj: TypeToZod) => { return z.object(obj); }; -export type Uuid = string +export type Uuid = string; -export type UserId = Uuid -export type ServerId = Uuid -export type ChannelId = Uuid -export type MessageId = Uuid +export type UserId = Uuid; +export type ServerId = Uuid; +export type ChannelId = Uuid; +export type MessageId = Uuid; export interface FullUser { - id: UserId - avatarUrl?: string - username: string - displayName?: string - email: string - bot: boolean - system: boolean - settings: any + id: UserId; + avatarUrl?: string; + username: string; + displayName?: string; + email: string; + bot: boolean; + system: boolean; + settings: any; } export interface Server { - id: ServerId - name: string - iconUrl?: string - ownerId: UserId + id: ServerId; + name: string; + iconUrl?: string; + ownerId: UserId; } export enum ChannelType { - SERVER_TEXT = 'server_text', - SERVER_VOICE = 'server_voice', - SERVER_CATEGORY = 'server_category', + SERVER_TEXT = "server_text", + SERVER_VOICE = "server_voice", + SERVER_CATEGORY = "server_category", - DIRECT_MESSAGE = 'direct_message', - GROUP = 'group', + DIRECT_MESSAGE = "direct_message", + GROUP = "group", } export interface Message { - id: MessageId - channelId: ChannelId - authorId: UserId - content: string - createdAt: Date - attachments: UploadedFile[] + id: MessageId; + channelId: ChannelId; + authorId: UserId; + content: string; + createdAt: Date; + attachments: UploadedFile[]; } export interface UploadedFile { - id: Uuid - filename: string - contentType: string - size: number - url: string + id: Uuid; + filename: string; + contentType: string; + size: number; + url: string; } export const uploadFileSchema = createZodObject({ @@ -88,7 +89,7 @@ export const uploadFileSchema = createZodObject({ contentType: z.string(), size: z.number(), url: z.string(), -}) +}); export const messageSchema = createZodObject({ id: z.string(), @@ -97,44 +98,44 @@ export const messageSchema = createZodObject({ content: z.string(), createdAt: z.coerce.date(), attachments: z.array(uploadFileSchema), -}) +}); export interface Channel { - id: ChannelId - name: string - type: ChannelType - lastMessageId?: MessageId + id: ChannelId; + name: string; + type: ChannelType; + lastMessageId?: MessageId; } export interface ServerChannel { - id: ChannelId - name: string - type: ChannelType - lastMessageId?: MessageId - serverId: ServerId - parentId?: ChannelId + id: ChannelId; + name: string; + type: ChannelType; + lastMessageId?: MessageId; + serverId: ServerId; + parentId?: ChannelId; } export interface ServerInvite { - code: string - serverId: ServerId - inviterId?: UserId - expiresAt?: string + code: string; + serverId: ServerId; + inviterId?: UserId; + expiresAt?: string; } export interface RecipientChannel { - id: ChannelId - name: string - type: ChannelType - lastMessageId?: MessageId - recipients: PartialUser[] + id: ChannelId; + name: string; + type: ChannelType; + lastMessageId?: MessageId; + recipients: PartialUser[]; } export interface PartialUser { - id: ChannelId - username: string - displayName?: string - avatarUrl?: string, - bot: boolean - system: boolean -} \ No newline at end of file + id: ChannelId; + username: string; + displayName?: string; + avatarUrl?: string; + bot: boolean; + system: boolean; +} diff --git a/app/lib/consts.ts b/app/lib/consts.ts index 369866c..2769b70 100644 --- a/app/lib/consts.ts +++ b/app/lib/consts.ts @@ -1 +1 @@ -export const API_URL = "http://localhost:12345/api/v1" +export const API_URL = "http://localhost:12345/api/v1"; diff --git a/app/lib/utils.ts b/app/lib/utils.ts index f4c1926..cdbfc1f 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -2,42 +2,46 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } export function getFirstLetters(str: string, n: number): string { - return str - .split(/\s+/) - .slice(0, n) - .map(word => word[0] || '') - .join(''); + return str + .split(/\s+/) + .slice(0, n) + .map((word) => word[0] || "") + .join(""); } export function formatFileSize(bytes: number, decimals = 2): string { - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return "0 Bytes"; const k = 1024; const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; } export function createPrefixedLogger(prefix: string, styles?: string[]) { - const result: Record void> = {}; + const result: Record void> = {}; - const methods = ['log', 'trace', 'debug', 'info', 'warn', 'error'] as const; + const methods = ["log", "trace", "debug", "info", "warn", "error"] as const; - for (const methodName of methods) { - const originalMethod = console[methodName].bind(console); + for (const methodName of methods) { + const originalMethod = console[methodName].bind(console); - result[methodName] = (...args: any[]) => { - if (typeof args[0] === 'string') { - originalMethod(`${prefix} ${args[0]}`, ...(styles || []), ...args.slice(1)); - } else { - originalMethod(prefix, styles, ...args); - } - }; - } + result[methodName] = (...args: any[]) => { + if (typeof args[0] === "string") { + originalMethod( + `${prefix} ${args[0]}`, + ...(styles || []), + ...args.slice(1), + ); + } else { + originalMethod(prefix, styles, ...args); + } + }; + } - return result; -} \ No newline at end of file + return result; +} diff --git a/app/lib/websocket/gateway/client.ts b/app/lib/websocket/gateway/client.ts index c5abe63..97a3638 100644 --- a/app/lib/websocket/gateway/client.ts +++ b/app/lib/websocket/gateway/client.ts @@ -1,5 +1,5 @@ -import type { ChannelId, ServerId, UserId } from '~/lib/api/types'; -import { createPrefixedLogger } from '~/lib/utils'; +import type { ChannelId, ServerId, UserId } from "~/lib/api/types"; +import { createPrefixedLogger } from "~/lib/utils"; import { type ClientMessage, ClientMessageType, @@ -8,11 +8,11 @@ import { type EventData, EventType, type ServerMessage, - ServerMessageType -} from './types'; + ServerMessageType, +} from "./types"; export type GatewayEvents = { - [K in EventType]: (data: Extract['data']) => void; + [K in EventType]: (data: Extract["data"]) => void; }; export type ControlEvents = { @@ -48,21 +48,21 @@ export class GatewayClient { this.options = { reconnect: options.reconnect ?? true, reconnectDelay: options.reconnectDelay ?? 5000, - maxReconnectAttempts: options.maxReconnectAttempts ?? 10 + maxReconnectAttempts: options.maxReconnectAttempts ?? 10, }; } // Public methods public connect(token: string): void { - logger.log('Connecting to %s', this.url); + logger.log("Connecting to %s", this.url); if (this.connectionLock) { - logger.warn('Connection already in progress'); + logger.warn("Connection already in progress"); return; } if (this.token === token) { - logger.warn('Token is the same as the current token'); + logger.warn("Token is the same as the current token"); return; } @@ -79,7 +79,7 @@ export class GatewayClient { } public disconnect(): void { - logger.log('Disconnecting'); + logger.log("Disconnecting"); this.closeInitiatedByClient = true; this.cleanupSocket(); @@ -91,18 +91,21 @@ export class GatewayClient { public updateVoiceState(serverId: ServerId, channelId: ChannelId): void { this.sendMessage({ type: ClientMessageType.VOICE_STATE_UPDATE, - data: { serverId, channelId } + data: { serverId, channelId }, }); } public requestVoiceStates(serverId: ServerId): void { this.sendMessage({ type: ClientMessageType.REQUEST_VOICE_STATES, - data: { serverId } + data: { serverId }, }); } - public onEvent(event: K | string, handler: GatewayEvents[K]): void { + public onEvent( + event: K | string, + handler: GatewayEvents[K], + ): void { this.serverEventHandlers[event as K] = handler; } @@ -110,7 +113,10 @@ export class GatewayClient { delete this.serverEventHandlers[event]; } - public onControl(event: K, handler: ControlEvents[K]): void { + public onControl( + event: K, + handler: ControlEvents[K], + ): void { this.eventHandlers[event] = handler; } @@ -142,7 +148,7 @@ export class GatewayClient { this.socket.onerror = this.onSocketError.bind(this); this.socket.onclose = this.onSocketClose.bind(this); } catch (error) { - this.emitError(new Error('Failed to create WebSocket connection')); + this.emitError(new Error("Failed to create WebSocket connection")); this.setState(ConnectionState.ERROR); } } @@ -150,16 +156,16 @@ export class GatewayClient { private onSocketOpen(): void { this.connectionLock = false; - logger.log('Socket opened'); + logger.log("Socket opened"); this.setState(ConnectionState.AUTHENTICATING); if (this.token) { this.sendMessage({ type: ClientMessageType.AUTHENTICATE, - data: { token: this.token } + data: { token: this.token }, }); } else { - this.emitError(new Error('No authentication token provided')); + this.emitError(new Error("No authentication token provided")); this.disconnect(); } } @@ -169,19 +175,23 @@ export class GatewayClient { const message = JSON.parse(event.data) as ServerMessage; this.handleServerMessage(message); } catch (error) { - this.emitError(new Error('Failed to parse WebSocket message', { cause: error })); + this.emitError( + new Error("Failed to parse WebSocket message", { + cause: error, + }), + ); } } private onSocketError(event: Event): void { this.connectionLock = false; - logger.log('Socket error: %s', event); + logger.log("Socket error: %s", event); - this.emitError(new Error('WebSocket error occurred')); + this.emitError(new Error("WebSocket error occurred")); } private onSocketClose(event: CloseEvent): void { - logger.log('Socket closed: %s', event); + logger.log("Socket closed: %s", event); this.connectionLock = false; @@ -190,7 +200,12 @@ export class GatewayClient { !this.closeInitiatedByClient && this.reconnectAttempts < this.options.maxReconnectAttempts ) { - logger.log('Reconnecting in %d seconds (%d/%d)', this.options.reconnectDelay / 1000, this.reconnectAttempts + 1, this.options.maxReconnectAttempts); + logger.log( + "Reconnecting in %d seconds (%d/%d)", + this.options.reconnectDelay / 1000, + this.reconnectAttempts + 1, + this.options.maxReconnectAttempts, + ); this.reconnectAttempts++; this.reconnectTimeout = setTimeout(() => { @@ -204,23 +219,30 @@ export class GatewayClient { } private handleServerMessage(message: ServerMessage): void { - logger.log('Received message: ', message); + logger.log("Received message: ", message); switch (message.type) { case ServerMessageType.AUTHENTICATE_ACCEPTED: this.userId = message.data.userId; this.sessionKey = message.data.sessionKey; this.setState(ConnectionState.CONNECTED); - this.emitControl('authenticated', message.data.userId, message.data.sessionKey); + this.emitControl( + "authenticated", + message.data.userId, + message.data.sessionKey, + ); break; case ServerMessageType.AUTHENTICATE_DENIED: - this.emitError(new Error('Authentication denied')); + this.emitError(new Error("Authentication denied")); this.disconnect(); break; case ServerMessageType.ERROR: - this.emitError(new Error(`Server error: ${message.data.code}`), message.data.code); + this.emitError( + new Error(`Server error: ${message.data.code}`), + message.data.code, + ); break; case ServerMessageType.EVENT: @@ -228,7 +250,7 @@ export class GatewayClient { break; default: - console.warn('Unhandled server message type:', message); + console.warn("Unhandled server message type:", message); } } @@ -237,39 +259,47 @@ export class GatewayClient { } private sendMessage(message: ClientMessage): void { - logger.log('Sending message: %o', message); + logger.log("Sending message: %o", message); if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(message)); } else { - this.emitError(new Error('Cannot send message: socket not connected')); + this.emitError( + new Error("Cannot send message: socket not connected"), + ); } } private setState(state: ConnectionState): void { if (this.state !== state) { - logger.log('State changed to %s', state); + logger.log("State changed to %s", state); this.state = state; - this.emitControl('stateChange', state); + this.emitControl("stateChange", state); } } private emitError(error: Error, code?: ErrorCode): void { - logger.error('Error: %s', error, error.cause); + logger.error("Error: %s", error, error.cause); this.setState(ConnectionState.ERROR); - this.emitControl('error', error, code); + this.emitControl("error", error, code); } - private emitControl(event: K, ...args: Parameters): void { + private emitControl( + event: K, + ...args: Parameters + ): void { const handler = this.eventHandlers[event]; if (handler) { (handler as Function)(...args); } } - private emitEvent(event: K, ...args: Parameters): void { + private emitEvent( + event: K, + ...args: Parameters + ): void { const handler = this.serverEventHandlers[event]; if (handler) { (handler as Function)(...args); @@ -277,7 +307,7 @@ export class GatewayClient { } private cleanupSocket(): void { - logger.log('Cleaning up socket'); + logger.log("Cleaning up socket"); if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); @@ -292,8 +322,10 @@ export class GatewayClient { this.socket.onclose = null; // Close the connection if it's still open - if (this.socket.readyState === WebSocket.OPEN || - this.socket.readyState === WebSocket.CONNECTING) { + if ( + this.socket.readyState === WebSocket.OPEN || + this.socket.readyState === WebSocket.CONNECTING + ) { this.socket.close(); } @@ -305,4 +337,7 @@ export class GatewayClient { } } -const logger = createPrefixedLogger('%cGateway WS%c:', ['color: red; font-weight: bold;', '']); +const logger = createPrefixedLogger("%cGateway WS%c:", [ + "color: red; font-weight: bold;", + "", +]); diff --git a/app/lib/websocket/gateway/types.ts b/app/lib/websocket/gateway/types.ts index df52ce0..957aab0 100644 --- a/app/lib/websocket/gateway/types.ts +++ b/app/lib/websocket/gateway/types.ts @@ -1,50 +1,58 @@ -import type { ChannelId, Message, MessageId, PartialUser, Server, ServerId, UserId } from "~/lib/api/types"; +import type { + ChannelId, + Message, + MessageId, + PartialUser, + Server, + ServerId, + UserId, +} from "~/lib/api/types"; type Channel = any; // TODO: Define Channel type export enum ServerMessageType { - AUTHENTICATE_ACCEPTED = 'AUTHENTICATE_ACCEPTED', - AUTHENTICATE_DENIED = 'AUTHENTICATE_DENIED', - EVENT = 'EVENT', - ERROR = 'ERROR' + AUTHENTICATE_ACCEPTED = "AUTHENTICATE_ACCEPTED", + AUTHENTICATE_DENIED = "AUTHENTICATE_DENIED", + EVENT = "EVENT", + ERROR = "ERROR", } export enum ClientMessageType { - AUTHENTICATE = 'AUTHENTICATE', - VOICE_STATE_UPDATE = 'VOICE_STATE_UPDATE', - REQUEST_VOICE_STATES = 'REQUEST_VOICE_STATES', + AUTHENTICATE = "AUTHENTICATE", + VOICE_STATE_UPDATE = "VOICE_STATE_UPDATE", + REQUEST_VOICE_STATES = "REQUEST_VOICE_STATES", } // Error codes from the server export enum ErrorCode { - AUTHENTICATION_FAILED = 'AUTHENTICATION_FAILED', - TOKEN_GENERATION_FAILED = 'TOKEN_GENERATION_FAILED' + AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED", + TOKEN_GENERATION_FAILED = "TOKEN_GENERATION_FAILED", } // Event types from the server export enum EventType { - ADD_SERVER = 'ADD_SERVER', - REMOVE_SERVER = 'REMOVE_SERVER', + ADD_SERVER = "ADD_SERVER", + REMOVE_SERVER = "REMOVE_SERVER", - ADD_DM_CHANNEL = 'ADD_DM_CHANNEL', - REMOVE_DM_CHANNEL = 'REMOVE_DM_CHANNEL', + ADD_DM_CHANNEL = "ADD_DM_CHANNEL", + REMOVE_DM_CHANNEL = "REMOVE_DM_CHANNEL", - ADD_SERVER_CHANNEL = 'ADD_SERVER_CHANNEL', - REMOVE_SERVER_CHANNEL = 'REMOVE_SERVER_CHANNEL', + ADD_SERVER_CHANNEL = "ADD_SERVER_CHANNEL", + REMOVE_SERVER_CHANNEL = "REMOVE_SERVER_CHANNEL", - ADD_USER = 'ADD_USER', - REMOVE_USER = 'REMOVE_USER', + ADD_USER = "ADD_USER", + REMOVE_USER = "REMOVE_USER", - ADD_SERVER_MEMBER = 'ADD_SERVER_MEMBER', - REMOVE_SERVER_MEMBER = 'REMOVE_SERVER_MEMBER', + ADD_SERVER_MEMBER = "ADD_SERVER_MEMBER", + REMOVE_SERVER_MEMBER = "REMOVE_SERVER_MEMBER", - ADD_MESSAGE = 'ADD_MESSAGE', - REMOVE_MESSAGE = 'REMOVE_MESSAGE', + ADD_MESSAGE = "ADD_MESSAGE", + REMOVE_MESSAGE = "REMOVE_MESSAGE", - VOICE_CHANNEL_CONNECTED = 'VOICE_CHANNEL_CONNECTED', - VOICE_CHANNEL_DISCONNECTED = 'VOICE_CHANNEL_DISCONNECTED', + VOICE_CHANNEL_CONNECTED = "VOICE_CHANNEL_CONNECTED", + VOICE_CHANNEL_DISCONNECTED = "VOICE_CHANNEL_DISCONNECTED", - VOICE_SERVER_UPDATE = 'VOICE_SERVER_UPDATE' + VOICE_SERVER_UPDATE = "VOICE_SERVER_UPDATE", } // Client message types @@ -70,7 +78,10 @@ export interface RequestVoiceStatesMessage { }; } -export type ClientMessage = AuthenticateMessage | VoiceStateUpdateMessage | RequestVoiceStatesMessage; +export type ClientMessage = + | AuthenticateMessage + | VoiceStateUpdateMessage + | RequestVoiceStatesMessage; // Server message types export interface AuthenticateAcceptedMessage { @@ -111,6 +122,7 @@ export interface AddDmChannelEvent { type: EventType.ADD_DM_CHANNEL; data: { channel: Channel; + recipients: PartialUser[]; }; } @@ -241,9 +253,9 @@ export type ServerMessage = // Connection states export enum ConnectionState { - DISCONNECTED = 'DISCONNECTED', - CONNECTING = 'CONNECTING', - AUTHENTICATING = 'AUTHENTICATING', - CONNECTED = 'CONNECTED', - ERROR = 'ERROR' -} \ No newline at end of file + DISCONNECTED = "DISCONNECTED", + CONNECTING = "CONNECTING", + AUTHENTICATING = "AUTHENTICATING", + CONNECTED = "CONNECTED", + ERROR = "ERROR", +} diff --git a/app/lib/websocket/voice/client.ts b/app/lib/websocket/voice/client.ts index 53e787c..c66123e 100644 --- a/app/lib/websocket/voice/client.ts +++ b/app/lib/websocket/voice/client.ts @@ -1,5 +1,11 @@ import { createPrefixedLogger } from "~/lib/utils"; -import { ClientMessageType, ConnectionState, ServerMessageType, type ClientMessage, type ServerMessage } from "./types"; +import { + ClientMessageType, + ConnectionState, + ServerMessageType, + type ClientMessage, + type ServerMessage, +} from "./types"; export class WebRTCClient { private socket: WebSocket | null = null; @@ -20,7 +26,7 @@ export class WebRTCClient { url: string, onStateChange: (state: ConnectionState) => void, onError: (error: Error) => void, - onRemoteStream: (stream: MediaStream) => void + onRemoteStream: (stream: MediaStream) => void, ) { this.url = url; this.onStateChange = onStateChange; @@ -30,26 +36,29 @@ export class WebRTCClient { public connect = async (token: string) => { if (this.connectionLock) { - warn('WebRTC: Connection already in progress'); + warn("WebRTC: Connection already in progress"); return; } this.connectionLock = true; - if (this.state !== ConnectionState.DISCONNECTED && this.state !== ConnectionState.ERROR) { + if ( + this.state !== ConnectionState.DISCONNECTED && + this.state !== ConnectionState.ERROR + ) { this.disconnect(); } if (this.disconnectPromise) { - warn('WebRTC: Waiting for previous disconnect to complete'); + warn("WebRTC: Waiting for previous disconnect to complete"); try { await this.disconnectPromise; } catch (error) { - console.error('WebRTC: Previous disconnect failed:', error); + console.error("WebRTC: Previous disconnect failed:", error); } } - log('Connecting to %s', this.url); + log("Connecting to %s", this.url); try { this.setState(ConnectionState.CONNECTING); @@ -57,25 +66,25 @@ export class WebRTCClient { this.socket = new WebSocket(this.url); this.socket.onopen = () => { - log('Socket opened'); + log("Socket opened"); this.connectionLock = false; this.setState(ConnectionState.AUTHENTICATING); this.sendMessage({ type: ClientMessageType.AUTHENTICATE, - data: { token } + data: { token }, }); }; this.socket.onmessage = this.handleServerMessage; this.socket.onerror = (event) => { - this.handleError(new Error('WebSocket error occurred')); + this.handleError(new Error("WebSocket error occurred")); }; this.socket.onclose = (e) => { - log('Socket closed', e); + log("Socket closed", e); this.cleanupResources(); if (this.state !== ConnectionState.ERROR) { this.setState(ConnectionState.DISCONNECTED); @@ -88,7 +97,9 @@ export class WebRTCClient { } }; } catch (error) { - this.handleError(error instanceof Error ? error : new Error('Unknown error')); + this.handleError( + error instanceof Error ? error : new Error("Unknown error"), + ); } }; @@ -112,16 +123,16 @@ export class WebRTCClient { }); const onSocketClose = () => { - this.socket?.removeEventListener('close', onSocketClose); + this.socket?.removeEventListener("close", onSocketClose); this.disconnectResolve?.(); this.disconnectResolve = null; this.disconnectPromise = null; }; - this.socket.addEventListener('close', onSocketClose); + this.socket.addEventListener("close", onSocketClose); if (this.socket.readyState !== WebSocket.CLOSING) { - this.socket.close(1000, 'WebRTC: Cleaning up resources'); + this.socket.close(1000, "WebRTC: Cleaning up resources"); } } else { this.cleanupResources(); @@ -131,21 +142,21 @@ export class WebRTCClient { public createOffer = async (localStream?: MediaStream): Promise => { if (this.state !== ConnectionState.CONNECTED) { - this.handleError(new Error('Cannot create offer: not connected')); + this.handleError(new Error("Cannot create offer: not connected")); return; } try { // Create RTCPeerConnection with standard configuration const configuration: RTCConfiguration = { - iceServers: [] + iceServers: [], }; this.peerConnection = new RTCPeerConnection(configuration); // Add local stream tracks if provided if (localStream) { - localStream.getTracks().forEach(track => { + localStream.getTracks().forEach((track) => { this.peerConnection!.addTrack(track, localStream); }); } @@ -174,20 +185,26 @@ export class WebRTCClient { this.sendMessage({ type: ClientMessageType.SDP_OFFER, data: { - sdp: this.peerConnection.localDescription - } + sdp: this.peerConnection.localDescription, + }, }); } } catch (error) { - this.handleError(error instanceof Error ? error : new Error('Error creating WebRTC offer')); + this.handleError( + error instanceof Error + ? error + : new Error("Error creating WebRTC offer"), + ); } }; - private handleServerMessage = async (event: MessageEvent): Promise => { + private handleServerMessage = async ( + event: MessageEvent, + ): Promise => { try { const message: ServerMessage = JSON.parse(event.data); - log('Received message: %o', message); + log("Received message: %o", message); switch (message.type) { case ServerMessageType.AUTHENTICATE_ACCEPTED: @@ -195,7 +212,7 @@ export class WebRTCClient { break; case ServerMessageType.AUTHENTICATE_DENIED: - this.handleError(new Error('Authentication failed')); + this.handleError(new Error("Authentication failed")); break; case ServerMessageType.SDP_ANSWER: @@ -203,54 +220,66 @@ export class WebRTCClient { break; default: - warn('Unhandled message type:', message); + warn("Unhandled message type:", message); } } catch (error) { - this.handleError(error instanceof Error ? error : new Error('Failed to process message')); + this.handleError( + error instanceof Error + ? error + : new Error("Failed to process message"), + ); } }; - private handleSdpAnswer = async (sdp: RTCSessionDescription): Promise => { - log('Received SDP answer: %o', sdp); + private handleSdpAnswer = async ( + sdp: RTCSessionDescription, + ): Promise => { + log("Received SDP answer: %o", sdp); if (!this.peerConnection) { - this.handleError(new Error('No peer connection established')); + this.handleError(new Error("No peer connection established")); return; } try { await this.peerConnection.setRemoteDescription(sdp); } catch (error) { - this.handleError(error instanceof Error ? error : new Error('Error setting remote description')); + this.handleError( + error instanceof Error + ? error + : new Error("Error setting remote description"), + ); } }; private sendMessage = (message: ClientMessage): void => { - log('Sending message: %o', message); + log("Sending message: %o", message); if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(message)); } else { - this.handleError(new Error('Cannot send message: socket not connected')); + this.handleError( + new Error("Cannot send message: socket not connected"), + ); } }; private setState = (state: ConnectionState): void => { - log('State changed to %s', state); + log("State changed to %s", state); this.state = state; this.onStateChange(state); }; private handleError = (error: Error): void => { - log('Error: %s', error.message); + log("Error: %s", error.message); this.setState(ConnectionState.ERROR); this.onError(error); }; private cleanupResources = (): void => { - log('Cleaning up resources'); + log("Cleaning up resources"); if (this.peerConnection) { this.peerConnection.close(); @@ -258,14 +287,13 @@ export class WebRTCClient { } if (this.socket) { - this.socket.close(1000, 'WebRTC: Cleaning up resources'); + this.socket.close(1000, "WebRTC: Cleaning up resources"); this.socket = null; } }; } -const { - log, - warn, - ...other -} = createPrefixedLogger('%cWebRTC WS%c:', ['color: blue; font-weight: bold;', '']); +const { log, warn, ...other } = createPrefixedLogger("%cWebRTC WS%c:", [ + "color: blue; font-weight: bold;", + "", +]); diff --git a/app/lib/websocket/voice/types.ts b/app/lib/websocket/voice/types.ts index 91f1292..478ac37 100644 --- a/app/lib/websocket/voice/types.ts +++ b/app/lib/websocket/voice/types.ts @@ -1,36 +1,38 @@ export enum ConnectionState { - DISCONNECTED = 'DISCONNECTED', - DISCONNECTING = 'DISCONNECTING', - CONNECTING = 'CONNECTING', - AUTHENTICATING = 'AUTHENTICATING', - CONNECTED = 'CONNECTED', - ERROR = 'ERROR', + DISCONNECTED = "DISCONNECTED", + DISCONNECTING = "DISCONNECTING", + CONNECTING = "CONNECTING", + AUTHENTICATING = "AUTHENTICATING", + CONNECTED = "CONNECTED", + ERROR = "ERROR", } export enum ServerMessageType { - AUTHENTICATE_ACCEPTED = 'AUTHENTICATE_ACCEPTED', - AUTHENTICATE_DENIED = 'AUTHENTICATE_DENIED', - SDP_ANSWER = 'SDP_ANSWER', + AUTHENTICATE_ACCEPTED = "AUTHENTICATE_ACCEPTED", + AUTHENTICATE_DENIED = "AUTHENTICATE_DENIED", + SDP_ANSWER = "SDP_ANSWER", } export type ServerMessage = | { type: ServerMessageType.AUTHENTICATE_ACCEPTED } | { type: ServerMessageType.AUTHENTICATE_DENIED } | { - type: ServerMessageType.SDP_ANSWER; data: { - sdp: RTCSessionDescription - } - } + type: ServerMessageType.SDP_ANSWER; + data: { + sdp: RTCSessionDescription; + }; + }; export enum ClientMessageType { - AUTHENTICATE = 'AUTHENTICATE', - SDP_OFFER = 'SDP_OFFER', + AUTHENTICATE = "AUTHENTICATE", + SDP_OFFER = "SDP_OFFER", } export type ClientMessage = | { type: ClientMessageType.AUTHENTICATE; data: { token: string } } | { - type: ClientMessageType.SDP_OFFER; data: { - sdp: RTCSessionDescription - } - }; \ No newline at end of file + type: ClientMessageType.SDP_OFFER; + data: { + sdp: RTCSessionDescription; + }; + }; diff --git a/app/root.tsx b/app/root.tsx index 27dfd5a..bfa4bf0 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,10 +1,10 @@ import { - isRouteErrorResponse, - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, } from "react-router"; import { ThemeProvider } from "~/components/theme/theme-provider"; @@ -13,69 +13,72 @@ import type { Route } from "./+types/root"; import "./app.css"; export const links: Route.LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, ]; export default function App() { - return ( - - - - ); + return ( + + + + ); } export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); + return ( + + + + + + + + + {children} + + + + + ); } export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - let message = "Oops!"; - let details = "An unexpected error occurred."; - let stack: string | undefined; + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; - if (isRouteErrorResponse(error)) { - message = error.status === 404 ? "404" : "Error"; - details = - error.status === 404 - ? "The requested page could not be found." - : error.statusText || details; - } else if (import.meta.env.DEV && error && error instanceof Error) { - details = error.message; - stack = error.stack; - } + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } - return ( - - {message} - {details} - {stack && ( - - {stack} - - )} - - ); + return ( + + {message} + {details} + {stack && ( + + {stack} + + )} + + ); } diff --git a/app/routes.ts b/app/routes.ts index fc6db3f..01ef0eb 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,4 +1,10 @@ -import { type RouteConfig, index, layout, prefix, route } from "@react-router/dev/routes"; +import { + type RouteConfig, + index, + layout, + prefix, + route, +} from "@react-router/dev/routes"; export default [ index("routes/index.tsx"), @@ -15,15 +21,19 @@ export default [ ...prefix("/@me", [ layout("routes/app/me/layout.tsx", [ index("routes/app/me/index.tsx"), - route("/channels/:channelId", "routes/app/me/channel.tsx"), - ]) + route( + "/channels/:channelId", + "routes/app/me/channel.tsx", + ), + ]), ]), ...prefix("/server/:serverId", [ layout("routes/app/server/layout.tsx", [ index("routes/app/server/index.tsx"), route("/:channelId", "routes/app/server/channel.tsx"), - ]) - ]) - ])]), + ]), + ]), + ]), + ]), ]), ] satisfies RouteConfig; diff --git a/app/routes/app/index.tsx b/app/routes/app/index.tsx index 06260be..e62bf8f 100644 --- a/app/routes/app/index.tsx +++ b/app/routes/app/index.tsx @@ -1,7 +1,7 @@ import { redirect } from "react-router"; export function clientLoader() { - return redirect("/app/@me") + return redirect("/app/@me"); } export default function Index() { diff --git a/app/routes/app/invite.tsx b/app/routes/app/invite.tsx index 247f02e..d206a65 100644 --- a/app/routes/app/invite.tsx +++ b/app/routes/app/invite.tsx @@ -1,17 +1,17 @@ import { redirect } from "react-router"; import type { Route } from "./+types/invite"; -export async function clientLoader( - { params }: Route.ClientLoaderArgs -) { - const inviteCode = params.inviteCode +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const inviteCode = params.inviteCode; try { - const response = await import("~/lib/api/client/server").then(m => m.default.getInvite(inviteCode)) + const response = await import("~/lib/api/client/server").then((m) => + m.default.getInvite(inviteCode), + ); - return redirect(`/app/server/${response.id}`) + return redirect(`/app/server/${response.id}`); } catch (error) { - return redirect("/app/@me") + return redirect("/app/@me"); } } diff --git a/app/routes/app/layout.tsx b/app/routes/app/layout.tsx index 47fb4ac..a452181 100644 --- a/app/routes/app/layout.tsx +++ b/app/routes/app/layout.tsx @@ -1,26 +1,46 @@ -import { Outlet, redirect } from "react-router"; +import { useQuery } from "@tanstack/react-query"; +import { Outlet } from "react-router"; import AppLayout from "~/components/app-layout"; import { useServerListStore } from "~/stores/server-list-store"; +import { useUsersStore } from "~/stores/users-store"; -export async function clientLoader() { - const { servers, addServers } = useServerListStore.getState() +async function fetchServers() { + const { addServers } = useServerListStore.getState(); - try { - if (!servers || Object.values(servers).length === 0) { - const newServers = await import("~/lib/api/client/server").then(m => m.default.list()) - addServers(newServers) - } + const newServers = await import("~/lib/api/client/server").then((m) => + m.default.list(), + ); + addServers(newServers); - } catch (error) { - return redirect("/login") - } + return null; +} + +async function fetchCurrentUser() { + const { setCurrentUserId, addUser } = useUsersStore.getState(); + + const user = await import("~/lib/api/client/user").then((m) => + m.default.me(), + ); + setCurrentUserId(user.id); + addUser(user); + + return null; } export default function Layout() { + useQuery({ + queryKey: ["servers"], + queryFn: fetchServers, + }); + + useQuery({ + queryKey: ["users", "@me"], + queryFn: fetchCurrentUser, + }); return ( - + ); -} \ No newline at end of file +} diff --git a/app/routes/app/me/channel.tsx b/app/routes/app/me/channel.tsx index 09b7c12..21029ef 100644 --- a/app/routes/app/me/channel.tsx +++ b/app/routes/app/me/channel.tsx @@ -6,32 +6,52 @@ import { Badge } from "~/components/ui/badge"; import { usePrivateChannelsStore } from "~/stores/private-channels-store"; import { useUsersStore } from "~/stores/users-store"; -export default function Channel({ - params -}: Route.ComponentProps) { - const channelId = params.channelId - const currentUserId = useUsersStore(state => state.currentUserId) +export default function Channel({ params }: Route.ComponentProps) { + const channelId = params.channelId; + const currentUserId = useUsersStore((state) => state.currentUserId); - const nativeChannel = usePrivateChannelsStore(useShallow(state => state.channels[channelId])) - const recipients = nativeChannel.recipients.filter(recipient => recipient.id !== currentUserId) + const nativeChannel = usePrivateChannelsStore( + useShallow((state) => state.channels[channelId]), + ); - const renderSystemBadge = recipients.some(recipient => recipient.system) && recipients.length === 1 + if (!nativeChannel) return null; + + const recipients = nativeChannel.recipients.filter( + (recipient) => recipient.id !== currentUserId, + ); + + const renderSystemBadge = + recipients.some((recipient) => recipient.system) && + recipients.length === 1; const channel = { ...nativeChannel, - name: <> - - - {recipients.map(recipient => recipient.displayName || recipient.username).join(", ")} + name: ( + <> + + + {recipients + .map( + (recipient) => + recipient.displayName || recipient.username, + ) + .join(", ")} + + {renderSystemBadge && ( + + {" "} + + System + + )} - {renderSystemBadge && System} - - > - } + > + ), + }; return ( <> > ); -} \ No newline at end of file +} diff --git a/app/routes/app/me/index.tsx b/app/routes/app/me/index.tsx index 7272bff..df54e83 100644 --- a/app/routes/app/me/index.tsx +++ b/app/routes/app/me/index.tsx @@ -1,4 +1,3 @@ - export default function Index() { return ( <> diff --git a/app/routes/app/me/layout.tsx b/app/routes/app/me/layout.tsx index a7b21c3..dc8a5b7 100644 --- a/app/routes/app/me/layout.tsx +++ b/app/routes/app/me/layout.tsx @@ -1,3 +1,4 @@ +import { useQuery } from "@tanstack/react-query"; import React from "react"; import { Outlet } from "react-router"; import { useShallow } from "zustand/react/shallow"; @@ -5,19 +6,33 @@ import PrivateChannelListItem from "~/components/custom-ui/private-channel-list- import { ScrollArea } from "~/components/ui/scroll-area"; import { usePrivateChannelsStore } from "~/stores/private-channels-store"; -export async function clientLoader() { - const { channels, addChannels: setChannels } = usePrivateChannelsStore.getState() +async function fetchPrivateChannels() { + const { addChannels } = usePrivateChannelsStore.getState(); - const channelList = Object.values(channels) + const channels = await import("~/lib/api/client/user").then((m) => + m.default.channels(), + ); + addChannels(channels); - if (!channels || channelList.length === 0) { - const channels = await import("~/lib/api/client/user").then(m => m.default.channels()) - setChannels(channels) - } + return null; } function ListComponent() { - const channels = usePrivateChannelsStore(useShallow(state => Object.values(state.channels))) + const channels = Object.values( + usePrivateChannelsStore(useShallow((state) => state.channels)), + ); + const sortedChannels = React.useMemo( + () => + channels.sort((a, b) => + (a.lastMessageId ?? a.id) < (b.lastMessageId ?? b.id) ? 1 : -1, + ), + [channels], + ); + + useQuery({ + queryKey: ["channels", "@me"], + queryFn: fetchPrivateChannels, + }); return ( @@ -29,7 +44,7 @@ function ListComponent() { - {channels.sort((a, b) => (a.lastMessageId ?? a.id) < (b.lastMessageId ?? b.id) ? 1 : -1).map((channel, _) => ( + {sortedChannels.map((channel) => ( @@ -37,12 +52,12 @@ function ListComponent() { - ) + ); } export const handle = { - listComponent: -} + listComponent: , +}; export default function Layout() { return ( diff --git a/app/routes/app/providers.tsx b/app/routes/app/providers.tsx index 3e9e3ab..e7285e4 100644 --- a/app/routes/app/providers.tsx +++ b/app/routes/app/providers.tsx @@ -1,30 +1,22 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Outlet, redirect } from "react-router"; -import { create } from "zustand"; +import { Outlet } from "react-router"; import { GatewayWebSocketConnectionManager } from "~/components/manager/gateway-websocket-connection-manager"; import { WebRTCConnectionManager } from "~/components/manager/webrtc-connection-manager"; import ModalProvider from "~/components/providers/modal-provider"; -import { useUsersStore } from "~/stores/users-store"; -export async function clientLoader() { - const { currentUserId, setCurrentUserId, addUser } = useUsersStore.getState() - - try { - if (!currentUserId) { - const user = await import("~/lib/api/client/user").then(m => m.default.me()) - setCurrentUserId(user.id) - addUser(user) - } - } catch (error) { - return redirect("/login") - } -} - -const useQueryClient = create(() => new QueryClient()); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + staleTime: Infinity, + refetchInterval: Infinity, + refetchOnReconnect: false, + refetchOnMount: false, + }, + }, +}); export default function Layout() { - const queryClient = useQueryClient(); - return ( <> @@ -35,4 +27,4 @@ export default function Layout() { > ); -} \ No newline at end of file +} diff --git a/app/routes/app/server/channel.tsx b/app/routes/app/server/channel.tsx index ae02632..629689a 100644 --- a/app/routes/app/server/channel.tsx +++ b/app/routes/app/server/channel.tsx @@ -1,16 +1,25 @@ +import { useNavigate } from "react-router"; import { useShallow } from "zustand/react/shallow"; import ChannelArea from "~/components/channel-area"; import { useServerChannelsStore } from "~/stores/server-channels-store"; import type { Route } from "./+types/channel"; -export default function Channel( - { params: { serverId, channelId } }: Route.ComponentProps -) { - const channel = useServerChannelsStore(useShallow(state => state.channels[serverId][channelId])) +export default function Channel({ + params: { serverId, channelId }, +}: Route.ComponentProps) { + const navigate = useNavigate(); + const channel = useServerChannelsStore( + useShallow((state) => state.channels[serverId][channelId]), + ); + + if (!channel) { + setTimeout(() => navigate(`/app/server/${serverId}`), 0); + return null; + } return ( <> > ); -} \ No newline at end of file +} diff --git a/app/routes/app/server/index.tsx b/app/routes/app/server/index.tsx index 7272bff..df54e83 100644 --- a/app/routes/app/server/index.tsx +++ b/app/routes/app/server/index.tsx @@ -1,4 +1,3 @@ - export default function Index() { return ( <> diff --git a/app/routes/app/server/layout.tsx b/app/routes/app/server/layout.tsx index 2f449b0..d0982a7 100644 --- a/app/routes/app/server/layout.tsx +++ b/app/routes/app/server/layout.tsx @@ -1,9 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; import React from "react"; -import { Outlet, redirect, useNavigate, useParams, type ShouldRevalidateFunctionArgs } from "react-router"; +import { Outlet, useNavigate, useParams } from "react-router"; import { useShallow } from "zustand/react/shallow"; import ServerChannelListItem from "~/components/custom-ui/channel-list-item"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "~/components/ui/dropdown-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; import { ScrollArea } from "~/components/ui/scroll-area"; +import { listChannels } from "~/lib/api/client/server"; import type { ServerId } from "~/lib/api/types"; import { useGatewayStore } from "~/stores/gateway-store"; import { ModalType, useModalStore } from "~/stores/modal-store"; @@ -12,45 +20,42 @@ import { useServerListStore } from "~/stores/server-list-store"; import { useUsersStore } from "~/stores/users-store"; import type { Route } from "../server/+types/layout"; -export async function clientLoader({ - params: { serverId } -}: Route.ClientLoaderArgs) { - const { channels, addChannels, addServer } = useServerChannelsStore.getState() +async function fetchServerChannels(serverId: ServerId) { + const { addChannels } = useServerChannelsStore.getState(); - const server = useServerListStore.getState().servers[serverId as ServerId] || undefined + const channels = await listChannels(serverId); + addChannels(channels); - if (!server) { - return redirect("/app/@me") - } + useGatewayStore.getState().requestVoiceStates(serverId as ServerId); - const channelList = channels[serverId as ServerId] - - if (channelList === undefined) { - const channels = await import("~/lib/api/client/server").then(m => m.default.listChannels(serverId as ServerId)) - addServer(serverId as ServerId) - addChannels(channels) - - useGatewayStore.getState().requestVoiceStates(serverId as ServerId) - } -} - -export function shouldRevalidate( - arg: ShouldRevalidateFunctionArgs -) { - return true + return null; } function ListComponent() { - const serverId = useParams<{ serverId: ServerId }>().serverId! - const currentUserId = useUsersStore(state => state.currentUserId) - const onOpen = useModalStore(state => state.onOpen) + const serverId = useParams<{ serverId: ServerId }>().serverId!; - const server = useServerListStore(useShallow(state => state.servers[serverId] || null)) + useQuery({ + queryKey: ["channels", serverId], + queryFn: async () => fetchServerChannels(serverId), + }); - const channels = Array.from(useServerChannelsStore(useShallow(state => Object.values(state.channels[serverId] || {})))) + const currentUserId = useUsersStore((state) => state.currentUserId); + const onOpen = useModalStore((state) => state.onOpen); + + const server = useServerListStore( + useShallow((state) => state.servers[serverId] || null), + ); + + const channels = Array.from( + Object.values( + useServerChannelsStore( + useShallow((state) => state.channels[serverId] || {}), + ), + ), + ); if (!server) { - return null + return null; } return ( @@ -63,42 +68,78 @@ function ListComponent() { - onOpen(ModalType.CREATE_SERVER_INVITE, { serverId })}>Create invite - onOpen(ModalType.CREATE_SERVER_CHANNEL, { serverId })}>Create channel + + onOpen(ModalType.CREATE_SERVER_INVITE, { + serverId, + }) + } + > + Create invite + + + onOpen(ModalType.CREATE_SERVER_CHANNEL, { + serverId, + }) + } + > + Create channel + - {currentUserId === server.ownerId && - onOpen(ModalType.DELETE_SERVER_CONFIRM, { serverId })}>Delete} - {currentUserId !== server.ownerId && - Leave} + {currentUserId === server.ownerId && ( + + onOpen(ModalType.DELETE_SERVER_CONFIRM, { + serverId, + }) + } + > + Delete + + )} + {currentUserId !== server.ownerId && ( + + Leave + + )} - {channels.sort((a, b) => (a.lastMessageId ?? a.id) < (b.lastMessageId ?? b.id) ? 1 : -1).map((channel, _) => ( - - - - ))} + {channels + .sort((a, b) => + (a.lastMessageId ?? a.id) < + (b.lastMessageId ?? b.id) + ? 1 + : -1, + ) + .map((channel, _) => ( + + + + ))} - ) + ); } export const handle = { - listComponent: -} + listComponent: , +}; -export default function Layout( - { params: { serverId } }: Route.ComponentProps -) { - const server = useServerListStore(useShallow(state => state.servers[serverId!] || null)) - const navigate = useNavigate() +export default function Layout({ params: { serverId } }: Route.ComponentProps) { + const server = useServerListStore( + useShallow((state) => state.servers[serverId!] || null), + ); + const navigate = useNavigate(); if (!server) { - setTimeout(() => navigate("/app/@me"), 0) + setTimeout(() => navigate("/app/@me"), 0); return null; } diff --git a/app/routes/app/settings.tsx b/app/routes/app/settings.tsx index 79a1763..1bfbf84 100644 --- a/app/routes/app/settings.tsx +++ b/app/routes/app/settings.tsx @@ -1,12 +1,72 @@ -import { LogOutIcon, PencilIcon, UserIcon } from "lucide-react"; -import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { LogOutIcon, UserIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; import { Button } from "~/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form"; +import { IconUploadField } from "~/components/ui/icon-upload-field"; import { Input } from "~/components/ui/input"; +import file from "~/lib/api/client/file"; +import { patchUser } from "~/lib/api/client/user"; +import { useTokenStore } from "~/stores/token-store"; import { useUsersStore } from "~/stores/users-store"; +const schema = z.object({ + displayName: z.string().min(1).max(32).optional().nullable(), + avatar: z.instanceof(File).optional().nullable(), +}); + // Note: This is a mockup based on the provided store structure export default function Settings() { - const user = useUsersStore(state => state.getCurrentUser()); + const setToken = useTokenStore((state) => state.setToken); + const user = useUsersStore((state) => state.getCurrentUser()); + + const form = useForm>({ + resolver: zodResolver(schema), + defaultValues: { + displayName: user?.displayName, + avatar: undefined, + }, + }); + + const onSubmit = async (values: z.infer) => { + if (!values) return; + + let avatarId: string | null | undefined = + values.avatar === null ? null : undefined; + if (values.avatar) { + avatarId = (await file.uploadFile(values.avatar))[0]; + } + + const response = await patchUser({ + displayName: + values.displayName === user?.displayName + ? undefined + : values.displayName === "" + ? null + : values.displayName, + avatarId, + }); + + form.control._defaultValues = { + displayName: user?.displayName, + avatar: undefined, + }; + + form.reset(); + }; + + const onLogout = () => { + setToken(undefined); + window.location.reload(); + }; return ( @@ -20,7 +80,11 @@ export default function Settings() { - + Logout @@ -30,74 +94,72 @@ export default function Settings() { {/* Main content */} - - - - - - - - - - - Change - - - - - - - - Username - - - - - - - Display name - - - - - - - Email - - - - - - - Password - - - - - - Save + + + ( + + + Avatar + + + + + + + + + )} + /> + ( + + + Display Name + + + + + + + )} + /> + + {form.formState.isSubmitting + ? "Saving..." + : "Save"} - - + + ); -} \ No newline at end of file +} diff --git a/app/routes/auth/login.tsx b/app/routes/auth/login.tsx index 1dc582f..53b3cea 100644 --- a/app/routes/auth/login.tsx +++ b/app/routes/auth/login.tsx @@ -7,7 +7,14 @@ import { useShallow } from "zustand/react/shallow"; import { PasswordInput } from "~/components/custom-ui/password-input"; import { ThemeToggle } from "~/components/theme/theme-toggle"; import { Button, buttonVariants } from "~/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; import { Form, FormControl, @@ -27,64 +34,84 @@ const schema = z.object({ }); export async function clientLoader() { - const { token, setToken } = useTokenStore.getState() + const { token, setToken } = useTokenStore.getState(); if (token) { try { - await import("~/lib/api/client/user").then(m => m.default.me()) + await import("~/lib/api/client/user").then((m) => m.default.me()); - return redirect("/app/@me") + return redirect("/app/@me"); } catch (error) { - const axiosError = error as AxiosError + const axiosError = error as AxiosError; if (axiosError.status === 401) { - setToken(undefined) + setToken(undefined); } } } } export default function Login() { - let navigate = useNavigate() - let setToken = useTokenStore(state => state.setToken) - const { setCurrentUserId, addUser } = useUsersStore(useShallow(state => { - return { - setCurrentUserId: state.setCurrentUserId, - addUser: state.addUser - } - })) + let navigate = useNavigate(); + let setToken = useTokenStore((state) => state.setToken); + const { setCurrentUserId, addUser } = useUsersStore( + useShallow((state) => { + return { + setCurrentUserId: state.setCurrentUserId, + addUser: state.addUser, + }; + }), + ); const form = useForm>({ resolver: zodResolver(schema), - }) + }); async function onSubmit(values: z.infer) { - const response = await auth.login(values) + const response = await auth.login(values); - setToken(response.token) - setCurrentUserId(response.user.id) - addUser(response.user) + setToken(response.token); + setCurrentUserId(response.user.id); + addUser(response.user); - navigate("/app") + navigate("/app"); } return ( - + Welcome back! Please sign in to continue. - + - + - + ( - + Username @@ -97,7 +124,12 @@ export default function Login() { control={form.control} name="password" render={({ field }) => ( - + Password @@ -106,16 +138,33 @@ export default function Login() { )} /> - Log In + + Log In + - Don't have an account? - Register + + Don't have an account? + + + Register + ); -} \ No newline at end of file +} diff --git a/app/routes/auth/register.tsx b/app/routes/auth/register.tsx index 370cb27..169da3d 100644 --- a/app/routes/auth/register.tsx +++ b/app/routes/auth/register.tsx @@ -5,7 +5,14 @@ import { z } from "zod"; import { PasswordInput } from "~/components/custom-ui/password-input"; import { ThemeToggle } from "~/components/theme/theme-toggle"; import { Button, buttonVariants } from "~/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; import { Form, FormControl, @@ -20,43 +27,70 @@ import auth from "~/lib/api/client/auth"; const schema = z.object({ email: z.string().email(), displayName: z.string().min(1).optional(), - username: z.string().min(3).regex(/^[a-zA-Z0-9_.]+$/), + username: z + .string() + .min(3) + .regex(/^[a-zA-Z0-9_.]+$/), password: z.string().min(8), }); export default function Register() { - let navigate = useNavigate() - + let navigate = useNavigate(); + const form = useForm>({ resolver: zodResolver(schema), - }) + }); async function onSubmit(values: z.infer) { - await auth.register(values) + await auth.register(values); - navigate("/login") + navigate("/login"); } return ( - + Create an account - Please fill out the form below to create an account. - + + Please fill out the form below to create an account. + + - + - + ( - + Email - + @@ -92,7 +126,12 @@ export default function Register() { control={form.control} name="password" render={({ field }) => ( - + Password @@ -101,16 +140,33 @@ export default function Register() { )} /> - Register + + Register + - Already have an account? - Log In + + Already have an account? + + + Log In + ); -} \ No newline at end of file +} diff --git a/app/routes/index.tsx b/app/routes/index.tsx index 3862077..26bcff7 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -1,16 +1,14 @@ import { redirect } from "react-router"; import type { Route } from "./+types/index"; -export function meta({ }: Route.MetaArgs) { - return [ - { title: "New React Router App" }, - ]; +export function meta({}: Route.MetaArgs) { + return [{ title: "New React Router App" }]; } export function clientLoader() { - return redirect("/login"); + return redirect("/login"); } export default function Index() { - return <>>; + return <>>; } diff --git a/app/stores/channels-voice-state.tsx b/app/stores/channels-voice-state.tsx index e1a643b..7bcbde1 100644 --- a/app/stores/channels-voice-state.tsx +++ b/app/stores/channels-voice-state.tsx @@ -13,32 +13,37 @@ interface ChannelVoiceState { interface ChannelsVoiceState { channels: Record; - addUser: (channelId: ChannelId, userId: UserId, userVoiceState: UserVoiceState) => void; + addUser: ( + channelId: ChannelId, + userId: UserId, + userVoiceState: UserVoiceState, + ) => void; removeUser: (channelId: ChannelId, userId: UserId) => void; removeChannel: (channelId: ChannelId) => void; } export const useChannelsVoiceStateStore = create()( - immer( - (set, get) => ({ - channels: {}, - addUser: (channelId, userId, userVoiceState) => set((state) => { + immer((set, get) => ({ + channels: {}, + addUser: (channelId, userId, userVoiceState) => + set((state) => { if (!state.channels[channelId]) { state.channels[channelId] = { - users: {} - } + users: {}, + }; } state.channels[channelId].users[userId] = userVoiceState; }), - removeUser: (channelId, userId) => set((state) => { + removeUser: (channelId, userId) => + set((state) => { if (state.channels[channelId]) { delete state.channels[channelId].users[userId]; } }), - removeChannel: (channelId) => set((state) => { + removeChannel: (channelId) => + set((state) => { delete state.channels[channelId]; - }) - }) - ) -) + }), + })), +); diff --git a/app/stores/gateway-store.ts b/app/stores/gateway-store.ts index 70063cb..ea01342 100644 --- a/app/stores/gateway-store.ts +++ b/app/stores/gateway-store.ts @@ -1,102 +1,187 @@ -import type { QueryClient } from '@tanstack/react-query'; -import { create } from 'zustand'; -import { messageSchema, type ChannelId, type Message, type MessageId, type ServerId } from '~/lib/api/types'; -import { GatewayClient } from '~/lib/websocket/gateway/client'; +import type { QueryClient } from "@tanstack/react-query"; +import { create } from "zustand"; +import { + messageSchema, + type ChannelId, + type Message, + type MessageId, + type ServerId, +} from "~/lib/api/types"; +import { GatewayClient } from "~/lib/websocket/gateway/client"; import { ConnectionState, EventType, type EventData, - type VoiceServerUpdateEvent -} from '~/lib/websocket/gateway/types'; -import { useChannelsVoiceStateStore } from './channels-voice-state'; -import { usePrivateChannelsStore } from './private-channels-store'; -import { useServerChannelsStore } from './server-channels-store'; -import { useServerListStore } from './server-list-store'; -import { useUsersStore } from './users-store'; + type VoiceServerUpdateEvent, +} from "~/lib/websocket/gateway/types"; +import { useChannelsVoiceStateStore } from "./channels-voice-state"; +import { usePrivateChannelsStore } from "./private-channels-store"; +import { useServerChannelsStore } from "./server-channels-store"; +import { useServerListStore } from "./server-list-store"; +import { useUsersStore } from "./users-store"; -const GATEWAY_URL = 'ws://localhost:12345/gateway/ws'; +const GATEWAY_URL = "ws://localhost:12345/gateway/ws"; const HANDLERS = { - [EventType.ADD_SERVER]: (self: GatewayState, data: Extract['data']) => { + [EventType.ADD_SERVER]: ( + self: GatewayState, + data: Extract["data"], + ) => { useServerListStore.getState().addServer(data.server); }, - [EventType.REMOVE_SERVER]: (self: GatewayState, data: Extract['data']) => { + [EventType.REMOVE_SERVER]: ( + self: GatewayState, + data: Extract["data"], + ) => { useServerListStore.getState().removeServer(data.serverId); useServerChannelsStore.getState().removeServer(data.serverId); useChannelsVoiceStateStore.getState().removeChannel(data.serverId); }, - [EventType.ADD_DM_CHANNEL]: (self: GatewayState, data: Extract['data']) => { - usePrivateChannelsStore.getState().addChannel(data.channel); + [EventType.ADD_DM_CHANNEL]: ( + self: GatewayState, + data: Extract["data"], + ) => { + usePrivateChannelsStore.getState().addChannel({ + ...data.channel, + recipients: data.recipients, + }); }, - [EventType.REMOVE_DM_CHANNEL]: (self: GatewayState, data: Extract['data']) => { + [EventType.REMOVE_DM_CHANNEL]: ( + self: GatewayState, + data: Extract["data"], + ) => { usePrivateChannelsStore.getState().removeChannel(data.channelId); useChannelsVoiceStateStore.getState().removeChannel(data.channelId); }, - [EventType.ADD_SERVER_CHANNEL]: (self: GatewayState, data: Extract['data']) => { + [EventType.ADD_SERVER_CHANNEL]: ( + self: GatewayState, + data: Extract< + EventData, + { type: EventType.ADD_SERVER_CHANNEL } + >["data"], + ) => { useServerChannelsStore.getState().addChannel(data.channel); }, - [EventType.REMOVE_SERVER_CHANNEL]: (self: GatewayState, data: Extract['data']) => { - useServerChannelsStore.getState().removeChannel(data.serverId, data.channelId); + [EventType.REMOVE_SERVER_CHANNEL]: ( + self: GatewayState, + data: Extract< + EventData, + { type: EventType.REMOVE_SERVER_CHANNEL } + >["data"], + ) => { + useServerChannelsStore + .getState() + .removeChannel(data.serverId, data.channelId); useChannelsVoiceStateStore.getState().removeChannel(data.serverId); }, - [EventType.ADD_USER]: (self: GatewayState, data: Extract['data']) => { + [EventType.ADD_USER]: ( + self: GatewayState, + data: Extract["data"], + ) => { useUsersStore.getState().addUser(data.user); }, - [EventType.REMOVE_USER]: (self: GatewayState, data: Extract['data']) => { + [EventType.REMOVE_USER]: ( + self: GatewayState, + data: Extract["data"], + ) => { useUsersStore.getState().removeUser(data.userId); }, - [EventType.ADD_SERVER_MEMBER]: (self: GatewayState, data: Extract['data']) => { + [EventType.ADD_SERVER_MEMBER]: ( + self: GatewayState, + data: Extract["data"], + ) => { useUsersStore.getState().addUser(data.user); }, - [EventType.REMOVE_SERVER_MEMBER]: (self: GatewayState, data: Extract['data']) => { + [EventType.REMOVE_SERVER_MEMBER]: ( + self: GatewayState, + data: Extract< + EventData, + { type: EventType.REMOVE_SERVER_MEMBER } + >["data"], + ) => { useUsersStore.getState().removeUser(data.userId); }, - [EventType.ADD_MESSAGE]: (self: GatewayState, data: Extract['data']) => { - const message = messageSchema.parse(data.message) + [EventType.ADD_MESSAGE]: ( + self: GatewayState, + data: Extract["data"], + ) => { + const message = messageSchema.parse(data.message); if (self.queryClient) { - self.queryClient.setQueryData(['messages', message.channelId], (oldData: { - pages: Message[][], - pageParams: MessageId[] - }) => { - return { - pages: oldData?.pages ? [[message, ...oldData.pages[0]], ...oldData.pages.slice(1)] : [[message]], - pageParams: oldData?.pageParams ?? [undefined, message.id] - } - }); + self.queryClient.setQueryData( + ["messages", message.channelId], + (oldData: { pages: Message[][]; pageParams: MessageId[] }) => { + return { + pages: oldData?.pages + ? [ + [message, ...oldData.pages[0]], + ...oldData.pages.slice(1), + ] + : [[message]], + pageParams: oldData?.pageParams ?? [ + undefined, + message.id, + ], + }; + }, + ); } }, - [EventType.REMOVE_MESSAGE]: (self: GatewayState, data: Extract['data']) => { + [EventType.REMOVE_MESSAGE]: ( + self: GatewayState, + data: Extract["data"], + ) => { if (self.queryClient) { - self.queryClient.setQueryData(['messages', data.channelId], (oldData: any) => { - if (!oldData) return []; - return oldData.filter((message: any) => message.id !== data.messageId); - }); + self.queryClient.setQueryData( + ["messages", data.channelId], + (oldData: any) => { + if (!oldData) return []; + return oldData.filter( + (message: any) => message.id !== data.messageId, + ); + }, + ); } }, - [EventType.VOICE_CHANNEL_CONNECTED]: (self: GatewayState, data: Extract['data']) => { - useChannelsVoiceStateStore.getState().addUser(data.channelId, data.userId, { - deaf: false, - muted: false - }); + [EventType.VOICE_CHANNEL_CONNECTED]: ( + self: GatewayState, + data: Extract< + EventData, + { type: EventType.VOICE_CHANNEL_CONNECTED } + >["data"], + ) => { + useChannelsVoiceStateStore + .getState() + .addUser(data.channelId, data.userId, { + deaf: false, + muted: false, + }); }, - [EventType.VOICE_CHANNEL_DISCONNECTED]: (self: GatewayState, data: Extract['data']) => { - useChannelsVoiceStateStore.getState().removeUser(data.channelId, data.userId); + [EventType.VOICE_CHANNEL_DISCONNECTED]: ( + self: GatewayState, + data: Extract< + EventData, + { type: EventType.VOICE_CHANNEL_DISCONNECTED } + >["data"], + ) => { + useChannelsVoiceStateStore + .getState() + .removeUser(data.channelId, data.userId); }, -} +}; interface GatewayState { client: GatewayClient | null; @@ -110,16 +195,22 @@ interface GatewayState { updateVoiceState: (serverId: ServerId, channelId: ChannelId) => void; requestVoiceStates: (serverId: ServerId) => void; - onVoiceServerUpdate: (handler: (event: VoiceServerUpdateEvent['data']) => void | Promise) => (() => void); + onVoiceServerUpdate: ( + handler: ( + event: VoiceServerUpdateEvent["data"], + ) => void | Promise, + ) => () => void; } export const useGatewayStore = create()((set, get) => { const client = new GatewayClient(GATEWAY_URL); - const voiceHandlers = new Set<(event: VoiceServerUpdateEvent['data']) => void>(); + const voiceHandlers = new Set< + (event: VoiceServerUpdateEvent["data"]) => void + >(); client.onEvent(EventType.VOICE_SERVER_UPDATE, (event) => { - voiceHandlers.forEach(handler => handler(event)); + voiceHandlers.forEach((handler) => handler(event)); }); for (const [type, handler] of Object.entries(HANDLERS)) { @@ -137,7 +228,7 @@ export const useGatewayStore = create()((set, get) => { client.connect(token); set({ status: client.connectionState }); - client.onControl('stateChange', (state) => { + client.onControl("stateChange", (state) => { set({ status: state }); }); }, @@ -163,9 +254,8 @@ export const useGatewayStore = create()((set, get) => { voiceHandlers.add(handler); return () => { - console.log("removing voice server update handler", handler); voiceHandlers.delete(handler); }; - } + }, }; -}); \ No newline at end of file +}); diff --git a/app/stores/modal-store.ts b/app/stores/modal-store.ts index 9e07a6d..6221d7d 100644 --- a/app/stores/modal-store.ts +++ b/app/stores/modal-store.ts @@ -20,23 +20,26 @@ export type DeleteServerConfirmModalData = { type: ModalType.CREATE_SERVER_CHANNEL; data: { serverId: ServerId; - } + }; }; export type CreateServerChannelModalData = { type: ModalType.CREATE_SERVER_CHANNEL; data: { serverId: ServerId; - } + }; }; -export type ModalData = CreateServerChannelModalData | CreateServerInviteModalData | DeleteServerConfirmModalData; +export type ModalData = + | CreateServerChannelModalData + | CreateServerInviteModalData + | DeleteServerConfirmModalData; interface ModalState { type: ModalType | null; - data?: ModalData['data']; + data?: ModalData["data"]; isOpen: boolean; - onOpen: (type: ModalType, data?: ModalData['data']) => void; + onOpen: (type: ModalType, data?: ModalData["data"]) => void; onClose: () => void; } @@ -46,4 +49,4 @@ export const useModalStore = create()((set) => ({ isOpen: false, onOpen: (type, data) => set({ type, data, isOpen: true }), onClose: () => set({ type: null, isOpen: false }), -})); \ No newline at end of file +})); diff --git a/app/stores/private-channels-store.ts b/app/stores/private-channels-store.ts index ff81cac..5e268af 100644 --- a/app/stores/private-channels-store.ts +++ b/app/stores/private-channels-store.ts @@ -1,25 +1,33 @@ -import { create } from 'zustand' -import { immer } from 'zustand/middleware/immer' -import type { ChannelId, RecipientChannel } from '~/lib/api/types' +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { ChannelId, RecipientChannel } from "~/lib/api/types"; type PrivateChannelsStore = { - channels: Record - addChannels: (channels: RecipientChannel[]) => void - addChannel: (channel: RecipientChannel) => void - removeChannel: (channelId: ChannelId) => void -} + channels: Record; + addChannels: (channels: RecipientChannel[]) => void; + addChannel: (channel: RecipientChannel) => void; + removeChannel: (channelId: ChannelId) => void; +}; export const usePrivateChannelsStore = create()( - immer( - (set) => ({ - channels: {}, - addChannels: (channels: RecipientChannel[]) => set((state) => { + immer((set) => ({ + channels: {}, + addChannels: (channels: RecipientChannel[]) => + set((state) => { for (const channel of channels) { - state.channels[channel.id] = channel + state.channels[channel.id] = channel; + console.log("add channel", channel); } }), - addChannel: (channel: RecipientChannel) => set((state) => { state.channels[channel.id] = channel }), - removeChannel: (channelId: ChannelId) => set((state) => { delete state.channels[channelId] }), - }) - ) -) \ No newline at end of file + addChannel: (channel: RecipientChannel) => + set((state) => { + state.channels[channel.id] = { + ...channel, + }; + }), + removeChannel: (channelId: ChannelId) => + set((state) => { + delete state.channels[channelId]; + }), + })), +); diff --git a/app/stores/server-channels-store.ts b/app/stores/server-channels-store.ts index 9e11f0a..98894d0 100644 --- a/app/stores/server-channels-store.ts +++ b/app/stores/server-channels-store.ts @@ -1,41 +1,48 @@ -import { create } from 'zustand' -import { immer } from 'zustand/middleware/immer' -import type { ChannelId, ServerChannel, ServerId } from "~/lib/api/types" +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { ChannelId, ServerChannel, ServerId } from "~/lib/api/types"; type ServerChannelsStore = { - channels: Record> - addServer: (serverId: ServerId) => void - addChannel: (channel: ServerChannel) => void - addChannels: (channels: ServerChannel[]) => void - removeChannel: (serverId: ServerId, channelId: ChannelId) => void - removeServer: (serverId: ServerId) => void -} + channels: Record>; + addServer: (serverId: ServerId) => void; + addChannel: (channel: ServerChannel) => void; + addChannels: (channels: ServerChannel[]) => void; + removeChannel: (serverId: ServerId, channelId: ChannelId) => void; + removeServer: (serverId: ServerId) => void; +}; export const useServerChannelsStore = create()( - immer( - (set, get) => ({ - channels: {}, - addServer: (serverId) => set((state) => { - state.channels[serverId] = {} + immer((set, get) => ({ + channels: {}, + addServer: (serverId) => + set((state) => { + state.channels[serverId] = {}; }), - addChannel: (channel) => set((state) => { + addChannel: (channel) => + set((state) => { if (state.channels[channel.serverId] === undefined) { - state.channels[channel.serverId] = {} + state.channels[channel.serverId] = {}; } - state.channels[channel.serverId][channel.id] = channel + state.channels[channel.serverId][channel.id] = channel; }), - addChannels: (channels) => set((state) => { + addChannels: (channels) => + set((state) => { for (const channel of channels) { if (state.channels[channel.serverId] === undefined) { - state.channels[channel.serverId] = {} + state.channels[channel.serverId] = {}; } - state.channels[channel.serverId][channel.id] = channel + state.channels[channel.serverId][channel.id] = channel; } }), - removeChannel: (serverId, channelId) => set((state) => { delete state.channels[serverId][channelId] }), - removeServer: (serverId) => set((state) => { delete state.channels[serverId] }), - }) - ) -) \ No newline at end of file + removeChannel: (serverId, channelId) => + set((state) => { + delete state.channels[serverId][channelId]; + }), + removeServer: (serverId) => + set((state) => { + delete state.channels[serverId]; + }), + })), +); diff --git a/app/stores/server-list-store.ts b/app/stores/server-list-store.ts index 883396e..a940918 100644 --- a/app/stores/server-list-store.ts +++ b/app/stores/server-list-store.ts @@ -1,25 +1,30 @@ -import { create } from 'zustand' -import { immer } from 'zustand/middleware/immer' -import type { Server, ServerId, Uuid } from '~/lib/api/types' +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import type { Server, ServerId, Uuid } from "~/lib/api/types"; type ServerListStore = { - servers: Record - addServers: (newServers: Server[]) => void - addServer: (server: Server) => void - removeServer: (serverId: Uuid) => void -} + servers: Record; + addServers: (newServers: Server[]) => void; + addServer: (server: Server) => void; + removeServer: (serverId: Uuid) => void; +}; export const useServerListStore = create()( - immer( - (set) => ({ - servers: {}, - addServers: (servers: Server[]) => set((state) => { + immer((set) => ({ + servers: {}, + addServers: (servers: Server[]) => + set((state) => { for (const server of servers) { - state.servers[server.id] = server + state.servers[server.id] = server; } }), - addServer: (server: Server) => set((state) => { state.servers[server.id] = server }), - removeServer: (serverId: Uuid) => set((state) => { delete state.servers[serverId] }), - }) - ) -) \ No newline at end of file + addServer: (server: Server) => + set((state) => { + state.servers[server.id] = server; + }), + removeServer: (serverId: Uuid) => + set((state) => { + delete state.servers[serverId]; + }), + })), +); diff --git a/app/stores/token-store.ts b/app/stores/token-store.ts index 894548f..4d929cf 100644 --- a/app/stores/token-store.ts +++ b/app/stores/token-store.ts @@ -1,11 +1,11 @@ -import { create } from 'zustand' -import { persist } from 'zustand/middleware' +import { create } from "zustand"; +import { persist } from "zustand/middleware"; type TokenStore = { - token?: string - setToken: (token?: string) => void - removeToken: () => void -} + token?: string; + setToken: (token?: string) => void; + removeToken: () => void; +}; export const useTokenStore = create()( persist( @@ -15,7 +15,7 @@ export const useTokenStore = create()( removeToken: () => set({ token: undefined }), }), { - name: 'token', + name: "token", }, ), -) \ No newline at end of file +); diff --git a/app/stores/users-store.tsx b/app/stores/users-store.tsx index c1002db..a427147 100644 --- a/app/stores/users-store.tsx +++ b/app/stores/users-store.tsx @@ -1,92 +1,78 @@ -import { useQuery } from "@tanstack/react-query" -import { create as batshitCreate, keyResolver } from "@yornaath/batshit" -import { create } from "zustand" -import { immer } from "zustand/middleware/immer" -import { getUser } from "~/lib/api/client/user" -import type { FullUser, PartialUser, UserId } from "~/lib/api/types" +import { create as batshitCreate, keyResolver } from "@yornaath/batshit"; +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { getUser } from "~/lib/api/client/user"; +import type { FullUser, PartialUser, UserId } from "~/lib/api/types"; type UsersStore = { - users: Record - currentUserId: UserId | undefined - fetchUsersIfNotPresent: (userIds: UserId[]) => Promise - addUser: (user: PartialUser) => void - removeUser: (userId: UserId) => void - setCurrentUserId: (userId: UserId) => void - getCurrentUser: () => FullUser | undefined -} + users: Record; + currentUserId: UserId | undefined; + fetchUsersIfNotPresent: (userIds: UserId[]) => Promise; + addUser: (user: PartialUser) => void; + removeUser: (userId: UserId) => void; + setCurrentUserId: (userId: UserId) => void; + getCurrentUser: () => FullUser | undefined; +}; const usersFetcher = batshitCreate({ fetcher: async (userIds: UserId[]) => { - let users = [] + let users = []; for (const userId of userIds) { - users.push(getUser(userId)) + users.push(getUser(userId)); } - return await Promise.all(users) + return await Promise.all(users); }, - resolver: keyResolver("id") -}) - -export const useUserQuery = (userId: UserId) => useQuery( - { - queryKey: ["users", userId], - queryFn: async () => { - const user = await getUser(userId) - return user - }, - select: (data) => { - useUsersStore.getState().addUser(data) - return data - } - } -) + resolver: keyResolver("id"), +}); export const useUsersStore = create()( - immer( - (set, get) => ({ - users: {}, - currentUserId: undefined, - fetchUsersIfNotPresent: async (userIds) => { - let userPromises: Promise[] = [] - for (const userId of userIds) { - const user = get().users[userId] - if (!user) { - userPromises.push(usersFetcher.fetch(userId)) - } + immer((set, get) => ({ + users: {}, + currentUserId: undefined, + fetchUsersIfNotPresent: async (userIds) => { + let userPromises: Promise[] = []; + for (const userId of userIds) { + const user = get().users[userId]; + if (!user) { + userPromises.push(usersFetcher.fetch(userId)); } + } - const users = await Promise.all(userPromises) - const activeUsers = users.filter(Boolean) + const users = await Promise.all(userPromises); + const activeUsers = users.filter(Boolean); - set((state) => { - for (const user of activeUsers) { - if (user?.id) - state.users[user.id] = user - } - - }) - }, - addUser: (user) => set((state) => { + set((state) => { + for (const user of activeUsers) { + if (user?.id) state.users[user.id] = user; + } + }); + }, + addUser: (user) => + set((state) => { if (user.id !== get().currentUserId) - state.users[user.id] = user + state.users[user.id] = user; else { - const currentUser = get().users[user.id] + const currentUser = get().users[user.id]; if (currentUser) - state.users[user.id] = { ...currentUser, ...user } - else - state.users[user.id] = user + state.users[user.id] = { ...currentUser, ...user }; + else state.users[user.id] = user; } }), - removeUser: (userId) => set((state) => { - delete state.users[userId] + removeUser: (userId) => + set((state) => { + delete state.users[userId]; }), - setCurrentUserId: (userId) => set((state) => { - state.currentUserId = userId + setCurrentUserId: (userId) => + set((state) => { + state.currentUserId = userId; }), - getCurrentUser: () => !!get().currentUserId ? get().users[get().currentUserId!] as FullUser : undefined - }), - ) -) \ No newline at end of file + getCurrentUser: () => + !!get().currentUserId + ? (get().users[get().currentUserId!] as FullUser) + : undefined, + })), +); diff --git a/app/stores/voice-state-store.ts b/app/stores/voice-state-store.ts index dbeac61..a82b524 100644 --- a/app/stores/voice-state-store.ts +++ b/app/stores/voice-state-store.ts @@ -1,5 +1,5 @@ -import { create } from 'zustand'; -import { useWebRTCStore } from './webrtc-store'; +import { create } from "zustand"; +import { useWebRTCStore } from "./webrtc-store"; interface VoiceState { activeChannel: { serverId: string; channelId: string } | null; @@ -20,7 +20,7 @@ export const useVoiceStateStore = create()((set, get) => { joinVoiceChannel: (serverId, channelId) => { set({ activeChannel: { serverId, channelId }, - error: null + error: null, }); }, @@ -41,6 +41,6 @@ export const useVoiceStateStore = create()((set, get) => { resetError: () => { set({ error: null }); - } + }, }; -}); \ No newline at end of file +}); diff --git a/app/stores/webrtc-store.ts b/app/stores/webrtc-store.ts index b03017a..a458c36 100644 --- a/app/stores/webrtc-store.ts +++ b/app/stores/webrtc-store.ts @@ -1,9 +1,9 @@ -import { create } from 'zustand'; -import { WebRTCClient } from '~/lib/websocket/voice/client'; -import { ConnectionState } from '~/lib/websocket/voice/types'; -import { useVoiceStateStore } from './voice-state-store'; +import { create } from "zustand"; +import { WebRTCClient } from "~/lib/websocket/voice/client"; +import { ConnectionState } from "~/lib/websocket/voice/types"; +import { useVoiceStateStore } from "./voice-state-store"; -const VOICE_GATEWAY_URL = 'ws://localhost:12345/voice/ws'; +const VOICE_GATEWAY_URL = "ws://localhost:12345/voice/ws"; interface WebRTCState { client: WebRTCClient | null; @@ -22,11 +22,11 @@ export const useWebRTCStore = create()((set, get) => { (error) => { set({ status: ConnectionState.ERROR, - error: error.message + error: error.message, }); useVoiceStateStore.getState().setError(error.message); }, - (stream) => set({ remoteStream: stream }) + (stream) => set({ remoteStream: stream }), ); return { @@ -43,12 +43,12 @@ export const useWebRTCStore = create()((set, get) => { client.disconnect(); set({ status: ConnectionState.DISCONNECTED, - remoteStream: null + remoteStream: null, }); }, createOffer: async (localStream) => { await client.createOffer(localStream); - } + }, }; -}); \ No newline at end of file +}); diff --git a/bun.lock b/bun.lock index 39c814d..4928690 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@hookform/resolvers": "^5.0.1", "@radix-ui/react-aspect-ratio": "^1.1.6", "@radix-ui/react-avatar": "^1.1.9", + "@radix-ui/react-context-menu": "^2.2.14", "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-label": "^2.1.6", @@ -35,14 +36,22 @@ "zustand": "^5.0.4", }, "devDependencies": { + "@eslint/js": "^9.27.0", "@react-router/dev": "^7.6.0", "@tailwindcss/vite": "^4.1.6", "@types/node": "^22.15.18", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", + "eslint": "^9.27.0", + "eslint-plugin-prettier": "5.4.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "globals": "^16.1.0", + "prettier": "3.5.3", "tailwindcss": "^4.1.6", "tw-animate-css": "^1.2.9", "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4", }, @@ -157,6 +166,24 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="], + + "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "@eslint/js": ["@eslint/js@9.27.0", "", {}, "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="], + "@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="], @@ -167,6 +194,14 @@ "@hookform/resolvers": ["@hookform/resolvers@5.0.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -183,6 +218,12 @@ "@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@npmcli/git": ["@npmcli/git@4.1.0", "", { "dependencies": { "@npmcli/promise-spawn": "^6.0.0", "lru-cache": "^7.4.4", "npm-pick-manifest": "^8.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^3.0.0" } }, "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ=="], "@npmcli/package-json": ["@npmcli/package-json@4.0.1", "", { "dependencies": { "@npmcli/git": "^4.1.0", "glob": "^10.2.2", "hosted-git-info": "^6.1.1", "json-parse-even-better-errors": "^3.0.0", "normalize-package-data": "^5.0.0", "proc-log": "^3.0.0", "semver": "^7.5.3" } }, "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q=="], @@ -191,6 +232,8 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@pkgr/core": ["@pkgr/core@0.2.4", "", {}, "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], @@ -207,6 +250,8 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.14", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-RUHvrJE2qKAd9pQ50HZZsePio4SMWEh8v6FWQwg/4t6K1fuxfb4Ec40VEVvni6V7nFxmj9srU4UZc7aYp8x0LQ=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.9", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.6", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.8", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], @@ -355,30 +400,74 @@ "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="], "@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="], "@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.32.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/type-utils": "8.32.1", "@typescript-eslint/utils": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.32.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.32.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w=="], + "@yornaath/batshit": ["@yornaath/batshit@0.10.1", "", { "dependencies": { "@yornaath/batshit-devtools": "^1.7.1" } }, "sha512-WGZ1WNoiVN6CLf28O73+6SCf+2lUn4U7TLGM9f4zOad0pn9mdoXIq8cwu3Kpf7N2OTYgWGK4eQPTflwFlduDGA=="], "@yornaath/batshit-devtools": ["@yornaath/batshit-devtools@1.7.1", "", {}, "sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + "array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.10", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA=="], @@ -389,7 +478,9 @@ "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], - "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="], @@ -399,12 +490,18 @@ "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001717", "", {}, "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], @@ -423,6 +520,8 @@ "compression": ["compression@1.8.0", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.0.2", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -437,10 +536,22 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], @@ -451,6 +562,8 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -467,34 +580,90 @@ "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.27.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.27.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.4.0", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.0" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], @@ -509,6 +678,10 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -519,9 +692,15 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], - "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.1.0", "", {}, "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], @@ -529,6 +708,16 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -541,34 +730,106 @@ "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "isbot": ["isbot@5.1.28", "", {}, "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="], @@ -591,8 +852,14 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@0.510.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q=="], @@ -605,15 +872,19 @@ "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -627,6 +898,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], @@ -641,18 +914,44 @@ "npm-pick-manifest": ["npm-pick-manifest@8.0.2", "", { "dependencies": { "npm-install-checks": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", "npm-package-arg": "^10.0.0", "semver": "^7.3.5" } }, "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], @@ -663,9 +962,15 @@ "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], - "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], "proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="], @@ -673,12 +978,18 @@ "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], @@ -689,6 +1000,8 @@ "react-hook-form": ["react-hook-form@7.56.3", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], @@ -701,12 +1014,30 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rollup": ["rollup@4.40.2", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.2", "@rollup/rollup-android-arm64": "4.40.2", "@rollup/rollup-darwin-arm64": "4.40.2", "@rollup/rollup-darwin-x64": "4.40.2", "@rollup/rollup-freebsd-arm64": "4.40.2", "@rollup/rollup-freebsd-x64": "4.40.2", "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", "@rollup/rollup-linux-arm-musleabihf": "4.40.2", "@rollup/rollup-linux-arm64-gnu": "4.40.2", "@rollup/rollup-linux-arm64-musl": "4.40.2", "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", "@rollup/rollup-linux-riscv64-gnu": "4.40.2", "@rollup/rollup-linux-riscv64-musl": "4.40.2", "@rollup/rollup-linux-s390x-gnu": "4.40.2", "@rollup/rollup-linux-x64-gnu": "4.40.2", "@rollup/rollup-linux-x64-musl": "4.40.2", "@rollup/rollup-win32-arm64-msvc": "4.40.2", "@rollup/rollup-win32-ia32-msvc": "4.40.2", "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], @@ -719,6 +1050,12 @@ "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -757,10 +1094,28 @@ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "synckit": ["synckit@0.11.6", "", { "dependencies": { "@pkgr/core": "^0.2.4" } }, "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw=="], + "tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="], "tailwindcss": ["tailwindcss@4.1.6", "", {}, "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg=="], @@ -771,18 +1126,36 @@ "tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "tsconfck": ["tsconfck@3.1.5", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tw-animate-css": ["tw-animate-css@1.2.9", "", {}, "sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typescript-eslint": ["typescript-eslint@8.32.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/parser": "8.32.1", "@typescript-eslint/utils": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici": ["undici@6.21.2", "", {}, "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -793,6 +1166,8 @@ "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -815,7 +1190,17 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], - "which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -823,6 +1208,8 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], "zustand": ["zustand@5.0.4", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ=="], @@ -833,8 +1220,22 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@npmcli/git/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "@npmcli/git/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + + "@npmcli/promise-spawn/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + + "@react-router/dev/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], @@ -847,6 +1248,10 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -855,18 +1260,24 @@ "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -885,12 +1296,14 @@ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -899,6 +1312,8 @@ "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/components.json b/components.json index d837ae5..fa1e81b 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "~/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..45b59f1 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from "@eslint/js"; +import eslintConfigPrettier from "eslint-config-prettier/flat"; +import pluginReact from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import { defineConfig } from "eslint/config"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default defineConfig([ + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + plugins: { js }, + extends: ["js/recommended"], + }, + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + languageOptions: { globals: globals.browser }, + }, + tseslint.configs.recommended, + pluginReact.configs.recommended, + reactHooks.configs.recommended, + eslintConfigPrettier, +]); diff --git a/package.json b/package.json index 8d92922..3f2253f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@hookform/resolvers": "^5.0.1", "@radix-ui/react-aspect-ratio": "^1.1.6", "@radix-ui/react-avatar": "^1.1.9", + "@radix-ui/react-context-menu": "^2.2.14", "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-label": "^2.1.6", @@ -40,15 +41,23 @@ "zustand": "^5.0.4" }, "devDependencies": { + "@eslint/js": "^9.27.0", "@react-router/dev": "^7.6.0", "@tailwindcss/vite": "^4.1.6", "@types/node": "^22.15.18", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", + "eslint": "^9.27.0", + "eslint-plugin-prettier": "5.4.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "globals": "^16.1.0", + "prettier": "3.5.3", "tailwindcss": "^4.1.6", "tw-animate-css": "^1.2.9", "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" } -} \ No newline at end of file +}
+ {file.name} +
+ {formatFileSize(file.size)} +
Remove attachment
Maximum 10 attachments
+ Add attachment ({attachmentsRemaining}{" "} + remaining) +
- {body} -
+ {body} +
{details}
- {stack} -
{stack}
+ {stack} +