This commit is contained in:
Owen Schwartz 2024-12-30 21:38:47 -05:00
commit 6530fff87e
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
33 changed files with 827 additions and 608 deletions

View file

@ -4,7 +4,7 @@ WORKDIR /app
COPY package.json ./ COPY package.json ./
RUN npm install --legacy-peer-deps RUN npm install
COPY . . COPY . .
@ -20,7 +20,7 @@ WORKDIR /app
COPY package.json ./ 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/.next ./.next
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist

View file

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

View file

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

View file

@ -14,7 +14,10 @@ const portSchema = z.number().positive().gt(0).lte(65535);
const environmentSchema = z.object({ const environmentSchema = z.object({
app: 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"]), log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.boolean() save_logs: z.boolean()
}), }),
@ -76,6 +79,7 @@ const environmentSchema = z.object({
.optional() .optional()
}); });
export function getConfig() {
const loadConfig = (configPath: string) => { const loadConfig = (configPath: string) => {
try { try {
const yamlContent = fs.readFileSync(configPath, "utf8"); const yamlContent = fs.readFileSync(configPath, "utf8");
@ -156,7 +160,8 @@ process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
?.require_email_verification ?.require_email_verification
? "true" ? "true"
: "false"; : "false";
process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name; process.env.SESSION_COOKIE_NAME =
parsedConfig.data.server.session_cookie_name;
process.env.RESOURCE_SESSION_COOKIE_NAME = process.env.RESOURCE_SESSION_COOKIE_NAME =
parsedConfig.data.server.resource_session_cookie_name; parsedConfig.data.server.resource_session_cookie_name;
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false"; process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
@ -169,4 +174,7 @@ process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
? "true" ? "true"
: "false"; : "false";
export default parsedConfig.data; return parsedConfig.data;
}
export default getConfig();

View file

@ -68,7 +68,7 @@ export const SendInviteLink = ({
<Section className="text-center my-6"> <Section className="text-center my-6">
<Button <Button
href={inviteLink} 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} Accept invitation to {orgName}
</Button> </Button>

View file

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

View file

@ -40,26 +40,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
const { toast } = useToast(); const { toast } = useToast();
const columns: ColumnDef<RoleRow>[] = [ 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", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
@ -67,14 +47,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
return ( return (
<> <>
<div className="flex items-center justify-end"> <div>
{roleRow.isAdmin && ( {roleRow.isAdmin && (
<Button <MoreHorizontal className="h-4 w-4 opacity-0" />
variant="ghost"
className="h-8 w-8 p-0 opacity-0 cursor-default"
>
Placeholder
</Button>
)} )}
{!roleRow.isAdmin && ( {!roleRow.isAdmin && (
<DropdownMenu> <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"
} }
]; ];

View file

@ -50,6 +50,64 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const { toast } = useToast(); const { toast } = useToast();
const columns: ColumnDef<UserRow>[] = [ 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", accessorKey: "email",
header: ({ column }) => { header: ({ column }) => {
@ -114,9 +172,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
const userRow = row.original; const userRow = row.original;
return ( return (
<>
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
{userRow.isOwner && ( {userRow.isOwner && (
<Button <Button
@ -127,60 +183,16 @@ export default function UsersTable({ users: u }: UsersTableProps) {
</Button> </Button>
)} )}
{!userRow.isOwner && ( {!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 <Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`} href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
> >
Manage User <Button variant={"gray"} className="ml-2">
</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 Manage
<ArrowRight className="ml-2 w-4 h-4" /> <ArrowRight className="ml-2 w-4 h-4" />
</Button> </Button>
</Link> </Link>
</>
)} )}
</div> </div>
</>
); );
} }
} }

View file

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

View file

@ -69,7 +69,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
)} )}
</div> </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" /> <LinkIcon className="h-4 w-4" />
<a <a
href={fullUrl} href={fullUrl}

View file

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

View file

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

View file

@ -74,6 +74,43 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
}; };
const columns: ColumnDef<ResourceRow>[] = [ 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", accessorKey: "name",
header: ({ column }) => { header: ({ column }) => {
@ -214,45 +251,9 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
{ {
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
const router = useRouter();
const resourceRow = row.original; const resourceRow = row.original;
return ( return (
<>
<div className="flex items-center justify-end"> <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 <Link
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`} href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
> >
@ -262,7 +263,6 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
</Button> </Button>
</Link> </Link>
</div> </div>
</>
); );
} }
} }

View file

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

View file

@ -86,6 +86,48 @@ export default function ShareLinksTable({
} }
const columns: ColumnDef<ShareLinkRow>[] = [ 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", accessorKey: "resourceName",
header: ({ column }) => { header: ({ column }) => {
@ -236,48 +278,6 @@ export default function ShareLinksTable({
} }
return "Never"; 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>
</>
);
}
} }
]; ];

View file

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

View file

@ -71,10 +71,48 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
const newRows = rows.filter((row) => row.id !== siteId); const newRows = rows.filter((row) => row.id !== siteId);
setRows(newRows);
}); });
}; };
const columns: ColumnDef<SiteRow>[] = [ 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", accessorKey: "name",
header: ({ column }) => { 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", accessorKey: "nice",
header: ({ column }) => { 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", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
const router = useRouter();
const siteRow = row.original; const siteRow = row.original;
return ( return (
<div className="flex items-center justify-end"> <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 <Link
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`} href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
> >

View file

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

View file

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

View file

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

View file

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

View file

@ -21,9 +21,9 @@ export function SidebarSettings({
limitWidth, limitWidth,
}: SideBarSettingsProps) { }: SideBarSettingsProps) {
return ( 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"> <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} /> <SidebarNav items={sidebarNavItems} disabled={disabled} />
</aside> </aside>
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}> <div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}>

View file

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

View file

@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...props}

View file

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...props}
@ -118,5 +118,5 @@ export {
DialogHeader, DialogHeader,
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription
}; };

View file

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,
@ -13,14 +13,14 @@ const Drawer = ({
shouldScaleBackground={shouldScaleBackground} shouldScaleBackground={shouldScaleBackground}
{...props} {...props}
/> />
) );
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< const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, React.ElementRef<typeof DrawerPrimitive.Overlay>,
@ -31,8 +31,8 @@ const DrawerOverlay = React.forwardRef<
className={cn("fixed inset-0 z-50 bg-black/80", className)} className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props} {...props}
/> />
)) ));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef< const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, React.ElementRef<typeof DrawerPrimitive.Content>,
@ -52,8 +52,8 @@ const DrawerContent = React.forwardRef<
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
)) ));
DrawerContent.displayName = "DrawerContent" DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ const DrawerHeader = ({
className, className,
@ -63,8 +63,8 @@ const DrawerHeader = ({
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props} {...props}
/> />
) );
DrawerHeader.displayName = "DrawerHeader" DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ const DrawerFooter = ({
className, className,
@ -74,8 +74,8 @@ const DrawerFooter = ({
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) );
DrawerFooter.displayName = "DrawerFooter" DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef< const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, React.ElementRef<typeof DrawerPrimitive.Title>,
@ -89,8 +89,8 @@ const DrawerTitle = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef< const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, React.ElementRef<typeof DrawerPrimitive.Description>,
@ -101,8 +101,8 @@ const DrawerDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export { export {
Drawer, Drawer,
@ -114,5 +114,5 @@ export {
DrawerHeader, DrawerHeader,
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription
} };

View file

@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( 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", isActive && "z-10 ring-2 ring-ring ring-offset-background",
className className
)} )}

View file

@ -15,7 +15,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
className={cn( 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 className
)} )}
ref={ref} ref={ref}
@ -39,7 +39,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( 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 className
)} )}
ref={ref} ref={ref}

View file

@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...props}

140
src/components/ui/sheet.tsx Normal file
View file

@ -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,
}

View file

@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva( 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: { variants: {
variant: { variant: {

View file

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

View file

@ -1,8 +1,8 @@
"use client"; "use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes"; 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) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>; return <NextThemesProvider {...props}>{children}</NextThemesProvider>;