refactor contexts, format zod errors, and more refactoring

This commit is contained in:
Milo Schwartz
2024-11-03 13:57:51 -05:00
parent 2635443105
commit 2852d62258
83 changed files with 2150 additions and 1264 deletions

View File

@@ -20,7 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from "@app/components/ui/select";
import { useToast } from "@app/hooks/use-toast";
import { useToast } from "@app/hooks/useToast";
import { ListOrgsResponse } from "@server/routers/org";
import Link from "next/link";
import { useRouter } from "next/navigation";

View File

@@ -8,6 +8,9 @@ import { internal } from "@app/api";
import { AxiosResponse } from "axios";
import { GetOrgResponse, ListOrgsResponse } from "@server/routers/org";
import { authCookieHeader } from "@app/api/cookies";
import { cache } from "react";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: `Settings - Pangolin`,
@@ -47,7 +50,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const { children } = props;
const user = await verifySession();
const getUser = cache(verifySession);
const user = await getUser();
if (!user) {
redirect("/auth/login");
@@ -56,20 +60,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const cookie = await authCookieHeader();
try {
await internal.get<AxiosResponse<GetOrgResponse>>(
`/org/${params.orgId}`,
cookie
const getOrg = cache(() =>
internal.get<AxiosResponse<GetOrgResponse>>(
`/org/${params.orgId}`,
cookie
)
);
const org = await getOrg();
} catch {
redirect(`/`);
}
let orgs: ListOrgsResponse["orgs"] = [];
try {
const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
`/orgs`,
cookie
const getOrgs = cache(() =>
internal.get<AxiosResponse<ListOrgsResponse>>(`/orgs`, cookie)
);
const res = await getOrgs();
if (res && res.data.data.orgs) {
orgs = res.data.data.orgs;
}

View File

@@ -8,7 +8,7 @@ import {
import { useForm } from "react-hook-form";
import { z } from "zod";
import { cn } from "@/lib/utils";
import { toast } from "@/hooks/use-toast";
import { toast } from "@/hooks/useToast";
import { Button} from "@/components/ui/button";
import {
Form,

View File

@@ -36,6 +36,8 @@ import { AxiosResponse } from "axios";
import api from "@app/api";
import { useParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { GetResourceResponse } from "@server/routers/resource";
import { useToast } from "@app/hooks/useToast";
const GeneralFormSchema = z.object({
name: z.string(),
@@ -49,6 +51,7 @@ export function GeneralForm() {
const orgId = params.orgId;
const { resource, updateResource } = useResourceContext();
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const { toast } = useToast();
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
@@ -72,7 +75,24 @@ export function GeneralForm() {
}, []);
async function onSubmit(data: GeneralFormValues) {
await updateResource({ name: data.name, siteId: data.siteId });
updateResource({ name: data.name, siteId: data.siteId });
await api
.post<AxiosResponse<GetResourceResponse>>(
`resource/${resource?.resourceId}`,
{
name: data.name,
siteId: data.siteId,
}
)
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update resource",
description:
e.response?.data?.message ||
"An error occurred while updating the resource",
});
});
}
return (

View File

@@ -5,7 +5,7 @@ import { 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 { toast } from "@/hooks/useToast";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Form,
@@ -78,7 +78,7 @@ export function CreateSiteForm() {
setKeypair(generatedKeypair);
setIsLoading(false);
api.get(`/org/${orgId}/pickSiteDefaults`)
api.get(`/org/${orgId}/pick-site-defaults`)
.catch((e) => {
toast({
title: "Error creating site...",

View File

@@ -15,6 +15,8 @@ import {
import { Input } from "@/components/ui/input";
import { useSiteContext } from "@app/hooks/useSiteContext";
import { useForm } from "react-hook-form";
import api from "@app/api";
import { useToast } from "@app/hooks/useToast";
const GeneralFormSchema = z.object({
name: z.string(),
@@ -24,6 +26,7 @@ type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
export function GeneralForm() {
const { site, updateSite } = useSiteContext();
const { toast } = useToast();
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
@@ -34,7 +37,21 @@ export function GeneralForm() {
});
async function onSubmit(data: GeneralFormValues) {
await updateSite({ name: data.name });
updateSite({ name: data.name });
await api
.post(`/site/${site?.siteId}`, {
name: data.name,
})
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
description:
e.message ||
"An error occurred while updating the site.",
});
});
}
return (

View File

@@ -92,7 +92,7 @@ export const columns: ColumnDef<SiteRow>[] = [
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Link
href={`/${siteRow.orgId}/settings/sites/${siteRow.id}`}
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
View settings
</Link>

View File

@@ -18,15 +18,30 @@ import {
SelectTrigger,
SelectValue,
} from "@app/components/ui/select";
import { useToast } from "@app/hooks/use-toast";
import { useToast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod";
import { InviteUserBody, InviteUserResponse } from "@server/routers/user";
import { AxiosResponse } from "axios";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useParams } from "next/navigation";
import CopyTextBox from "@app/components/CopyTextBox";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
} from "@app/components/Credenza";
import { useOrgContext } from "@app/hooks/useOrgContext";
type InviteUserFormProps = {
open: boolean;
setOpen: (open: boolean) => void;
};
const formSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
@@ -34,9 +49,9 @@ const formSchema = z.object({
roleId: z.string(),
});
export default function InviteUserForm() {
export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
const { toast } = useToast();
const { orgId } = useParams();
const { org } = useOrgContext();
const [inviteLink, setInviteLink] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@@ -74,7 +89,7 @@ export default function InviteUserForm() {
const res = await api
.post<AxiosResponse<InviteUserResponse>>(
`/org/${orgId}/create-invite`,
`/org/${org?.org.orgId}/create-invite`,
{
email: values.email,
roleId: parseInt(values.roleId),
@@ -107,117 +122,151 @@ export default function InviteUserForm() {
return (
<>
{!inviteLink && (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="Enter an email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map((role) => (
<SelectItem
key={role.roleId}
value={role.roleId.toString()}
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Invite User</CredenzaTitle>
<CredenzaDescription>
Give new users access to your organization
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
{!inviteLink && (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="invite-user-form"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="Enter an email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={field.value.toString()}
>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>Valid For</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select duration" />
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map((option) => (
<SelectItem
key={option.hours}
value={option.hours.toString()}
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map((role) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>Valid For</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={field.value.toString()}
>
{option.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select duration" />
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map(
(option) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
)}
{inviteLink && (
<div className="max-w-md">
<p className="mb-4">
The user has been successfully invited. They
must access the link below to accept the
invitation.
</p>
<p className="mb-4">
The invite will expire in{" "}
<b>
{expiresInDays}{" "}
{expiresInDays === 1 ? "day" : "days"}
</b>
.
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
)}
</CredenzaBody>
<CredenzaFooter>
<Button
type="submit"
className="w-full"
form="invite-user-form"
loading={loading}
disabled={inviteLink !== null}
>
Invite User
Create Invitation
</Button>
</form>
</Form>
)}
{inviteLink && (
<div className="max-w-md">
<p className="mb-4">
The user has been successfully invited. They must access
the link below to accept the invitation.
</p>
<p className="mb-4">
The invite will expire in{" "}
<b>
{expiresInDays}{" "}
{expiresInDays === 1 ? "day" : "days"}
</b>
.
</p>
<CopyTextBox text={inviteLink} wrapText={false} />
</div>
)}
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}

View File

@@ -10,16 +10,8 @@ import {
import { Button } from "@app/components/ui/button";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { UsersDataTable } from "./UsersDataTable";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@app/components/ui/dialog";
import { useState } from "react";
import InviteUserForm from "./InviteUserForm";
import { Credenza, CredenzaTitle, CredenzaDescription, CredenzaHeader, CredenzaClose, CredenzaFooter, CredenzaContent, CredenzaBody } from "@app/components/Credenza";
export type UserRow = {
id: string;
@@ -74,19 +66,10 @@ export default function UsersTable({ users }: UsersTableProps) {
return (
<>
<Credenza open={isInviteModalOpen} onOpenChange={setIsInviteModalOpen}>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Invite User</CredenzaTitle>
<CredenzaDescription>
Give new users access to your organization
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<InviteUserForm />
</CredenzaBody>
</CredenzaContent>
</Credenza>
<InviteUserForm
open={isInviteModalOpen}
setOpen={setIsInviteModalOpen}
/>
<UsersDataTable
columns={columns}

View File

@@ -3,6 +3,9 @@ import { authCookieHeader } from "@app/api/cookies";
import { ListUsersResponse } from "@server/routers/user";
import { AxiosResponse } from "axios";
import UsersTable, { UserRow } from "./components/UsersTable";
import { GetOrgResponse } from "@server/routers/org";
import { cache } from "react";
import OrgProvider from "@app/providers/OrgProvider";
type UsersPageProps = {
params: Promise<{ orgId: string }>;
@@ -10,15 +13,36 @@ type UsersPageProps = {
export default async function UsersPage(props: UsersPageProps) {
const params = await props.params;
let users: ListUsersResponse["users"] = [];
try {
const res = await internal.get<AxiosResponse<ListUsersResponse>>(
const res = await internal
.get<AxiosResponse<ListUsersResponse>>(
`/org/${params.orgId}/users`,
await authCookieHeader()
);
)
.catch((e) => {
console.error(e);
});
if (res && res.status === 200) {
users = res.data.data.users;
} catch (e) {
console.error("Error fetching users", e);
}
let org: GetOrgResponse | null = null;
const getOrg = cache(async () =>
internal
.get<AxiosResponse<GetOrgResponse>>(
`/org/${params.orgId}`,
await authCookieHeader()
)
.catch((e) => {
console.error(e);
})
);
const orgRes = await getOrg();
if (orgRes && orgRes.status === 200) {
org = orgRes.data.data;
}
const userRows: UserRow[] = users.map((user) => {
@@ -40,7 +64,9 @@ export default async function UsersPage(props: UsersPageProps) {
</p>
</div>
<UsersTable users={userRows} />
<OrgProvider org={org}>
<UsersTable users={userRows} />
</OrgProvider>
</>
);
}

View File

@@ -2,12 +2,16 @@ import LoginForm from "@app/app/auth/login/LoginForm";
import { verifySession } from "@app/lib/auth/verifySession";
import Link from "next/link";
import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = 'force-dynamic';
export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const user = await verifySession();
const getUser = cache(verifySession);
const user = await getUser();
if (user) {
redirect("/");

View File

@@ -2,12 +2,16 @@ import SignupForm from "@app/app/auth/signup/SignupForm";
import { verifySession } from "@app/lib/auth/verifySession";
import Link from "next/link";
import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = 'force-dynamic';
export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const user = await verifySession();
const getUser = cache(verifySession);
const user = await getUser();
if (user) {
redirect("/");

View File

@@ -32,7 +32,7 @@ import { AxiosResponse } from "axios";
import { VerifyEmailResponse } from "@server/routers/auth";
import { Loader2 } from "lucide-react";
import { Alert, AlertDescription } from "../../../components/ui/alert";
import { useToast } from "@app/hooks/use-toast";
import { useToast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
const FormSchema = z.object({

View File

@@ -1,6 +1,9 @@
import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm";
import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = "force-dynamic";
export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -10,7 +13,8 @@ export default async function Page(props: {
}
const searchParams = await props.searchParams;
const user = await verifySession();
const getUser = cache(verifySession);
const user = await getUser();
if (!user) {
redirect("/");

View File

@@ -7,12 +7,16 @@ import { AxiosResponse } from "axios";
import { ArrowUpRight } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { cache } from "react";
export const dynamic = "force-dynamic";
export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const params = await props.searchParams; // this is needed to prevent static optimization
const user = await verifySession();
const getUser = cache(verifySession);
const user = await getUser();
if (!user) {
redirect("/auth/login");

View File

@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import {
Command,

View File

@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button, buttonVariants } from "@/components/ui/button"
import {
Form,

View File

@@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {

View File

@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {

View File

@@ -6,7 +6,7 @@ import { useFieldArray, useForm } from "react-hook-form"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import {

View File

@@ -5,7 +5,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import api from "@app/api";
import { toast } from "@app/hooks/use-toast";
import { toast } from "@app/hooks/useToast";
import { useCallback, useEffect, useState } from "react";
import {
Card,

View File

@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import {
Command,

View File

@@ -1,169 +1,179 @@
"use client"
"use client";
import { zodResolver } from "@hookform/resolvers/zod"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod";
import { 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 { cn } from "@/lib/utils";
import { toast } from "@/hooks/useToast";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { useSiteContext } from "@app/hooks/useSiteContext"
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useSiteContext } from "@app/hooks/useSiteContext";
const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark"], {
required_error: "Please select a theme.",
}),
font: z.enum(["inter", "manrope", "system"], {
invalid_type_error: "Select a font",
required_error: "Please select a font.",
}),
})
theme: z.enum(["light", "dark"], {
required_error: "Please select a theme.",
}),
font: z.enum(["inter", "manrope", "system"], {
invalid_type_error: "Select a font",
required_error: "Please select a font.",
}),
});
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: "light",
}
theme: "light",
};
export function AppearanceForm() {
const site = useSiteContext();
console.log(site);
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceFormSchema),
defaultValues,
})
function onSubmit(data: AppearanceFormValues) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceFormSchema),
defaultValues,
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="font"
render={({ field }) => (
<FormItem>
<FormLabel>Font</FormLabel>
<div className="relative w-max">
<FormControl>
<select
className={cn(
buttonVariants({ variant: "outline" }),
"w-[200px] appearance-none font-normal"
function onSubmit(data: AppearanceFormValues) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">
{JSON.stringify(data, null, 2)}
</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="font"
render={({ field }) => (
<FormItem>
<FormLabel>Font</FormLabel>
<div className="relative w-max">
<FormControl>
<select
className={cn(
buttonVariants({
variant: "outline",
}),
"w-[200px] appearance-none font-normal"
)}
{...field}
>
<option value="inter">Inter</option>
<option value="manrope">Manrope</option>
<option value="system">System</option>
</select>
</FormControl>
<ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
</div>
<FormDescription>
Set the font you want to use in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
{...field}
>
<option value="inter">Inter</option>
<option value="manrope">Manrope</option>
<option value="system">System</option>
</select>
</FormControl>
<ChevronDownIcon className="absolute right-3 top-2.5 h-4 w-4 opacity-50" />
</div>
<FormDescription>
Set the font you want to use in the dashboard.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="theme"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>Theme</FormLabel>
<FormDescription>
Select the theme for the dashboard.
</FormDescription>
<FormMessage />
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="grid max-w-md grid-cols-2 gap-8 pt-2"
>
<FormItem>
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
<FormControl>
<RadioGroupItem value="light" className="sr-only" />
</FormControl>
<div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
<div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
</div>
</div>
<span className="block w-full p-2 text-center font-normal">
Light
</span>
</FormLabel>
</FormItem>
<FormItem>
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
<FormControl>
<RadioGroupItem value="dark" className="sr-only" />
</FormControl>
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
<div className="space-y-2 rounded-sm bg-slate-950 p-2">
<div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div className="h-2 w-[80px] rounded-lg bg-slate-400" />
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-slate-400" />
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-slate-400" />
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
</div>
</div>
<span className="block w-full p-2 text-center font-normal">
Dark
</span>
</FormLabel>
</FormItem>
</RadioGroup>
</FormItem>
)}
/>
/>
<FormField
control={form.control}
name="theme"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>Theme</FormLabel>
<FormDescription>
Select the theme for the dashboard.
</FormDescription>
<FormMessage />
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="grid max-w-md grid-cols-2 gap-8 pt-2"
>
<FormItem>
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
<FormControl>
<RadioGroupItem
value="light"
className="sr-only"
/>
</FormControl>
<div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
<div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
<div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
</div>
</div>
<span className="block w-full p-2 text-center font-normal">
Light
</span>
</FormLabel>
</FormItem>
<FormItem>
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
<FormControl>
<RadioGroupItem
value="dark"
className="sr-only"
/>
</FormControl>
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
<div className="space-y-2 rounded-sm bg-slate-950 p-2">
<div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div className="h-2 w-[80px] rounded-lg bg-slate-400" />
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-slate-400" />
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div className="h-4 w-4 rounded-full bg-slate-400" />
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
</div>
</div>
<span className="block w-full p-2 text-center font-normal">
Dark
</span>
</FormLabel>
</FormItem>
</RadioGroup>
</FormItem>
)}
/>
<Button type="submit">Update preferences</Button>
</form>
</Form>
)
<Button type="submit">Update preferences</Button>
</form>
</Form>
);
}

View File

@@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {

View File

@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {

View File

@@ -6,7 +6,7 @@ import { useFieldArray, useForm } from "react-hook-form"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { toast } from "@/hooks/use-toast"
import { toast } from "@/hooks/useToast"
import { Button } from "@/components/ui/button"
import {

View File

@@ -1,6 +1,6 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import { useToast } from "@/hooks/useToast"
import {
Toast,
ToastClose,

View File

@@ -0,0 +1,11 @@
import { GetOrgResponse } from "@server/routers/org";
import { createContext } from "react";
interface OrgContextType {
org: GetOrgResponse | null;
updateOrg: (updateOrg: Partial<GetOrgResponse>) => void;
}
const OrgContext = createContext<OrgContextType | undefined>(undefined);
export default OrgContext;

View File

@@ -3,9 +3,11 @@ import { createContext } from "react";
interface ResourceContextType {
resource: GetResourceResponse | null;
updateResource: (updatedResource: Partial<GetResourceResponse>) => Promise<void>;
updateResource: (updatedResource: Partial<GetResourceResponse>) => void;
}
const ResourceContext = createContext<ResourceContextType | undefined>(undefined);
const ResourceContext = createContext<ResourceContextType | undefined>(
undefined
);
export default ResourceContext;
export default ResourceContext;

View File

@@ -3,9 +3,9 @@ import { createContext } from "react";
interface SiteContextType {
site: GetSiteResponse | null;
updateSite: (updatedSite: Partial<GetSiteResponse>) => Promise<void>;
updateSite: (updatedSite: Partial<GetSiteResponse>) => void;
}
const SiteContext = createContext<SiteContextType | undefined>(undefined);
export default SiteContext;
export default SiteContext;

View File

@@ -0,0 +1,10 @@
import OrgContext from "@app/contexts/orgContext";
import { useContext } from "react";
export function useOrgContext() {
const context = useContext(OrgContext);
if (context === undefined) {
throw new Error("useOrgContext must be used within a OrgProvider");
}
return context;
}

View File

@@ -4,7 +4,9 @@ import { useContext } from "react";
export function useResourceContext() {
const context = useContext(ResourceContext);
if (context === undefined) {
throw new Error('useResourceContext must be used within a ResourceProvider');
throw new Error(
"useResourceContext must be used within a ResourceProvider"
);
}
return context;
}
}

View File

@@ -14,4 +14,4 @@ export async function verifySession(): Promise<GetUserResponse | null> {
} catch {
return null;
}
}
}

View File

@@ -0,0 +1,39 @@
"use client";
import OrgContext from "@app/contexts/orgContext";
import { GetOrgResponse } from "@server/routers/org";
import { useState } from "react";
interface OrgProviderProps {
children: React.ReactNode;
org: GetOrgResponse | null;
}
export function OrgProvider({ children, org: serverOrg }: OrgProviderProps) {
const [org, setOrg] = useState<GetOrgResponse | null>(serverOrg);
const updateOrg = (updatedOrg: Partial<GetOrgResponse>) => {
if (!org) {
throw new Error("No org to update");
}
setOrg((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
...updatedOrg,
};
});
};
return (
<OrgContext.Provider value={{ org, updateOrg }}>
{children}
</OrgContext.Provider>
);
}
export default OrgProvider;

View File

@@ -1,10 +1,7 @@
"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 {
@@ -12,34 +9,36 @@ interface ResourceProviderProps {
resource: GetResourceResponse | null;
}
export function ResourceProvider({ children, resource: serverResource }: ResourceProviderProps) {
const [resource, setResource] = useState<GetResourceResponse | null>(serverResource);
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 updateResource = (updatedResource: Partial<GetResourceResponse>) => {
if (!resource) {
throw new Error("No resource to update");
}
setResource((prev) => {
if (!prev) {
return prev;
}
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({
variant: "destructive",
title: "Error updating resource...",
})
}
return {
...prev,
...updatedResource,
};
});
};
return <ResourceContext.Provider value={{ resource, updateResource }}>{children}</ResourceContext.Provider>;
return (
<ResourceContext.Provider value={{ resource, updateResource }}>
{children}
</ResourceContext.Provider>
);
}
export default ResourceProvider;
export default ResourceProvider;

View File

@@ -1,10 +1,7 @@
"use client";
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 { AxiosResponse } from "axios";
import { useState } from "react";
interface SiteProviderProps {
@@ -12,34 +9,32 @@ interface SiteProviderProps {
site: GetSiteResponse | null;
}
export function SiteProvider({ children, site: serverSite }: SiteProviderProps) {
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({
variant: "destructive",
title: "Error updating site...",
})
const updateSite = (updatedSite: Partial<GetSiteResponse>) => {
if (!site) {
throw new Error("No site to update");
}
setSite((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
...updatedSite,
};
});
};
return <SiteContext.Provider value={{ site, updateSite }}>{children}</SiteContext.Provider>;
return (
<SiteContext.Provider value={{ site, updateSite }}>
{children}
</SiteContext.Provider>
);
}
export default SiteProvider;
export default SiteProvider;