feat: domain slide

This commit is contained in:
Derock 2024-02-24 22:18:43 +01:00
parent 0fd3056259
commit f067a01e47
No known key found for this signature in database
8 changed files with 177 additions and 122 deletions

View file

@ -1,9 +1,9 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { motion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { PlusIcon } from "lucide-react";
import { useEffect } from "react";
import { useEffect, type ReactNode } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { uuidv7 } from "uuidv7";
import { z } from "zod";
@ -37,8 +37,10 @@ export type DomainsListForm = z.infer<typeof formValidator>;
export default function DomainsList({
defaultData,
children,
}: {
defaultData: RouterOutputs["projects"]["services"]["get"];
children: ReactNode;
}) {
const service = useService(undefined, defaultData);
const updateDomain = api.projects.services.updateDomain.useMutation();
@ -65,11 +67,6 @@ export default function DomainsList({
);
}, [form, service.data?.domains]);
console.log(
"Rendering fields with ids: ",
domainsForm.fields.map((field) => field.id),
);
return (
<Form {...form}>
<form
@ -153,8 +150,6 @@ export default function DomainsList({
internalPort: 8080,
};
console.log("add domain: ", domain.domainId);
domainsForm.append(domain);
}}
>
@ -164,6 +159,8 @@ export default function DomainsList({
{/* <FormUnsavedChangesIndicator form={form} /> */}
</motion.div>
{/* </AnimatePresence> */}
<AnimatePresence>{children}</AnimatePresence>
</form>
</Form>
);

View file

@ -0,0 +1,54 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState, type ReactNode } from "react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "~/components/ui/sheet";
export default function DomainSlideLayout({
children,
}: {
children: ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
// need to keep state separately just so DOM elements dont get unloaded and break close animation
const [open, setOpen] = useState(true);
// re-open on new nav
useEffect(() => {
setOpen(true);
}, [pathname]);
return (
<Sheet
open={open}
onOpenChange={(open) => {
if (!open) {
setOpen(false);
router.push(
new URL(pathname + "/../", window.location.href).pathname,
);
}
}}
>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit Domain</SheetTitle>
<SheetDescription>
Make advanced changes to the reverse-proxy configuration for this
domain. Hit the save button on the main page to apply changes.
Reloading will cause you to lose unsaved edits.
</SheetDescription>
</SheetHeader>
{children}
</SheetContent>
</Sheet>
);
}

View file

@ -0,0 +1,9 @@
import { CgSpinner } from "react-icons/cg";
export default function DomainSlideLoading() {
return (
<div className="flex min-h-24 w-full items-center justify-center">
<CgSpinner className="flex-shrink animate-spin" size="24" />
</div>
);
}

View file

@ -1,46 +1,42 @@
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "~/components/ui/sheet";
"use client";
import { useSelectedLayoutSegment } from "next/navigation";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { Switch } from "~/components/ui/switch";
import { SimpleFormField } from "~/hooks/forms";
import { type DomainsListForm } from "../DomainsList";
export default function DomainPage() {
const form = useFormContext<DomainsListForm>();
const domainId = useSelectedLayoutSegment();
const index = useMemo(
() =>
form.getValues("domains").findIndex(
(domain) =>
domain.domainId === domainId ||
// @ts-expect-error this exists
domain.id === domainId,
),
[form, domainId],
);
return (
<Sheet>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" value="Pedro Duarte" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">
Username
</Label>
<Input id="username" value="@peduarte" className="col-span-3" />
</div>
</div>
<SheetFooter>
<SheetClose asChild>
<Button type="submit">Save changes</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
<div className="flex flex-col gap-4 pt-4">
<SimpleFormField
control={form.control}
friendlyName="Force HTTPS"
description={
<>
Automatically redirects all HTTP requests to HTTPS. Recommended by
default. Redirects will go to the same page using the same domain
and all query paramters will be preserved.
</>
}
name={`domains.${index}.forceSSL`}
render={({ field }) => <Switch {...field} className="my-4 block" />}
/>
</div>
);
}

View file

@ -1,39 +1,35 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { motion } from "framer-motion";
import { ArrowRight, CogIcon, TrashIcon } from "lucide-react";
import { forwardRef, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { forwardRef } from "react";
import { useFormContext, type UseFieldArrayReturn } from "react-hook-form";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
import { Switch } from "~/components/ui/switch";
import { SimpleFormField } from "~/hooks/forms";
import { useProject } from "../../../../_context/ProjectContext";
import { type DomainsListForm } from "../DomainsList";
type FieldData = {
id: string; // internal ID for react-form-hook
domainId?: string;
domain: string;
internalPort: number;
https: boolean;
forceSSL: boolean;
};
const DomainEntry = forwardRef<
HTMLDivElement,
{
field: FieldData;
field: DomainsListForm["domains"][number] & {
id: string; // react-hook-form adds this
};
index: number;
domains: UseFieldArrayReturn<DomainsListForm, "domains", "id">;
}
>(({ field, index, domains }, ref) => {
const form = useFormContext();
const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
const pathname = usePathname();
const project = useProject();
return (
<motion.div
ref={ref}
// layout
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
@ -80,7 +76,16 @@ const DomainEntry = forwardRef<
icon={CogIcon}
className="mr-2"
onClick={() => {
setIsOpen(!isOpen);
const domains = pathname.trim().endsWith("/domains")
? pathname.trim()
: pathname.replace(/\/[A-z0-9]*?$/, ``);
console.log(`${domains}/${field.domainId ?? field.id}`);
router.push(
`${project.servicePath}/domains/${
field.domainId ?? field.id
}`,
);
}}
/>
@ -95,27 +100,6 @@ const DomainEntry = forwardRef<
/>
</div>
</div>
<AnimatePresence>
{isOpen && (
<motion.div className="mt-4 grid grid-cols-2 rounded-md bg-background p-8">
<h1 className="col-span-2 pb-4">Advanced Settings</h1>
<SimpleFormField
control={form.control}
name={`domains.${index}.forceSSL`}
friendlyName="Force SSL"
render={({ field }) => (
<div className="pt-2">
<Switch {...field} className="mr-auto block" />
</div>
)}
/>
{/* TODO: allow custom SSL certificates */}
</motion.div>
)}
</AnimatePresence>
</Card>
</motion.div>
);

View file

@ -15,5 +15,20 @@ export default async function DomainsLayout(props: {
serviceId: props.params.serviceId,
});
return <DomainsList defaultData={service} />;
return (
<>
<div className="flex flex-row gap-4 rounded-lg border-border bg-card">
<div className="w-2 rounded-bl-lg rounded-tl-lg bg-primary" />
<p className="py-4 pr-4 text-primary-foreground">
By exposing this service, it will be added to a global network for the
reverse proxy and thus will be able to communicate with other services
from different projects that also have an exposed domain. To avoid
this, consider setting up a separate proxy (traefik, freenginx, etc)
for this project and expose that instead.
</p>
</div>
<DomainsList defaultData={service}>{props.children}</DomainsList>
</>
);
}

View file

@ -1,4 +1,4 @@
import { GetInputProps, UseFormReturnType } from "@mantine/form/lib/types";
import { type UseFormReturnType } from "@mantine/form/lib/types";
import { Label } from "./ui/label";
import { Required } from "./ui/required";

View file

@ -1,19 +1,19 @@
"use client"
"use client";
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cva, type VariantProps } from "class-variance-authority"
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "~/utils/utils.ts"
import { cn } from "~/utils/utils";
const Sheet = SheetPrimitive.Root
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
@ -22,13 +22,13 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
@ -46,8 +46,8 @@ const sheetVariants = cva(
defaultVariants: {
side: "right",
},
}
)
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
@ -71,8 +71,8 @@ const SheetContent = React.forwardRef<
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
@ -81,12 +81,12 @@ const SheetHeader = ({
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
className,
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
@ -95,12 +95,12 @@ const SheetFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
@ -111,8 +111,8 @@ const SheetTitle = React.forwardRef<
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
@ -123,18 +123,18 @@ const SheetDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTrigger,
};