Create resource working
This commit is contained in:
parent
0ff183796c
commit
091d649997
17 changed files with 721 additions and 130 deletions
|
@ -1,7 +1,7 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { db } from '@server/db';
|
import { db } from '@server/db';
|
||||||
import { resources, roleResources, roles, userResources } from '@server/db/schema';
|
import { orgs, resources, roleResources, roles, userResources } from '@server/db/schema';
|
||||||
import response from "@server/utils/response";
|
import response from "@server/utils/response";
|
||||||
import HttpCode from '@server/types/HttpCode';
|
import HttpCode from '@server/types/HttpCode';
|
||||||
import createHttpError from 'http-errors';
|
import createHttpError from 'http-errors';
|
||||||
|
@ -10,7 +10,7 @@ import logger from '@server/logger';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
const createResourceParamsSchema = z.object({
|
const createResourceParamsSchema = z.object({
|
||||||
siteId: z.number().int().positive(),
|
siteId: z.string().transform(Number).pipe(z.number().int().positive()),
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -58,8 +58,23 @@ export async function createResource(req: Request, res: Response, next: NextFunc
|
||||||
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have a role'));
|
return next(createHttpError(HttpCode.FORBIDDEN, 'User does not have a role'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the org
|
||||||
|
const org = await db.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (org.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Organization with ID ${orgId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a unique resourceId
|
// Generate a unique resourceId
|
||||||
const resourceId = "subdomain" // TODO: create the subdomain here
|
const resourceId = `${subdomain}.${org[0].domain}`;
|
||||||
|
|
||||||
// Create new resource in the database
|
// Create new resource in the database
|
||||||
const newResource = await db.insert(resources).values({
|
const newResource = await db.insert(resources).values({
|
||||||
|
@ -70,8 +85,6 @@ export async function createResource(req: Request, res: Response, next: NextFunc
|
||||||
subdomain,
|
subdomain,
|
||||||
}).returning();
|
}).returning();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// find the superuser roleId and also add the resource to the superuser role
|
// find the superuser roleId and also add the resource to the superuser role
|
||||||
const superuserRole = await db.select()
|
const superuserRole = await db.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
|
@ -108,7 +121,7 @@ export async function createResource(req: Request, res: Response, next: NextFunc
|
||||||
status: HttpCode.CREATED,
|
status: HttpCode.CREATED,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
throw error;
|
||||||
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
|
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred..."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,13 @@ const getResourceSchema = z.object({
|
||||||
resourceId: z.string().uuid()
|
resourceId: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type GetResourceResponse = {
|
||||||
|
resourceId: string;
|
||||||
|
siteId: number;
|
||||||
|
orgId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getResource(req: Request, res: Response, next: NextFunction): Promise<any> {
|
export async function getResource(req: Request, res: Response, next: NextFunction): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// Validate request parameters
|
// Validate request parameters
|
||||||
|
@ -51,7 +58,12 @@ export async function getResource(req: Request, res: Response, next: NextFunctio
|
||||||
}
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: resource[0],
|
data: {
|
||||||
|
resourceId: resource[0].resourceId,
|
||||||
|
siteId: resource[0].siteId,
|
||||||
|
orgId: resource[0].orgId,
|
||||||
|
name: resource[0].name
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Resource retrieved successfully",
|
message: "Resource retrieved successfully",
|
||||||
|
|
|
@ -16,7 +16,7 @@ import logger from "@server/logger";
|
||||||
|
|
||||||
const listResourcesParamsSchema = z
|
const listResourcesParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
siteId: z.number().int().positive().optional(),
|
siteId: z.string().optional().transform(Number).pipe(z.number().int().positive()),
|
||||||
orgId: z.string().optional(),
|
orgId: z.string().optional(),
|
||||||
})
|
})
|
||||||
.refine((data) => !!data.siteId !== !!data.orgId, {
|
.refine((data) => !!data.siteId !== !!data.orgId, {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SidebarNav } from "@app/components/sidebar-nav";
|
||||||
|
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||||
|
|
||||||
|
const sidebarNavItems = [
|
||||||
|
{
|
||||||
|
title: "General",
|
||||||
|
href: "/{orgId}/resources/{resourceId}",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "Appearance",
|
||||||
|
// href: "/{orgId}/resources/{resourceId}/appearance",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Notifications",
|
||||||
|
// href: "/{orgId}/resources/{resourceId}/notifications",
|
||||||
|
// },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) {
|
||||||
|
const { resource } = useResourceContext();
|
||||||
|
return (<div className="hidden space-y-6 0 pb-16 md:block">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">
|
||||||
|
{isCreate
|
||||||
|
? "New Resource"
|
||||||
|
: resource?.name + " Settings"}
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{isCreate
|
||||||
|
? "Create a new resource"
|
||||||
|
: "Configure the settings on your resource: " +
|
||||||
|
resource?.name || ""}
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-6 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||||
|
<aside className="-mx-4 lg:w-1/5">
|
||||||
|
<SidebarNav
|
||||||
|
items={sidebarNavItems}
|
||||||
|
disabled={isCreate}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
<div className="flex-1 lg:max-w-2xl">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { CalendarIcon, CaretSortIcon, CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { toast } from "@/hooks/use-toast"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { api } from "@/api";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Checkbox } from "@app/components/ui/checkbox"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
|
import { ListSitesResponse } from "@server/routers/site"
|
||||||
|
import { AxiosResponse } from "axios"
|
||||||
|
import CustomDomainInput from "./CustomDomainInput"
|
||||||
|
|
||||||
|
const method = [
|
||||||
|
{ label: "Wireguard", value: "wg" },
|
||||||
|
{ label: "Newt", value: "newt" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const accountFormSchema = z.object({
|
||||||
|
subdomain: z
|
||||||
|
.string()
|
||||||
|
.min(2, {
|
||||||
|
message: "Name must be at least 2 characters.",
|
||||||
|
})
|
||||||
|
.max(30, {
|
||||||
|
message: "Name must not be longer than 30 characters.",
|
||||||
|
}),
|
||||||
|
name: z.string(),
|
||||||
|
siteId: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
type AccountFormValues = z.infer<typeof accountFormSchema>;
|
||||||
|
|
||||||
|
const defaultValues: Partial<AccountFormValues> = {
|
||||||
|
subdomain: "someanimalherefromapi",
|
||||||
|
name: "My Resource"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateResourceForm() {
|
||||||
|
const params = useParams();
|
||||||
|
const orgId = params.orgId;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||||
|
const [domainSuffix, setDomainSuffix] = useState<string>(".example.com");
|
||||||
|
|
||||||
|
const form = useForm<AccountFormValues>({
|
||||||
|
resolver: zodResolver(accountFormSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const fetchSites = async () => {
|
||||||
|
const res = await api.get<AxiosResponse<ListSitesResponse>>(`/org/${orgId}/sites/`);
|
||||||
|
setSites(res.data.data.sites);
|
||||||
|
};
|
||||||
|
fetchSites();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function onSubmit(data: AccountFormValues) {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const res = await api
|
||||||
|
.put(`/org/${orgId}/site/${data.siteId}/resource/`, {
|
||||||
|
name: data.name,
|
||||||
|
subdomain: data.subdomain,
|
||||||
|
// subdomain: data.subdomain,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
title: "Error creating resource..."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 201) {
|
||||||
|
const niceId = res.data.data.niceId;
|
||||||
|
// navigate to the resource page
|
||||||
|
router.push(`/${orgId}/resources/${niceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is the name that will be displayed for this resource.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="subdomain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Subdomain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
{/* <Input placeholder="Your name" {...field} /> */}
|
||||||
|
<CustomDomainInput {...field}
|
||||||
|
domainSuffix={domainSuffix}
|
||||||
|
placeholder="Enter subdomain"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is the fully qualified domain name that will be used to access the resource.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* <FormField
|
||||||
|
control={form.control}
|
||||||
|
name="subdomain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Subdomain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The subdomain of the resource. This will be used to access resources on the resource.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="siteId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Site</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"w-[200px] justify-between",
|
||||||
|
!field.value && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? sites.find(
|
||||||
|
(site) => site.siteId === field.value
|
||||||
|
)?.name
|
||||||
|
: "Select site"}
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search site..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No site found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{sites.map((site) => (
|
||||||
|
<CommandItem
|
||||||
|
value={site.name}
|
||||||
|
key={site.siteId}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("siteId", site.siteId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
site.siteId === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{site.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormDescription>
|
||||||
|
This is the site that will be used in the dashboard.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">Create Resource</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
|
||||||
|
interface CustomDomainInputProps {
|
||||||
|
domainSuffix: string
|
||||||
|
placeholder?: string
|
||||||
|
onChange?: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CustomDomainInput({
|
||||||
|
domainSuffix,
|
||||||
|
placeholder = "Enter subdomain",
|
||||||
|
onChange
|
||||||
|
}: CustomDomainInputProps = {
|
||||||
|
domainSuffix: ".example.com"
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = React.useState("")
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = event.target.value
|
||||||
|
setValue(newValue)
|
||||||
|
if (onChange) {
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full max-w-sm">
|
||||||
|
<div className="flex">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="rounded-r-none flex-grow"
|
||||||
|
/>
|
||||||
|
<div className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-muted-foreground">
|
||||||
|
<span className="text-sm">{domainSuffix}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import Link from "next/link"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { toast } from "@/hooks/use-toast"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { useSiteContext } from "@app/hooks/useSiteContext"
|
||||||
|
import api from "@app/api"
|
||||||
|
|
||||||
|
const GeneralFormSchema = z.object({
|
||||||
|
name: z.string()
|
||||||
|
// email: z
|
||||||
|
// .string({
|
||||||
|
// required_error: "Please select an email to display.",
|
||||||
|
// })
|
||||||
|
// .email(),
|
||||||
|
// bio: z.string().max(160).min(4),
|
||||||
|
// urls: z
|
||||||
|
// .array(
|
||||||
|
// z.object({
|
||||||
|
// value: z.string().url({ message: "Please enter a valid URL." }),
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// .optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>
|
||||||
|
|
||||||
|
export function GeneralForm() {
|
||||||
|
const { site, updateSite } = useSiteContext();
|
||||||
|
|
||||||
|
const form = useForm<GeneralFormValues>({
|
||||||
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: site?.name
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
})
|
||||||
|
|
||||||
|
// const { fields, append } = useFieldArray({
|
||||||
|
// name: "urls",
|
||||||
|
// control: form.control,
|
||||||
|
// })
|
||||||
|
|
||||||
|
async function onSubmit(data: GeneralFormValues) {
|
||||||
|
await updateSite({ name: data.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is the display name of the site.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* <FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a verified email to display" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="m@example.com">m@example.com</SelectItem>
|
||||||
|
<SelectItem value="m@google.com">m@google.com</SelectItem>
|
||||||
|
<SelectItem value="m@support.com">m@support.com</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
You can manage verified email addresses in your{" "}
|
||||||
|
<Link href="/examples/forms">email settings</Link>.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="bio"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bio</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Tell us a little bit about yourself"
|
||||||
|
className="resize-none"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
You can <span>@mention</span> other users and organizations to
|
||||||
|
link to them.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
key={field.id}
|
||||||
|
name={`urls.${index}.value`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className={cn(index !== 0 && "sr-only")}>
|
||||||
|
URLs
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription className={cn(index !== 0 && "sr-only")}>
|
||||||
|
Add links to your website, blog, or social media profiles.
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={() => append({ value: "" })}
|
||||||
|
>
|
||||||
|
Add URL
|
||||||
|
</Button>
|
||||||
|
</div> */}
|
||||||
|
<Button type="submit">Update Site</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,52 +1,81 @@
|
||||||
import { Metadata } from "next"
|
import { Metadata } from "next";
|
||||||
import Image from "next/image"
|
import Image from "next/image";
|
||||||
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { SidebarNav } from "@/components/sidebar-nav"
|
import { SidebarNav } from "@/components/sidebar-nav";
|
||||||
|
import ResourceProvider from "@app/providers/ResourceProvider";
|
||||||
|
import { internal } from "@app/api";
|
||||||
|
import { GetResourceResponse } from "@server/routers/resource";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { authCookieHeader } from "@app/api/cookies";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowLeft, ChevronLeft } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "@app/hooks/use-toast";
|
||||||
|
import { ClientLayout } from "./components/ClientLayout";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Forms",
|
title: "Forms",
|
||||||
description: "Advanced form example using react-hook-form and Zod.",
|
description: "Advanced form example using react-hook-form and Zod.",
|
||||||
}
|
};
|
||||||
|
|
||||||
const sidebarNavItems = [
|
|
||||||
{
|
|
||||||
title: "Profile",
|
|
||||||
href: "/{orgId}/resources/{resourceId}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Appearance",
|
|
||||||
href: "/{orgId}/resources/{resourceId}/appearance",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Notifications",
|
|
||||||
href: "/{orgId}/resources/{resourceId}/notifications",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
interface SettingsLayoutProps {
|
interface SettingsLayoutProps {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode;
|
||||||
params: { resourceId: string, orgId: string }
|
params: { resourceId: string; orgId: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsLayout({ children, params }: SettingsLayoutProps) {
|
export default async function SettingsLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: SettingsLayoutProps) {
|
||||||
|
let resource = null;
|
||||||
|
|
||||||
|
if (params.resourceId !== "create") {
|
||||||
|
try {
|
||||||
|
const res = await internal.get<AxiosResponse<GetResourceResponse>>(
|
||||||
|
`/org/${params.orgId}/resource/${params.resourceId}`,
|
||||||
|
authCookieHeader(),
|
||||||
|
);
|
||||||
|
resource = res.data.data;
|
||||||
|
} catch {
|
||||||
|
redirect(`/${params.orgId}/resources`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div className="md:hidden">
|
||||||
<div className="space-y-0.5">
|
<Image
|
||||||
<h2 className="text-2xl font-bold tracking-tight">Settings</h2>
|
src="/configuration/forms-light.png"
|
||||||
<p className="text-muted-foreground">
|
width={1280}
|
||||||
Manage your account settings and set e-mail preferences.
|
height={791}
|
||||||
</p>
|
alt="Forms"
|
||||||
</div>
|
className="block dark:hidden"
|
||||||
<Separator className="my-6" />
|
/>
|
||||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
<Image
|
||||||
<aside className="-mx-4 lg:w-1/5">
|
src="/configuration/forms-dark.png"
|
||||||
<SidebarNav items={sidebarNavItems.map(i => { i.href = i.href.replace("{resourceId}", params.resourceId).replace("{orgId}", params.orgId); return i})} />
|
width={1280}
|
||||||
</aside>
|
height={791}
|
||||||
<div className="flex-1 lg:max-w-2xl">{children}</div>
|
alt="Forms"
|
||||||
</div>
|
className="hidden dark:block"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<Link
|
||||||
|
href={`/${params.orgId}/resources`}
|
||||||
|
className="text-primary font-medium"
|
||||||
|
>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResourceProvider resource={resource}>
|
||||||
|
<ClientLayout
|
||||||
|
isCreate={params.resourceId === "create"}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ClientLayout></ResourceProvider>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,30 @@
|
||||||
import { Separator } from "@/components/ui/separator"
|
import React from "react";
|
||||||
import { ProfileForm } from "@app/components/profile-form"
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { CreateResourceForm } from "./components/CreateResource";
|
||||||
|
import { GeneralForm } from "./components/GeneralForm";
|
||||||
|
|
||||||
export default function SettingsProfilePage() {
|
export default function SettingsPage({
|
||||||
return (
|
params,
|
||||||
<div className="space-y-6">
|
}: {
|
||||||
<div>
|
params: { resourceId: string };
|
||||||
<h3 className="text-lg font-medium">Profile</h3>
|
}) {
|
||||||
<p className="text-sm text-muted-foreground">
|
const isCreate = params.resourceId === "create";
|
||||||
This is how others will see you on the site.
|
|
||||||
</p>
|
return (
|
||||||
</div>
|
<div className="space-y-6">
|
||||||
<Separator />
|
<div>
|
||||||
<ProfileForm />
|
<h3 className="text-lg font-medium">
|
||||||
</div>
|
{isCreate ? "Create Resource" : "General"}
|
||||||
)
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isCreate
|
||||||
|
? "Create a new resource"
|
||||||
|
: "Edit basic resource settings"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{isCreate ? <CreateResourceForm /> : <GeneralForm />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
import { AppearanceForm } from "@/components/appearance-form"
|
|
||||||
|
|
||||||
export default function SettingsAppearancePage() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium">Appearance</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Customize the appearance of the app. Automatically switch between day
|
|
||||||
and night themes.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<AppearanceForm />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -5,21 +5,21 @@ import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||||
|
|
||||||
const sidebarNavItems = [
|
const sidebarNavItems = [
|
||||||
{
|
{
|
||||||
title: "Profile",
|
title: "General",
|
||||||
href: "/{orgId}/sites/{niceId}",
|
href: "/{orgId}/sites/{niceId}",
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: "Appearance",
|
// title: "Appearance",
|
||||||
href: "/{orgId}/sites/{niceId}/appearance",
|
// href: "/{orgId}/sites/{niceId}/appearance",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: "Notifications",
|
// title: "Notifications",
|
||||||
href: "/{orgId}/sites/{niceId}/notifications",
|
// href: "/{orgId}/sites/{niceId}/notifications",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: "Display",
|
// title: "Display",
|
||||||
href: "/{orgId}/sites/{niceId}/display",
|
// href: "/{orgId}/sites/{niceId}/display",
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) {
|
export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) {
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
import { DisplayForm } from "@/components/display-form"
|
|
||||||
|
|
||||||
export default function SettingsDisplayPage() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium">Display</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Turn items on or off to control what's displayed in the app.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<DisplayForm />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
import { NotificationsForm } from "@/components/notifications-form"
|
|
||||||
|
|
||||||
export default function SettingsNotificationsPage() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium">Notifications</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Configure how you receive notifications.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<NotificationsForm />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ import { Separator } from "@/components/ui/separator";
|
||||||
import { CreateSiteForm } from "./components/CreateSite";
|
import { CreateSiteForm } from "./components/CreateSite";
|
||||||
import { GeneralForm } from "./components/GeneralForm";
|
import { GeneralForm } from "./components/GeneralForm";
|
||||||
|
|
||||||
export default function SettingsProfilePage({
|
export default function SettingsPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { niceId: string };
|
params: { niceId: string };
|
||||||
|
@ -14,12 +14,12 @@ export default function SettingsProfilePage({
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">
|
<h3 className="text-lg font-medium">
|
||||||
{isCreate ? "Create Site" : "Profile"}
|
{isCreate ? "Create Site" : "General"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{isCreate
|
{isCreate
|
||||||
? "Create a new site for your profile."
|
? "Create a new site"
|
||||||
: "This is how others will see you on the site."}
|
: "Edit basic site settings"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
11
src/contexts/resourceContext.ts
Normal file
11
src/contexts/resourceContext.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
interface ResourceContextType {
|
||||||
|
resource: GetResourceResponse | null;
|
||||||
|
updateResource: (updatedResource: Partial<GetResourceResponse>) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourceContext = createContext<ResourceContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export default ResourceContext;
|
10
src/hooks/useResourceContext.ts
Normal file
10
src/hooks/useResourceContext.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import ResourceContext from "@app/contexts/resourceContext";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
export function useResourceContext() {
|
||||||
|
const context = useContext(ResourceContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useResourceContext must be used within a ResourceProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
44
src/providers/ResourceProvider.tsx
Normal file
44
src/providers/ResourceProvider.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import api from "@app/api";
|
||||||
|
import ResourceContext from "@app/contexts/resourceContext";
|
||||||
|
import { toast } from "@app/hooks/use-toast";
|
||||||
|
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ResourceProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
resource: GetResourceResponse | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourceProvider({ children, resource: serverResource }: ResourceProviderProps) {
|
||||||
|
const [resource, setResource] = useState<GetResourceResponse | null>(serverResource);
|
||||||
|
|
||||||
|
const updateResource = async (updatedResource: Partial<GetResourceResponse>) => {
|
||||||
|
try {
|
||||||
|
if (!resource) {
|
||||||
|
throw new Error("No resource to update");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.post<AxiosResponse<GetResourceResponse>>(
|
||||||
|
`resource/${resource.resourceId}`,
|
||||||
|
updatedResource,
|
||||||
|
);
|
||||||
|
setResource(res.data.data);
|
||||||
|
toast({
|
||||||
|
title: "Resource updated!",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
title: "Error updating resource...",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return <ResourceContext.Provider value={{ resource, updateResource }}>{children}</ResourceContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResourceProvider;
|
Loading…
Add table
Reference in a new issue