Przeglądaj źródła

Playing with the layout

Owen Schwartz 9 miesięcy temu
rodzic
commit
11c687fc3a

+ 64 - 0
src/app/configuration/layout.tsx

@@ -0,0 +1,64 @@
+import { Metadata } from "next"
+import Image from "next/image"
+
+import { Separator } from "@/components/ui/separator"
+import { SidebarNav } from "@/components/sidebar-nav"
+
+export const metadata: Metadata = {
+    title: "Forms",
+    description: "Advanced form example using react-hook-form and Zod.",
+}
+
+const sidebarNavItems = [
+    {
+        title: "Sites",
+        href: "/configuration/sites",
+    },
+    {
+        title: "Resources",
+        href: "/configuration/resources",
+    },
+]
+
+interface SettingsLayoutProps {
+    children: React.ReactNode,
+    params: { siteId: string }
+}
+
+export default function SettingsLayout({ children, params }: SettingsLayoutProps) {
+    return (
+        <>
+            <div className="md:hidden">
+                <Image
+                    src="/configuration/forms-light.png"
+                    width={1280}
+                    height={791}
+                    alt="Forms"
+                    className="block dark:hidden"
+                />
+                <Image
+                    src="/configuration/forms-dark.png"
+                    width={1280}
+                    height={791}
+                    alt="Forms"
+                    className="hidden dark:block"
+                />
+            </div>
+            <div className="hidden space-y-6 p-10 pb-16 md:block">
+                <div className="space-y-0.5">
+                    <h2 className="text-2xl font-bold tracking-tight">Settings</h2>
+                    <p className="text-muted-foreground">
+                        { params.siteId == "create" ? "Create site..." : "Manage settings on site " + params.siteId }.
+                    </p>
+                </div>
+                <Separator className="my-6" />
+                <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
+                    <aside className="-mx-4 lg:w-1/5">
+                        <SidebarNav items={sidebarNavItems.map(i => { i.href = i.href.replace("{siteId}", params.siteId); return i})} disabled={params.siteId == "create"} />
+                    </aside>
+                    <div className="flex-1 lg:max-w-2xl">{children}</div>
+                </div>
+            </div>
+        </>
+    )
+}

+ 0 - 18
src/app/configuration/sites/[siteId]/account/page.tsx

@@ -1,18 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { AccountForm } from "@/components/account-form"
-
-export default function SettingsAccountPage() {
-  return (
-    <div className="space-y-6">
-      <div>
-        <h3 className="text-lg font-medium">Account</h3>
-        <p className="text-sm text-muted-foreground">
-          Update your account settings. Set your preferred language and
-          timezone.
-        </p>
-      </div>
-      <Separator />
-      <AccountForm />
-    </div>
-  )
-}

+ 189 - 0
src/app/configuration/sites/[siteId]/components/create-site.tsx

