瀏覽代碼

Merge branch 'main' of https://github.com/fosrl/pangolin

Owen Schwartz 7 月之前
父節點
當前提交
6530fff87e
共有 33 個文件被更改,包括 822 次插入603 次删除
  1. 2 2
      Dockerfile
  2. 60 58
      package.json
  3. 1 1
      server/apiServer.ts
  4. 86 78
      server/config.ts
  5. 1 1
      server/emails/templates/SendInviteLink.tsx
  6. 5 3
      src/api/index.ts
  7. 22 27
      src/app/[orgId]/settings/access/roles/components/RolesTable.tsx
  8. 77 65
      src/app/[orgId]/settings/access/users/components/UsersTable.tsx
  9. 9 6
      src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx
  10. 1 1
      src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx
  11. 63 58
      src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx
  12. 1 1
      src/app/[orgId]/settings/resources/[resourceId]/layout.tsx
  13. 47 47
      src/app/[orgId]/settings/resources/components/ResourcesTable.tsx
  14. 11 11
      src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx
  15. 42 42
      src/app/[orgId]/settings/share-links/components/ShareLinksTable.tsx
  16. 1 1
      src/app/[orgId]/settings/sites/[niceId]/layout.tsx
  17. 73 63
      src/app/[orgId]/settings/sites/components/SitesTable.tsx
  18. 6 6
      src/app/globals.css
  19. 2 2
      src/app/invite/InviteStatusCard.tsx
  20. 47 13
      src/components/Credenza.tsx
  21. 12 11
      src/components/Enable2FaForm.tsx
  22. 2 2
      src/components/SidebarSettings.tsx
  23. 10 3
      src/components/sidebar-nav.tsx
  24. 1 1
      src/components/ui/command.tsx
  25. 2 2
      src/components/ui/dialog.tsx
  26. 90 90
      src/components/ui/drawer.tsx
  27. 1 1
      src/components/ui/input-otp.tsx
  28. 2 2
      src/components/ui/input.tsx
  29. 1 1
      src/components/ui/select.tsx
  30. 140 0
      src/components/ui/sheet.tsx
  31. 1 1
      src/components/ui/toast.tsx
  32. 1 1
      src/hooks/useToast.ts
  33. 2 2
      src/providers/ThemeProvider.tsx

+ 2 - 2
Dockerfile

@@ -4,7 +4,7 @@ WORKDIR /app
 
 COPY package.json ./
 
-RUN npm install --legacy-peer-deps
+RUN npm install
 
 COPY . .
 
@@ -20,7 +20,7 @@ WORKDIR /app
 
 COPY package.json ./
 
-RUN npm install --omit=dev --legacy-peer-deps
+RUN npm install --omit=dev
 
 COPY --from=builder /app/.next ./.next
 COPY --from=builder /app/dist ./dist

+ 60 - 58
package.json

@@ -13,95 +13,97 @@
         "email": "email dev --dir server/emails/templates --port 3005"
     },
     "dependencies": {
-        "@hookform/resolvers": "3.9.0",
-        "@node-rs/argon2": "1.8.3",
+        "@hookform/resolvers": "3.9.1",
+        "@node-rs/argon2": "2.0.2",
         "@oslojs/crypto": "1.0.1",
         "@oslojs/encoding": "1.1.0",
-        "@radix-ui/react-avatar": "1.1.1",
-        "@radix-ui/react-checkbox": "1.1.2",
-        "@radix-ui/react-dialog": "1.1.2",
-        "@radix-ui/react-dropdown-menu": "2.1.2",
-        "@radix-ui/react-icons": "1.3.0",
-        "@radix-ui/react-label": "2.1.0",
-        "@radix-ui/react-popover": "1.1.2",
-        "@radix-ui/react-radio-group": "1.2.1",
-        "@radix-ui/react-select": "2.1.2",
-        "@radix-ui/react-separator": "1.1.0",
-        "@radix-ui/react-slot": "1.1.0",
-        "@radix-ui/react-switch": "1.1.1",
-        "@radix-ui/react-tabs": "1.1.1",
-        "@radix-ui/react-toast": "1.2.2",
-        "@react-email/components": "0.0.28",
-        "@react-email/tailwind": "1.0.2",
-        "@tanstack/react-table": "8.20.5",
-        "axios": "1.7.7",
-        "better-sqlite3": "11.3.0",
-        "class-variance-authority": "0.7.0",
+        "@radix-ui/react-avatar": "1.1.2",
+        "@radix-ui/react-checkbox": "1.1.3",
+        "@radix-ui/react-dialog": "1.1.4",
+        "@radix-ui/react-dropdown-menu": "2.1.4",
+        "@radix-ui/react-icons": "1.3.2",
+        "@radix-ui/react-label": "2.1.1",
+        "@radix-ui/react-popover": "1.1.4",
+        "@radix-ui/react-radio-group": "1.2.2",
+        "@radix-ui/react-select": "2.1.4",
+        "@radix-ui/react-separator": "1.1.1",
+        "@radix-ui/react-slot": "1.1.1",
+        "@radix-ui/react-switch": "1.1.2",
+        "@radix-ui/react-tabs": "1.1.2",
+        "@radix-ui/react-toast": "1.2.4",
+        "@react-email/components": "0.0.31",
+        "@react-email/tailwind": "1.0.4",
+        "@tanstack/react-table": "8.20.6",
+        "axios": "1.7.9",
+        "better-sqlite3": "11.7.0",
+        "class-variance-authority": "0.7.1",
         "clsx": "2.1.1",
-        "cmdk": "1.0.0",
-        "cookie-parser": "1.4.6",
+        "cmdk": "1.0.4",
+        "cookie-parser": "1.4.7",
         "cors": "2.8.5",
-        "drizzle-orm": "0.33.0",
-        "emblor": "1.4.6",
-        "eslint": "9.15.0",
-        "eslint-config-next": "15.0.3",
-        "express": "4.21.0",
-        "express-rate-limit": "7.4.0",
+        "drizzle-orm": "0.38.3",
+        "emblor": "1.4.7",
+        "eslint": "9.17.0",
+        "eslint-config-next": "15.1.3",
+        "express": "4.21.2",
+        "express-rate-limit": "7.5.0",
         "glob": "11.0.0",
-        "helmet": "7.1.0",
+        "helmet": "8.0.0",
         "http-errors": "2.0.0",
-        "input-otp": "1.2.4",
+        "input-otp": "1.4.1",
         "js-yaml": "4.1.0",
-        "lucide-react": "0.447.0",
+        "lucide-react": "0.469.0",
         "moment": "2.30.1",
-        "next": "15.0.1",
-        "next-themes": "0.3.0",
+        "next": "15.1.3",
+        "next-themes": "0.4.4",
         "node-fetch": "3.3.2",
-        "nodemailer": "6.9.15",
+        "nodemailer": "6.9.16",
         "oslo": "1.2.1",
         "qrcode.react": "4.2.0",
-        "react": "19.0.0-rc.1",
-        "react-dom": "19.0.0-rc.1",
-        "react-hook-form": "7.53.0",
+        "react": "19.0.0",
+        "react-dom": "19.0.0",
+        "react-hook-form": "7.54.2",
         "rebuild": "0.1.2",
         "semver": "7.6.3",
-        "tailwind-merge": "2.5.3",
+        "tailwind-merge": "2.6.0",
         "tailwindcss-animate": "1.0.7",
-        "vaul": "1.1.1",
-        "winston": "3.14.2",
+        "vaul": "1.1.2",
+        "winston": "3.17.0",
         "winston-daily-rotate-file": "5.0.0",
         "ws": "8.18.0",
-        "zod": "3.23.8",
+        "zod": "3.24.1",
         "zod-validation-error": "3.4.0"
     },
     "devDependencies": {
-        "@dotenvx/dotenvx": "1.14.2",
+        "@dotenvx/dotenvx": "1.32.0",
         "@esbuild-plugins/tsconfig-paths": "0.1.2",
-        "@types/better-sqlite3": "7.6.11",
-        "@types/cookie-parser": "1.4.7",
+        "@types/better-sqlite3": "7.6.12",
+        "@types/cookie-parser": "1.4.8",
         "@types/cors": "2.8.17",
         "@types/express": "5.0.0",
         "@types/js-yaml": "4.0.9",
-        "@types/node": "^20",
-        "@types/nodemailer": "6.4.16",
-        "@types/react": "npm:types-react@19.0.0-rc.1",
-        "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
+        "@types/node": "^22",
+        "@types/nodemailer": "6.4.17",
+        "@types/react": "19.0.2",
+        "@types/react-dom": "19.0.2",
         "@types/semver": "7.5.8",
         "@types/ws": "8.5.13",
         "@types/yargs": "17.0.33",
-        "drizzle-kit": "0.24.2",
-        "esbuild": "0.20.1",
-        "esbuild-node-externals": "1.13.0",
+        "drizzle-kit": "0.30.1",
+        "esbuild": "0.24.2",
+        "esbuild-node-externals": "1.16.0",
         "postcss": "^8",
-        "react-email": "3.0.2",
-        "tailwindcss": "^3.4.1",
+        "react-email": "3.0.4",
+        "tailwindcss": "^3.4.17",
         "tsc-alias": "1.8.10",
-        "tsx": "4.19.1",
+        "tsx": "4.19.2",
         "typescript": "^5",
         "yargs": "17.7.2"
     },
     "overrides": {
-        "@types/react": "npm:types-react@19.0.0-rc.1",
-        "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
+        "emblor": {
+            "react": "19.0.0",
+            "react-dom": "19.0.0"
+        }
     }
 }

