Bläddra i källkod

Create resource working

Owen Schwartz 8 månader sedan
förälder
incheckning
091d649997

+ 19 - 6
server/routers/resource/createResource.ts

@@ -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..."));
     }
     }
 }
 }

+ 13 - 1
server/routers/resource/getResource.ts

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

+ 1 - 1
server/routers/resource/listResources.ts

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

+ 51 - 0
src/app/[orgId]/resources/[resourceId]/components/ClientLayout.tsx

@@ -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>);
+}

+ 241 - 0
src/app/[orgId]/resources/[resourceId]/components/CreateResource.tsx

@@ -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>
+        </>
+    );
+}

+ 45 - 0
src/app/[orgId]/resources/[resourceId]/components/CustomDomainInput.tsx

@@ -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>
+  )
+}

+ 174 - 0
src/app/[orgId]/resources/[resourceId]/components/GeneralForm.tsx

@@ -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>
+    )
+}

+ 67 - 38
src/app/[orgId]/resources/[resourceId]/layout.tsx

@@ -1,52 +1,81 @@
-import { Metadata } from "next"
-import Image from "next/image"
+import { Metadata } from "next";
+import Image from "next/image";
 
 
-import { Separator } from "@/components/ui/separator"
-import { SidebarNav } from "@/components/sidebar-nav"
+import { Separator } from "@/components/ui/separator";
+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,
-    params: { resourceId: string, orgId: string }
+    children: React.ReactNode;
+    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="space-y-0.5">
-                    <h2 className="text-2xl font-bold tracking-tight">Settings</h2>
-                    <p className="text-muted-foreground">
-                        Manage your account settings and set e-mail preferences.
-                    </p>
-                </div>
-                <Separator className="my-6" />
-                <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
-                    <aside className="-mx-4 lg:w-1/5">
-                        <SidebarNav items={sidebarNavItems.map(i => { i.href = i.href.replace("{resourceId}", params.resourceId).replace("{orgId}", params.orgId); return i})} />
-                    </aside>
-                    <div className="flex-1 lg:max-w-2xl">{children}</div>
-                </div>
+            <div className="md:hidden">
+                <Image
+                    src="/configuration/forms-light.png"
+                    width={1280}
+                    height={791}
+                    alt="Forms"
+                    className="block dark:hidden"
+                />
+                <Image
+                    src="/configuration/forms-dark.png"
+                    width={1280}
+                    height={791}
+                    alt="Forms"
+                    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>
         </>
         </>
-    )
+    );
 }
 }

+ 28 - 15
src/app/[orgId]/resources/[resourceId]/page.tsx

@@ -1,17 +1,30 @@
-import { Separator } from "@/components/ui/separator"
-import { ProfileForm } from "@app/components/profile-form"
+import React from "react";
+import { Separator } from "@/components/ui/separator";
+import { CreateResourceForm } from "./components/CreateResource";
+import { GeneralForm } from "./components/GeneralForm";
 
 
-export default function SettingsProfilePage() {
-  return (
-    <div className="space-y-6">
-      <div>
-        <h3 className="text-lg font-medium">Profile</h3>
-        <p className="text-sm text-muted-foreground">
-          This is how others will see you on the site.
-        </p>
-      </div>
-      <Separator />
-      <ProfileForm />
-    </div>
-  )
+export default function SettingsPage({
+    params,
+}: {
+    params: { resourceId: string };
+}) {
+    const isCreate = params.resourceId === "create";
+
+    return (
+        <div className="space-y-6">
+            <div>
+                <h3 className="text-lg font-medium">
+                    {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>
+    );
 }
 }

+ 0 - 18
src/app/[orgId]/sites/[niceId]/appearance/page.tsx

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

+ 13 - 13
src/app/[orgId]/sites/[niceId]/components/ClientLayout.tsx

@@ -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",
-        href: "/{orgId}/sites/{niceId}/appearance",
-    },
-    {
-        title: "Notifications",
-        href: "/{orgId}/sites/{niceId}/notifications",
-    },
-    {
-        title: "Display",
-        href: "/{orgId}/sites/{niceId}/display",
-    },
+    // {
+    //     title: "Appearance",
+    //     href: "/{orgId}/sites/{niceId}/appearance",
+    // },
+    // {
+    //     title: "Notifications",
+    //     href: "/{orgId}/sites/{niceId}/notifications",
+    // },
+    // {
+    //     title: "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 }) {

+ 0 - 17
src/app/[orgId]/sites/[niceId]/display/page.tsx

@@ -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&apos;s displayed in the app.
-        </p>
-      </div>
-      <Separator />
-      <DisplayForm />
-    </div>
-  )
-}

+ 0 - 17
src/app/[orgId]/sites/[niceId]/notifications/page.tsx

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

+ 4 - 4
src/app/[orgId]/sites/[niceId]/page.tsx

@@ -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."
-                        : "This is how others will see you on the site."}
+                        ? "Create a new site"
+                        : "Edit basic site settings"}
                 </p>
                 </p>
             </div>
             </div>
             <Separator />
             <Separator />

+ 11 - 0
src/contexts/resourceContext.ts

@@ -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 - 0
src/hooks/useResourceContext.ts

@@ -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 - 0
src/providers/ResourceProvider.tsx

@@ -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;