@@ -0,0 +1,189 @@
+"use client"
+
+import { zodResolver } from "@hookform/resolvers/zod"
+import { CalendarIcon, CaretSortIcon, CheckIcon } 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 } from "@/components/ui/button"
+import {
+    Command,
+    CommandEmpty,
+    CommandGroup,
+    CommandInput,
+    CommandItem,
+    CommandList,
+} from "@/components/ui/command"
+import {
+    Form,
+    FormControl,
+    FormDescription,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import {
+    Popover,
+    PopoverContent,
+    PopoverTrigger,
+} from "@/components/ui/popover"
+import { generateKeypair } from "./wireguard-config";
+import { NewtConfig } from "./newt-config";
+import { useState } from "react"
+
+const method = [
+    { label: "Wireguard", value: "wg" },
+    { label: "Newt", value: "newt" },
+] as const;
+
+const accountFormSchema = z.object({
+    name: z
+        .string()
+        .min(2, {
+            message: "Name must be at least 2 characters.",
+        })
+        .max(30, {
+            message: "Name must not be longer than 30 characters.",
+        }),
+    method: z.string({
+        required_error: "Please select a method.",
+    }),
+});
+
+type AccountFormValues = z.infer<typeof accountFormSchema>;
+
+const defaultValues: Partial<AccountFormValues> = {
+    name: "Wombat",
+    method: "wg"
+};
+
+export function CreateSiteForm() {
+    const [methodValue, setMethodValue] = useState("wg");
+
+    const form = useForm<AccountFormValues>({
+        resolver: zodResolver(accountFormSchema),
+        defaultValues,
+    });
+
+    function onSubmit(data: AccountFormValues) {
+        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 keypair = generateKeypair();
+
+    const config = `[Interface]
+  Address = 10.0.0.2/24
+  ListenPort = 51820
+  PrivateKey = ${keypair.privateKey}
+  
+  [Peer]
+  PublicKey = ${keypair.publicKey}
+  AllowedIPs = 0.0.0.0/0, ::/0
+  Endpoint = myserver.dyndns.org:51820
+  PersistentKeepalive = 5`;
+
+
+    return (
+        <>
+            <Form {...form}>
+                <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
+                    <FormField
+                        control={form.control}
+                        name="name"
+                        render={({ field }) => (
+                            <FormItem>
+                                <FormLabel>Name</FormLabel>
+                                <FormControl>
+                                    <Input placeholder="Your name" {...field} />
+                                </FormControl>
+                                <FormDescription>
+                                    This is the name that will be displayed for this site.
+                                </FormDescription>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
+                    <FormField
+                        control={form.control}
+                        name="method"
+                        render={({ field }) => (
+                            <FormItem className="flex flex-col">
+                                <FormLabel>Method</FormLabel>
+                                <Popover>
+                                    <PopoverTrigger asChild>
+                                        <FormControl>
+                                            <Button
+                                                variant="outline"
+                                                role="combobox"
+                                                className={cn(
+                                                    "w-[200px] justify-between",
+                                                    !field.value && "text-muted-foreground"
+                                                )}
+                                            >
+                                                {field.value
+                                                    ? method.find(
+                                                        (language) => language.value === field.value
+                                                    )?.label
+                                                    : "Select language"}
+                                                <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                                            </Button>
+                                        </FormControl>
+                                    </PopoverTrigger>
+                                    <PopoverContent className="w-[200px] p-0">
+                                        <Command>
+                                            <CommandInput placeholder="Search method..." />
+                                            <CommandList>
+                                                <CommandEmpty>No method found.</CommandEmpty>
+                                                <CommandGroup>
+                                                    {method.map((method) => (
+                                                        <CommandItem
+                                                            value={method.label}
+                                                            key={method.value}
+                                                            onSelect={() => {
+                                                                form.setValue("method", method.value);
+                                                                setMethodValue(method.value);
+                                                            }}
+                                                        >
+                                                            <CheckIcon
+                                                                className={cn(
+                                                                    "mr-2 h-4 w-4",
+                                                                    method.value === field.value
+                                                                        ? "opacity-100"
+                                                                        : "opacity-0"
+                                                                )}
+                                                            />
+                                                            {method.label}
+                                                        </CommandItem>
+                                                    ))}
+                                                </CommandGroup>
+                                            </CommandList>
+                                        </Command>
+                                    </PopoverContent>
+                                </Popover>
+                                <FormDescription>
+                                    This is how you will connect your site to Fossorial.
+                                </FormDescription>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
+                    <Button type="submit">Create Site</Button>
+                </form>
+            </Form>
+            {methodValue === "wg" ? <pre className="mt-2 w-full rounded-md bg-slate-950 p-4 overflow-x-auto">
+                <code className="text-white whitespace-pre-wrap">{config}</code>
+            </pre> : <NewtConfig />}
+        </>
+    );
+}

+ 12 - 0
src/app/configuration/sites/[siteId]/components/newt-config.tsx

@@ -0,0 +1,12 @@
+"use client"
+
+export function NewtConfig() {
+  const config = `curl -fsSL https://get.docker.com -o get-docker.sh
+sh get-docker.sh`;
+
+  return (
+    <pre className="mt-2 w-full rounded-md bg-slate-950 p-4 overflow-x-auto">
+      <code className="text-white whitespace-pre-wrap">{config}</code>
+    </pre>
+  );
+};

+ 180 - 0
src/app/configuration/sites/[siteId]/components/wireguard-config.ts

@@ -0,0 +1,180 @@
+/*! SPDX-License-Identifier: GPL-2.0
+ *
+ * Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+function gf(init: number[] | undefined = undefined) {
+    var r = new Float64Array(16);
+    if (init) {
+        for (var i = 0; i < init.length; ++i)
+            r[i] = init[i];
+    }
+    return r;
+}
+
+function pack(o: Uint8Array, n: Float64Array) {
+    var b, m = gf(), t = gf();
+    for (var i = 0; i < 16; ++i)
+        t[i] = n[i];
+    carry(t);
+    carry(t);
+    carry(t);
+    for (var j = 0; j < 2; ++j) {
+        m[0] = t[0] - 0xffed;
+        for (var i = 1; i < 15; ++i) {
+            m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
+            m[i - 1] &= 0xffff;
+        }
+        m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
+        b = (m[15] >> 16) & 1;
+        m[14] &= 0xffff;
+        cswap(t, m, 1 - b);
+    }
+    for (var i = 0; i < 16; ++i) {
+        o[2 * i] = t[i] & 0xff;
+        o[2 * i + 1] = t[i] >> 8;
+    }
+}
+
+function carry(o: Float64Array) {
+    var c;
+    for (var i = 0; i < 16; ++i) {
+        o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536);
+        o[i] &= 0xffff;
+    }
+}
+
+function cswap(p: Float64Array, q: Float64Array, b: number) {
+    var t, c = ~(b - 1);
+    for (var i = 0; i < 16; ++i) {
+        t = c & (p[i] ^ q[i]);
+        p[i] ^= t;
+        q[i] ^= t;
+    }
+}
+
+function add(o: Float64Array, a: Float64Array, b: Float64Array) {
+    for (var i = 0; i < 16; ++i)
+        o[i] = (a[i] + b[i]) | 0;
+}
+
+function subtract(o: Float64Array, a: Float64Array, b: Float64Array) {
+    for (var i = 0; i < 16; ++i)
+        o[i] = (a[i] - b[i]) | 0;
+}
+
+function multmod(o: Float64Array, a: Float64Array, b: Float64Array) {
+    var t = new Float64Array(31);
+    for (var i = 0; i < 16; ++i) {
+        for (var j = 0; j < 16; ++j)
+            t[i + j] += a[i] * b[j];
+    }
+    for (var i = 0; i < 15; ++i)
+        t[i] += 38 * t[i + 16];
+    for (var i = 0; i < 16; ++i)
+        o[i] = t[i];
+    carry(o);
+    carry(o);
+}
+
+function invert(o: Float64Array, i: Float64Array) {
+    var c = gf();
+    for (var a = 0; a < 16; ++a)
+        c[a] = i[a];
+    for (var a = 253; a >= 0; --a) {
+        multmod(c, c, c);
+        if (a !== 2 && a !== 4)
+            multmod(c, c, i);
+    }
+    for (var a = 0; a < 16; ++a)
+        o[a] = c[a];
+}
+
+function clamp(z: Uint8Array) {
+    z[31] = (z[31] & 127) | 64;
+    z[0] &= 248;
+}
+
+function generatePublicKey(privateKey: Uint8Array) {
+    var r, z = new Uint8Array(32);
+    var a = gf([1]),
+        b = gf([9]),
+        c = gf(),
+        d = gf([1]),
+        e = gf(),
+        f = gf(),
+        _121665 = gf([0xdb41, 1]),
+        _9 = gf([9]);
+    for (var i = 0; i < 32; ++i)
+        z[i] = privateKey[i];
+    clamp(z);
+    for (var i = 254; i >= 0; --i) {
+        r = (z[i >>> 3] >>> (i & 7)) & 1;
+        cswap(a, b, r);
+        cswap(c, d, r);
+        add(e, a, c);
+        subtract(a, a, c);
+        add(c, b, d);
+        subtract(b, b, d);
+        multmod(d, e, e);
+        multmod(f, a, a);
+        multmod(a, c, a);
+        multmod(c, b, e);
+        add(e, a, c);
+        subtract(a, a, c);
+        multmod(b, a, a);
+        subtract(c, d, f);
+        multmod(a, c, _121665);
+        add(a, a, d);
+        multmod(c, c, a);
+        multmod(a, d, f);
+        multmod(d, b, _9);
+        multmod(b, e, e);
+        cswap(a, b, r);
+        cswap(c, d, r);
+    }
+    invert(c, c);
+    multmod(a, a, c);
+    pack(z, a);
+    return z;
+}
+
+function generatePresharedKey() {
+    var privateKey = new Uint8Array(32);
+    crypto.getRandomValues(privateKey);
+    return privateKey;
+}
+
+function generatePrivateKey() {
+    var privateKey = generatePresharedKey();
+    clamp(privateKey);
+    return privateKey;
+}
+
+function encodeBase64(dest: Uint8Array, src: Uint8Array) {
+    var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
+    for (var i = 0; i < 4; ++i)
+        dest[i] = input[i] + 65 +
+            (((25 - input[i]) >> 8) & 6) -
+            (((51 - input[i]) >> 8) & 75) -
+            (((61 - input[i]) >> 8) & 15) +
+            (((62 - input[i]) >> 8) & 3);
+}
+
+function keyToBase64(key: Uint8Array) {
+    var i, base64 = new Uint8Array(44);
+    for (i = 0; i < 32 / 3; ++i)
+        encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
+    encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
+    base64[43] = 61;
+    return String.fromCharCode.apply(null, base64 as any);
+}
+
+export function generateKeypair() {
+    var privateKey = generatePrivateKey();
+    var publicKey = generatePublicKey(privateKey);
+    return {
+        publicKey: keyToBase64(publicKey),
+        privateKey: keyToBase64(privateKey)
+    };
+}

+ 2 - 2
src/app/configuration/sites/[siteId]/layout.tsx

@@ -60,13 +60,13 @@ export default function SettingsLayout({ children, params }: SettingsLayoutProps
                 <div className="space-y-0.5">
                     <h2 className="text-2xl font-bold tracking-tight">Settings</h2>
                     <p className="text-muted-foreground">
-                        Manage your account settings and set e-mail preferences.
+                        { params.siteId == "create" ? "Create site..." : "Manage settings on site " + params.siteId }.
                     </p>
                 </div>
                 <Separator className="my-6" />
                 <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
                     <aside className="-mx-4 lg:w-1/5">
-                        <SidebarNav items={sidebarNavItems.map(i => { i.href = i.href.replace("{siteId}", params.siteId); return i})} />
+                        <SidebarNav items={sidebarNavItems.map(i => { i.href = i.href.replace("{siteId}", params.siteId); return i})} disabled={params.siteId == "create"} />
                     </aside>
                     <div className="flex-1 lg:max-w-2xl">{children}</div>
                 </div>

+ 17 - 8
src/app/configuration/sites/[siteId]/page.tsx

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

+ 3 - 0
src/app/configuration/sites/page.tsx

@@ -1,7 +1,10 @@
+import Link from "next/link";
+
 export default async function Page() {
     return (
         <>
            <p>This is where the table goes...</p>
+            <Link href="/configuration/sites/123">Open up the site 123</Link>
         </>
     );
 }

+ 0 - 44
src/app/profile/components/sidebar-nav.tsx

@@ -1,44 +0,0 @@
-"use client"
-
-import Link from "next/link"
-import { usePathname } from "next/navigation"
-
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
-
-interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
-  items: {
-    href: string
-    title: string
-  }[]
-}
-
-export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
-  const pathname = usePathname()
-
-  return (
-    <nav
-      className={cn(
-        "flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
-        className
-      )}
-      {...props}
-    >
-      {items.map((item) => (
-        <Link
-          key={item.href}
-          href={item.href}
-          className={cn(
-            buttonVariants({ variant: "ghost" }),
-            pathname === item.href
-              ? "bg-muted hover:bg-muted"
-              : "hover:bg-transparent hover:underline",
-            "justify-start"
-          )}
-        >
-          {item.title}
-        </Link>
-      ))}
-    </nav>
-  )
-}

+ 1 - 1
src/app/profile/layout.tsx

@@ -2,7 +2,7 @@ import { Metadata } from "next"
 import Image from "next/image"
 
 import { Separator } from "@/components/ui/separator"
-import { SidebarNav } from "@/app/configuration/components/sidebar-nav"
+import { SidebarNav } from "@/components/sidebar-nav"
 
 export const metadata: Metadata = {
   title: "Forms",

+ 10 - 5
src/components/sidebar-nav.tsx

@@ -1,8 +1,7 @@
 "use client"
-
+import React from 'react'
 import Link from "next/link"
 import { usePathname } from "next/navigation"
-
 import { cn } from "@/lib/utils"
 import { buttonVariants } from "@/components/ui/button"
 
@@ -11,15 +10,17 @@ interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
     href: string
     title: string
   }[]
+  disabled?: boolean
 }
 
-export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
+export function SidebarNav({ className, items, disabled = false, ...props }: SidebarNavProps) {
   const pathname = usePathname()
 
   return (
     <nav
       className={cn(
         "flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
+        disabled && "opacity-50 pointer-events-none",
         className
       )}
       {...props}
@@ -33,12 +34,16 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
             pathname === item.href
               ? "bg-muted hover:bg-muted"
               : "hover:bg-transparent hover:underline",
-            "justify-start"
+            "justify-start",
+            disabled && "cursor-not-allowed"
           )}
+          onClick={disabled ? (e) => e.preventDefault() : undefined}
+          tabIndex={disabled ? -1 : undefined}
+          aria-disabled={disabled}
         >
           {item.title}
         </Link>
       ))}
     </nav>
   )
-}
+}