diff --git a/Client/.dockerignore b/Client/.dockerignore
new file mode 100644
index 0000000..dd6bcd2
--- /dev/null
+++ b/Client/.dockerignore
@@ -0,0 +1,7 @@
+## Docker Ignore Files ##
+
+# 'OS Waste' File
+*.DS_Store
+
+# 'VSCode Waste' File
+*.vscode
\ No newline at end of file
diff --git a/Client/.env.example b/Client/.env.example
new file mode 100644
index 0000000..e2806ce
--- /dev/null
+++ b/Client/.env.example
@@ -0,0 +1,14 @@
+## (Example) Environment Variables ##
+
+# Shortify Server 'Base URL'
+NEXT_PUBLIC_SHORTIFY_SERVER_BASE_URL = "Your_Shortify_Server_Base_URL"
+
+# Clerk 'Service'
+NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = "Your_Clerk_Publishable_Key"
+CLERK_SECRET_KEY = "Your_Clerk_Secret_Key"
+
+# Clerk 'Routes'
+NEXT_PUBLIC_CLERK_SIGN_IN_URL = "/sign-in"
+NEXT_PUBLIC_CLERK_SIGN_UP_URL = "/sign-up"
+NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL = "/"
+NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL = "/"
\ No newline at end of file
diff --git a/Client/.gitattributes b/Client/.gitattributes
index 70894c8..764a6fe 100644
--- a/Client/.gitattributes
+++ b/Client/.gitattributes
@@ -1,2 +1,4 @@
+## Git Attributes ##
+
# Auto detect the 'text' files and perform the 'LF' normalization
* text=auto
diff --git a/Client/Components/Footer/Footer.tsx b/Client/Components/Footer/Footer.tsx
deleted file mode 100644
index 9da0a97..0000000
--- a/Client/Components/Footer/Footer.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-"use client";
-
-import React from 'react';
-import Data from '@/Interface/constant/data';
-
-// # Footer Component
-const Footer = () => {
- return (
-
diff --git a/Client/app/contact/page.tsx b/Client/app/contact/page.tsx
deleted file mode 100644
index 53a4a72..0000000
--- a/Client/app/contact/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from "react";
-import Footer from '@/Components/Footer/Footer';
-
-
-
-// # Contact Page
-const page = () => {
- return (
-
-
-
- );
-};
-
-export default page;
diff --git a/Client/app/globals.css b/Client/app/globals.css
index 4d7f47f..c9cc230 100644
--- a/Client/app/globals.css
+++ b/Client/app/globals.css
@@ -1,31 +1,71 @@
-/* ## App Global CSS ## */
+/* ## App "Global" CSS ## */
-/* # Tailwind CSS # */
+/* # "Tailwind" CSS # */
@tailwind base;
@tailwind components;
@tailwind utilities;
-/* # Scrollbar CSS # */
+/* # "Layer" CSS # */
+@layer base {
-/* Firefox */
-* {
- scrollbar-width: auto;
- scrollbar-color: #5b8be0 #ffffff;
-}
+ /* # "Light" Theme # */
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+ --radius: 0.5rem;
+ }
-/* Chrome | Edge | Safari */
-*::-webkit-scrollbar {
- width: 12px;
- height: 20px;
+ /* # "Dark" Theme # */
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
}
-*::-webkit-scrollbar-track {
- background: transparent;
- margin: 10px;
-}
+/* # "Layer" CSS # */
+@layer base {
+
+ /* # "Border" CSS # */
+ * {
+ @apply border-border;
+ }
-*::-webkit-scrollbar-thumb {
- background-color: #5b8be0;
- border-radius: 10px;
- border: 3px solid black;
+ /* # "Body" CSS # */
+ body {
+ @apply bg-background text-foreground;
+ }
}
\ No newline at end of file
diff --git a/Client/app/layout.jsx b/Client/app/layout.jsx
new file mode 100644
index 0000000..1a42f1d
--- /dev/null
+++ b/Client/app/layout.jsx
@@ -0,0 +1,22 @@
+import './globals.css'
+import MetaData from '../Interface/constant/metadata';
+import { ClerkProvider } from '@clerk/nextjs';
+import "react-toastify/dist/ReactToastify.css";
+
+export const metadata = MetaData
+
+const RootLayout = ({ children }) => {
+ return (
+ <>
+
+
+
+ {children}
+
+
+
+ >
+ );
+};
+
+export default RootLayout;
\ No newline at end of file
diff --git a/Client/app/layout.tsx b/Client/app/layout.tsx
deleted file mode 100644
index 1f75169..0000000
--- a/Client/app/layout.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import './globals.css'
-import { Inter } from 'next/font/google'
-import { ToastContainer } from "react-toastify";
-import MetaData from '@/Interface/constant/metadata';
-import { ClerkProvider } from '@clerk/nextjs';
-import "react-toastify/dist/ReactToastify.css";
-
-const inter = Inter({ subsets: ['latin'] })
-
-export const metadata = MetaData
-
-export default function RootLayout({
- children
-}: {
- children: React.ReactNode
-}) {
- return (
-
-
-
- {children}
-
-
-
-
- )
-}
diff --git a/Client/app/page.jsx b/Client/app/page.jsx
new file mode 100644
index 0000000..4f3c8d0
--- /dev/null
+++ b/Client/app/page.jsx
@@ -0,0 +1,232 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useUser } from '@clerk/nextjs';
+import { Plus } from 'lucide-react';
+import { Button } from '../components/ui/button';
+import { Card, CardContent } from '../components/ui/card';
+import Navbar from '../components/Navbar';
+import Footer from '../components/Footer';
+import LinkCard from '../components/LinkCard';
+import CreateLinkDialog from '../components/CreateLinkDialog';
+import LinkDetailDialog from '../components/LinkDetailDialog';
+import EditUsernameDialog from '../components/EditUsernameDialog';
+import Toast from '../components/Toast';
+import { userAPI, linkAPI } from '@/lib/api';
+import { getSuccessMsg } from '@/lib/success';
+
+const Dashboard = () => {
+ const { user, isLoaded } = useUser();
+ const [links, setLinks] = useState([]);
+ const [selectedLink, setSelectedLink] = useState(null);
+ const [userData, setUserData] = useState(null);
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
+ const [isEditUserOpen, setIsEditUserOpen] = useState(false);
+ const [isDetailOpen, setIsDetailOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [toast, setToast] = useState(null);
+
+ useEffect(() => {
+ if (user) {
+ fetchUser();
+ fetchLinks();
+ }
+ }, [user]);
+
+ const showToast = (message, type) => {
+ setToast({ message, type });
+ };
+
+ const fetchUser = async () => {
+ try {
+ const response = await userAPI.getUser(user.id);
+ if (response.success) {
+ setUserData(response.payload);
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
+ }
+ };
+
+ const fetchLinks = async () => {
+ setLoading(true);
+ try {
+ const response = await linkAPI.getLinks(user.id);
+ if (response.success) {
+ setLinks(response.payload);
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchLinkDetails = async (link) => {
+ try {
+ const response = await linkAPI.getLink(link._id);
+ if (response.success) {
+ setSelectedLink(response.payload);
+ setIsDetailOpen(true);
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
+ }
+ };
+
+ const createLink = async (data) => {
+ try {
+ const response = await linkAPI.createLink(user.id, data);
+ if (response.success) {
+ const successMsg = getSuccessMsg(response.payload);
+ showToast(successMsg, 'success');
+ setIsCreateOpen(false);
+ fetchLinks();
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
+ setIsCreateOpen(false);
+ }
+ };
+
+ const updateLink = async (linkId, data) => {
+ try {
+ const response = await linkAPI.updateLink(user.id, linkId, data);
+ if (response.success) {
+ const successMsg = getSuccessMsg(response.payload);
+ showToast(successMsg, 'success');
+ setIsDetailOpen(false);
+ fetchLinks();
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
+ setIsDetailOpen(false);
+ }
+ };
+
+ const deleteLink = async (linkId) => {
+ if (!confirm('Are you sure you want to delete this link?')) return;
+ try {
+ const response = await linkAPI.deleteLink(linkId);
+ if (response.success) {
+ const successMsg = getSuccessMsg(response.payload);
+ showToast(successMsg, 'success');
+ fetchLinks();
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
+ fetchLinks();
+ }
+ };
+
+ const updateUsername = async (username) => {
+ try {
+ const response = await userAPI.updateUsername(user.id, username);
+ if (response.success) {
+ const successMsg = getSuccessMsg(response.payload);
+ showToast(successMsg, 'success');
+ setIsEditUserOpen(false);
+ fetchUser();
+ }
+ } catch (err) {
+ showToast(err.message, 'error');
+ setIsEditUserOpen(false);
+ }
+ };
+
+ const copyToClipboard = (text) => {
+ navigator.clipboard.writeText(text);
+ showToast('Copied to clipboard!', 'success');
+ };
+
+ if (!isLoaded) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+
setIsEditUserOpen(true)}
+ />
+
+
+
+
+
Link Management
+
Create and manage your shortened URLs
+
+
+
+
+ {loading ? (
+
+ ) : !links || links.length === 0 ? (
+
+
+
+ No links yet
+ Create your first shortened URL to get started
+
+
+
+ ) : (
+
+ {links.map((link) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ {toast && (
+ setToast(null)}
+ />
+ )}
+
+ >
+ );
+};
+
+export default Dashboard;
diff --git a/Client/app/page.tsx b/Client/app/page.tsx
deleted file mode 100644
index a128a1e..0000000
--- a/Client/app/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-"use client";
-
-import React from "react";
-import Footer from '@/Components/Footer/Footer';
-
-// # Shortify Page - Entry Point
-const page = () => {
- return (
- <>
-
- >
- );
-};
-
-export default page;
diff --git a/Client/components.json b/Client/components.json
new file mode 100644
index 0000000..1a7ba74
--- /dev/null
+++ b/Client/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": false,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "app/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
\ No newline at end of file
diff --git a/Client/components/CreateLinkDialog.jsx b/Client/components/CreateLinkDialog.jsx
new file mode 100644
index 0000000..2e42828
--- /dev/null
+++ b/Client/components/CreateLinkDialog.jsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { Plus } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+
+const CreateLinkDialog = ({ open, onOpenChange, onCreate }) => {
+ const [formData, setFormData] = useState({
+ destination_url: '',
+ slug: '',
+ title: '',
+ description: ''
+ });
+
+ // Clear form when dialog closes
+ useEffect(() => {
+ if (!open) {
+ setFormData({ destination_url: '', slug: '', title: '', description: '' });
+ }
+ }, [open]);
+
+ const handleSubmit = () => {
+ if (!formData.destination_url || !formData.slug) {
+ alert('Destination URL and Slug are required');
+ return;
+ }
+ onCreate(formData);
+ };
+
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default CreateLinkDialog;
diff --git a/Client/components/EditUsernameDialog.jsx b/Client/components/EditUsernameDialog.jsx
new file mode 100644
index 0000000..a514b57
--- /dev/null
+++ b/Client/components/EditUsernameDialog.jsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+
+const EditUsernameDialog = ({ open, onOpenChange, currentUsername, onUpdate }) => {
+ const [username, setUsername] = useState('');
+
+ useEffect(() => {
+ if (open) {
+ setUsername(currentUsername || '');
+ } else {
+ // Clear on close
+ setUsername('');
+ }
+ }, [currentUsername, open]);
+
+ const handleUpdate = () => {
+ if (!username.trim()) {
+ alert('Username cannot be empty');
+ return;
+ }
+ onUpdate(username);
+ };
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default EditUsernameDialog;
diff --git a/Client/components/Footer.jsx b/Client/components/Footer.jsx
new file mode 100644
index 0000000..9b819e1
--- /dev/null
+++ b/Client/components/Footer.jsx
@@ -0,0 +1,19 @@
+import Data from '@/Interface/constant/data';
+
+const Footer = () => {
+ return (
+
+ );
+};
+
+export default Footer
diff --git a/Client/components/LinkCard.jsx b/Client/components/LinkCard.jsx
new file mode 100644
index 0000000..49ba1e3
--- /dev/null
+++ b/Client/components/LinkCard.jsx
@@ -0,0 +1,74 @@
+'use client';
+
+import { Copy, Trash2, ExternalLink } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+
+const LinkCard = ({ link, onDelete, onClick, onCopy }) => {
+ return (
+ <>
+
onClick(link)}
+ >
+
+
+ {link.title || 'Untitled'}
+
+
+
+
+
+
+
Short URL
+
+
+ {link.short_url}
+
+
+
+
+
+
Destination URL
+
{link.destination_url}
+
+
+
+
+
+ >
+ );
+};
+
+export default LinkCard;
\ No newline at end of file
diff --git a/Client/components/LinkDetailDialog.jsx b/Client/components/LinkDetailDialog.jsx
new file mode 100644
index 0000000..52ba432
--- /dev/null
+++ b/Client/components/LinkDetailDialog.jsx
@@ -0,0 +1,133 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Copy } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+
+const LinkDetailDialog = ({ link, open, onOpenChange, onUpdate, onDelete }) => {
+ const [formData, setFormData] = useState({
+ destination_url: '',
+ slug: '',
+ title: '',
+ description: ''
+ });
+
+ useEffect(() => {
+ if (link) {
+ setFormData({
+ destination_url: link.destination_url || '',
+ slug: link.slug || '',
+ title: link.title || '',
+ description: link.description || ''
+ });
+ }
+ }, [link]);
+
+ const handleUpdate = () => {
+ if (!formData.destination_url || !formData.slug) {
+ alert('Destination URL and Slug are required');
+ return;
+ }
+ onUpdate(link._id, formData);
+ };
+
+ const copyToClipboard = (text) => {
+ navigator.clipboard.writeText(text);
+ };
+
+ if (!link) return null;
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default LinkDetailDialog;
diff --git a/Client/components/Navbar.jsx b/Client/components/Navbar.jsx
new file mode 100644
index 0000000..89e1978
--- /dev/null
+++ b/Client/components/Navbar.jsx
@@ -0,0 +1,109 @@
+'use client';
+
+import { UserButton } from '@clerk/nextjs';
+import { Edit2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import ThemeToggle from './ThemeToggle';
+import Image from 'next/image';
+import { useEffect, useState } from 'react';
+import UIData from '@/Interface/constant/ui';
+
+const Navbar = ({ username, onEditUsername }) => {
+ const [theme, setTheme] = useState('light');
+
+ useEffect(() => {
+ // Get initial theme
+ const savedTheme = localStorage.getItem('theme') || 'light';
+ setTheme(savedTheme);
+
+ // Watch for theme changes
+ const observer = new MutationObserver(() => {
+ const isDark = document.documentElement.classList.contains('dark');
+ setTheme(isDark ? 'dark' : 'light');
+ });
+
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['class']
+ });
+
+ return () => observer.disconnect();
+ }, []);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default Navbar;
\ No newline at end of file
diff --git a/Client/components/ThemeToggle.jsx b/Client/components/ThemeToggle.jsx
new file mode 100644
index 0000000..867b611
--- /dev/null
+++ b/Client/components/ThemeToggle.jsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { Moon, Sun } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { useEffect, useState } from 'react';
+
+const ThemeToggle = () => {
+ const [theme, setTheme] = useState('light');
+
+ useEffect(() => {
+ const savedTheme = localStorage.getItem('theme') || 'light';
+ setTheme(savedTheme);
+ document.documentElement.classList.toggle('dark', savedTheme === 'dark');
+ }, []);
+
+ const toggleTheme = () => {
+ const newTheme = theme === 'light' ? 'dark' : 'light';
+ setTheme(newTheme);
+ localStorage.setItem('theme', newTheme);
+ document.documentElement.classList.toggle('dark', newTheme === 'dark');
+ };
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default ThemeToggle;
diff --git a/Client/components/Toast.jsx b/Client/components/Toast.jsx
new file mode 100644
index 0000000..fee9091
--- /dev/null
+++ b/Client/components/Toast.jsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { useEffect } from 'react';
+import { X, CheckCircle, XCircle } from 'lucide-react';
+
+const Toast = ({ message, type, onClose }) => {
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ onClose();
+ }, 5000);
+
+ return () => clearTimeout(timer);
+ }, [onClose]);
+
+ return (
+ <>
+
+ {type === 'success' ? (
+
+ ) : (
+
+ )}
+
+ {message}
+
+
+
+ >
+ );
+};
+
+export default Toast;
\ No newline at end of file
diff --git a/Client/components/ui/alert.jsx b/Client/components/ui/alert.jsx
new file mode 100644
index 0000000..358dbdf
--- /dev/null
+++ b/Client/components/ui/alert.jsx
@@ -0,0 +1,49 @@
+'use client';
+
+import * as React from "react"
+import { cva } from "class-variance-authority";
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/Client/components/ui/button.jsx b/Client/components/ui/button.jsx
new file mode 100644
index 0000000..4916d38
--- /dev/null
+++ b/Client/components/ui/button.jsx
@@ -0,0 +1,50 @@
+'use client';
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva } from "class-variance-authority";
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ );
+})
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/Client/components/ui/card.jsx b/Client/components/ui/card.jsx
new file mode 100644
index 0000000..5d4321d
--- /dev/null
+++ b/Client/components/ui/card.jsx
@@ -0,0 +1,52 @@
+'use client';
+
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/Client/components/ui/dialog.jsx b/Client/components/ui/dialog.jsx
new file mode 100644
index 0000000..0d7981b
--- /dev/null
+++ b/Client/components/ui/dialog.jsx
@@ -0,0 +1,96 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/Client/components/ui/input.jsx b/Client/components/ui/input.jsx
new file mode 100644
index 0000000..4da401f
--- /dev/null
+++ b/Client/components/ui/input.jsx
@@ -0,0 +1,21 @@
+'use client';
+
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef(({ className, type, ...props }, ref) => {
+ return (
+
+ );
+})
+Input.displayName = "Input"
+
+export { Input }
diff --git a/Client/components/ui/textarea.jsx b/Client/components/ui/textarea.jsx
new file mode 100644
index 0000000..66ad730
--- /dev/null
+++ b/Client/components/ui/textarea.jsx
@@ -0,0 +1,20 @@
+'use client';
+
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Textarea = React.forwardRef(({ className, ...props }, ref) => {
+ return (
+
+ );
+})
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/Client/jsconfig.json b/Client/jsconfig.json
new file mode 100644
index 0000000..a31418f
--- /dev/null
+++ b/Client/jsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ }
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/Client/lib/api.js b/Client/lib/api.js
new file mode 100644
index 0000000..d3139f8
--- /dev/null
+++ b/Client/lib/api.js
@@ -0,0 +1,224 @@
+import { sendError } from "./error";
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_SHORTIFY_SERVER_BASE_URL;
+
+if (!API_BASE_URL) {
+ let errMsg = "🚫 Client Error: MISSING.API_BASE_URL";
+ console.error(errMsg);
+ errMsg = "";
+ sendError(errMsg);
+}
+
+// # "User" API #
+const userAPI = {
+ // # Get 'User'
+ async getUser(clerkUserId) {
+ const apiEndpoint = `${API_BASE_URL}/user/?clerk_user_id=${clerkUserId}`;
+ const apiRequestOptions = {
+ method: 'GET'
+ }
+
+ const response = await fetch(
+ apiEndpoint,
+ apiRequestOptions
+ );
+
+ const res = await response.json();
+
+ if (!response.ok) {
+ let errMsg = res?.error;
+ if (!errMsg) {
+ errMsg = '🚫 Client Error: Failed to get the user';
+ console.log(errMsg);
+ errMsg = "";
+ }
+ sendError(errMsg);
+ }
+
+ return res;
+ },
+
+ // # Update 'Username'
+ async updateUsername(clerkUserId, username) {
+ const apiEndpoint = `${API_BASE_URL}/user/username?clerk_user_id=${clerkUserId}&username=${encodeURIComponent(username)}`;
+ const apiRequestOptions = {
+ method: 'PUT'
+ };
+
+ const response = await fetch(
+ apiEndpoint,
+ apiRequestOptions
+ );
+
+ const res = await response.json();
+
+ if (!response.ok) {
+ let errMsg = res?.error;
+ if (!errMsg) {
+ errMsg = '🚫 Client Error: Failed to update the username';
+ console.log(errMsg);
+ errMsg = "";
+ }
+ sendError(errMsg);
+ }
+
+ return res;
+ }
+};
+
+// # "Link" API #
+const linkAPI = {
+ // # Create 'Link'
+ async createLink(clerkUserId, data) {
+ apiEndpoint = `${API_BASE_URL}/link/?clerk_user_id=${clerkUserId}`;
+ apiRequestOptions = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(
+ {
+ data
+ }
+ )
+ };
+
+ const response = await fetch(
+ apiEndpoint,
+ apiRequestOptions
+ );
+
+ const res = await response.json();
+
+ if (!response.ok) {
+ let errMsg = res?.error;
+ if (!errMsg) {
+ errMsg = '🚫 Client Error: Failed to create the link';
+ console.log(errMsg);
+ errMsg = "";
+ }
+ sendError(errMsg);
+ }
+
+ return res;
+ },
+
+ // # Update 'Link'
+ async updateLink(clerkUserId, linkId, data) {
+ const apiEndpoint = `${API_BASE_URL}/link/?clerk_user_id=${clerkUserId}&link_id=${linkId}`;
+ const apiRequestOptions = {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(
+ {
+ data
+ }
+ )
+ };
+
+ const response = await fetch(
+ apiEndpoint,
+ apiRequestOptions
+ );
+
+ const res = await response.json();
+
+ if (!response.ok) {
+ let errMsg = res?.error;
+ if (!errMsg) {
+ errMsg = '🚫 Client Error: Failed to update the link';
+ console.log(errMsg);
+ errMsg = "";
+ }
+ sendError(errMsg);
+ }
+
+ return res;
+ },
+
+ // # Delete 'Link'
+ async deleteLink(linkId) {
+ const apiEndpoint = `${API_BASE_URL}/link/?link_id=${linkId}`;
+ const apiRequestOptions = {
+ method: 'DELETE'
+ }
+
+ const response = await fetch(
+ apiEndpoint,
+ apiRequestOptions
+ );
+
+ const res = await response.json();
+
+ if (!response.ok) {
+ let errMsg = res?.error;
+ if (!errMsg) {
+ errMsg = '🚫 Client Error: Failed to delete the link';
+ console.log(errMsg);
+ }
+ sendError(errMsg);
+ }
+
+ return res;
+ },
+
+ // # Get 'Link'
+ async getLink(linkId) {
+ const apiEndpoint = `${API_BASE_URL}/link/?link_id=${linkId}`;
+ const apiRequestOptions = {
+ method: 'GET'
+ }
+
+ const response = await fetch(
+ apiEndpoint,
+ apiRequestOptions
+ );
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ let errMsg = res?.error;
+ if (!errMsg) {
+ errMsg = '🚫 Client Error: Failed to get the link';
+ console.log(errMsg);
+ }
+ sendError(errMsg);
+ }
+
+ return data;
+ },
+
+ // # Get 'Links'
+ async getLinks(clerkUserId) {
+ const apiEndpoint = `${API_BASE_URL}/link/list?clerk_user_id=${clerkUserId}`;
+ const apiRequestOptions = {
+ method: 'GET'
+ }
+
+ const response = await fetch(
+ apiEndpoint,
+ apiRequestOptions
+ );
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ let errMsg = res?.error;
+ if (!errMsg) {
+ errMsg = '🚫 Client Error: Failed to get the links';
+ console.log(errMsg);
+ errMsg = ""
+ }
+ sendError(errMsg);
+ }
+
+ return data;
+ },
+};
+
+export {
+ userAPI,
+ linkAPI
+};
\ No newline at end of file
diff --git a/Client/lib/error.js b/Client/lib/error.js
new file mode 100644
index 0000000..6814124
--- /dev/null
+++ b/Client/lib/error.js
@@ -0,0 +1,14 @@
+const defaultErrMsg = "Something Went Wrong!";
+
+const sendError = (errMsg) => {
+ if (!errMsg) {
+ errMsg = defaultErrMsg;
+ }
+
+ const err = new Error(errMsg);
+ throw err;
+};
+
+export {
+ sendError
+};
\ No newline at end of file
diff --git a/Client/lib/success.js b/Client/lib/success.js
new file mode 100644
index 0000000..d8382b3
--- /dev/null
+++ b/Client/lib/success.js
@@ -0,0 +1,13 @@
+const defaultSuccessMsg = "Operation Successful!";
+
+const getSuccessMsg = (successMsg) => {
+ if (!successMsg) {
+ successMsg = defaultSuccessMsg;
+ }
+
+ return successMsg;
+};
+
+export {
+ getSuccessMsg
+};
\ No newline at end of file
diff --git a/Client/lib/utils.js b/Client/lib/utils.js
new file mode 100644
index 0000000..d49aa24
--- /dev/null
+++ b/Client/lib/utils.js
@@ -0,0 +1,12 @@
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+const cn = (...inputs) => {
+ const cls = clsx(inputs);
+ const twm = twMerge(cls);
+ return twm;
+};
+
+export {
+ cn
+};
diff --git a/Client/middleware.js b/Client/middleware.js
new file mode 100644
index 0000000..6dadef6
--- /dev/null
+++ b/Client/middleware.js
@@ -0,0 +1,33 @@
+// ## Clerk 'Middleware' ##
+import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
+
+// # Define the clerk 'public' routes
+const isPublicRoute = createRouteMatcher(
+ [
+ '/sign-in(.*)',
+ '/sign-up(.*)'
+ ]
+)
+
+// # Define the clerk 'middleware'
+const clerkMiddleWare = clerkMiddleware(
+ async (auth, req) => {
+ if (!isPublicRoute(req)) {
+ await auth.protect()
+ }
+ }
+)
+
+// # Define the clerk 'config'
+const clerkConfig = {
+ matcher: [
+ // # Skip the 'Next.js' internals and all the 'static' files, unless found in the 'search' params
+ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
+ // # Always run for the 'API' routes
+ '/(api|trpc)(.*)'
+ ]
+}
+
+// # Export the clerk 'middleware' and the 'config'
+export default clerkMiddleWare
+export { clerkConfig }
\ No newline at end of file
diff --git a/Client/middleware.ts b/Client/middleware.ts
deleted file mode 100644
index 13d65ff..0000000
--- a/Client/middleware.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// ## Clerk 'Proxy' ##
-import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
-
-const isPublicRoute = createRouteMatcher([
- '/sign-in(.*)',
- '/sign-up(.*)'
-])
-
-export default clerkMiddleware(async (auth, req) => {
- if (!isPublicRoute(req)) {
- await auth.protect()
- }
-})
-
-export const config = {
- matcher: [
- // Skip Next.js internals and all static files, unless found in search params
- '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
- // Always run for API routes
- '/(api|trpc)(.*)',
- ],
-}
\ No newline at end of file
diff --git a/Client/next.config.js b/Client/next.config.js
index 8a57617..92871ed 100644
--- a/Client/next.config.js
+++ b/Client/next.config.js
@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
- // # comment out for clerk middleware
+ // # Comment out the "output" for the clerk 'middleware'
// output: "export",
trailingSlash: true,
images: {
diff --git a/Client/package-lock.json b/Client/package-lock.json
index e73e592..4fc55e5 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -9,26 +9,27 @@
"version": "1.0.0",
"dependencies": {
"@clerk/nextjs": "^6.36.5",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-slot": "^1.2.4",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.562.0",
"next": "^15.5.9",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-toastify": "^11.0.5",
- "react-tooltip": "^5.30.0",
"sharp": "^0.34.5",
- "styled-components": "^6.1.19"
+ "tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
- "@types/node": "^25.0.3",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
"@wojtekmaj/react-hooks": "^2.0.4",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.1",
"postcss": "^8.5.6",
- "tailwindcss": "^4.1.18",
- "typescript": "^5.9.3"
+ "tailwindcss": "^3.4.19",
+ "tailwindcss-animate": "^1.0.7"
}
},
"node_modules/@alloc/quick-lru": {
@@ -414,31 +415,10 @@
"tslib": "^2.4.0"
}
},
- "node_modules/@emotion/is-prop-valid": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
- "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
- "license": "MIT",
- "dependencies": {
- "@emotion/memoize": "^0.8.1"
- }
- },
- "node_modules/@emotion/memoize": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
- "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
- "license": "MIT"
- },
- "node_modules/@emotion/unitless": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
- "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
- "license": "MIT"
- },
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
- "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -579,31 +559,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@floating-ui/core": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
- "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/utils": "^0.2.10"
- }
- },
- "node_modules/@floating-ui/dom": {
- "version": "1.7.4",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
- "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/core": "^1.7.3",
- "@floating-ui/utils": "^0.2.10"
- }
- },
- "node_modules/@floating-ui/utils": {
- "version": "0.2.10",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
- "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
- "license": "MIT"
- },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1376,6 +1331,373 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1414,6 +1736,13 @@
"tailwindcss": "4.1.18"
}
},
+ "node_modules/@tailwindcss/node/node_modules/tailwindcss": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
+ "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/oxide": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
@@ -1669,6 +1998,13 @@
"tailwindcss": "4.1.18"
}
},
+ "node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
+ "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -1689,59 +2025,16 @@
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/json5": {
- "version": "0.0.29",
- "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "25.0.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
- "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~7.16.0"
- }
- },
- "node_modules/@types/react": {
- "version": "19.2.7",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
- "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.2.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.2.3",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
- "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.2.0"
- }
- },
- "node_modules/@types/react/node_modules/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
- "node_modules/@types/stylis": {
- "version": "4.2.5",
- "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
- "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
@@ -2357,6 +2650,34 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2364,6 +2685,18 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -2641,6 +2974,19 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -2759,13 +3105,14 @@
"node": ">=6"
}
},
- "node_modules/camelize": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
- "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
"license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "engines": {
+ "node": ">= 6"
}
},
"node_modules/caniuse-lite": {
@@ -2805,11 +3152,55 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
- "node_modules/classnames": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
- "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
- "license": "MIT"
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
},
"node_modules/client-only": {
"version": "0.0.1",
@@ -2846,6 +3237,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2884,26 +3285,19 @@
"node": ">= 8"
}
},
- "node_modules/css-color-keywords": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
- "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
- "license": "ISC",
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
"engines": {
"node": ">=4"
}
},
- "node_modules/css-to-react-native": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
- "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
- "license": "MIT",
- "dependencies": {
- "camelize": "^1.0.0",
- "css-color-keywords": "^1.0.0",
- "postcss-value-parser": "^4.0.2"
- }
- },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3050,6 +3444,26 @@
"node": ">=8"
}
},
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3685,9 +4099,9 @@
}
},
"node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -3891,6 +4305,21 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3977,6 +4406,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -4308,6 +4746,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-boolean-object": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
@@ -5108,6 +5559,26 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -5154,6 +5625,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.562.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
+ "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5228,6 +5708,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -5356,6 +5848,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5366,6 +5868,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -5606,6 +6118,26 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -5613,23 +6145,135 @@
"dev": true,
"license": "MIT",
"engines": {
- "node": ">= 0.4"
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
}
},
- "node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
{
"type": "github",
"url": "https://github.com/sponsors/ai"
@@ -5637,18 +6281,34 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
+ "postcss-selector-parser": "^6.1.1"
},
"engines": {
- "node": "^10 || ^12 || >=14"
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
@@ -5732,6 +6392,75 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
@@ -5745,18 +6474,27 @@
"react-dom": "^18 || ^19"
}
},
- "node_modules/react-tooltip": {
- "version": "5.30.0",
- "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.30.0.tgz",
- "integrity": "sha512-Yn8PfbgQ/wmqnL7oBpz1QiDaLKrzZMdSUUdk7nVeGTwzbxCAJiJzR4VSYW+eIO42F1INt57sPUmpgKv0KwJKtg==",
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@floating-ui/dom": "^1.6.1",
- "classnames": "^2.3.0"
+ "picomatch": "^2.2.1"
},
- "peerDependencies": {
- "react": ">=16.14.0",
- "react-dom": ">=16.14.0"
+ "engines": {
+ "node": ">=8.10.0"
}
},
"node_modules/reflect.getprototypeof": {
@@ -6005,12 +6743,6 @@
"node": ">= 0.4"
}
},
- "node_modules/shallowequal": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
- "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
- "license": "MIT"
- },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -6348,68 +7080,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/styled-components": {
- "version": "6.1.19",
- "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz",
- "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==",
- "license": "MIT",
- "dependencies": {
- "@emotion/is-prop-valid": "1.2.2",
- "@emotion/unitless": "0.8.1",
- "@types/stylis": "4.2.5",
- "css-to-react-native": "3.2.0",
- "csstype": "3.1.3",
- "postcss": "8.4.49",
- "shallowequal": "1.1.0",
- "stylis": "4.3.2",
- "tslib": "2.6.2"
- },
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/styled-components"
- },
- "peerDependencies": {
- "react": ">= 16.8.0",
- "react-dom": ">= 16.8.0"
- }
- },
- "node_modules/styled-components/node_modules/postcss": {
- "version": "8.4.49",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
- "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/styled-components/node_modules/tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
- "license": "0BSD"
- },
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -6433,11 +7103,28 @@
}
}
},
- "node_modules/stylis": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
- "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
- "license": "MIT"
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
},
"node_modules/supports-color": {
"version": "7.2.0",
@@ -6478,12 +7165,103 @@
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
+ "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
- "version": "4.1.18",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
- "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -6499,6 +7277,29 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -6573,6 +7374,13 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -6702,6 +7510,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6753,13 +7562,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/unrs-resolver": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
@@ -6836,6 +7638,49 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
@@ -6845,6 +7690,13 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6981,9 +7833,9 @@
}
},
"node_modules/zod": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
- "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.2.tgz",
+ "integrity": "sha512-b8L8yn4rIVfiXyHAmnr52/ZEpDumlT0bmxiq3Ws1ybrinhflGpt12Hvv54kYnEsGPRs6o/Ka3/ppA2OWY21IVg==",
"dev": true,
"license": "MIT",
"funding": {
diff --git a/Client/package.json b/Client/package.json
index 322243d..aa1e887 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -5,6 +5,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
+ "export": "next export",
"start": "next start",
"lint": "next lint"
},
@@ -16,25 +17,26 @@
],
"dependencies": {
"@clerk/nextjs": "^6.36.5",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-slot": "^1.2.4",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.562.0",
"next": "^15.5.9",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-toastify": "^11.0.5",
- "react-tooltip": "^5.30.0",
"sharp": "^0.34.5",
- "styled-components": "^6.1.19"
+ "tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
- "@types/node": "^25.0.3",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
"@wojtekmaj/react-hooks": "^2.0.4",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.1",
"postcss": "^8.5.6",
- "tailwindcss": "^4.1.18",
- "typescript": "^5.9.3"
+ "tailwindcss": "^3.4.19",
+ "tailwindcss-animate": "^1.0.7"
}
-}
+}
\ No newline at end of file
diff --git a/Client/postcss.config.js b/Client/postcss.config.js
index 668a5b9..90d08b4 100644
--- a/Client/postcss.config.js
+++ b/Client/postcss.config.js
@@ -1,6 +1,6 @@
module.exports = {
plugins: {
- '@tailwindcss/postcss': {},
- autoprefixer: {},
- },
-}
+ tailwindcss: {},
+ autoprefixer: {}
+ }
+}
\ No newline at end of file
diff --git a/Client/public/black_logo.png b/Client/public/black_logo.png
new file mode 100644
index 0000000..46d0d02
Binary files /dev/null and b/Client/public/black_logo.png differ
diff --git a/Client/public/white_logo.png b/Client/public/white_logo.png
new file mode 100644
index 0000000..a2fe514
Binary files /dev/null and b/Client/public/white_logo.png differ
diff --git a/Client/tailwind.config.js b/Client/tailwind.config.js
index a36975f..fe4b0af 100644
--- a/Client/tailwind.config.js
+++ b/Client/tailwind.config.js
@@ -1,21 +1,78 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
+ darkMode: ["class"],
content: [
- './app/**/*.{js,ts,jsx,tsx,mdx}',
- './components/**/*.{js,ts,jsx,tsx,mdx}',
+ './pages/**/*.{js,jsx}',
+ './components/**/*.{js,jsx}',
+ './app/**/*.{js,jsx}',
+ './src/**/*.{js,jsx}'
],
theme: {
- extend: {
+ container: {
+ center: true,
+ padding: "2rem",
screens: {
- 'sm': '640px',
- 'md': '768px',
- 'lg': '1024px',
+ "2xl": "1400px"
+ }
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))"
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))"
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))"
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))"
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))"
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))"
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))"
+ },
},
- backgroundImage: {
- 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
- 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)"
},
- },
+ keyframes: {
+ "accordion-down": {
+ from: { height: 0 },
+ to: { height: "var(--radix-accordion-content-height)" }
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: 0 }
+ }
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out"
+ }
+ }
},
- plugins: [],
-}
+ plugins: [
+ require("tailwindcss-animate")
+ ]
+}
\ No newline at end of file
diff --git a/Client/tsconfig.json b/Client/tsconfig.json
deleted file mode 100644
index 3a15bf6..0000000
--- a/Client/tsconfig.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2017",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "forceConsistentCasingInFileNames": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "node",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": [
- "./*"
- ]
- }
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts"
- ],
- "exclude": [
- "node_modules"
- ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 0080721..6ac369d 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,152 @@
-
+
+# 🕸️ Welcome To Shortify - Modern URL Shortener 🕸️
-# URL Shortener
+Shortify is a modern, full‑stack URL shortening service built with Go (backend), MongoDB (data store), Clerk (authentication), and Next.js (frontend). It's designed for rapid development and production readiness with clean APIs and a polished UI.
-A simple URL shortening service built in Go
+---
-## Overview
+## Tech Stack
-This project provides a basic URL-shortening service implemented in Go. It allows users to shorten long URLs into more manageable and shareable links. The service also includes a redirect feature to redirect users from the shortened URL to the original long URL.
+- **Backend:** Go
+- **Database:** MongoDB
+- **Auth:** Clerk
+- **Frontend:** Next.js
-## Installation
+---
-To use the URL shortener, you need to have Go installed on your system. You can download and install Go from the [official Go website](https://go.dev/).
+## Key Features
-Clone the repository to your local machine:
+- Shorten URLs into vanity links (username + slug)
+- Fast redirect service for shortened links
+- User management and authentication via Clerk
+- MongoDB for reliable storage of users and links
+- Next.js frontend with responsive UI and Clerk integration
-```sh
-git clone https://github.com/Suraj-Encoding/Shortify.git
-```
+---
+
+## Project Structure (high level)
+
+- `Server/` — Go backend, API handlers, app logic and MongoDB integration
+- `Client/` — Next.js frontend, components, and Clerk auth routes
+- `README.md` — This file
+
+---
-## Usage
+## Quick Start
-### Running the Server
+Prerequisites:
-Navigate to the project directory and run the following command to start the server:
+- Go 1.20+ installed
+- MongoDB accessible (local or hosted)
+- Clerk account and API keys (for auth)
-```sh
-go run main.go
+1. Clone the repo
+
+```bash
+mkdir Shortify
+cd Shortify
+git clone https://github.com/Suraj-Encoding/Shortify.git .
```
-The server will start listening on port `8080` by default. You can change the port in the `main.go` file if needed.
+2. Configure application settings (local & production)
+
+Before running the app locally or deploying, make sure your runtime configuration is in place:
+
+- Local development: copy `Server/.env.example` to `Server/.env` and `Client/.env.example` to `Client/.env`, then open those files and fill in values specific to your environment. Keep sensitive values out of version control.
+
+- Production: configure required settings and secrets in your hosting platform's environment manager (Vercel, Render, etc.). Ensure callback/webhook URLs (for Clerk and other services) point to your deployed URLs.
-### Shortening a URL
+Use the example files in `Server/.env.example` and `Client/.env.example` as the authoritative list of keys to provide; the README avoids enumerating individual variable names to keep configuration details centralized in the example files.
-To shorten a URL, send a POST request to the `/shorten` endpoint with a JSON payload containing the original URL:
+3. Run the backend
-```sh
-curl -X POST http://localhost:8080/shorten -H "Content-Type: application/json" -d '{"url": "https://www.linkedin.com/username"}'
+```bash
+cd Server
+go mod tidy
+go build main.go
+./main
```
-The response will contain a JSON object with the shortened URL:
+4. Frontend: install and run
-```json
-{
- "short_url": "http://localhost:8080/redirect/a2352b"
-}
+```bash
+cd Client
+npm install
+npm run build
+npm run dev
```
-### Redirecting to the Original URL
+The frontend typically runs on `http://localhost:3000` and the backend on `http://localhost:3001` (configurable).
-To redirect to the original URL, visit the shortened URL in your browser or send a GET request to the `/redirect/{id}` endpoint, where `{id}` is the shortened URL ID:
+---
-```sh
-curl http://localhost:8080/redirect/a2352b
-```
+## Common Endpoints
+
+- Redirect (public): `GET /{username}/{slug}` — redirects to the destination URL of the link
+- API base: `/api/v1`
+ - User webhook: `POST /api/v1/user/webhook` (Clerk)
+ - Update username: `PUT /api/v1/user/username`
+ - Links CRUD: under `/api/v1/link`
+
+---
+
+## Testing & Development Tips
+
+- Use `curl -v http://localhost:3001/surajdalvi1/github` to test redirects.
+- Run `go build main.go` to check for backend compile errors.
+- Ensure Clerk webhooks point to the server's `/api/v1/user/webhook` during integration.
+
+---
+
+## Deployment
-This will redirect you to the original URL associated with the shortened URL.
+Below are quick deployment flows for the frontend (Vercel) and backend (Render). These are minimal steps — adapt them for your environment and secrets manager.
+
+- Vercel (Frontend)
+
+ 1. Create a Vercel project and connect it to this repository.
+ 2. Set the Project Root to `Client` (or import as a monorepo and point the app to `Client`).
+ 3. Build command: `npm run build`
+ 4. Output directory: leave default (Next.js handled by Vercel).
+ 5. Configure required settings and secrets in Vercel (Dashboard → Settings → Environment Variables).
+ 6. Deploy — Vercel will run builds on every push.
+
+- Render (Backend)
+
+ 1. Create a new Web Service on Render and connect it to the repository.
+ 2. Set the root directory to `Server`.
+ 3. Environment & Build:
+ - Build command: `go build main.go`
+ - Start command: `./main`
+ 4. Configure required settings and secrets in Render (Service settings → Environment).
+ 5. Deploy — Render will build and start the service; check logs for startup errors.
+
+Tips:
+
+- Use Vercel for the Next.js frontend (serverless/edge‑optimized) and Render (or similar) for the Go backend.
+- Keep production secrets in the platform's environment manager — never commit them.
+- If using webhooks (Clerk), configure callback URLs in Clerk to point to your deployed `POST /api/v1/user/webhook` endpoint.
+
+---
## Contributing
-Contributions are welcome! Feel free to fork the repository and submit pull requests.
\ No newline at end of file
+Contributions welcome — open issues or submit PRs. Follow these steps:
+
+1. Fork the repo
+2. Create a feature branch
+3. Make changes and run `go build` and `npm run dev` locally
+4. Open a PR with a clear description
+
+---
+
+## License
+
+This project uses the license in the repository. Feel free to adapt as needed.
+
+---
+
+## Acknowledgments
+Enjoy Shortify — deployment and CI-CD guidance have been added above. Open an issue or PR if you want badges, examples, or more detailed deployment templates.
+
+---
\ No newline at end of file
diff --git a/Server/.dockerignore b/Server/.dockerignore
index 8c9e44d..dd6bcd2 100644
--- a/Server/.dockerignore
+++ b/Server/.dockerignore
@@ -1,10 +1,7 @@
## Docker Ignore Files ##
-# OS Waste File
+# 'OS Waste' File
*.DS_Store
-# VSCode Waste File
-*.vscode
-
-# AWS EC2 Instance Secret Key-Value Pair
-*/key
\ No newline at end of file
+# 'VSCode Waste' File
+*.vscode
\ No newline at end of file
diff --git a/Server/.env.example b/Server/.env.example
index d18dd81..325ef52 100644
--- a/Server/.env.example
+++ b/Server/.env.example
@@ -1,4 +1,4 @@
-## Environment Variables ##
+## (Example) Environment Variables ##
# Server 'Port'
PORT = "Your_Port"
diff --git a/Server/.gitattributes b/Server/.gitattributes
new file mode 100644
index 0000000..764a6fe
--- /dev/null
+++ b/Server/.gitattributes
@@ -0,0 +1,4 @@
+## Git Attributes ##
+
+# Auto detect the 'text' files and perform the 'LF' normalization
+* text=auto
diff --git a/Server/Dockerfile b/Server/Dockerfile
index 2f01de4..afe3237 100644
--- a/Server/Dockerfile
+++ b/Server/Dockerfile
@@ -1,29 +1,29 @@
-## Dockerfile : For Go Application ##
+## DockerFile: For "Go" Application ##
-## Stage 1: Build the Go application
+## Stage-1: Build the "Go" application
-# Start from the latest golang base image (alpine)
+# Start from the latest "golang" base image (alpine)
FROM golang:1.23.0-alpine3.20 AS build
-# Set `app` as a CWD inside the container
+# Set the 'app' as a 'CWD' inside the 'container'
WORKDIR /app
-# Copy go.mod and go.sum files first to leverage Docker layer caching
+# Copy the 'go.mod' and 'go.sum' files first to 'leverage' the 'Docker' layer 'caching'
COPY go.mod go.sum ./
-# Download all dependencies using tidy command
+# Download all the 'dependencies' using the 'tidy' command
RUN go mod tidy
-# Copy the rest of the application code
+# Copy the rest of the "Go" application code
COPY . .
-## Stage 2: Run the Go application
+## Stage-2: Run the "Go" application
-# Build the Go app
+# Build the "Go" app
RUN go build -o main .
-# Expose port 3000 to the outside world
+# Expose the port "3000" to the 'outside' world
EXPOSE 3000
-# Run the binary program
+# Run the 'binary' program
CMD ["/app/main"]
\ No newline at end of file
diff --git a/Server/api/link.go b/Server/api/link.go
index 3a71d1f..69a2f4e 100644
--- a/Server/api/link.go
+++ b/Server/api/link.go
@@ -20,7 +20,6 @@ func CreateLink(w http.ResponseWriter, r *http.Request) {
// # Get the 'link' data from the 'request'
contentLength := r.ContentLength
if contentLength == 0 {
- utils.LogError(err, "API.CreateLink")
errMsg = "Empty request body provided for the create link"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -50,7 +49,6 @@ func CreateLink(w http.ResponseWriter, r *http.Request) {
var link *schema.Link
if linkForm.Data == nil {
- utils.LogError(err, "API.CreateLink")
errMsg = "Empty link data provided"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -64,7 +62,6 @@ func CreateLink(w http.ResponseWriter, r *http.Request) {
// # Validate the 'link' data
if link.DestinationURL == nil || utils.GetStringValue(link.DestinationURL) == "" {
- utils.LogError(err, "API.CreateLink")
errMsg = "Link destination URL cannot be empty"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -74,7 +71,6 @@ func CreateLink(w http.ResponseWriter, r *http.Request) {
return
}
if link.Slug == nil || utils.GetStringValue(link.Slug) == "" {
- utils.LogError(err, "API.CreateLink")
errMsg = "Link slug cannot be empty"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -100,7 +96,6 @@ func CreateLink(w http.ResponseWriter, r *http.Request) {
// # Create the 'link'
res, err := app.CreateLink(clerkUserID, link)
if err != nil {
- utils.LogError(err, "API.CreateLink")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
@@ -122,7 +117,6 @@ func UpdateLink(w http.ResponseWriter, r *http.Request) {
// # Get the 'link' data from the 'request'
contentLength := r.ContentLength
if contentLength == 0 {
- utils.LogError(err, "API.UpdateLink")
errMsg = "Empty request body provided for the update link"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -152,7 +146,6 @@ func UpdateLink(w http.ResponseWriter, r *http.Request) {
var link *schema.Link
if linkForm.Data == nil {
- utils.LogError(err, "API.UpdateLink")
errMsg = "Empty link data provided"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -166,7 +159,6 @@ func UpdateLink(w http.ResponseWriter, r *http.Request) {
// # Validate the 'link' data
if link.DestinationURL == nil || utils.GetStringValue(link.DestinationURL) == "" {
- utils.LogError(err, "API.UpdateLink")
errMsg = "Link destination URL cannot be empty"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -176,7 +168,6 @@ func UpdateLink(w http.ResponseWriter, r *http.Request) {
return
}
if link.Slug == nil || utils.GetStringValue(link.Slug) == "" {
- utils.LogError(err, "API.UpdateLink")
errMsg = "Link slug cannot be empty"
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
@@ -199,10 +190,35 @@ func UpdateLink(w http.ResponseWriter, r *http.Request) {
return
}
+ // # Get the 'link ID' from the 'query params'
+ linkIDStr := r.URL.Query().Get("link_id")
+ linkIDStr = utils.GetTrimmedValue(linkIDStr)
+ if linkIDStr == "" {
+ errMsg = "Empty link ID provided"
+ errRes = schema.Error{
+ StatusCode: http.StatusBadRequest,
+ Message: errMsg,
+ }
+ utils.SetAppError(w, &errRes)
+ return
+ }
+
+ // # Convert the 'link ID' from type 'string' to type 'object ID'
+ linkID, err := primitive.ObjectIDFromHex(linkIDStr)
+ if err != nil {
+ utils.LogError(err, "API.DeleteLink")
+ errMsg = "Invalid link ID provided"
+ errRes = schema.Error{
+ StatusCode: http.StatusBadRequest,
+ Message: errMsg,
+ }
+ utils.SetAppError(w, &errRes)
+ return
+ }
+
// # Update the 'link'
- res, err := app.UpdateLink(clerkUserID, link)
+ res, err := app.UpdateLink(clerkUserID, &linkID, link)
if err != nil {
- utils.LogError(err, "API.UpdateLink")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
@@ -250,7 +266,6 @@ func DeleteLink(w http.ResponseWriter, r *http.Request) {
// # Delete the 'link'
res, err := app.DeleteLink(&linkID)
if err != nil {
- utils.LogError(err, "API.DeleteLink")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
@@ -303,7 +318,6 @@ func GetLink(w http.ResponseWriter, r *http.Request) {
// # Get the 'link'
res, err := app.GetLink(filter)
if err != nil {
- utils.LogError(err, "API.GetLink")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
@@ -338,7 +352,6 @@ func GetLinks(w http.ResponseWriter, r *http.Request) {
// # Get the 'links'
res, err := app.GetLinks(clerkUserID)
if err != nil {
- utils.LogError(err, "API.GetLinks")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
diff --git a/Server/api/redirect.go b/Server/api/redirect.go
index 8652b5e..1502244 100644
--- a/Server/api/redirect.go
+++ b/Server/api/redirect.go
@@ -19,6 +19,8 @@ func RedirectURL(w http.ResponseWriter, r *http.Request) {
// # Get the 'URL Path'
urlPath := strings.Trim(r.URL.Path, "/")
+ // # Print the 'URL Path'
+ fmt.Println("🔗 URL Path:", urlPath)
urlPathParts := strings.Split(urlPath, "/")
urlPathPartLength := len(urlPathParts)
diff --git a/Server/api/user.go b/Server/api/user.go
index 3b47bec..d03f2e5 100644
--- a/Server/api/user.go
+++ b/Server/api/user.go
@@ -23,8 +23,9 @@ func HandleClerkUserWebhook(w http.ResponseWriter, r *http.Request) {
// # Get the 'clerk user' data from the 'request'
contentLength := r.ContentLength
if contentLength == 0 {
- utils.LogError(err, "API.HandleClerkUserWebhook")
errMsg = "Empty request body provided for the clerk user webhook"
+ err = errors.New(errMsg)
+ utils.LogError(err, "API.HandleClerkUserWebhook")
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
Message: errMsg,
@@ -53,8 +54,9 @@ func HandleClerkUserWebhook(w http.ResponseWriter, r *http.Request) {
var clerkUser *schema.ClerkUser
if clerkUserForm.Data == nil {
- utils.LogError(err, "API.HandleClerkUserWebhook")
errMsg = "Empty clerk user data provided"
+ err = errors.New(errMsg)
+ utils.LogError(err, "API.HandleClerkUserWebhook")
errRes = schema.Error{
StatusCode: http.StatusBadRequest,
Message: errMsg,
@@ -186,7 +188,6 @@ func UpdateUsername(w http.ResponseWriter, r *http.Request) {
// # Update the 'username'
res, err := app.UpdateUsername(clerkUserID, username)
if err != nil {
- utils.LogError(err, "API.UpdateUsername")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
@@ -226,7 +227,6 @@ func GetUser(w http.ResponseWriter, r *http.Request) {
// # Get the 'user'
user, err := app.GetUser(filter)
if err != nil {
- utils.LogError(err, "API.GetUser")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
@@ -248,7 +248,6 @@ func GetUsers(w http.ResponseWriter, r *http.Request) {
// # Get the 'users'
users, err := app.GetUsers()
if err != nil {
- utils.LogError(err, "API.GetUsers")
errMsg = err.Error()
errRes = schema.Error{
StatusCode: http.StatusInternalServerError,
diff --git a/Server/app/link.go b/Server/app/link.go
index c9ddca5..1a906fe 100644
--- a/Server/app/link.go
+++ b/Server/app/link.go
@@ -13,6 +13,7 @@ import (
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
)
// # Create 'Link'
@@ -37,11 +38,10 @@ func CreateLink(clerkUserID string, link *schema.Link) (*string, error) {
"slug": utils.GetStringValue(link.Slug),
}
- // # Check if the provided 'slug' is already 'assigned' to another 'link'
+ // # Check if the provided 'slug' is already 'assigned' to another 'link' of the given 'user'
Link, err := GetLink(filter)
if Link != nil {
- utils.LogError(err, "App.UpdateLink")
- errMsg = "The provided slug is already assigned to another link. Please choose a different one."
+ errMsg = "The provided slug is already assigned to another link. Please choose a different slug."
err = errors.New(errMsg)
return nil, err
}
@@ -99,7 +99,7 @@ func CreateLink(clerkUserID string, link *schema.Link) (*string, error) {
}
// # Update 'Link'
-func UpdateLink(clerkUserID string, link *schema.Link) (*string, error) {
+func UpdateLink(clerkUserID string, linkID *primitive.ObjectID, link *schema.Link) (*string, error) {
ctx := context.TODO()
var err error
@@ -112,15 +112,19 @@ func UpdateLink(clerkUserID string, link *schema.Link) (*string, error) {
"slug": utils.GetStringValue(link.Slug),
}
- // # Check if the provided 'slug' already 'exists'
+ // # Check if the provided 'slug' is already 'assigned' to another 'link' of the given 'user'
Link, err := GetLink(filter)
- if Link != nil {
- utils.LogError(err, "App.UpdateLink")
- errMsg = "The provided slug already exists. Please use a different slug."
+ if Link != nil && Link.ID.Hex() != linkID.Hex() {
+ errMsg = "The provided slug is already assigned to another link. Please choose a different slug."
err = errors.New(errMsg)
return nil, err
}
+ // # Base 'Filter'
+ filter = bson.M{
+ "_id": linkID,
+ }
+
collection := db.GetMongoCollection(model.UserColl)
// # Get the 'current system time' in the 'IST' timezone
@@ -264,7 +268,6 @@ func GetLink(filter bson.M) (*model.Link, error) {
}
}
if link.IsUserDeleted {
- utils.LogError(err, "App.GetLink")
errMsg = "User associated with the given link is already deleted"
err = errors.New(errMsg)
return nil, err
@@ -290,10 +293,19 @@ func GetLinks(clerkUserID string) ([]*model.Link, error) {
},
}
+ // # Base 'Sort'
+ sort := bson.M{
+ "created_at": -1, // # Sort the 'documents' (links) by the 'created_at' field in the 'descending' order
+ }
+
+ // # Set the 'Find' options
+ opts := options.Find()
+ opts.SetSort(sort)
+
var links []*model.Link
// # Get the 'links'
- cur, err := collection.Find(ctx, filter)
+ cur, err := collection.Find(ctx, filter, opts)
if err != nil {
utils.LogError(err, "App.GetLinks")
errMsg = "Failed to get the links"
diff --git a/Server/app/user.go b/Server/app/user.go
index 2c2ffd9..dc15ec6 100644
--- a/Server/app/user.go
+++ b/Server/app/user.go
@@ -74,16 +74,16 @@ func CreateUser(clerkUser *schema.ClerkUser) (*string, error) {
return nil, err
}
- createdBy := "clerk_webhook"
+ user := "clerk_webhook"
- user := model.User{
+ User := model.User{
ID: utils.GetNewObjectID(),
ClerkUserID: utils.GetTrimmedValue(clerkUser.ID),
FirstName: utils.GetStringValue(clerkUser.FirstName),
LastName: utils.GetStringValue(clerkUser.LastName),
Email: utils.GetStringValue(clerkUser.EmailAddresss[0].EmailAddress),
CreatedAt: time,
- CreatedBy: createdBy,
+ CreatedBy: utils.GetActionUser(user),
}
// # Generate the 'user code'
@@ -94,13 +94,13 @@ func CreateUser(clerkUser *schema.ClerkUser) (*string, error) {
err = errors.New(errMsg)
return nil, err
}
- user.Code = *userCode
+ User.Code = *userCode
- username := fmt.Sprintf("%s%s%d", strings.ToLower(user.FirstName), strings.ToLower(user.LastName), user.Code)
- user.Username = username
+ username := fmt.Sprintf("%s%s%d", strings.ToLower(User.FirstName), strings.ToLower(User.LastName), User.Code)
+ User.Username = username
// # Insert the 'new user'
- _, err = collection.InsertOne(ctx, &user)
+ _, err = collection.InsertOne(ctx, &User)
if err != nil {
utils.LogError(err, "App.CreateUser")
errMsg = "Failed to create the new user"
@@ -130,7 +130,7 @@ func UpdateUser(clerkUser *schema.ClerkUser) (*string, error) {
return nil, err
}
- updatedBy := "clerk_webhook"
+ user := "clerk_webhook"
// # Base 'Filter'
filter := bson.M{
@@ -174,7 +174,7 @@ func UpdateUser(clerkUser *schema.ClerkUser) (*string, error) {
if setLength > 0 || unsetLength > 0 {
set["updated_at"] = time
- set["updated_by"] = updatedBy
+ set["updated_by"] = utils.GetActionUser(user)
// # Base 'Update'
update := bson.M{
@@ -241,7 +241,7 @@ func DeleteUser(clerkUser *schema.ClerkUser) (*string, error) {
userSet := bson.M{
"is_deleted": clerkUser.Deleted,
"deleted_at": time,
- "deleted_by": user,
+ "deleted_by": utils.GetActionUser(user),
}
// # Base 'User Update'
@@ -274,7 +274,7 @@ func DeleteUser(clerkUser *schema.ClerkUser) (*string, error) {
linkSet := bson.M{
"is_user_deleted": clerkUser.Deleted,
"updated_at": time,
- "updated_by": user,
+ "updated_by": utils.GetActionUser(user),
}
// # Base 'Link Update'
@@ -312,8 +312,11 @@ func UpdateUsername(clerkUserID, username string) (*string, error) {
// # Check if the provided 'username' is already 'taken' by the another 'user'
user, err := GetUser(filter)
if user != nil {
- utils.LogError(err, "App.UpdateUsername")
- errMsg = "Username already taken. Please choose another one."
+ if user.ClerkUserID == clerkUserID {
+ errMsg = "This is already your current username. Please choose a new username."
+ } else {
+ errMsg = "Username already taken by another user. Please choose a different username."
+ }
err = errors.New(errMsg)
return nil, err
}
@@ -420,9 +423,18 @@ func GetUsers() ([]*model.User, error) {
},
}
+ // # Base 'Sort'
+ sort := bson.M{
+ "created_at": -1, // # Sort the 'documents' (users) by the 'created_at' field in the 'descending' order
+ }
+
+ // # Set the 'Find' options
+ opts := options.Find()
+ opts.SetSort(sort)
+
var users []*model.User
- cur, err := collection.Find(ctx, filter)
+ cur, err := collection.Find(ctx, filter, opts)
if err != nil {
utils.LogError(err, "App.GetUsers")
errMsg = "Failed to get the users"
diff --git a/Server/router/handler.go b/Server/router/handler.go
index efa3033..764d911 100644
--- a/Server/router/handler.go
+++ b/Server/router/handler.go
@@ -13,6 +13,7 @@ func AllowCORS(router *mux.Router) http.Handler {
// # Set the 'CORS' headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
diff --git a/Server/router/router.go b/Server/router/router.go
index 841045a..7d7a769 100644
--- a/Server/router/router.go
+++ b/Server/router/router.go
@@ -33,14 +33,14 @@ func SetupRouter() *mux.Router {
UserV1.HandleFunc("/webhook", api.HandleClerkUserWebhook).Methods("POST")
UserV1.HandleFunc("/username", api.UpdateUsername).Methods("PUT")
UserV1.HandleFunc("/", api.GetUser).Methods("GET")
- UserV1.HandleFunc("/all", api.GetUsers).Methods("GET")
+ UserV1.HandleFunc("/list", api.GetUsers).Methods("GET")
// # 'Link' Routes
LinkV1.HandleFunc("/", api.CreateLink).Methods("POST")
LinkV1.HandleFunc("/", api.UpdateLink).Methods("PUT")
LinkV1.HandleFunc("/", api.DeleteLink).Methods("DELETE")
LinkV1.HandleFunc("/", api.GetLink).Methods("GET")
- LinkV1.HandleFunc("/", api.GetLinks).Methods("GET")
+ LinkV1.HandleFunc("/list", api.GetLinks).Methods("GET")
// # Return the 'configured' mux 'router' instance
return router
diff --git a/Server/utils/user.go b/Server/utils/user.go
index 7c0504b..bb7975b 100644
--- a/Server/utils/user.go
+++ b/Server/utils/user.go
@@ -1,10 +1,10 @@
package utils
// # Get 'Action User'
-func GetActionUser(clerkUserID string) string {
+func GetActionUser(user string) string {
var actionUser string
- if clerkUserID != "" {
- actionUser = clerkUserID
+ if user != "" {
+ actionUser = user
} else {
actionUser = "system"
}