This commit is contained in:
Milo Schwartz 2024-10-19 15:49:34 -04:00
commit 0ff183796c
No known key found for this signature in database
11 changed files with 301 additions and 71 deletions

View file

@ -46,10 +46,10 @@ authenticated.put("/org/:orgId/site", verifyOrgAccess, site.createSite);
authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
authenticated.get("/org/:orgId/site/:niceId", verifyOrgAccess, site.getSite);
authenticated.get("/site/siteId/:siteId", verifySiteAccess, site.getSite);
authenticated.get("/site/siteId/:siteId/roles", verifySiteAccess, site.listSiteRoles);
authenticated.post("/site/siteId/:siteId", verifySiteAccess, site.updateSite);
authenticated.delete("/site/siteId/:siteId", verifySiteAccess, site.deleteSite);
authenticated.get("/site/:siteId", verifySiteAccess, site.getSite);
authenticated.get("/site/:siteId/roles", verifySiteAccess, site.listSiteRoles);
authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite);
authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
authenticated.put(
"/org/:orgId/site/:siteId/resource",

View file

@ -0,0 +1,54 @@
"use client";
import { SidebarNav } from "@app/components/sidebar-nav";
import { useSiteContext } from "@app/hooks/useSiteContext";
const sidebarNavItems = [
{
title: "Profile",
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",
},
];
export function ClientLayout({ isCreate, children }: { isCreate: boolean; children: React.ReactNode }) {
const { site } = useSiteContext();
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 Site"
: site?.name + " Settings"}
</h2>
<p className="text-muted-foreground">
{isCreate
? "Create a new site"
: "Configure the settings on your site: " +
site?.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>);
}

View file

@ -18,7 +18,7 @@ import {
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { generateKeypair } from "./wireguard-config";
import { generateKeypair } from "./wireguardConfig";
import React, { useState, useEffect } from "react";
import { api } from "@/api";
import { useParams } from "next/navigation";

View file

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

View file

@ -11,31 +11,15 @@ 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 = {
title: "Forms",
description: "Advanced form example using react-hook-form and Zod.",
};
const sidebarNavItems = [
{
title: "Profile",
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",
},
];
interface SettingsLayoutProps {
children: React.ReactNode;
params: { niceId: string; orgId: string };
@ -46,6 +30,7 @@ export default async function SettingsLayout({
params,
}: SettingsLayoutProps) {
let site = null;
if (params.niceId !== "create") {
try {
const res = await internal.get<AxiosResponse<GetSiteResponse>>(
@ -85,33 +70,12 @@ export default async function SettingsLayout({
</Link>
</div>
<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">
{params.niceId == "create"
? "New Site"
: site?.name + " Settings" || "Site Settings"}
</h2>
<p className="text-muted-foreground">
{params.niceId == "create"
? "Create a new site"
: "Configure the settings on your site: " +
site?.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={params.niceId == "create"}
/>
</aside>
<div className="flex-1 lg:max-w-2xl">
<SiteProvider site={site}>{children}</SiteProvider>
</div>
</div>
</div>
<SiteProvider site={site}>
<ClientLayout
isCreate={params.niceId === "create"}
>
{children}
</ClientLayout></SiteProvider>
</>
);
}

View file

@ -1,30 +1,30 @@
import React from "react";
import { Separator } from "@/components/ui/separator";
import { ProfileForm } from "@app/components/profile-form";
import { CreateSiteForm } from "./components/create-site";
import { CreateSiteForm } from "./components/CreateSite";
import { GeneralForm } from "./components/GeneralForm";
export default function SettingsProfilePage({
params,
}: {
params: { niceId: string };
}) {
const isCreateForm = params.niceId === "create";
const isCreate = params.niceId === "create";
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">
{isCreateForm ? "Create Site" : "Profile"}
{isCreate ? "Create Site" : "Profile"}
</h3>
<p className="text-sm text-muted-foreground">
{isCreateForm
{isCreate
? "Create a new site for your profile."
: "This is how others will see you on the site."}
</p>
</div>
<Separator />
{isCreateForm ? <CreateSiteForm /> : <ProfileForm />}
{isCreate ? <CreateSiteForm /> : <GeneralForm />}
</div>
);
}

View file

@ -1,4 +1,11 @@
import { GetSiteResponse } from "@server/routers/site/getSite";
import { createContext } from "react";
export const SiteContext = createContext<GetSiteResponse | null>(null);
interface SiteContextType {
site: GetSiteResponse | null;
updateSite: (updatedSite: Partial<GetSiteResponse>) => Promise<void>;
}
const SiteContext = createContext<SiteContextType | undefined>(undefined);
export default SiteContext;

View file

@ -1,7 +1,10 @@
import { SiteContext } from "@app/contexts/siteContext";
import SiteContext from "@app/contexts/siteContext";
import { useContext } from "react";
export function useSiteContext() {
const site = useContext(SiteContext);
return site;
const context = useContext(SiteContext);
if (context === undefined) {
throw new Error('useSiteContext must be used within a SiteProvider');
}
return context;
}

View file

@ -1,16 +1,44 @@
"use client";
import { SiteContext } from "@app/contexts/siteContext";
import api from "@app/api";
import SiteContext from "@app/contexts/siteContext";
import { toast } from "@app/hooks/use-toast";
import { GetSiteResponse } from "@server/routers/site/getSite";
import { ReactNode } from "react";
import { AxiosResponse } from "axios";
import { useState } from "react";
type LandingProviderProps = {
interface SiteProviderProps {
children: React.ReactNode;
site: GetSiteResponse | null;
children: ReactNode;
};
}
export function SiteProvider({ site, children }: LandingProviderProps) {
return <SiteContext.Provider value={site}>{children}</SiteContext.Provider>;
export function SiteProvider({ children, site: serverSite }: SiteProviderProps) {
const [site, setSite] = useState<GetSiteResponse | null>(serverSite);
const updateSite = async (updatedSite: Partial<GetSiteResponse>) => {
try {
if (!site) {
throw new Error("No site to update");
}
const res = await api.post<AxiosResponse<GetSiteResponse>>(
`site/${site.siteId}`,
updatedSite,
);
setSite(res.data.data);
toast({
title: "Site updated!",
});
} catch (error) {
console.error(error);
toast({
title: "Error updating site...",
})
}
};
return <SiteContext.Provider value={{ site, updateSite }}>{children}</SiteContext.Provider>;
}
export default SiteProvider;