Openstatus www.openstatus.dev

chore: lots of wip (#76)

* chore: lots of wip

* fix: issue on base /app page

* chore: improve empty-state

* chore: edit and delete status page

* chore: include social icons

* Tinybird improvement (#74)

* chore: update tinybird

* chore: update tinybird

* fix: turso

---------

Co-authored-by: Thibault Le Ouay <thibaultleouay@gmail.Com>

authored by

Maximilian Kaske
Thibault Le Ouay
and committed by
GitHub
9b9a417d 76415ac7

+1047 -108
+2
.gitignore
··· 47 47 openstatus.db-wal 48 48 openstatus.db 49 49 openstatus.db-shm 50 + 51 + 50 52 packages/tinybird/.tinyb
+1
apps/web/package.json
··· 18 18 "@openstatus/upstash": "workspace:*", 19 19 "@radix-ui/react-accordion": "^1.1.2", 20 20 "@radix-ui/react-alert-dialog": "^1.0.4", 21 + "@radix-ui/react-checkbox": "^1.0.4", 21 22 "@radix-ui/react-dialog": "^1.0.4", 22 23 "@radix-ui/react-dropdown-menu": "^2.0.5", 23 24 "@radix-ui/react-hover-card": "^1.0.6",
+8 -6
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/_components/action-button.tsx
··· 122 122 </AlertDialogFooter> 123 123 </AlertDialogContent> 124 124 </AlertDialog> 125 - <DialogContent> 125 + <DialogContent className="flex max-h-screen flex-col"> 126 126 <DialogHeader> 127 127 <DialogTitle>Update Monitor</DialogTitle> 128 128 <DialogDescription>Change your settings.</DialogDescription> 129 129 </DialogHeader> 130 - <MonitorForm 131 - id="monitor-update" 132 - onSubmit={onUpdate} 133 - defaultValues={props} 134 - /> 130 + <div className="-mx-1 flex-1 overflow-y-scroll px-1"> 131 + <MonitorForm 132 + id="monitor-update" 133 + onSubmit={onUpdate} 134 + defaultValues={props} 135 + /> 136 + </div> 135 137 <DialogFooter> 136 138 <Button 137 139 type="submit"
+7 -3
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/_components/create-form.tsx
··· 20 20 } from "@/components/ui/dialog"; 21 21 import { api } from "@/trpc/client"; 22 22 23 + type MonitorSchema = z.infer<typeof insertMonitorSchema>; 24 + 23 25 interface Props { 24 26 workspaceId: number; 25 27 } ··· 29 31 const [open, setOpen] = React.useState(false); 30 32 const [saving, setSaving] = React.useState(false); 31 33 32 - async function onCreate(values: z.infer<typeof insertMonitorSchema>) { 34 + async function onCreate(values: MonitorSchema) { 33 35 setSaving(true); 34 36 // await api.monitor.getMonitorsByWorkspace.revalidate(); 35 37 await api.monitor.createMonitor.mutate({ ...values, workspaceId }); ··· 43 45 <DialogTrigger asChild> 44 46 <Button>Create</Button> 45 47 </DialogTrigger> 46 - <DialogContent> 48 + <DialogContent className="flex max-h-screen flex-col"> 47 49 <DialogHeader> 48 50 <DialogTitle>Create Monitor</DialogTitle> 49 51 <DialogDescription>Choose the settings.</DialogDescription> 50 52 </DialogHeader> 51 - <MonitorForm id="monitor-create" onSubmit={onCreate} /> 53 + <div className="-mx-1 flex-1 overflow-y-scroll px-1"> 54 + <MonitorForm id="monitor-create" onSubmit={onCreate} /> 55 + </div> 52 56 <DialogFooter> 53 57 <Button type="submit" form="monitor-create" disabled={saving}> 54 58 {!saving ? "Confirm" : <LoadingAnimation />}
+13
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/_components/empty-state.tsx
··· 1 + import { EmptyState as DefaultEmptyState } from "@/components/dashboard/empty-state"; 2 + import { CreateForm } from "./create-form"; 3 + 4 + export function EmptyState({ workspaceId }: { workspaceId: number }) { 5 + return ( 6 + <DefaultEmptyState 7 + icon="activity" 8 + title="No monitors" 9 + description="Create your first monitor" 10 + action={<CreateForm {...{ workspaceId }} />} 11 + /> 12 + ); 13 + }
+47 -40
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/page.tsx
··· 7 7 import { api } from "@/trpc/server"; 8 8 import { ActionButton } from "./_components/action-button"; 9 9 import { CreateForm } from "./_components/create-form"; 10 + import { EmptyState } from "./_components/empty-state"; 10 11 11 12 export default async function MonitorPage({ 12 13 params, ··· 23 24 <Header title="Monitors" description="Overview of all your monitors."> 24 25 <CreateForm {...{ workspaceId }} /> 25 26 </Header> 26 - {monitors?.map((monitor, index) => ( 27 - <Container 28 - key={index} 29 - title={monitor.name} 30 - description={monitor.description} 31 - > 32 - <ActionButton {...monitor} /> 33 - <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 34 - <div className="flex min-w-0 items-center justify-between gap-3"> 35 - <dt>Status</dt> 36 - <dd> 37 - <Badge 38 - variant={monitor.status === "active" ? "default" : "outline"} 39 - className="capitalize" 40 - > 41 - {monitor.status} 42 - <span 43 - className={cn( 44 - "ml-1 h-1.5 w-1.5 rounded-full", 45 - monitor.status === "active" 46 - ? "bg-green-500" 47 - : "bg-red-500", 48 - )} 49 - /> 50 - </Badge> 51 - </dd> 52 - </div> 53 - <div className="flex min-w-0 items-center justify-between gap-3"> 54 - <dt>Periodicity</dt> 55 - <dd className="font-mono">{monitor.periodicity}</dd> 56 - </div> 57 - <div className="flex min-w-0 items-center justify-between gap-3"> 58 - <dt>URL</dt> 59 - <dd className="overflow-hidden text-ellipsis font-semibold"> 60 - {monitor.url} 61 - </dd> 62 - </div> 63 - </dl> 64 - </Container> 65 - ))} 27 + {Boolean(monitors?.length) ? ( 28 + monitors?.map((monitor, index) => ( 29 + <Container 30 + key={index} 31 + title={monitor.name} 32 + description={monitor.description} 33 + > 34 + <ActionButton {...monitor} /> 35 + <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 36 + <div className="flex min-w-0 items-center justify-between gap-3"> 37 + <dt>Status</dt> 38 + <dd> 39 + <Badge 40 + variant={ 41 + monitor.status === "active" ? "default" : "outline" 42 + } 43 + className="capitalize" 44 + > 45 + {monitor.status} 46 + <span 47 + className={cn( 48 + "ml-1 h-1.5 w-1.5 rounded-full", 49 + monitor.status === "active" 50 + ? "bg-green-500" 51 + : "bg-red-500", 52 + )} 53 + /> 54 + </Badge> 55 + </dd> 56 + </div> 57 + <div className="flex min-w-0 items-center justify-between gap-3"> 58 + <dt>Periodicity</dt> 59 + <dd className="font-mono">{monitor.periodicity}</dd> 60 + </div> 61 + <div className="flex min-w-0 items-center justify-between gap-3"> 62 + <dt>URL</dt> 63 + <dd className="overflow-hidden text-ellipsis font-semibold"> 64 + {monitor.url} 65 + </dd> 66 + </div> 67 + </dl> 68 + </Container> 69 + )) 70 + ) : ( 71 + <EmptyState {...{ workspaceId }} /> 72 + )} 66 73 </div> 67 74 ); 68 75 }
+1 -1
apps/web/src/app/app/(dashboard)/[workspaceId]/page.tsx
··· 1 1 import { redirect } from "next/navigation"; 2 2 3 - export default function DiscordRedirect({ 3 + export default function DashboardRedirect({ 4 4 params, 5 5 }: { 6 6 params: { workspaceId: string };
+39 -17
apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/_components/action-button.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 - import Link from "next/link"; 5 - import { useParams, usePathname, useRouter } from "next/navigation"; 4 + import { useRouter } from "next/navigation"; 6 5 import { MoreVertical } from "lucide-react"; 7 6 import type * as z from "zod"; 8 7 9 - import type { insertPageSchema } from "@openstatus/db/src/schema"; 8 + import type { 9 + insertMonitorSchema, 10 + insertPageSchema, 11 + } from "@openstatus/db/src/schema"; 10 12 11 13 import { StatusPageForm } from "@/components/forms/status-page-form"; 12 14 import { LoadingAnimation } from "@/components/loading-animation"; ··· 37 39 DropdownMenuItem, 38 40 DropdownMenuTrigger, 39 41 } from "@/components/ui/dropdown-menu"; 40 - import { wait } from "@/lib/utils"; 41 42 import { api } from "@/trpc/client"; 42 43 43 - type Schema = z.infer<typeof insertPageSchema>; 44 + type MonitorSchema = z.infer<typeof insertMonitorSchema>; 45 + type PageSchema = z.infer<typeof insertPageSchema>; 44 46 45 - export function ActionButton(props: Schema) { 47 + // allMonitors 48 + interface ActionButtonProps { 49 + page: PageSchema; 50 + allMonitors?: MonitorSchema[]; 51 + } 52 + 53 + export function ActionButton({ page, allMonitors }: ActionButtonProps) { 46 54 const router = useRouter(); 47 55 const [dialogOpen, setDialogOpen] = React.useState(false); 48 56 const [alertOpen, setAlertOpen] = React.useState(false); 49 57 const [saving, setSaving] = React.useState(false); 50 58 51 - async function onUpdate(values: Schema) { 59 + async function onUpdate({ 60 + monitors, 61 + workspaceId, 62 + ...props 63 + }: PageSchema & { monitors: string[] }) { 52 64 setSaving(true); 53 - // await api.monitor.updateMonitor.mutate({ id: props.id, ...values }); 54 - await wait(1000); 65 + await api.page.updatePage.mutate({ 66 + id: page.id, 67 + workspaceId: page.workspaceId, 68 + ...props, 69 + }); 55 70 router.refresh(); 56 71 setSaving(false); 57 72 setDialogOpen(false); ··· 59 74 60 75 async function onDelete() { 61 76 setSaving(true); 62 - // await api.monitor.deleteMonitor.mutate({ monitorId: Number(props.id) }); 63 - await wait(1000); 77 + await api.page.deletePage.mutate({ pageId: Number(page.id) }); 64 78 router.refresh(); 65 79 setSaving(false); 66 80 setAlertOpen(false); ··· 116 130 </AlertDialogFooter> 117 131 </AlertDialogContent> 118 132 </AlertDialog> 119 - <DialogContent> 133 + <DialogContent className="flex max-h-screen flex-col"> 120 134 <DialogHeader> 121 135 <DialogTitle>Update Page</DialogTitle> 122 136 <DialogDescription>Change your settings.</DialogDescription> 123 137 </DialogHeader> 124 - <StatusPageForm 125 - id="status-page-update" 126 - onSubmit={onUpdate} 127 - defaultValues={props} 128 - /> 138 + <div className="-mx-1 flex-1 overflow-y-scroll px-1"> 139 + <StatusPageForm 140 + id="status-page-update" 141 + onSubmit={onUpdate} 142 + defaultValues={page} 143 + allMonitors={ 144 + allMonitors?.map((m) => ({ 145 + label: m.name || "", 146 + value: String(m.id) || "", 147 + })) ?? [] 148 + } 149 + /> 150 + </div> 129 151 <DialogFooter> 130 152 <Button 131 153 type="submit"
+29 -7
apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/_components/create-form.tsx
··· 4 4 import { useRouter } from "next/navigation"; 5 5 import type * as z from "zod"; 6 6 7 - import type { insertPageSchema } from "@openstatus/db/src/schema"; 7 + import type { 8 + insertMonitorSchema, 9 + insertPageSchema, 10 + } from "@openstatus/db/src/schema"; 8 11 9 12 import { StatusPageForm } from "@/components/forms/status-page-form"; 10 13 import { LoadingAnimation } from "@/components/loading-animation"; ··· 20 23 } from "@/components/ui/dialog"; 21 24 import { api } from "@/trpc/client"; 22 25 26 + type MonitorSchema = z.infer<typeof insertMonitorSchema>; 27 + type PageSchema = z.infer<typeof insertPageSchema>; 28 + 23 29 interface Props { 24 30 workspaceId: number; 31 + allMonitors?: MonitorSchema[]; 32 + disabled?: boolean; 25 33 } 26 34 27 - export function CreateForm({ workspaceId }: Props) { 35 + export function CreateForm({ workspaceId, allMonitors, disabled }: Props) { 28 36 const router = useRouter(); 29 37 const [open, setOpen] = React.useState(false); 30 38 const [saving, setSaving] = React.useState(false); 31 39 32 - async function onCreate(values: z.infer<typeof insertPageSchema>) { 40 + async function onCreate({ 41 + monitors, // TODO: 42 + ...props 43 + }: PageSchema & { monitors: string[] }) { 33 44 setSaving(true); 34 45 // await api.monitor.getMonitorsByWorkspace.revalidate(); 35 - await api.page.createPage.mutate({ ...values, workspaceId }); 46 + await api.page.createPage.mutate({ ...props, workspaceId }); 36 47 router.refresh(); 37 48 setSaving(false); 38 49 setOpen(false); ··· 41 52 return ( 42 53 <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 43 54 <DialogTrigger asChild> 44 - <Button>Create</Button> 55 + <Button disabled={disabled}>Create</Button> 45 56 </DialogTrigger> 46 - <DialogContent> 57 + <DialogContent className="flex max-h-screen flex-col"> 47 58 <DialogHeader> 48 59 <DialogTitle>Create Status Page</DialogTitle> 49 60 <DialogDescription>Choose the settings.</DialogDescription> 50 61 </DialogHeader> 51 - <StatusPageForm id="status-page-create" onSubmit={onCreate} /> 62 + <div className="-mx-1 flex-1 overflow-y-scroll px-1"> 63 + <StatusPageForm 64 + id="status-page-create" 65 + onSubmit={onCreate} 66 + allMonitors={ 67 + allMonitors?.map((m) => ({ 68 + label: m.name || "", 69 + value: String(m.id) || "", 70 + })) ?? [] 71 + } 72 + /> 73 + </div> 52 74 <DialogFooter> 53 75 <Button type="submit" form="status-page-create" disabled={saving}> 54 76 {!saving ? "Confirm" : <LoadingAnimation />}
+42
apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/_components/empty-state.tsx
··· 1 + import Link from "next/link"; 2 + import type * as z from "zod"; 3 + 4 + import type { insertMonitorSchema } from "@openstatus/db/src/schema"; 5 + 6 + import { EmptyState as DefaultEmptyState } from "@/components/dashboard/empty-state"; 7 + import { Button } from "@/components/ui/button"; 8 + import { CreateForm } from "./create-form"; 9 + 10 + type MonitorSchema = z.infer<typeof insertMonitorSchema>; 11 + 12 + export function EmptyState({ 13 + workspaceId, 14 + allMonitors, 15 + }: { 16 + workspaceId: number; 17 + allMonitors?: MonitorSchema[]; 18 + }) { 19 + // Navigate user to monitor if they don't have one 20 + if (!Boolean(allMonitors?.length)) { 21 + return ( 22 + <DefaultEmptyState 23 + icon="panel-top" 24 + title="No pages" 25 + description="First create a monitor before creating a page." 26 + action={ 27 + <Button asChild> 28 + <Link href="./monitors">Go to monitors</Link> 29 + </Button> 30 + } 31 + /> 32 + ); 33 + } 34 + return ( 35 + <DefaultEmptyState 36 + icon="panel-top" 37 + title="No pages" 38 + description="Create your first page." 39 + action={<CreateForm {...{ workspaceId, allMonitors }} />} 40 + /> 41 + ); 42 + }
+29 -17
apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/page.tsx
··· 5 5 import { api } from "@/trpc/server"; 6 6 import { ActionButton } from "./_components/action-button"; 7 7 import { CreateForm } from "./_components/create-form"; 8 + import { EmptyState } from "./_components/empty-state"; 8 9 9 10 export default async function Page({ 10 11 params, ··· 15 16 const pages = await api.page.getPageByWorkspace.query({ 16 17 workspaceId, 17 18 }); 18 - // iterate over pages 19 + const monitors = await api.monitor.getMonitorsByWorkspace.query({ 20 + workspaceId, 21 + }); 22 + 19 23 return ( 20 24 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 21 25 <Header 22 26 title="Status Page" 23 27 description="Overview of all your status page." 24 28 > 25 - <CreateForm {...{ workspaceId }} /> 29 + <CreateForm 30 + workspaceId={workspaceId} 31 + allMonitors={monitors} 32 + disabled={!Boolean(monitors)} 33 + /> 26 34 </Header> 27 - {pages.map((page, index) => ( 28 - <Container 29 - key={index} 30 - title={page.title} 31 - description={page.description} 32 - > 33 - <ActionButton {...page} /> 34 - <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 35 - <div className="flex min-w-0 items-center justify-between gap-3"> 36 - <dt>Slug</dt> 37 - <dd className="font-mono">{page.slug}</dd> 38 - </div> 39 - </dl> 40 - </Container> 41 - ))} 35 + {Boolean(pages.length) ? ( 36 + pages.map((page, index) => ( 37 + <Container 38 + key={index} 39 + title={page.title} 40 + description={page.description} 41 + > 42 + <ActionButton page={page} allMonitors={monitors} /> 43 + <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 44 + <div className="flex min-w-0 items-center justify-between gap-3"> 45 + <dt>Slug</dt> 46 + <dd className="font-mono">{page.slug}</dd> 47 + </div> 48 + </dl> 49 + </Container> 50 + )) 51 + ) : ( 52 + <EmptyState workspaceId={workspaceId} allMonitors={monitors} /> 53 + )} 42 54 </div> 43 55 ); 44 56 }
apps/web/src/app/app/(dashboard)/layout.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/layout.tsx
+27
apps/web/src/app/app/page.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { useRouter } from "next/navigation"; 5 + 6 + import { Shell } from "@/components/dashboard/shell"; 7 + import { LoadingAnimation } from "@/components/loading-animation"; 8 + 9 + // TODO: discuss how to make that page a bit more enjoyable 10 + export default function Page() { 11 + const router = useRouter(); 12 + 13 + // waiting for the workspace to be created 14 + setTimeout(() => router.refresh(), 1000); 15 + 16 + return ( 17 + <div className="flex min-h-screen flex-col items-center justify-center"> 18 + <Shell className="mx-auto grid w-auto gap-4"> 19 + <div className="grid gap-1 text-center"> 20 + <p className="text-lg font-bold">Creating Workspace</p> 21 + <p className="text-muted-foreground">Should be done in a second.</p> 22 + </div> 23 + <LoadingAnimation variant="inverse" size="lg" /> 24 + </Shell> 25 + </div> 26 + ); 27 + }
+5
apps/web/src/app/github/page.tsx
··· 1 + import { redirect } from "next/navigation"; 2 + 3 + export default function GithubRedirect() { 4 + return redirect("https://github.com/openstatusHQ/openstatus"); 5 + }
+27
apps/web/src/components/dashboard/empty-state.tsx
··· 1 + import * as React from "react"; 2 + 3 + import type { ValidIcon } from "@/components/icons"; 4 + import { Icons } from "@/components/icons"; 5 + 6 + interface Props extends React.ComponentProps<"div"> { 7 + icon: ValidIcon; 8 + title: string; 9 + description: string; 10 + action: React.ReactNode; 11 + } 12 + 13 + export function EmptyState({ icon, title, description, action }: Props) { 14 + const Icon = Icons[icon]; 15 + return ( 16 + <div className="border-border bg-background col-span-full w-full rounded-lg border border-dashed p-8"> 17 + <div className="flex flex-col items-center justify-center gap-4"> 18 + <div className="flex flex-col items-center justify-center gap-1"> 19 + <Icon className="h-6 w-6" /> 20 + <p className="text-foreground text-base">{title}</p> 21 + <p className="text-muted-foreground">{description}</p> 22 + </div> 23 + <div>{action}</div> 24 + </div> 25 + </div> 26 + ); 27 + }
+74 -6
apps/web/src/components/forms/status-page-form.tsx
··· 1 1 "use client"; 2 2 3 + import * as React from "react"; 3 4 import { zodResolver } from "@hookform/resolvers/zod"; 4 5 import { useForm } from "react-hook-form"; 5 - import type * as z from "zod"; 6 + import * as z from "zod"; 6 7 7 8 import { insertPageSchema } from "@openstatus/db/src/schema"; 8 9 ··· 16 17 FormMessage, 17 18 } from "@/components/ui/form"; 18 19 import { Input } from "@/components/ui/input"; 20 + import { Checkbox } from "../ui/checkbox"; 19 21 20 - type Schema = z.infer<typeof insertPageSchema>; 22 + // REMINDER: only use the props you need! 23 + const schema = insertPageSchema 24 + .pick({ title: true, slug: true, description: true }) 25 + .merge( 26 + z.object({ 27 + monitors: z.string().array().optional(), // HOW TO PASS 28 + }), 29 + ); 30 + 31 + type Schema = z.infer<typeof schema>; 21 32 22 33 interface Props { 23 34 id: string; 24 35 defaultValues?: Schema; 25 36 onSubmit: (values: Schema) => Promise<void>; 37 + allMonitors?: Record<"label" | "value", string>[]; 26 38 } 27 39 28 - export function StatusPageForm({ id, defaultValues, onSubmit }: Props) { 40 + export function StatusPageForm({ 41 + id, 42 + defaultValues, 43 + onSubmit, 44 + allMonitors, 45 + }: Props) { 29 46 const form = useForm<Schema>({ 30 - resolver: zodResolver(insertPageSchema), 47 + resolver: zodResolver(schema), 31 48 defaultValues: { 32 49 title: defaultValues?.title || "", 33 - slug: defaultValues?.slug || "", 50 + slug: defaultValues?.slug || "", // TODO: verify if is unique 34 51 description: defaultValues?.description || "", 35 - workspaceId: defaultValues?.workspaceId || 0, 52 + monitors: [], 36 53 }, 37 54 }); 38 55 ··· 82 99 <FormDescription> 83 100 Give your user some information about it. 84 101 </FormDescription> 102 + <FormMessage /> 103 + </FormItem> 104 + )} 105 + /> 106 + <FormField 107 + control={form.control} 108 + name="monitors" 109 + render={() => ( 110 + <FormItem> 111 + <div className="mb-4"> 112 + <FormLabel className="text-base">Monitor</FormLabel> 113 + <FormDescription> 114 + Select the monitors you want to display. 115 + </FormDescription> 116 + </div> 117 + {allMonitors?.map((item) => ( 118 + <FormField 119 + key={item.value} 120 + control={form.control} 121 + name="monitors" 122 + render={({ field }) => { 123 + return ( 124 + <FormItem 125 + key={item.value} 126 + className="flex flex-row items-start space-x-3 space-y-0" 127 + > 128 + <FormControl> 129 + <Checkbox 130 + checked={field.value?.includes(item.value)} 131 + onCheckedChange={(checked) => { 132 + return checked 133 + ? field.onChange([ 134 + ...(field.value || []), 135 + item.value, 136 + ]) 137 + : field.onChange( 138 + field.value?.filter( 139 + (value) => value !== item.value, 140 + ), 141 + ); 142 + }} 143 + /> 144 + </FormControl> 145 + <FormLabel className="font-normal"> 146 + {item.label} 147 + </FormLabel> 148 + </FormItem> 149 + ); 150 + }} 151 + /> 152 + ))} 85 153 <FormMessage /> 86 154 </FormItem> 87 155 )}
+16
apps/web/src/components/icons.tsx
··· 18 18 siren: Siren, 19 19 "panel-top": PanelTop, 20 20 table: Table, 21 + discord: ({ ...props }: LucideProps) => ( 22 + <svg viewBox="0 0 640 512" {...props}> 23 + <path 24 + fill="currentColor" 25 + d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z" 26 + /> 27 + </svg> 28 + ), 29 + github: (props: LucideProps) => ( 30 + <svg viewBox="0 0 438.549 438.549" {...props}> 31 + <path 32 + fill="currentColor" 33 + d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" 34 + ></path> 35 + </svg> 36 + ), 21 37 } as const;
+25 -5
apps/web/src/components/layout/app-header.tsx
··· 3 3 import Link from "next/link"; 4 4 import { UserButton, useUser } from "@clerk/nextjs"; 5 5 6 + import { socialsConfig } from "@/config/socials"; 6 7 import { Shell } from "../dashboard/shell"; 8 + import { Icons } from "../icons"; 9 + import { Button } from "../ui/button"; 7 10 import { Skeleton } from "../ui/skeleton"; 8 11 9 12 export function AppHeader() { ··· 18 21 > 19 22 openstatus 20 23 </Link> 21 - {!isLoaded && !isSignedIn ? ( 22 - <Skeleton className="h-8 w-8 rounded-full" /> 23 - ) : ( 24 - <UserButton /> 25 - )} 24 + <div className="flex items-center gap-4"> 25 + {/* can be moved to a different place */} 26 + <ul className="flex gap-2"> 27 + {socialsConfig.map(({ title, href, icon }) => { 28 + const Icon = Icons[icon]; 29 + return ( 30 + <li key={title} className="w-full"> 31 + <Button size="icon" variant="ghost" asChild> 32 + <a href={href} target="_blank" rel="noreferrer"> 33 + <Icon className="h-5 w-5" /> 34 + </a> 35 + </Button> 36 + </li> 37 + ); 38 + })} 39 + </ul> 40 + {!isLoaded && !isSignedIn ? ( 41 + <Skeleton className="h-8 w-8 rounded-full" /> 42 + ) : ( 43 + <UserButton /> 44 + )} 45 + </div> 26 46 </Shell> 27 47 </header> 28 48 );
+1 -1
apps/web/src/components/layout/app-menu.tsx
··· 33 33 <SheetContent> 34 34 <SheetHeader> 35 35 <SheetTitle className="text-left">Navigation</SheetTitle> 36 - <AppSidebar /> 37 36 </SheetHeader> 37 + <AppSidebar /> 38 38 </SheetContent> 39 39 </Sheet> 40 40 );
+36 -5
apps/web/src/components/loading-animation.tsx
··· 1 + import { cva } from "class-variance-authority"; 2 + import type { VariantProps } from "class-variance-authority"; 3 + 1 4 import { cn } from "@/lib/utils"; 2 5 3 - type Props = React.HTMLAttributes<HTMLDivElement>; 6 + const loadingVariants = cva( 7 + "animate-pulse rounded-full direction-alternate duration-700", 8 + { 9 + variants: { 10 + variant: { 11 + // we might want to inverse both styles 12 + default: "bg-primary-foreground", 13 + inverse: "bg-primary", 14 + }, 15 + size: { 16 + default: "h-1 w-1", 17 + lg: "h-1.5 w-1.5", 18 + }, 19 + }, 20 + defaultVariants: { 21 + variant: "default", 22 + size: "default", 23 + }, 24 + }, 25 + ); 4 26 5 - export function LoadingAnimation({ className, ...props }: Props) { 27 + interface Props 28 + extends React.ComponentProps<"div">, 29 + VariantProps<typeof loadingVariants> {} 30 + 31 + export function LoadingAnimation({ 32 + className, 33 + variant, 34 + size, 35 + ...props 36 + }: Props) { 6 37 return ( 7 38 <div 8 39 className={cn("flex items-center justify-center gap-1", className)} 9 40 {...props} 10 41 > 11 - <div className="direction-alternate bg-primary-foreground h-1 w-1 animate-pulse rounded-full duration-700" /> 12 - <div className="direction-alternate bg-primary-foreground h-1 w-1 animate-pulse rounded-full delay-150 duration-700" /> 13 - <div className="direction-alternate bg-primary-foreground h-1 w-1 animate-pulse rounded-full delay-300 duration-700" /> 42 + <div className={cn(loadingVariants({ variant, size }))} /> 43 + <div className={cn(loadingVariants({ variant, size }), "delay-150")} /> 44 + <div className={cn(loadingVariants({ variant, size }), "delay-300")} /> 14 45 </div> 15 46 ); 16 47 }
+473
apps/web/src/components/ui/auto-form.tsx
··· 1 + "use client"; 2 + 3 + import React from "react"; 4 + import { zodResolver } from "@hookform/resolvers/zod"; 5 + import type { 6 + ControllerRenderProps, 7 + DefaultValues, 8 + FieldValues, 9 + } from "react-hook-form"; 10 + import { useForm } from "react-hook-form"; 11 + import type { z } from "zod"; 12 + 13 + import { 14 + Select, 15 + SelectContent, 16 + SelectItem, 17 + SelectTrigger, 18 + SelectValue, 19 + } from "@/components/ui/select"; 20 + import { cn } from "@/lib/utils"; 21 + import { Button } from "./button"; 22 + import { Checkbox } from "./checkbox"; 23 + import { DatePicker } from "./date-picker"; 24 + import { 25 + Form, 26 + FormControl, 27 + FormDescription, 28 + FormField, 29 + FormItem, 30 + FormLabel, 31 + FormMessage, 32 + } from "./form"; 33 + import { Input } from "./input"; 34 + import { Switch } from "./switch"; 35 + 36 + /** 37 + * Beautify a camelCase string. 38 + * e.g. "myString" -> "My String" 39 + */ 40 + function beautifyObjectName(string: string) { 41 + let output = string.replace(/([A-Z])/g, " $1"); 42 + output = output.charAt(0).toUpperCase() + output.slice(1); 43 + return output; 44 + } 45 + 46 + /** 47 + * Get the type name of the lowest level Zod type. 48 + * This will unpack optionals, refinements, etc. 49 + */ 50 + function getBaseType(schema: z.ZodAny): string { 51 + if ("innerType" in schema._def) { 52 + return getBaseType(schema._def.innerType as z.ZodAny); 53 + } 54 + if ("schema" in schema._def) { 55 + return getBaseType(schema._def.schema as z.ZodAny); 56 + } 57 + return schema._def.typeName; 58 + } 59 + 60 + /** 61 + * Search for a "ZodDefult" in the Zod stack and return its value. 62 + */ 63 + function getDefaultValueInZodStack(schema: z.ZodAny): any { 64 + const typedSchema = schema as unknown as z.ZodDefault< 65 + z.ZodNumber | z.ZodString 66 + >; 67 + 68 + if (typedSchema._def.typeName === "ZodDefault") { 69 + return typedSchema._def.defaultValue(); 70 + } 71 + 72 + if ("innerType" in typedSchema._def) { 73 + return getDefaultValueInZodStack( 74 + typedSchema._def.innerType as unknown as z.ZodAny, 75 + ); 76 + } 77 + if ("schema" in typedSchema._def) { 78 + return getDefaultValueInZodStack( 79 + (typedSchema._def as any).schema as z.ZodAny, 80 + ); 81 + } 82 + return undefined; 83 + } 84 + 85 + /** 86 + * Get all default values from a Zod schema. 87 + */ 88 + function getDefaultValues<Schema extends z.ZodObject<any, any>>( 89 + schema: Schema, 90 + ) { 91 + const { shape } = schema; 92 + type DefaultValuesType = DefaultValues<Partial<z.infer<Schema>>>; 93 + const defaultValues = {} as DefaultValuesType; 94 + 95 + for (const key of Object.keys(shape)) { 96 + const item = shape[key] as z.ZodAny; 97 + const defaultValue = getDefaultValueInZodStack(item); 98 + if (defaultValue !== undefined) { 99 + defaultValues[key as keyof DefaultValuesType] = defaultValue; 100 + } 101 + } 102 + 103 + return defaultValues; 104 + } 105 + 106 + /** 107 + * Convert a Zod schema to HTML input props to give direct feedback to the user. 108 + * Once submitted, the schema will be validated completely. 109 + */ 110 + function zodToHtmlInputProps( 111 + schema: 112 + | z.ZodNumber 113 + | z.ZodString 114 + | z.ZodOptional<z.ZodNumber | z.ZodString> 115 + | any, 116 + ): React.InputHTMLAttributes<HTMLInputElement> { 117 + if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) { 118 + const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>; 119 + return { 120 + ...zodToHtmlInputProps(typedSchema._def.innerType), 121 + required: false, 122 + }; 123 + } 124 + 125 + const typedSchema = schema as z.ZodNumber | z.ZodString; 126 + 127 + if (!("checks" in typedSchema._def)) return {}; 128 + 129 + const { checks } = typedSchema._def; 130 + const inputProps: React.InputHTMLAttributes<HTMLInputElement> = { 131 + required: true, 132 + }; 133 + const type = getBaseType(schema); 134 + 135 + for (const check of checks) { 136 + if (check.kind === "min") { 137 + if (type === "ZodString") { 138 + inputProps.minLength = check.value; 139 + } else { 140 + inputProps.min = check.value; 141 + } 142 + } 143 + if (check.kind === "max") { 144 + if (type === "ZodString") { 145 + inputProps.maxLength = check.value; 146 + } else { 147 + inputProps.max = check.value; 148 + } 149 + } 150 + } 151 + 152 + return inputProps; 153 + } 154 + 155 + export type FieldConfigItem = { 156 + description?: React.ReactNode; 157 + inputProps?: React.InputHTMLAttributes<HTMLInputElement>; 158 + fieldType?: keyof typeof INPUT_COMPONENTS; 159 + 160 + startAdornment?: React.ReactNode; 161 + endAdornment?: React.ReactNode; 162 + }; 163 + 164 + export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = { 165 + [key in keyof SchemaType]?: FieldConfigItem; 166 + }; 167 + 168 + /** 169 + * A FormInput component can handle a specific Zod type (e.g. "ZodBoolean") 170 + */ 171 + type AutoFormInputComponentProps = { 172 + zodInputProps: React.InputHTMLAttributes<HTMLInputElement>; 173 + field: ControllerRenderProps<FieldValues, any>; 174 + fieldConfigItem: FieldConfigItem; 175 + label: string; 176 + isRequired: boolean; 177 + fieldProps: any; 178 + zodItem: z.ZodAny; 179 + }; 180 + 181 + function AutoFormInput({ 182 + label, 183 + isRequired, 184 + fieldConfigItem, 185 + fieldProps, 186 + }: AutoFormInputComponentProps) { 187 + return ( 188 + <FormItem> 189 + <FormLabel> 190 + {label} 191 + {isRequired && <span className="text-destructive"> *</span>} 192 + </FormLabel> 193 + <FormControl> 194 + <Input type="text" {...fieldProps} /> 195 + </FormControl> 196 + {fieldConfigItem.description && ( 197 + <FormDescription>{fieldConfigItem.description}</FormDescription> 198 + )} 199 + <FormMessage /> 200 + </FormItem> 201 + ); 202 + } 203 + 204 + function AutoFormCheckbox({ 205 + label, 206 + isRequired, 207 + field, 208 + fieldConfigItem, 209 + fieldProps, 210 + }: AutoFormInputComponentProps) { 211 + return ( 212 + <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4"> 213 + <FormControl> 214 + <Checkbox 215 + checked={field.value} 216 + onCheckedChange={field.onChange} 217 + {...fieldProps} 218 + /> 219 + </FormControl> 220 + <div className="space-y-1 leading-none"> 221 + <FormLabel> 222 + {label} 223 + {isRequired && <span className="text-destructive"> *</span>} 224 + </FormLabel> 225 + {fieldConfigItem.description && ( 226 + <FormDescription>{fieldConfigItem.description}</FormDescription> 227 + )} 228 + </div> 229 + </FormItem> 230 + ); 231 + } 232 + 233 + function AutoFormSwitch({ 234 + label, 235 + isRequired, 236 + field, 237 + fieldConfigItem, 238 + fieldProps, 239 + }: AutoFormInputComponentProps) { 240 + return ( 241 + <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4"> 242 + <FormControl> 243 + <Switch 244 + checked={field.value} 245 + onCheckedChange={field.onChange} 246 + {...fieldProps} 247 + /> 248 + </FormControl> 249 + <div className="space-y-1 leading-none"> 250 + <FormLabel> 251 + {label} 252 + {isRequired && <span className="text-destructive"> *</span>} 253 + </FormLabel> 254 + {fieldConfigItem.description && ( 255 + <FormDescription>{fieldConfigItem.description}</FormDescription> 256 + )} 257 + </div> 258 + </FormItem> 259 + ); 260 + } 261 + 262 + function AutoFormDate({ 263 + label, 264 + isRequired, 265 + field, 266 + fieldConfigItem, 267 + fieldProps, 268 + }: AutoFormInputComponentProps) { 269 + return ( 270 + <FormItem> 271 + <FormLabel> 272 + {label} 273 + {isRequired && <span className="text-destructive"> *</span>} 274 + </FormLabel> 275 + <FormControl> 276 + <DatePicker 277 + date={field.value} 278 + setDate={field.onChange} 279 + {...fieldProps} 280 + /> 281 + </FormControl> 282 + {fieldConfigItem.description && ( 283 + <FormDescription>{fieldConfigItem.description}</FormDescription> 284 + )} 285 + <FormMessage /> 286 + </FormItem> 287 + ); 288 + } 289 + 290 + function AutoFormEnum({ 291 + label, 292 + isRequired, 293 + field, 294 + fieldConfigItem, 295 + zodItem, 296 + }: AutoFormInputComponentProps) { 297 + const values = (zodItem as unknown as z.ZodEnum<any>)._def.values; 298 + 299 + return ( 300 + <FormItem> 301 + <FormLabel> 302 + {label} 303 + {isRequired && <span className="text-destructive"> *</span>} 304 + </FormLabel> 305 + <FormControl> 306 + <Select onValueChange={field.onChange} defaultValue={field.value}> 307 + <SelectTrigger> 308 + <SelectValue className="w-full"> 309 + {field.value ?? "Select an option"} 310 + </SelectValue> 311 + </SelectTrigger> 312 + <SelectContent> 313 + {values.map((value: any) => ( 314 + <SelectItem value={value} key={value}> 315 + {value} 316 + </SelectItem> 317 + ))} 318 + </SelectContent> 319 + </Select> 320 + </FormControl> 321 + {fieldConfigItem.description && ( 322 + <FormDescription>{fieldConfigItem.description}</FormDescription> 323 + )} 324 + <FormMessage /> 325 + </FormItem> 326 + ); 327 + } 328 + 329 + const INPUT_COMPONENTS = { 330 + checkbox: AutoFormCheckbox, 331 + date: AutoFormDate, 332 + select: AutoFormEnum, 333 + switch: AutoFormSwitch, 334 + fallback: AutoFormInput, 335 + }; 336 + 337 + /** 338 + * Define handlers for specific Zod types. 339 + * You can expand this object to support more types. 340 + */ 341 + const DEFAULT_ZOD_HANDLERS: { 342 + [key: string]: keyof typeof INPUT_COMPONENTS; 343 + } = { 344 + ZodBoolean: "checkbox", 345 + ZodDate: "date", 346 + ZodEnum: "select", 347 + }; 348 + 349 + function AutoFormObject<SchemaType extends z.ZodObject<any, any>>({ 350 + schema, 351 + form, 352 + fieldConfig, 353 + }: { 354 + schema: SchemaType; 355 + form: ReturnType<typeof useForm>; 356 + fieldConfig?: FieldConfig<z.infer<SchemaType>>; 357 + }) { 358 + const { shape } = schema; 359 + 360 + return ( 361 + <> 362 + {Object.keys(shape).map((name) => { 363 + const item = shape[name] as z.ZodAny; 364 + const fieldConfigItem = fieldConfig?.[name] ?? {}; 365 + const zodInputProps = zodToHtmlInputProps(item); 366 + const isRequired = 367 + zodInputProps.required ?? 368 + fieldConfigItem.inputProps?.required ?? 369 + false; 370 + const zodBaseType = getBaseType(item); 371 + 372 + return ( 373 + <FormField 374 + control={form.control} 375 + name={name} 376 + key={name} 377 + render={({ field }) => { 378 + const inputType = 379 + fieldConfigItem.fieldType ?? 380 + DEFAULT_ZOD_HANDLERS[zodBaseType] ?? 381 + "fallback"; 382 + const InputComponent = INPUT_COMPONENTS[inputType]; 383 + 384 + return ( 385 + <React.Fragment key={name}> 386 + {fieldConfigItem.startAdornment} 387 + <InputComponent 388 + zodInputProps={zodInputProps} 389 + field={field} 390 + fieldConfigItem={fieldConfigItem} 391 + label={item._def.description ?? beautifyObjectName(name)} 392 + isRequired={isRequired} 393 + zodItem={item} 394 + fieldProps={{ 395 + ...zodInputProps, 396 + ...field, 397 + ...fieldConfigItem.inputProps, 398 + }} 399 + /> 400 + {fieldConfigItem.endAdornment} 401 + </React.Fragment> 402 + ); 403 + }} 404 + /> 405 + ); 406 + })} 407 + </> 408 + ); 409 + } 410 + 411 + export function AutoFormSubmit({ children }: { children?: React.ReactNode }) { 412 + return <Button type="submit">{children ?? "Submit"}</Button>; 413 + } 414 + 415 + function AutoForm<SchemaType extends z.ZodObject<any, any>>({ 416 + formSchema, 417 + values: valuesProp, 418 + onValuesChange: onValuesChangeProp, 419 + onSubmit: onSubmitProp, 420 + fieldConfig, 421 + children, 422 + className, 423 + }: { 424 + formSchema: SchemaType; 425 + values?: Partial<z.infer<SchemaType>>; 426 + onValuesChange?: (values: Partial<z.infer<SchemaType>>) => void; 427 + onSubmit?: (values: z.infer<SchemaType>) => void; 428 + fieldConfig?: FieldConfig<z.infer<SchemaType>>; 429 + children?: React.ReactNode; 430 + className?: string; 431 + }) { 432 + const defaultValues: DefaultValues<z.infer<typeof formSchema>> = 433 + getDefaultValues(formSchema); 434 + 435 + const form = useForm<z.infer<typeof formSchema>>({ 436 + resolver: zodResolver(formSchema), 437 + defaultValues, 438 + values: valuesProp, 439 + }); 440 + 441 + function onSubmit(values: z.infer<typeof formSchema>) { 442 + const parsedValues = formSchema.safeParse(values); 443 + if (parsedValues.success) { 444 + onSubmitProp?.(parsedValues.data); 445 + } 446 + } 447 + 448 + return ( 449 + <Form {...form}> 450 + <form 451 + onSubmit={form.handleSubmit(onSubmit)} 452 + onChange={() => { 453 + const values = form.getValues(); 454 + const parsedValues = formSchema.safeParse(values); 455 + if (parsedValues.success) { 456 + onValuesChangeProp?.(parsedValues.data); 457 + } 458 + }} 459 + className={cn("space-y-5", className)} 460 + > 461 + <AutoFormObject 462 + schema={formSchema} 463 + form={form} 464 + fieldConfig={fieldConfig} 465 + /> 466 + 467 + {children} 468 + </form> 469 + </Form> 470 + ); 471 + } 472 + 473 + export default AutoForm;
+28
apps/web/src/components/ui/checkbox.tsx
··· 1 + import * as React from "react"; 2 + import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 3 + import { Check } from "lucide-react"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + const Checkbox = React.forwardRef< 8 + React.ElementRef<typeof CheckboxPrimitive.Root>, 9 + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> 10 + >(({ className, ...props }, ref) => ( 11 + <CheckboxPrimitive.Root 12 + ref={ref} 13 + className={cn( 14 + "border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer h-4 w-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 15 + className, 16 + )} 17 + {...props} 18 + > 19 + <CheckboxPrimitive.Indicator 20 + className={cn("flex items-center justify-center text-current")} 21 + > 22 + <Check className="h-4 w-4" /> 23 + </CheckboxPrimitive.Indicator> 24 + </CheckboxPrimitive.Root> 25 + )); 26 + Checkbox.displayName = CheckboxPrimitive.Root.displayName; 27 + 28 + export { Checkbox };
+46
apps/web/src/components/ui/date-picker.tsx
··· 1 + "use client"; 2 + 3 + import { format } from "date-fns"; 4 + import { Calendar as CalendarIcon } from "lucide-react"; 5 + 6 + import { Button } from "@/components/ui/button"; 7 + import { Calendar } from "@/components/ui/calendar"; 8 + import { 9 + Popover, 10 + PopoverContent, 11 + PopoverTrigger, 12 + } from "@/components/ui/popover"; 13 + import { cn } from "@/lib/utils"; 14 + 15 + export function DatePicker({ 16 + date, 17 + setDate, 18 + }: { 19 + date?: Date; 20 + setDate: (date?: Date) => void; 21 + }) { 22 + return ( 23 + <Popover> 24 + <PopoverTrigger asChild> 25 + <Button 26 + variant={"outline"} 27 + className={cn( 28 + "w-full justify-start text-left font-normal", 29 + !date && "text-muted-foreground", 30 + )} 31 + > 32 + <CalendarIcon className="mr-2 h-4 w-4" /> 33 + {date ? format(date, "PPP") : <span>Pick a date</span>} 34 + </Button> 35 + </PopoverTrigger> 36 + <PopoverContent className="w-auto p-0"> 37 + <Calendar 38 + mode="single" 39 + selected={date} 40 + onSelect={setDate} 41 + initialFocus 42 + /> 43 + </PopoverContent> 44 + </Popover> 45 + ); 46 + }
+1
apps/web/src/config/pages.ts
··· 33 33 description: "War room where you handle the incidents.", 34 34 href: "/incidents", 35 35 icon: "siren", 36 + disabled: true, 36 37 }, 37 38 // ... 38 39 ];
+21
apps/web/src/config/socials.ts
··· 1 + import type { ValidIcon } from "@/components/icons"; 2 + 3 + type Social = { 4 + title: string; 5 + href: string; 6 + icon: ValidIcon; 7 + }; 8 + 9 + export const socialsConfig: Social[] = [ 10 + { 11 + title: "Discord", 12 + href: "/discord", 13 + icon: "discord", 14 + }, 15 + { 16 + title: "GitHub", 17 + href: "/github", 18 + icon: "github", 19 + }, 20 + // add cal.com 21 + ];
+18
packages/api/src/router/page.ts
··· 27 27 with: { monitors: true, incidents: true }, 28 28 }); 29 29 }), 30 + updatePage: protectedProcedure 31 + .input(insertPageSchema) 32 + .mutation(async (opts) => { 33 + console.log(opts.input); 34 + const r = await opts.ctx.db 35 + .update(page) 36 + .set(opts.input) 37 + .where(eq(page.id, Number(opts.input.id))) 38 + .returning() 39 + .get(); 40 + console.log(r); 41 + return r; 42 + }), 43 + deletePage: protectedProcedure 44 + .input(z.object({ pageId: z.number() })) 45 + .mutation(async (opts) => { 46 + await opts.ctx.db.delete(page).where(eq(page.id, opts.input.pageId)); 47 + }), 30 48 getPageByWorkspace: protectedProcedure 31 49 .input(z.object({ workspaceId: z.number() })) 32 50 .query(async (opts) => {
+31
pnpm-lock.yaml
··· 71 71 '@radix-ui/react-alert-dialog': 72 72 specifier: ^1.0.4 73 73 version: 1.0.4(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 74 + '@radix-ui/react-checkbox': 75 + specifier: ^1.0.4 76 + version: 1.0.4(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 74 77 '@radix-ui/react-dialog': 75 78 specifier: ^1.0.4 76 79 version: 1.0.4(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) ··· 2661 2664 dependencies: 2662 2665 '@babel/runtime': 7.22.5 2663 2666 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2667 + '@types/react': 18.2.12 2668 + '@types/react-dom': 18.2.5 2669 + react: 18.2.0 2670 + react-dom: 18.2.0(react@18.2.0) 2671 + dev: false 2672 + 2673 + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): 2674 + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} 2675 + peerDependencies: 2676 + '@types/react': '*' 2677 + '@types/react-dom': '*' 2678 + react: ^16.8 || ^17.0 || ^18.0 2679 + react-dom: ^16.8 || ^17.0 || ^18.0 2680 + peerDependenciesMeta: 2681 + '@types/react': 2682 + optional: true 2683 + '@types/react-dom': 2684 + optional: true 2685 + dependencies: 2686 + '@babel/runtime': 7.22.5 2687 + '@radix-ui/primitive': 1.0.1 2688 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2689 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2690 + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2691 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2692 + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2693 + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2694 + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2664 2695 '@types/react': 18.2.12 2665 2696 '@types/react-dom': 18.2.5 2666 2697 react: 18.2.0