+ 1 - 1
server/apiServer.ts

@@ -1,4 +1,4 @@
-import express, { Request, Response } from "express";
+import express from "express";
 import cors from "cors";
 import cookieParser from "cookie-parser";
 import config from "@server/config";

+ 86 - 78
server/config.ts

@@ -14,7 +14,10 @@ const portSchema = z.number().positive().gt(0).lte(65535);
 
 const environmentSchema = z.object({
     app: z.object({
-        base_url: z.string().url().transform((url) => url.toLowerCase()),
+        base_url: z
+            .string()
+            .url()
+            .transform((url) => url.toLowerCase()),
         log_level: z.enum(["debug", "info", "warn", "error"]),
         save_logs: z.boolean()
     }),
@@ -76,97 +79,102 @@ const environmentSchema = z.object({
         .optional()
 });
 
-const loadConfig = (configPath: string) => {
-    try {
-        const yamlContent = fs.readFileSync(configPath, "utf8");
-        const config = yaml.load(yamlContent);
-        return config;
-    } catch (error) {
-        if (error instanceof Error) {
-            throw new Error(
-                `Error loading configuration file: ${error.message}`
-            );
-        }
-        throw error;
-    }
-};
-
-const configFilePath1 = path.join(APP_PATH, "config.yml");
-const configFilePath2 = path.join(APP_PATH, "config.yaml");
-
-let environment: any;
-if (fs.existsSync(configFilePath1)) {
-    environment = loadConfig(configFilePath1);
-} else if (fs.existsSync(configFilePath2)) {
-    environment = loadConfig(configFilePath2);
-}
-if (!environment) {
-    const exampleConfigPath = path.join(__DIRNAME, "config.example.yml");
-    if (fs.existsSync(exampleConfigPath)) {
+export function getConfig() {
+    const loadConfig = (configPath: string) => {
         try {
-            const exampleConfigContent = fs.readFileSync(
-                exampleConfigPath,
-                "utf8"
-            );
-            fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8");
-            environment = loadConfig(configFilePath1);
+            const yamlContent = fs.readFileSync(configPath, "utf8");
+            const config = yaml.load(yamlContent);
+            return config;
         } catch (error) {
             if (error instanceof Error) {
                 throw new Error(
-                    `Error creating configuration file from example: ${error.message}`
+                    `Error loading configuration file: ${error.message}`
                 );
             }
             throw error;
         }
-    } else {
-        throw new Error(
-            "No configuration file found and no example configuration available"
-        );
+    };
+
+    const configFilePath1 = path.join(APP_PATH, "config.yml");
+    const configFilePath2 = path.join(APP_PATH, "config.yaml");
+
+    let environment: any;
+    if (fs.existsSync(configFilePath1)) {
+        environment = loadConfig(configFilePath1);
+    } else if (fs.existsSync(configFilePath2)) {
+        environment = loadConfig(configFilePath2);
+    }
+    if (!environment) {
+        const exampleConfigPath = path.join(__DIRNAME, "config.example.yml");
+        if (fs.existsSync(exampleConfigPath)) {
+            try {
+                const exampleConfigContent = fs.readFileSync(
+                    exampleConfigPath,
+                    "utf8"
+                );
+                fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8");
+                environment = loadConfig(configFilePath1);
+            } catch (error) {
+                if (error instanceof Error) {
+                    throw new Error(
+                        `Error creating configuration file from example: ${error.message}`
+                    );
+                }
+                throw error;
+            }
+        } else {
+            throw new Error(
+                "No configuration file found and no example configuration available"
+            );
+        }
     }
-}
 
-if (!environment) {
-    throw new Error("No configuration file found");
-}
+    if (!environment) {
+        throw new Error("No configuration file found");
+    }
 
-const parsedConfig = environmentSchema.safeParse(environment);
+    const parsedConfig = environmentSchema.safeParse(environment);
 
-if (!parsedConfig.success) {
-    const errors = fromError(parsedConfig.error);
-    throw new Error(`Invalid configuration file: ${errors}`);
-}
+    if (!parsedConfig.success) {
+        const errors = fromError(parsedConfig.error);
+        throw new Error(`Invalid configuration file: ${errors}`);
+    }
 
-const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
-let packageJson: any;
-if (fs.existsSync && fs.existsSync(packageJsonPath)) {
-    const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
-    packageJson = JSON.parse(packageJsonContent);
+    const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
+    let packageJson: any;
+    if (fs.existsSync && fs.existsSync(packageJsonPath)) {
+        const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
+        packageJson = JSON.parse(packageJsonContent);
 
-    if (packageJson.version) {
-        process.env.APP_VERSION = packageJson.version;
+        if (packageJson.version) {
+            process.env.APP_VERSION = packageJson.version;
+        }
     }
-}
 
-process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
-process.env.SERVER_EXTERNAL_PORT =
-    parsedConfig.data.server.external_port.toString();
-process.env.SERVER_INTERNAL_PORT =
-    parsedConfig.data.server.internal_port.toString();
-process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
-    ?.require_email_verification
-    ? "true"
-    : "false";
-process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name;
-process.env.RESOURCE_SESSION_COOKIE_NAME =
-    parsedConfig.data.server.resource_session_cookie_name;
-process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
-process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
-    ?.disable_signup_without_invite
-    ? "true"
-    : "false";
-process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
-    ?.disable_user_create_org
-    ? "true"
-    : "false";
+    process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
+    process.env.SERVER_EXTERNAL_PORT =
+        parsedConfig.data.server.external_port.toString();
+    process.env.SERVER_INTERNAL_PORT =
+        parsedConfig.data.server.internal_port.toString();
+    process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
+        ?.require_email_verification
+        ? "true"
+        : "false";
+    process.env.SESSION_COOKIE_NAME =
+        parsedConfig.data.server.session_cookie_name;
+    process.env.RESOURCE_SESSION_COOKIE_NAME =
+        parsedConfig.data.server.resource_session_cookie_name;
+    process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
+    process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
+        ?.disable_signup_without_invite
+        ? "true"
+        : "false";
+    process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
+        ?.disable_user_create_org
+        ? "true"
+        : "false";
+
+    return parsedConfig.data;
+}
 
-export default parsedConfig.data;
+export default getConfig();

+ 1 - 1
server/emails/templates/SendInviteLink.tsx

@@ -68,7 +68,7 @@ export const SendInviteLink = ({
                         <Section className="text-center my-6">
                             <Button
                                 href={inviteLink}
-                                className="rounded-lg bg-primary px-[12px] py-[9px] text-center font-semibold text-white cursor-pointer"
+                                className="rounded-lg bg-primary px-[12px] py-[9px] text-center font-semibold text-white cursor-pointer text-xl"
                             >
                                 Accept invitation to {orgName}
                             </Button>

+ 5 - 3
src/api/index.ts

@@ -8,8 +8,9 @@ export function createApiClient({ env }: { env: env }): AxiosInstance {
         return apiInstance;
     }
 
-    if (apiInstance) {
-        return apiInstance
+    if (typeof window === "undefined") {
+        // @ts-ignore
+        return;
     }
 
     let baseURL;
@@ -45,7 +46,8 @@ export const internal = axios.create({
     baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`,
     timeout: 10000,
     headers: {
-        "Content-Type": "application/json"
+        "Content-Type": "application/json",
+        "X-CSRF-Token": "x-csrf-protection"
     }
 });
 

+ 22 - 27
src/app/[orgId]/settings/access/roles/components/RolesTable.tsx

@@ -40,26 +40,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
     const { toast } = useToast();
 
     const columns: ColumnDef<RoleRow>[] = [
-        {
-            accessorKey: "name",
-            header: ({ column }) => {
-                return (
-                    <Button
-                        variant="ghost"
-                        onClick={() =>
-                            column.toggleSorting(column.getIsSorted() === "asc")
-                        }
-                    >
-                        Name
-                        <ArrowUpDown className="ml-2 h-4 w-4" />
-                    </Button>
-                );
-            }
-        },
-        {
-            accessorKey: "description",
-            header: "Description"
-        },
         {
             id: "actions",
             cell: ({ row }) => {
@@ -67,14 +47,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
 
                 return (
                     <>
-                        <div className="flex items-center justify-end">
+                        <div>
                             {roleRow.isAdmin && (
-                                <Button
-                                    variant="ghost"
-                                    className="h-8 w-8 p-0 opacity-0 cursor-default"
-                                >
-                                    Placeholder
-                                </Button>
+                                <MoreHorizontal className="h-4 w-4 opacity-0" />
                             )}
                             {!roleRow.isAdmin && (
                                 <DropdownMenu>
@@ -107,6 +82,26 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
                     </>
                 );
             }
+        },
+        {
+            accessorKey: "name",
+            header: ({ column }) => {
+                return (
+                    <Button
+                        variant="ghost"
+                        onClick={() =>
+                            column.toggleSorting(column.getIsSorted() === "asc")
+                        }
+                    >
+                        Name
+                        <ArrowUpDown className="ml-2 h-4 w-4" />
+                    </Button>
+                );
+            }
+        },
+        {
+            accessorKey: "description",
+            header: "Description"
         }
     ];
 

+ 77 - 65
src/app/[orgId]/settings/access/users/components/UsersTable.tsx

@@ -50,6 +50,64 @@ export default function UsersTable({ users: u }: UsersTableProps) {
     const { toast } = useToast();
 
     const columns: ColumnDef<UserRow>[] = [
+        {
+            id: "dots",
+            cell: ({ row }) => {
+                const userRow = row.original;
+                return (
+                    <>
+                        <div>
+                            {userRow.isOwner && (
+                                <MoreHorizontal className="h-4 w-4 opacity-0" />
+                            )}
+                            {!userRow.isOwner && (
+                                <>
+                                    <DropdownMenu>
+                                        <DropdownMenuTrigger asChild>
+                                            <Button
+                                                variant="ghost"
+                                                className="h-8 w-8 p-0"
+                                            >
+                                                <span className="sr-only">
+                                                    Open menu
+                                                </span>
+                                                <MoreHorizontal className="h-4 w-4" />
+                                            </Button>
+                                        </DropdownMenuTrigger>
+                                        <DropdownMenuContent align="end">
+                                            <DropdownMenuItem>
+                                                <Link
+                                                    href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
+                                                    className="block w-full"
+                                                >
+                                                    Manage User
+                                                </Link>
+                                            </DropdownMenuItem>
+                                            {userRow.email !== user?.email && (
+                                                <DropdownMenuItem
+                                                    onClick={() => {
+                                                        setIsDeleteModalOpen(
+                                                            true
+                                                        );
+                                                        setSelectedUser(
+                                                            userRow
+                                                        );
+                                                    }}
+                                                >
+                                                    <span className="text-red-500">
+                                                        Remove User
+                                                    </span>
+                                                </DropdownMenuItem>
+                                            )}
+                                        </DropdownMenuContent>
+                                    </DropdownMenu>
+                                </>
+                            )}
+                        </div>
+                    </>
+                );
+            }
+        },
         {
             accessorKey: "email",
             header: ({ column }) => {
@@ -114,73 +172,27 @@ export default function UsersTable({ users: u }: UsersTableProps) {
             id: "actions",
             cell: ({ row }) => {
                 const userRow = row.original;
-
                 return (
-                    <>
-                        <div className="flex items-center justify-end">
-                            {userRow.isOwner && (
-                                <Button
-                                    variant="ghost"
-                                    className="opacity-0 cursor-default"
-                                >
-                                    Placeholder
+                    <div className="flex items-center justify-end">
+                        {userRow.isOwner && (
+                            <Button
+                                variant="ghost"
+                                className="opacity-0 cursor-default"
+                            >
+                                Placeholder
+                            </Button>
+                        )}
+                        {!userRow.isOwner && (
+                            <Link
+                                href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
+                            >
+                                <Button variant={"gray"} className="ml-2">
+                                    Manage
+                                    <ArrowRight className="ml-2 w-4 h-4" />
                                 </Button>
-                            )}
-                            {!userRow.isOwner && (
-                                <>
-                                    <DropdownMenu>
-                                        <DropdownMenuTrigger asChild>
-                                            <Button
-                                                variant="ghost"
-                                                className="h-8 w-8 p-0"
-                                            >
-                                                <span className="sr-only">
-                                                    Open menu
-                                                </span>
-                                                <MoreHorizontal className="h-4 w-4" />
-                                            </Button>
-                                        </DropdownMenuTrigger>
-                                        <DropdownMenuContent align="end">
-                                            <DropdownMenuItem>
-                                                <Link
-                                                    href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
-                                                >
-                                                    Manage User
-                                                </Link>
-                                            </DropdownMenuItem>
-                                            {userRow.email !== user?.email && (
-                                                <DropdownMenuItem
-                                                    onClick={() => {
-                                                        setIsDeleteModalOpen(
-                                                            true
-                                                        );
-                                                        setSelectedUser(
-                                                            userRow
-                                                        );
-                                                    }}
-                                                >
-                                                    <span className="text-red-500">
-                                                        Remove User
-                                                    </span>
-                                                </DropdownMenuItem>
-                                            )}
-                                        </DropdownMenuContent>
-                                    </DropdownMenu>
-                                    <Link
-                                        href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
-                                    >
-                                        <Button
-                                            variant={"gray"}
-                                            className="ml-2"
-                                        >
-                                            Manage
-                                            <ArrowRight className="ml-2 w-4 h-4" />
-                                        </Button>
-                                    </Link>
-                                </>
-                            )}
-                        </div>
-                    </>
+                            </Link>
+                        )}
+                    </div>
                 );
             }
         }

+ 9 - 6
src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx

@@ -421,6 +421,7 @@ export default function ResourceAuthenticationPage() {
                                     <FormItem className="flex flex-col items-start">
                                         <FormLabel>Roles</FormLabel>
                                         <FormControl>
+                                            {/* @ts-ignore */}
                                             <TagInput
                                                 {...field}
                                                 activeTagIndex={
@@ -454,9 +455,9 @@ export default function ResourceAuthenticationPage() {
                                                     tag: {
                                                         body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
                                                     },
-                                                    input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
+                                                    input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
                                                     inlineTagsContainer:
-                                                        "bg-transparent"
+                                                        "bg-transparent p-2"
                                                 }}
                                             />
                                         </FormControl>
@@ -476,6 +477,7 @@ export default function ResourceAuthenticationPage() {
                                     <FormItem className="flex flex-col items-start">
                                         <FormLabel>Users</FormLabel>
                                         <FormControl>
+                                            {/* @ts-ignore */}
                                             <TagInput
                                                 {...field}
                                                 activeTagIndex={
@@ -509,9 +511,9 @@ export default function ResourceAuthenticationPage() {
                                                     tag: {
                                                         body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
                                                     },
-                                                    input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
+                                                    input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
                                                     inlineTagsContainer:
-                                                        "bg-transparent"
+                                                        "bg-transparent p-2"
                                                 }}
                                             />
                                         </FormControl>
@@ -649,6 +651,7 @@ export default function ResourceAuthenticationPage() {
                                                         Whitelisted Emails
                                                     </FormLabel>
                                                     <FormControl>
+                                                        {/* @ts-ignore */}
                                                         <TagInput
                                                             {...field}
                                                             activeTagIndex={
@@ -691,9 +694,9 @@ export default function ResourceAuthenticationPage() {
                                                                 tag: {
                                                                     body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
                                                                 },
-                                                                input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
+                                                                input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
                                                                 inlineTagsContainer:
-                                                                    "bg-transparent"
+                                                                    "bg-transparent p-2"
                                                             }}
                                                         />
                                                     </FormControl>

+ 1 - 1
src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx

@@ -69,7 +69,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
                         )}
                     </div>
 
-                    <div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md">
+                    <div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md lg:max-w-xl">
                         <LinkIcon className="h-4 w-4" />
                         <a
                             href={fullUrl}

+ 63 - 58
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -9,7 +9,7 @@ import {
     SelectContent,
     SelectItem,
     SelectTrigger,
-    SelectValue,
+    SelectValue
 } from "@/components/ui/select";
 import { Switch } from "@/components/ui/switch";
 import { AxiosResponse } from "axios";
@@ -24,7 +24,7 @@ import {
     FormField,
     FormItem,
     FormLabel,
-    FormMessage,
+    FormMessage
 } from "@app/components/ui/form";
 import { CreateTargetResponse } from "@server/routers/target";
 import {
@@ -34,7 +34,7 @@ import {
     getPaginationRowModel,
     getCoreRowModel,
     useReactTable,
-    flexRender,
+    flexRender
 } from "@tanstack/react-table";
 import {
     Table,
@@ -42,7 +42,7 @@ import {
     TableCell,
     TableHead,
     TableHeader,
-    TableRow,
+    TableRow
 } from "@app/components/ui/table";
 import { useToast } from "@app/hooks/useToast";
 import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
@@ -59,9 +59,9 @@ const addTargetSchema = z.object({
     port: z
         .string()
         .refine((val) => !isNaN(Number(val)), {
-            message: "Port must be a number",
+            message: "Port must be a number"
         })
-        .transform((val) => Number(val)),
+        .transform((val) => Number(val))
     // protocol: z.string(),
 });
 
@@ -99,16 +99,16 @@ export default function ReverseProxyTargets(props: {
         defaultValues: {
             ip: "",
             method: "http",
-            port: "80",
+            port: "80"
             // protocol: "TCP",
-        },
+        }
     });
 
     useEffect(() => {
         const fetchTargets = async () => {
             try {
                 const res = await api.get<AxiosResponse<ListTargetsResponse>>(
-                    `/resource/${params.resourceId}/targets`,
+                    `/resource/${params.resourceId}/targets`
                 );
 
                 if (res.status === 200) {
@@ -121,8 +121,8 @@ export default function ReverseProxyTargets(props: {
                     title: "Failed to fetch targets",
                     description: formatAxiosError(
                         err,
-                        "An error occurred while fetching targets",
-                    ),
+                        "An error occurred while fetching targets"
+                    )
                 });
             } finally {
                 setPageLoading(false);
@@ -133,7 +133,7 @@ export default function ReverseProxyTargets(props: {
         const fetchSite = async () => {
             try {
                 const res = await api.get<AxiosResponse<GetSiteResponse>>(
-                    `/site/${resource.siteId}`,
+                    `/site/${resource.siteId}`
                 );
 
                 if (res.status === 200) {
@@ -146,27 +146,28 @@ export default function ReverseProxyTargets(props: {
                     title: "Failed to fetch resource",
                     description: formatAxiosError(
                         err,
-                        "An error occurred while fetching resource",
-                    ),
+                        "An error occurred while fetching resource"
+                    )
                 });
             }
-        }
+        };
         fetchSite();
     }, []);
 
     async function addTarget(data: AddTargetFormValues) {
         // Check if target with same IP, port and method already exists
         const isDuplicate = targets.some(
-            target => target.ip === data.ip &&
-                     target.port === data.port &&
-                     target.method === data.method
+            (target) =>
+                target.ip === data.ip &&
+                target.port === data.port &&
+                target.method === data.method
         );
 
         if (isDuplicate) {
             toast({
                 variant: "destructive",
                 title: "Duplicate target",
-                description: "A target with these settings already exists",
+                description: "A target with these settings already exists"
             });
             return;
         }
@@ -179,7 +180,7 @@ export default function ReverseProxyTargets(props: {
                 toast({
                     variant: "destructive",
                     title: "Invalid target IP",
-                    description: "Target IP must be within the site subnet",
+                    description: "Target IP must be within the site subnet"
                 });
                 return;
             }
@@ -190,7 +191,7 @@ export default function ReverseProxyTargets(props: {
             enabled: true,
             targetId: new Date().getTime(),
             new: true,
-            resourceId: resource.resourceId,
+            resourceId: resource.resourceId
         };
 
         setTargets([...targets, newTarget]);
@@ -199,7 +200,7 @@ export default function ReverseProxyTargets(props: {
 
     const removeTarget = (targetId: number) => {
         setTargets([
-            ...targets.filter((target) => target.targetId !== targetId),
+            ...targets.filter((target) => target.targetId !== targetId)
         ]);
 
         if (!targets.find((target) => target.targetId === targetId)?.new) {
@@ -212,8 +213,8 @@ export default function ReverseProxyTargets(props: {
             targets.map((target) =>
                 target.targetId === targetId
                     ? { ...target, ...data, updated: true }
-                    : target,
-            ),
+                    : target
+            )
         );
     }
 
@@ -222,7 +223,7 @@ export default function ReverseProxyTargets(props: {
             setLoading(true);
 
             const res = await api.post(`/resource/${params.resourceId}`, {
-                ssl: sslEnabled,
+                ssl: sslEnabled
             });
 
             updateResource({ ssl: sslEnabled });
@@ -233,7 +234,7 @@ export default function ReverseProxyTargets(props: {
                     port: target.port,
                     // protocol: target.protocol,
                     method: target.method,
-                    enabled: target.enabled,
+                    enabled: target.enabled
                 };
 
                 if (target.new) {
@@ -244,7 +245,7 @@ export default function ReverseProxyTargets(props: {
                 } else if (target.updated) {
                     const res = await api.post(
                         `/target/${target.targetId}`,
-                        data,
+                        data
                     );
                 }
 
@@ -253,23 +254,23 @@ export default function ReverseProxyTargets(props: {
                         let res = {
                             ...t,
                             new: false,
-                            updated: false,
+                            updated: false
                         };
                         return res;
-                    }),
+                    })
                 ]);
             }
 
             for (const targetId of targetsToRemove) {
                 await api.delete(`/target/${targetId}`);
                 setTargets(
-                    targets.filter((target) => target.targetId !== targetId),
+                    targets.filter((target) => target.targetId !== targetId)
                 );
             }
 
             toast({
                 title: "Resource updated",
-                description: "Resource and targets updated successfully",
+                description: "Resource and targets updated successfully"
             });
 
             setTargetsToRemove([]);
@@ -280,8 +281,8 @@ export default function ReverseProxyTargets(props: {
                 title: "Operation failed",
                 description: formatAxiosError(
                     err,
-                    "An error occurred during the save operation",
-                ),
+                    "An error occurred during the save operation"
+                )
             });
         }
 
@@ -299,13 +300,15 @@ export default function ReverseProxyTargets(props: {
                         updateTarget(row.original.targetId, { method: value })
                     }
                 >
-                    <SelectTrigger>{row.original.method}</SelectTrigger>
+                    <SelectTrigger className="min-w-[100px]">
+                        {row.original.method}
+                    </SelectTrigger>
                     <SelectContent>
                         <SelectItem value="http">http</SelectItem>
                         <SelectItem value="https">https</SelectItem>
                     </SelectContent>
                 </Select>
-            ),
+            )
         },
         {
             accessorKey: "ip",
@@ -313,13 +316,14 @@ export default function ReverseProxyTargets(props: {
             cell: ({ row }) => (
                 <Input
                     defaultValue={row.original.ip}
+                    className="min-w-[150px]"
                     onBlur={(e) =>
                         updateTarget(row.original.targetId, {
-                            ip: e.target.value,
+                            ip: e.target.value
                         })
                     }
                 />
-            ),
+            )
         },
         {
             accessorKey: "port",
@@ -328,13 +332,14 @@ export default function ReverseProxyTargets(props: {
                 <Input
                     type="number"
                     defaultValue={row.original.port}
+                    className="min-w-[100px]"
                     onBlur={(e) =>
                         updateTarget(row.original.targetId, {
-                            port: parseInt(e.target.value, 10),
+                            port: parseInt(e.target.value, 10)
                         })
                     }
                 />
-            ),
+            )
         },
         // {
         //     accessorKey: "protocol",
@@ -364,7 +369,7 @@ export default function ReverseProxyTargets(props: {
                         updateTarget(row.original.targetId, { enabled: val })
                     }
                 />
-            ),
+            )
         },
         {
             id: "actions",
@@ -387,8 +392,8 @@ export default function ReverseProxyTargets(props: {
                         </Button>
                     </div>
                 </>
-            ),
-        },
+            )
+        }
     ];
 
     const table = useReactTable({
@@ -397,7 +402,7 @@ export default function ReverseProxyTargets(props: {
         getCoreRowModel: getCoreRowModel(),
         getPaginationRowModel: getPaginationRowModel(),
         getSortedRowModel: getSortedRowModel(),
-        getFilteredRowModel: getFilteredRowModel(),
+        getFilteredRowModel: getFilteredRowModel()
     });
 
     if (pageLoading) {
@@ -437,7 +442,7 @@ export default function ReverseProxyTargets(props: {
                         <Form {...addTargetForm}>
                             <form
                                 onSubmit={addTargetForm.handleSubmit(
-                                    addTarget as any,
+                                    addTarget as any
                                 )}
                                 className="space-y-4"
                             >
@@ -452,11 +457,11 @@ export default function ReverseProxyTargets(props: {
                                                     <Select
                                                         {...field}
                                                         onValueChange={(
-                                                            value,
+                                                            value
                                                         ) => {
                                                             addTargetForm.setValue(
                                                                 "method",
-                                                                value,
+                                                                value
                                                             );
                                                         }}
                                                     >
@@ -585,10 +590,10 @@ export default function ReverseProxyTargets(props: {
                                                                           .column
                                                                           .columnDef
                                                                           .header,
-                                                                      header.getContext(),
+                                                                      header.getContext()
                                                                   )}
                                                         </TableHead>
-                                                    ),
+                                                    )
                                                 )}
                                             </TableRow>
                                         ))}
@@ -607,7 +612,7 @@ export default function ReverseProxyTargets(props: {
                                                                 cell.column
                                                                     .columnDef
                                                                     .cell,
-                                                                cell.getContext(),
+                                                                cell.getContext()
                                                             )}
                                                         </TableCell>
                                                     ))}
@@ -644,36 +649,36 @@ export default function ReverseProxyTargets(props: {
 
 function isIPInSubnet(subnet: string, ip: string): boolean {
     // Split subnet into IP and mask parts
-    const [subnetIP, maskBits] = subnet.split('/');
+    const [subnetIP, maskBits] = subnet.split("/");
     const mask = parseInt(maskBits);
-    
+
     if (mask < 0 || mask > 32) {
-        throw new Error('Invalid subnet mask. Must be between 0 and 32.');
+        throw new Error("Invalid subnet mask. Must be between 0 and 32.");
     }
 
     // Convert IP addresses to binary numbers
     const subnetNum = ipToNumber(subnetIP);
     const ipNum = ipToNumber(ip);
-    
+
     // Calculate subnet mask
     const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1);
-    
+
     // Check if the IP is in the subnet
     return (subnetNum & maskNum) === (ipNum & maskNum);
 }
 
 function ipToNumber(ip: string): number {
     // Validate IP address format
-    const parts = ip.split('.');
+    const parts = ip.split(".");
     if (parts.length !== 4) {
-        throw new Error('Invalid IP address format');
+        throw new Error("Invalid IP address format");
     }
-    
+
     // Convert IP octets to 32-bit number
     return parts.reduce((num, octet) => {
         const oct = parseInt(octet);
         if (isNaN(oct) || oct < 0 || oct > 255) {
-            throw new Error('Invalid IP address octet');
+            throw new Error("Invalid IP address octet");
         }
         return (num << 8) + oct;
     }, 0);

+ 1 - 1
src/app/[orgId]/settings/resources/[resourceId]/layout.tsx

@@ -123,7 +123,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
             <OrgProvider org={org}>
                 <ResourceProvider resource={resource} authInfo={authInfo}>
                     <SidebarSettings sidebarNavItems={sidebarNavItems}>
-                        <div className="mb-8 lg:max-w-2xl">
+                        <div className="mb-8">
                             <ResourceInfoBox />
                         </div>
                         {children}

+ 47 - 47
src/app/[orgId]/settings/resources/components/ResourcesTable.tsx

@@ -74,6 +74,43 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
     };
 
     const columns: ColumnDef<ResourceRow>[] = [
+        {
+            accessorKey: "dots",
+            header: "",
+            cell: ({ row }) => {
+                const resourceRow = row.original;
+                const router = useRouter();
+
+                return (
+                    <DropdownMenu>
+                        <DropdownMenuTrigger asChild>
+                            <Button variant="ghost" className="h-8 w-8 p-0">
+                                <span className="sr-only">Open menu</span>
+                                <MoreHorizontal className="h-4 w-4" />
+                            </Button>
+                        </DropdownMenuTrigger>
+                        <DropdownMenuContent align="end">
+                            <DropdownMenuItem>
+                                <Link
+                                    className="block w-full"
+                                    href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
+                                >
+                                    View settings
+                                </Link>
+                            </DropdownMenuItem>
+                            <DropdownMenuItem
+                                onClick={() => {
+                                    setSelectedResource(resourceRow);
+                                    setIsDeleteModalOpen(true);
+                                }}
+                            >
+                                <span className="text-red-500">Delete</span>
+                            </DropdownMenuItem>
+                        </DropdownMenuContent>
+                    </DropdownMenu>
+                );
+            }
+        },
         {
             accessorKey: "name",
             header: ({ column }) => {
@@ -214,55 +251,18 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
         {
             id: "actions",
             cell: ({ row }) => {
-                const router = useRouter();
-
                 const resourceRow = row.original;
-
                 return (
-                    <>
-                        <div className="flex items-center justify-end">
-                            <DropdownMenu>
-                                <DropdownMenuTrigger asChild>
-                                    <Button
-                                        variant="ghost"
-                                        className="h-8 w-8 p-0"
-                                    >
-                                        <span className="sr-only">
-                                            Open menu
-                                        </span>
-                                        <MoreHorizontal className="h-4 w-4" />
-                                    </Button>
-                                </DropdownMenuTrigger>
-                                <DropdownMenuContent align="end">
-                                    <DropdownMenuItem>
-                                        <Link
-                                            href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
-                                        >
-                                            View settings
-                                        </Link>
-                                    </DropdownMenuItem>
-                                    <DropdownMenuItem
-                                        onClick={() => {
-                                            setSelectedResource(resourceRow);
-                                            setIsDeleteModalOpen(true);
-                                        }}
-                                    >
-                                        <span className="text-red-500">
-                                            Delete
-                                        </span>
-                                    </DropdownMenuItem>
-                                </DropdownMenuContent>
-                            </DropdownMenu>
-                            <Link
-                                href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
-                            >
-                                <Button variant={"gray"} className="ml-2">
-                                    Edit
-                                    <ArrowRight className="ml-2 w-4 h-4" />
-                                </Button>
-                            </Link>
-                        </div>
-                    </>
+                    <div className="flex items-center justify-end">
+                        <Link
+                            href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
+                        >
+                            <Button variant={"gray"} className="ml-2">
+                                Edit
+                                <ArrowRight className="ml-2 w-4 h-4" />
+                            </Button>
+                        </Link>
+                    </div>
                 );
             }
         }

+ 11 - 11
src/app/[orgId]/settings/share-links/components/CreateShareLinkForm.tsx

@@ -63,7 +63,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
 import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
 import { constructShareLink } from "@app/lib/shareLinks";
 import { ShareLinkRow } from "./ShareLinksTable";
-import { QRCodeSVG } from "qrcode.react";
+import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
 
 type FormProps = {
     open: boolean;
@@ -449,23 +449,23 @@ export default function CreateShareLinkForm({
                             {link && (
                                 <div className="max-w-md space-y-4">
                                     <p>
-                                        You will only be able to see this link once.
-                                        Make sure to copy it.
+                                        You will only be able to see this link
+                                        once. Make sure to copy it.
                                     </p>
                                     <p>
                                         Anyone with this link can access the
                                         resource. Share it with care.
                                     </p>
 
-                                    <div className="w-64 h-64 mx-auto flex items-center justify-center">
-                                        <QRCodeSVG
-                                            value={link}
-                                            size={256}
-                                        />
+                                    <div className="h-[250px] w-full mx-auto flex items-center justify-center">
+                                        <QRCodeCanvas value={link} size={200} />
                                     </div>
 
                                     <div className="mx-auto">
-                                        <CopyTextBox text={link} wrapText={false} />
+                                        <CopyTextBox
+                                            text={link}
+                                            wrapText={false}
+                                        />
                                     </div>
                                 </div>
                             )}
@@ -473,8 +473,8 @@ export default function CreateShareLinkForm({
                     </CredenzaBody>
                     <CredenzaFooter>
                         <Button
-                            type="submit"
-                            form="share-link-form"
+                            type="button"
+                            onClick={form.handleSubmit(onSubmit)}
                             loading={loading}
                             disabled={link !== null || loading}
                         >

+ 42 - 42
src/app/[orgId]/settings/share-links/components/ShareLinksTable.tsx

@@ -86,6 +86,48 @@ export default function ShareLinksTable({
     }
 
     const columns: ColumnDef<ShareLinkRow>[] = [
+        {
+            id: "actions",
+            cell: ({ row }) => {
+                const router = useRouter();
+
+                const resourceRow = row.original;
+
+                return (
+                    <>
+                        <div>
+                            <DropdownMenu>
+                                <DropdownMenuTrigger asChild>
+                                    <Button
+                                        variant="ghost"
+                                        className="h-8 w-8 p-0"
+                                    >
+                                        <span className="sr-only">
+                                            Open menu
+                                        </span>
+                                        <MoreHorizontal className="h-4 w-4" />
+                                    </Button>
+                                </DropdownMenuTrigger>
+                                <DropdownMenuContent align="end">
+                                    <DropdownMenuItem>
+                                        <button
+                                            onClick={() =>
+                                                deleteSharelink(
+                                                    resourceRow.accessTokenId
+                                                )
+                                            }
+                                            className="text-red-500"
+                                        >
+                                            Delete
+                                        </button>
+                                    </DropdownMenuItem>
+                                </DropdownMenuContent>
+                            </DropdownMenu>
+                        </div>
+                    </>
+                );
+            }
+        },
         {
             accessorKey: "resourceName",
             header: ({ column }) => {
@@ -236,48 +278,6 @@ export default function ShareLinksTable({
                 }
                 return "Never";
             }
-        },
-        {
-            id: "actions",
-            cell: ({ row }) => {
-                const router = useRouter();
-
-                const resourceRow = row.original;
-
-                return (
-                    <>
-                        <div className="flex items-center justify-end">
-                            <DropdownMenu>
-                                <DropdownMenuTrigger asChild>
-                                    <Button
-                                        variant="ghost"
-                                        className="h-8 w-8 p-0"
-                                    >
-                                        <span className="sr-only">
-                                            Open menu
-                                        </span>
-                                        <MoreHorizontal className="h-4 w-4" />
-                                    </Button>
-                                </DropdownMenuTrigger>
-                                <DropdownMenuContent align="end">
-                                    <DropdownMenuItem>
-                                        <button
-                                            onClick={() =>
-                                                deleteSharelink(
-                                                    resourceRow.accessTokenId
-                                                )
-                                            }
-                                            className="text-red-500"
-                                        >
-                                            Delete
-                                        </button>
-                                    </DropdownMenuItem>
-                                </DropdownMenuContent>
-                            </DropdownMenu>
-                        </div>
-                    </>
-                );
-            }
         }
     ];
 

+ 1 - 1
src/app/[orgId]/settings/sites/[niceId]/layout.tsx

@@ -50,7 +50,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
                 <Breadcrumb>
                     <BreadcrumbList>
                         <BreadcrumbItem>
-                            <Link href="../../">Sites</Link>
+                            <Link href="../">Sites</Link>
                         </BreadcrumbItem>
                         <BreadcrumbSeparator />
                         <BreadcrumbItem>

+ 73 - 63
src/app/[orgId]/settings/sites/components/SitesTable.tsx

@@ -71,10 +71,48 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
                 setIsDeleteModalOpen(false);
 
                 const newRows = rows.filter((row) => row.id !== siteId);
+
+                setRows(newRows);
             });
     };
 
     const columns: ColumnDef<SiteRow>[] = [
+        {
+            id: "dots",
+            cell: ({ row }) => {
+                const siteRow = row.original;
+                const router = useRouter();
+
+                return (
+                    <DropdownMenu>
+                        <DropdownMenuTrigger asChild>
+                            <Button variant="ghost" className="h-8 w-8 p-0">
+                                <span className="sr-only">Open menu</span>
+                                <MoreHorizontal className="h-4 w-4" />
+                            </Button>
+                        </DropdownMenuTrigger>
+                        <DropdownMenuContent align="end">
+                            <DropdownMenuItem>
+                                <Link
+                                    className="block w-full"
+                                    href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
+                                >
+                                    View settings
+                                </Link>
+                            </DropdownMenuItem>
+                            <DropdownMenuItem
+                                onClick={() => {
+                                    setSelectedSite(siteRow);
+                                    setIsDeleteModalOpen(true);
+                                }}
+                            >
+                                <span className="text-red-500">Delete</span>
+                            </DropdownMenuItem>
+                        </DropdownMenuContent>
+                    </DropdownMenu>
+                );
+            }
+        },
         {
             accessorKey: "name",
             header: ({ column }) => {
@@ -91,6 +129,41 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
                 );
             }
         },
+        {
+            accessorKey: "online",
+            header: ({ column }) => {
+                return (
+                    <Button
+                        variant="ghost"
+                        onClick={() =>
+                            column.toggleSorting(column.getIsSorted() === "asc")
+                        }
+                    >
+                        Online
+                        <ArrowUpDown className="ml-2 h-4 w-4" />
+                    </Button>
+                );
+            },
+            cell: ({ row }) => {
+                const originalRow = row.original;
+
+                if (originalRow.online) {
+                    return (
+                        <span className="text-green-500 flex items-center space-x-2">
+                            <div className="w-2 h-2 bg-green-500 rounded-full"></div>
+                            <span>Online</span>
+                        </span>
+                    );
+                } else {
+                    return (
+                        <span className="text-neutral-500 flex items-center space-x-2">
+                            <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
+                            <span>Offline</span>
+                        </span>
+                    );
+                }
+            }
+        },
         {
             accessorKey: "nice",
             header: ({ column }) => {
@@ -174,75 +247,12 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
                 }
             }
         },
-        {
-            accessorKey: "online",
-            header: ({ column }) => {
-                return (
-                    <Button
-                        variant="ghost"
-                        onClick={() =>
-                            column.toggleSorting(column.getIsSorted() === "asc")
-                        }
-                    >
-                        Online
-                        <ArrowUpDown className="ml-2 h-4 w-4" />
-                    </Button>
-                );
-            },
-            cell: ({ row }) => {
-                const originalRow = row.original;
-
-                if (originalRow.online) {
-                    return (
-                        <span className="text-green-500 flex items-center space-x-2">
-                            <div className="w-2 h-2 bg-green-500 rounded-full"></div>
-                            <span>Online</span>
-                        </span>
-                    );
-                } else {
-                    return (
-                        <span className="text-gray-500 flex items-center space-x-2">
-                            <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
-                            <span>Offline</span>
-                        </span>
-                    );
-                }
-            }
-        },
         {
             id: "actions",
             cell: ({ row }) => {
-                const router = useRouter();
-
                 const siteRow = row.original;
-
                 return (
                     <div className="flex items-center justify-end">
-                        <DropdownMenu>
-                            <DropdownMenuTrigger asChild>
-                                <Button variant="ghost" className="h-8 w-8 p-0">
-                                    <span className="sr-only">Open menu</span>
-                                    <MoreHorizontal className="h-4 w-4" />
-                                </Button>
-                            </DropdownMenuTrigger>
-                            <DropdownMenuContent align="end">
-                                <DropdownMenuItem>
-                                    <Link
-                                        href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
-                                    >
-                                        View settings
-                                    </Link>
-                                </DropdownMenuItem>
-                                <DropdownMenuItem
-                                    onClick={() => {
-                                        setSelectedSite(siteRow);
-                                        setIsDeleteModalOpen(true);
-                                    }}
-                                >
-                                    <span className="text-red-500">Delete</span>
-                                </DropdownMenuItem>
-                            </DropdownMenuContent>
-                        </DropdownMenu>
                         <Link
                             href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
                         >

+ 6 - 6
src/app/globals.css

@@ -6,11 +6,11 @@
 @layer base {
   :root {
     --background: 0 0% 100%;
-    --foreground: 20 5.0% 10.0%;
+    --foreground: 20 0.0% 10.0%;
     --card: 0 0% 100%;
-    --card-foreground: 20 5.0% 10.0%;
+    --card-foreground: 20 0.0% 10.0%;
     --popover: 0 0% 100%;
-    --popover-foreground: 20 5.0% 10.0%;
+    --popover-foreground: 20 0.0% 10.0%;
     --primary: 24.6 95% 53.1%;
     --primary-foreground: 60 9.1% 97.8%;
     --secondary: 60 4.8% 95.9%;
@@ -33,11 +33,11 @@
   }
 
   .dark {
-    --background: 20 5.0% 10.0%;
+    --background: 20 0.0% 10.0%;
     --foreground: 60 9.1% 97.8%;
-    --card: 20 5.0% 10.0%;
+    --card: 20 0.0% 10.0%;
     --card-foreground: 60 9.1% 97.8%;
-    --popover: 20 5.0% 10.0%;
+    --popover: 20 0.0% 10.0%;
     --popover-foreground: 60 9.1% 97.8%;
     --primary: 20.5 90.2% 48.2%;
     --primary-foreground: 60 9.1% 97.8%;

+ 2 - 2
src/app/invite/InviteStatusCard.tsx

@@ -7,7 +7,7 @@ import {
     CardContent,
     CardFooter,
     CardHeader,
-    CardTitle
+    CardTitle,
 } from "@app/components/ui/card";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { XCircle } from "lucide-react";
@@ -20,7 +20,7 @@ type InviteStatusCardProps = {
 
 export default function InviteStatusCard({
     type,
-    token
+    token,
 }: InviteStatusCardProps) {
     const router = useRouter();
 

+ 47 - 13
src/components/Credenza.tsx

@@ -12,7 +12,7 @@ import {
     DialogFooter,
     DialogHeader,
     DialogTitle,
-    DialogTrigger,
+    DialogTrigger
 } from "@/components/ui/dialog";
 import {
     Drawer,
@@ -22,8 +22,17 @@ import {
     DrawerFooter,
     DrawerHeader,
     DrawerTitle,
-    DrawerTrigger,
+    DrawerTrigger
 } from "@/components/ui/drawer";
+import {
+    Sheet,
+    SheetContent,
+    SheetDescription,
+    SheetFooter,
+    SheetHeader,
+    SheetTitle,
+    SheetTrigger
+} from "./ui/sheet";
 
 interface BaseProps {
     children: React.ReactNode;
@@ -43,14 +52,17 @@ const desktop = "(min-width: 768px)";
 
 const Credenza = ({ children, ...props }: RootCredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
-    const Credenza = isDesktop ? Dialog : Drawer;
+    // const isDesktop = true;
+    const Credenza = isDesktop ? Dialog : Sheet;
 
     return <Credenza {...props}>{children}</Credenza>;
 };
 
 const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
-    const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
+    // const isDesktop = true;
+
+    const CredenzaTrigger = isDesktop ? DialogTrigger : SheetTrigger;
 
     return (
         <CredenzaTrigger className={className} {...props}>
@@ -61,10 +73,12 @@ const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
 
 const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
+    // const isDesktop = true;
+
     const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
 
     return (
-        <CredenzaClose className={className} {...props}>
+        <CredenzaClose className={cn("mb-3 md:mb-0", className)} {...props}>
             {children}
         </CredenzaClose>
     );
@@ -72,10 +86,16 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
 
 const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
-    const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
+    // const isDesktop = true;
+
+    const CredenzaContent = isDesktop ? DialogContent : SheetContent;
 
     return (
-        <CredenzaContent className={className} {...props}>
+        <CredenzaContent
+            className={cn("overflow-y-auto max-h-screen", className)}
+            {...props}
+            side={"bottom"}
+        >
             {children}
         </CredenzaContent>
     );
@@ -87,9 +107,11 @@ const CredenzaDescription = ({
     ...props
 }: CredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
+    // const isDesktop = true;
+
     const CredenzaDescription = isDesktop
         ? DialogDescription
-        : DrawerDescription;
+        : SheetDescription;
 
     return (
         <CredenzaDescription className={className} {...props}>
@@ -100,7 +122,9 @@ const CredenzaDescription = ({
 
 const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
-    const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
+    // const isDesktop = true;
+
+    const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader;
 
     return (
         <CredenzaHeader className={className} {...props}>
@@ -111,7 +135,9 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
 
 const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
-    const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
+    // const isDesktop = true;
+
+    const CredenzaTitle = isDesktop ? DialogTitle : SheetTitle;
 
     return (
         <CredenzaTitle className={className} {...props}>
@@ -121,8 +147,14 @@ const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
 };
 
 const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
+    // return (
+    //     <div className={cn("px-4 md:px-0 mb-4", className)} {...props}>
+    //         {children}
+    //     </div>
+    // );
+
     return (
-        <div className={cn("px-4 md:px-0 mb-4", className)} {...props}>
+        <div className={cn("px-0 mb-4", className)} {...props}>
             {children}
         </div>
     );
@@ -130,7 +162,9 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
 
 const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
     const isDesktop = useMediaQuery(desktop);
-    const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
+    // const isDesktop = true;
+
+    const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
 
     return (
         <CredenzaFooter className={className} {...props}>
@@ -148,5 +182,5 @@ export {
     CredenzaHeader,
     CredenzaTitle,
     CredenzaBody,
-    CredenzaFooter,
+    CredenzaFooter
 };

+ 12 - 11
src/components/Enable2FaForm.tsx

@@ -38,7 +38,7 @@ import {
 import { useToast } from "@app/hooks/useToast";
 import { formatAxiosError } from "@app/lib/utils";
 import CopyTextBox from "@app/components/CopyTextBox";
-import { QRCodeSVG } from "qrcode.react";
+import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
 import { useUserContext } from "@app/hooks/useUserContext";
 
 const enableSchema = z.object({
@@ -221,15 +221,10 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
                                 Scan this QR code with your authenticator app or
                                 enter the secret key manually:
                             </p>
-                            <div className="w-64 h-64 mx-auto flex items-center justify-center">
-                                <QRCodeSVG value={secretUri} size={256} />
-                            </div>
-                            <div className="max-w-md mx-auto">
-                                <CopyTextBox
-                                    text={secretKey}
-                                    wrapText={false}
-                                />
+                            <div className="h-[250px] mx-auto flex items-center justify-center">
+                                <QRCodeCanvas value={secretUri} size={200} />
                             </div>
+                            <CopyTextBox text={secretUri} wrapText={false} />
 
                             <Form {...confirmForm}>
                                 <form
@@ -288,10 +283,16 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
                 <CredenzaFooter>
                     {(step === 1 || step === 2) && (
                         <Button
-                            type="submit"
-                            form="form"
+                            type="button"
                             loading={loading}
                             disabled={loading}
+                            onClick={() => {
+                                if (step === 1) {
+                                    enableForm.handleSubmit(request2fa)();
+                                } else {
+                                    confirmForm.handleSubmit(confirm2fa)();
+                                }
+                            }}
                         >
                             Submit
                         </Button>

+ 2 - 2
src/components/SidebarSettings.tsx

@@ -21,9 +21,9 @@ export function SidebarSettings({
     limitWidth,
 }: SideBarSettingsProps) {
     return (
-        <div className="space-y-8 0 pb-16k">
+        <div className="space-y-8 pb-16k">
             <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-32 lg:space-y-0">
-                <aside className="-mx-4 lg:w-1/5">
+                <aside className="lg:w-1/5">
                     <SidebarNav items={sidebarNavItems} disabled={disabled} />
                 </aside>
                 <div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}>

+ 10 - 3
src/components/sidebar-nav.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import React from "react";
+import React, { useEffect } from "react";
 import Link from "next/link";
 import { useParams, usePathname, useRouter } from "next/navigation";
 import { cn } from "@/lib/utils";
@@ -35,6 +35,12 @@ export function SidebarNav({
     const resourceId = params.resourceId as string;
     const userId = params.userId as string;
 
+    const [selectedValue, setSelectedValue] = React.useState<string>(getSelectedValue());
+
+    useEffect(() => {
+        setSelectedValue(getSelectedValue());
+    }, [usePathname()]);
+
     const router = useRouter();
 
     const handleSelectChange = (value: string) => {
@@ -58,9 +64,10 @@ export function SidebarNav({
 
     return (
         <div>
-            <div className="block lg:hidden px-4">
+            <div className="block lg:hidden">
                 <Select
-                    defaultValue={getSelectedValue()}
+                    defaultValue={selectedValue}
+                    value={selectedValue}
                     onValueChange={handleSelectChange}
                     disabled={disabled}
                 >

+ 1 - 1
src/components/ui/command.tsx

@@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
     <CommandPrimitive.Input
       ref={ref}
       className={cn(
-        "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
+        "flex h-11 w-full rounded-md bg-transparent py-3 text-base md:text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
         className
       )}
       {...props}

+ 2 - 2
src/components/ui/dialog.tsx

@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
     <DialogPrimitive.Overlay
         ref={ref}
         className={cn(
-            "fixed inset-0 z-50 bg-black/30  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+            "fixed inset-0 z-50 bg-black/50  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
             className
         )}
         {...props}
@@ -118,5 +118,5 @@ export {
     DialogHeader,
     DialogFooter,
     DialogTitle,
-    DialogDescription,
+    DialogDescription
 };

+ 90 - 90
src/components/ui/drawer.tsx

@@ -1,118 +1,118 @@
-"use client"
+"use client";
 
-import * as React from "react"
-import { Drawer as DrawerPrimitive } from "vaul"
+import * as React from "react";
+import { Drawer as DrawerPrimitive } from "vaul";
 
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
 
 const Drawer = ({
-  shouldScaleBackground = true,
-  ...props
+    shouldScaleBackground = true,
+    ...props
 }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
-  <DrawerPrimitive.Root
-    shouldScaleBackground={shouldScaleBackground}
-    {...props}
-  />
-)
-Drawer.displayName = "Drawer"
+    <DrawerPrimitive.Root
+        shouldScaleBackground={shouldScaleBackground}
+        {...props}
+    />
+);
+Drawer.displayName = "Drawer";
 
-const DrawerTrigger = DrawerPrimitive.Trigger
+const DrawerTrigger = DrawerPrimitive.Trigger;
 
-const DrawerPortal = DrawerPrimitive.Portal
+const DrawerPortal = DrawerPrimitive.Portal;
 
-const DrawerClose = DrawerPrimitive.Close
+const DrawerClose = DrawerPrimitive.Close;
 
 const DrawerOverlay = React.forwardRef<
-  React.ElementRef<typeof DrawerPrimitive.Overlay>,
-  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
+    React.ElementRef<typeof DrawerPrimitive.Overlay>,
+    React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
 >(({ className, ...props }, ref) => (
-  <DrawerPrimitive.Overlay
-    ref={ref}
-    className={cn("fixed inset-0 z-50 bg-black/80", className)}
-    {...props}
-  />
-))
-DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
+    <DrawerPrimitive.Overlay
+        ref={ref}
+        className={cn("fixed inset-0 z-50 bg-black/80", className)}
+        {...props}
+    />
+));
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
 
 const DrawerContent = React.forwardRef<
-  React.ElementRef<typeof DrawerPrimitive.Content>,
-  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
+    React.ElementRef<typeof DrawerPrimitive.Content>,
+    React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
 >(({ className, children, ...props }, ref) => (
-  <DrawerPortal>
-    <DrawerOverlay />
-    <DrawerPrimitive.Content
-      ref={ref}
-      className={cn(
-        "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
-        className
-      )}
-      {...props}
-    >
-      <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
-      {children}
-    </DrawerPrimitive.Content>
-  </DrawerPortal>
-))
-DrawerContent.displayName = "DrawerContent"
+    <DrawerPortal>
+        <DrawerOverlay />
+        <DrawerPrimitive.Content
+            ref={ref}
+            className={cn(
+                "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
+                className
+            )}
+            {...props}
+        >
+            <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
+            {children}
+        </DrawerPrimitive.Content>
+    </DrawerPortal>
+));
+DrawerContent.displayName = "DrawerContent";
 
 const DrawerHeader = ({
-  className,
-  ...props
+    className,
+    ...props
 }: React.HTMLAttributes<HTMLDivElement>) => (
-  <div
-    className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
-    {...props}
-  />
-)
-DrawerHeader.displayName = "DrawerHeader"
+    <div
+        className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
+        {...props}
+    />
+);
+DrawerHeader.displayName = "DrawerHeader";
 
 const DrawerFooter = ({
-  className,
-  ...props
+    className,
+    ...props
 }: React.HTMLAttributes<HTMLDivElement>) => (
-  <div
-    className={cn("mt-auto flex flex-col gap-2 p-4", className)}
-    {...props}
-  />
-)
-DrawerFooter.displayName = "DrawerFooter"
+    <div
+        className={cn("mt-auto flex flex-col gap-2 p-4", className)}
+        {...props}
+    />
+);
+DrawerFooter.displayName = "DrawerFooter";
 
 const DrawerTitle = React.forwardRef<
-  React.ElementRef<typeof DrawerPrimitive.Title>,
-  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
+    React.ElementRef<typeof DrawerPrimitive.Title>,
+    React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
 >(({ className, ...props }, ref) => (
-  <DrawerPrimitive.Title
-    ref={ref}
-    className={cn(
-      "text-lg font-semibold leading-none tracking-tight",
-      className
-    )}
-    {...props}
-  />
-))
-DrawerTitle.displayName = DrawerPrimitive.Title.displayName
+    <DrawerPrimitive.Title
+        ref={ref}
+        className={cn(
+            "text-lg font-semibold leading-none tracking-tight",
+            className
+        )}
+        {...props}
+    />
+));
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
 
 const DrawerDescription = React.forwardRef<
-  React.ElementRef<typeof DrawerPrimitive.Description>,
-  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
+    React.ElementRef<typeof DrawerPrimitive.Description>,
+    React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
 >(({ className, ...props }, ref) => (
-  <DrawerPrimitive.Description
-    ref={ref}
-    className={cn("text-sm text-muted-foreground", className)}
-    {...props}
-  />
-))
-DrawerDescription.displayName = DrawerPrimitive.Description.displayName
+    <DrawerPrimitive.Description
+        ref={ref}
+        className={cn("text-sm text-muted-foreground", className)}
+        {...props}
+    />
+));
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
 
 export {
-  Drawer,
-  DrawerPortal,
-  DrawerOverlay,
-  DrawerTrigger,
-  DrawerClose,
-  DrawerContent,
-  DrawerHeader,
-  DrawerFooter,
-  DrawerTitle,
-  DrawerDescription,
-}
+    Drawer,
+    DrawerPortal,
+    DrawerOverlay,
+    DrawerTrigger,
+    DrawerClose,
+    DrawerContent,
+    DrawerHeader,
+    DrawerFooter,
+    DrawerTitle,
+    DrawerDescription
+};

+ 1 - 1
src/components/ui/input-otp.tsx

@@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
     <div
       ref={ref}
       className={cn(
-        "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
+        "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
         isActive && "z-10 ring-2 ring-ring ring-offset-background",
         className
       )}

+ 2 - 2
src/components/ui/input.tsx

@@ -15,7 +15,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
                 <input
                     type={showPassword ? "text" : "password"}
                     className={cn(
-                        "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+                        "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
                         className
                     )}
                     ref={ref}
@@ -39,7 +39,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
             <input
                 type={type}
                 className={cn(
-                    "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+                    "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
                     className
                 )}
                 ref={ref}

+ 1 - 1
src/components/ui/select.tsx

@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
   <SelectPrimitive.Trigger
     ref={ref}
     className={cn(
-      "flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+      "flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
       className
     )}
     {...props}

+ 140 - 0
src/components/ui/sheet.tsx

@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Overlay>,
+  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+  <SheetPrimitive.Overlay
+    className={cn(
+      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+      className
+    )}
+    {...props}
+    ref={ref}
+  />
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+  "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+  {
+    variants: {
+      side: {
+        top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+        bottom:
+          "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+        left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+        right:
+          "inset-y-0 right-0 h-full w-3/4  border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+      },
+    },
+    defaultVariants: {
+      side: "right",
+    },
+  }
+)
+
+interface SheetContentProps
+  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
+    VariantProps<typeof sheetVariants> {}
+
+const SheetContent = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Content>,
+  SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+  <SheetPortal>
+    <SheetOverlay />
+    <SheetPrimitive.Content
+      ref={ref}
+      className={cn(sheetVariants({ side }), className)}
+      {...props}
+    >
+      {children}
+      <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
+        <X className="h-4 w-4" />
+        <span className="sr-only">Close</span>
+      </SheetPrimitive.Close>
+    </SheetPrimitive.Content>
+  </SheetPortal>
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col space-y-2 text-center sm:text-left",
+      className
+    )}
+    {...props}
+  />
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+  className,
+  ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
+      className
+    )}
+    {...props}
+  />
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Title>,
+  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
+>(({ className, ...props }, ref) => (
+  <SheetPrimitive.Title
+    ref={ref}
+    className={cn("text-lg font-semibold text-foreground", className)}
+    {...props}
+  />
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+  React.ElementRef<typeof SheetPrimitive.Description>,
+  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
+>(({ className, ...props }, ref) => (
+  <SheetPrimitive.Description
+    ref={ref}
+    className={cn("text-sm text-muted-foreground", className)}
+    {...props}
+  />
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+  Sheet,
+  SheetPortal,
+  SheetOverlay,
+  SheetTrigger,
+  SheetClose,
+  SheetContent,
+  SheetHeader,
+  SheetFooter,
+  SheetTitle,
+  SheetDescription,
+}

+ 1 - 1
src/components/ui/toast.tsx

@@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
 ToastViewport.displayName = ToastPrimitives.Viewport.displayName
 
 const toastVariants = cva(
-  "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+  "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
   {
     variants: {
       variant: {

+ 1 - 1
src/hooks/useToast.ts

@@ -6,7 +6,7 @@ import * as React from "react";
 import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
 
 const TOAST_LIMIT = 3;
-const TOAST_REMOVE_DELAY = 5 * 1000;
+const TOAST_REMOVE_DELAY = 1 * 1000;
 
 type ToasterToast = ToastProps & {
     id: string;

+ 2 - 2
src/providers/ThemeProvider.tsx

@@ -1,8 +1,8 @@
 "use client";
 
-import * as React from "react";
 import { ThemeProvider as NextThemesProvider } from "next-themes";
-import { type ThemeProviderProps } from "next-themes/dist/types";
+
+type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
 
 export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
     return <NextThemesProvider {...props}>{children}</NextThemesProvider>;