diff --git a/Dockerfile b/Dockerfile index a5be0f1a..98d3cfc5 100644 --- a/Dockerfile +++ b/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 diff --git a/package.json b/package.json index 56510177..7426c951 100644 --- a/package.json +++ b/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" + } } } diff --git a/server/apiServer.ts b/server/apiServer.ts index d4fe98f8..1de0329b 100644 --- a/server/apiServer.ts +++ b/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"; diff --git a/server/config.ts b/server/config.ts index ea8de136..fe1f863e 100644 --- a/server/config.ts +++ b/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) { - throw new Error("No configuration file found"); -} - -const parsedConfig = environmentSchema.safeParse(environment); - -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); - - if (packageJson.version) { - process.env.APP_VERSION = packageJson.version; + 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"); + } + + const parsedConfig = environmentSchema.safeParse(environment); + + 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); + + 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"; + + return parsedConfig.data; } -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"; - -export default parsedConfig.data; +export default getConfig(); diff --git a/server/emails/templates/SendInviteLink.tsx b/server/emails/templates/SendInviteLink.tsx index d7a4228d..85ec5ec0 100644 --- a/server/emails/templates/SendInviteLink.tsx +++ b/server/emails/templates/SendInviteLink.tsx @@ -68,7 +68,7 @@ export const SendInviteLink = ({
diff --git a/src/api/index.ts b/src/api/index.ts index b59445db..d9ac4bd5 100644 --- a/src/api/index.ts +++ b/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" } }); diff --git a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx index e70a989d..7b67c08b 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx +++ b/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[] = [ - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "description", - header: "Description" - }, { id: "actions", cell: ({ row }) => { @@ -67,14 +47,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) { return ( <> -
+
{roleRow.isAdmin && ( - + )} {!roleRow.isAdmin && ( @@ -107,6 +82,26 @@ export default function UsersTable({ roles: r }: RolesTableProps) { ); } + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "description", + header: "Description" } ]; diff --git a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx index 143bf91f..6b45fd4f 100644 --- a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx +++ b/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[] = [ + { + id: "dots", + cell: ({ row }) => { + const userRow = row.original; + return ( + <> +
+ {userRow.isOwner && ( + + )} + {!userRow.isOwner && ( + <> + + + + + + + + Manage User + + + {userRow.email !== user?.email && ( + { + setIsDeleteModalOpen( + true + ); + setSelectedUser( + userRow + ); + }} + > + + Remove User + + + )} + + + + )} +
+ + ); + } + }, { accessorKey: "email", header: ({ column }) => { @@ -114,73 +172,27 @@ export default function UsersTable({ users: u }: UsersTableProps) { id: "actions", cell: ({ row }) => { const userRow = row.original; - return ( - <> -
- {userRow.isOwner && ( - + )} + {!userRow.isOwner && ( + + - )} - {!userRow.isOwner && ( - <> - - - - - - - - Manage User - - - {userRow.email !== user?.email && ( - { - setIsDeleteModalOpen( - true - ); - setSelectedUser( - userRow - ); - }} - > - - Remove User - - - )} - - - - - - - )} -
- + + )} +
); } } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index 41041039..a3310a46 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -421,6 +421,7 @@ export default function ResourceAuthenticationPage() { Roles + {/* @ts-ignore */} @@ -476,6 +477,7 @@ export default function ResourceAuthenticationPage() { Users + {/* @ts-ignore */} @@ -649,6 +651,7 @@ export default function ResourceAuthenticationPage() { Whitelisted Emails + {/* @ts-ignore */} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx index abcf9f9f..6eacfc22 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/components/ResourceInfoBox.tsx @@ -69,7 +69,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { )}
-
+
!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>( - `/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>( - `/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 }) } > - {row.original.method} + + {row.original.method} + http https - ), + ) }, { accessorKey: "ip", @@ -313,13 +316,14 @@ export default function ReverseProxyTargets(props: { cell: ({ row }) => ( updateTarget(row.original.targetId, { - ip: e.target.value, + ip: e.target.value }) } /> - ), + ) }, { accessorKey: "port", @@ -328,13 +332,14 @@ export default function ReverseProxyTargets(props: { 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: {
- ), - }, + ) + } ]; 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: {
@@ -452,11 +457,11 @@ export default function ReverseProxyTargets(props: { diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 1a37e67d..190f291c 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -46,7 +46,7 @@ const CommandInput = React.forwardRef< ) => ( - -) -Drawer.displayName = "Drawer" + +); +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, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; const DrawerContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - - -
- {children} - - -)) -DrawerContent.displayName = "DrawerContent" + + + +
+ {children} + + +)); +DrawerContent.displayName = "DrawerContent"; const DrawerHeader = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes) => ( -
-) -DrawerHeader.displayName = "DrawerHeader" +
+); +DrawerHeader.displayName = "DrawerHeader"; const DrawerFooter = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes) => ( -
-) -DrawerFooter.displayName = "DrawerFooter" +
+); +DrawerFooter.displayName = "DrawerFooter"; const DrawerTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; const DrawerDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +)); +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 +}; diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx index f66fcfa0..00994bd8 100644 --- a/src/components/ui/input-otp.tsx +++ b/src/components/ui/input-otp.tsx @@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
( ( 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} diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 00000000..a37f17ba --- /dev/null +++ b/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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, 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, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index 521b94b0..bcafdadb 100644 --- a/src/components/ui/toast.tsx +++ b/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: { diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts index 992106aa..758958e7 100644 --- a/src/hooks/useToast.ts +++ b/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; diff --git a/src/providers/ThemeProvider.tsx b/src/providers/ThemeProvider.tsx index 6b5fce1a..e5fb1e3e 100644 --- a/src/providers/ThemeProvider.tsx +++ b/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; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return {children};