Merge branch 'main' of https://github.com/fosrl/pangolin
This commit is contained in:
commit
0ff183796c
11 changed files with 301 additions and 71 deletions
|
@ -46,10 +46,10 @@ authenticated.put("/org/:orgId/site", verifyOrgAccess, site.createSite);
|
||||||
authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
|
authenticated.get("/org/:orgId/sites", verifyOrgAccess, site.listSites);
|
||||||
authenticated.get("/org/:orgId/site/:niceId", verifyOrgAccess, site.getSite);
|
authenticated.get("/org/:orgId/site/:niceId", verifyOrgAccess, site.getSite);
|
||||||
|
|
||||||
authenticated.get("/site/siteId/:siteId", verifySiteAccess, site.getSite);
|
authenticated.get("/site/:siteId", verifySiteAccess, site.getSite);
|
||||||
authenticated.get("/site/siteId/:siteId/roles", verifySiteAccess, site.listSiteRoles);
|
authenticated.get("/site/:siteId/roles", verifySiteAccess, site.listSiteRoles);
|
||||||
authenticated.post("/site/siteId/:siteId", verifySiteAccess, site.updateSite);
|
authenticated.post("/site/:siteId", verifySiteAccess, site.updateSite);
|
||||||
authenticated.delete("/site/siteId/:siteId", verifySiteAccess, site.deleteSite);
|
authenticated.delete("/site/:siteId", verifySiteAccess, site.deleteSite);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/site/:siteId/resource",
|
"/org/:orgId/site/:siteId/resource",
|
||||||
|
|
54
src/app/[orgId]/sites/[niceId]/components/ClientLayout.tsx
Normal file
54
src/app/[orgId]/sites/[niceId]/components/ClientLayout.tsx
Normal 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>);
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { generateKeypair } from "./wireguard-config";
|
import { generateKeypair } from "./wireguardConfig";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
174
src/app/[orgId]/sites/[niceId]/components/GeneralForm.tsx
Normal file
174
src/app/[orgId]/sites/[niceId]/components/GeneralForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,31 +11,15 @@ import { redirect } from "next/navigation";
|
||||||
import { authCookieHeader } from "@app/api/cookies";
|
import { authCookieHeader } from "@app/api/cookies";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowLeft, ChevronLeft } from "lucide-react";
|
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}/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 {
|
interface SettingsLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: { niceId: string; orgId: string };
|
params: { niceId: string; orgId: string };
|
||||||
|
@ -46,6 +30,7 @@ export default async function SettingsLayout({
|
||||||
params,
|
params,
|
||||||
}: SettingsLayoutProps) {
|
}: SettingsLayoutProps) {
|
||||||
let site = null;
|
let site = null;
|
||||||
|
|
||||||
if (params.niceId !== "create") {
|
if (params.niceId !== "create") {
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<AxiosResponse<GetSiteResponse>>(
|
const res = await internal.get<AxiosResponse<GetSiteResponse>>(
|
||||||
|
@ -85,33 +70,12 @@ export default async function SettingsLayout({
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden space-y-6 0 pb-16 md:block">
|
<SiteProvider site={site}>
|
||||||
<div className="space-y-0.5">
|
<ClientLayout
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
isCreate={params.niceId === "create"}
|
||||||
{params.niceId == "create"
|
>
|
||||||
? "New Site"
|
{children}
|
||||||
: site?.name + " Settings" || "Site Settings"}
|
</ClientLayout></SiteProvider>
|
||||||
</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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ProfileForm } from "@app/components/profile-form";
|
import { CreateSiteForm } from "./components/CreateSite";
|
||||||
import { CreateSiteForm } from "./components/create-site";
|
import { GeneralForm } from "./components/GeneralForm";
|
||||||
|
|
||||||
export default function SettingsProfilePage({
|
export default function SettingsProfilePage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { niceId: string };
|
params: { niceId: string };
|
||||||
}) {
|
}) {
|
||||||
const isCreateForm = params.niceId === "create";
|
const isCreate = params.niceId === "create";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">
|
<h3 className="text-lg font-medium">
|
||||||
{isCreateForm ? "Create Site" : "Profile"}
|
{isCreate ? "Create Site" : "Profile"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{isCreateForm
|
{isCreate
|
||||||
? "Create a new site for your profile."
|
? "Create a new site for your profile."
|
||||||
: "This is how others will see you on the site."}
|
: "This is how others will see you on the site."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{isCreateForm ? <CreateSiteForm /> : <ProfileForm />}
|
{isCreate ? <CreateSiteForm /> : <GeneralForm />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { GetSiteResponse } from "@server/routers/site/getSite";
|
import { GetSiteResponse } from "@server/routers/site/getSite";
|
||||||
import { createContext } from "react";
|
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;
|
|
@ -1,7 +1,10 @@
|
||||||
import { SiteContext } from "@app/contexts/siteContext";
|
import SiteContext from "@app/contexts/siteContext";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
|
||||||
export function useSiteContext() {
|
export function useSiteContext() {
|
||||||
const site = useContext(SiteContext);
|
const context = useContext(SiteContext);
|
||||||
return site;
|
if (context === undefined) {
|
||||||
|
throw new Error('useSiteContext must be used within a SiteProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
}
|
}
|
|
@ -1,16 +1,44 @@
|
||||||
"use client";
|
"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 { 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;
|
site: GetSiteResponse | null;
|
||||||
children: ReactNode;
|
}
|
||||||
|
|
||||||
|
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...",
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SiteProvider({ site, children }: LandingProviderProps) {
|
|
||||||
return <SiteContext.Provider value={site}>{children}</SiteContext.Provider>;
|
return <SiteContext.Provider value={{ site, updateSite }}>{children}</SiteContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SiteProvider;
|
export default SiteProvider;
|
Loading…
Add table
Reference in a new issue