Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: onboarding #77

Merged
merged 11 commits into from
Jun 8, 2024
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@trpc/react-query": "next",
"@trpc/server": "next",
"date-fns": "^3.6.0",
"framer-motion": "^11.2.10",
"lucide-react": "^0.379.0",
"minio": "^8.0.0",
"next": "14.2.3",
Expand All @@ -39,6 +40,7 @@
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.5",
"server-only": "^0.0.1",
"shiki": "^1.6.3",
"sonner": "^1.4.41",
"superjson": "^2.2.1",
"ts-pattern": "^5.1.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/(landing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ const HomePage = () => {
<div className="bg-[#d4d4d4] dark:bg-[#404040]"></div>
</div>
<div>
<div className="overflow-scroll py-4">
<div className="py-4">
<pre className="px-[--card-padding]">
<code className="font-mono text-sm">
{`<form
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/app/(main)/_components/user-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export const UserDropdown = ({
>
<Link href="/dashboard/settings">Settings</Link>
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer text-muted-foreground"
asChild
>
<Link href="/onboarding">Onboarding</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
Expand Down
40 changes: 0 additions & 40 deletions apps/web/src/app/(main)/account/page.tsx

This file was deleted.

6 changes: 3 additions & 3 deletions apps/web/src/app/(main)/form/[id]/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { Copy } from 'lucide-react';
import { ClipboardIcon } from 'lucide-react';
import { toast } from 'sonner';

import { useCopyToClipboard } from '~/lib/hooks/use-copy-to-clipboard';
Expand All @@ -17,11 +17,11 @@ export default function CopyFormId({ formId }: CopyFormIdProps) {
<span className="inline-flex items-center rounded-lg bg-muted px-2 py-0.5 text-sm font-medium">
{formId}
</span>
<Copy
<ClipboardIcon
onClick={() =>
copy(formId).then(() => {
toast('Copied to Clipboard', {
icon: <Copy className="h-4 w-4" />,
icon: <ClipboardIcon className="h-4 w-4" />,
});
})
}
Expand Down
94 changes: 94 additions & 0 deletions apps/web/src/app/(main)/onboarding/form/code-example-step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@formbase/ui/primitives/tabs';
import { cn } from '@formbase/ui/utils/cn';

import { CopyButton } from '~/components/copy-button';
import { highlightCode } from '~/lib/highlight-code';

import SendFormSubmissionButton from './send-submission-button';

type CodeExampleStepProps = {
formId: string | null;
};

export const CodeExampleStep = async ({ formId }: CodeExampleStepProps) => {
const htmlCode = await highlightCode(`<form
action="https://formbase.dev/s/${formId ?? 'abcdefghijkl'}" method="POST"
enctype="multipart/form-data"
>
<input type="text" name="name" />
<input type="email" name="email" />
<textarea name="message"></textarea>

<button type="submit">Submit</button>
</form>`);
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved

const reactCode =
await highlightCode(`export default function FormbaseForm() {
return (
<form
action="https://formbase.dev/s/${formId ?? 'abcdefghijkl'}"
method="POST"
encType="multipart/form-data"
>
<input type="text" name="name" />
<input type="email" name="email" />
<textarea name="message"></textarea>

<button type="submit">Submit</button>
</form>
);
}`);

return (
<div
className={cn('-mt-0.5', {
'pointer-events-none opacity-50 select-none': formId === null,
})}
>
<h2 className="text-xl font-semibold">Send a submission</h2>
<div className="text-gray-600 dark:text-muted-foreground mt-2">
<div className="space-y-4">
<p>Use the code below to recieve your first submission</p>

<Tabs defaultValue="html">
<TabsList>
<TabsTrigger value="html">HTML</TabsTrigger>
<TabsTrigger value="react">React</TabsTrigger>
</TabsList>
<TabsContent value="html" className="relative">
<div
className="mt-4 rounded-md"
dangerouslySetInnerHTML={{
__html: htmlCode,
}}
/>
<CopyButton
text={htmlCode}
className="absolute top-4 text-white right-4"
/>
</TabsContent>
<TabsContent value="react" className="relative">
<div
className="mt-4 rounded-md"
dangerouslySetInnerHTML={{
__html: reactCode,
}}
/>
<CopyButton
text={reactCode}
className="absolute top-4 text-white right-4"
/>
</TabsContent>
</Tabs>

<SendFormSubmissionButton formId={formId} />
</div>
</div>
</div>
);
};
169 changes: 169 additions & 0 deletions apps/web/src/app/(main)/onboarding/form/create-form-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
'use client';

import React, { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';

import { zodResolver } from '@hookform/resolvers/zod';
import { InfoCircledIcon } from '@radix-ui/react-icons';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';

import { Button } from '@formbase/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@formbase/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@formbase/ui/primitives/form';
import { Input } from '@formbase/ui/primitives/input';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@formbase/ui/primitives/tooltip';

import { api } from '~/lib/trpc/react';

const FormSchema = z.object({
name: z.string().min(1, {
message: 'Form title is required.',
}),
returnUrl: z.string().optional(),
description: z.string().optional(),
keys: z.array(z.string()).optional(),
});

export function CreateFormDialog() {
const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: '',
description: '',
returnUrl: '',
keys: [''],
},
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
});
const [showDialog, setShowDialog] = useState<boolean>(false);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, startCreateTransaction] = useTransition();

const { mutateAsync: createNewForm } =
api.form.createOnboardingForm.useMutation();

const createPost = (data: z.infer<typeof FormSchema>) => {
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved
startCreateTransaction(async () => {
await createNewForm(
{
title: data.name,
description: data.description,
returnUrl: data.returnUrl,
},
{
onSuccess: () => {
toast.success('New form endpoint created');
router.refresh();
},
onError: () => {
toast.error('Failed to create form, try again');
},
},
);
});
};
ephraimduncan marked this conversation as resolved.
Show resolved Hide resolved

function onSubmit(data: z.infer<typeof FormSchema>) {
createPost(data);

setShowDialog(false);
}

return (
<Form {...form}>
<div className="space-y-6">
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogTrigger asChild>
<Button>New Form Endpoint</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>New Form Endpoint</DialogTitle>
</DialogHeader>

<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="returnUrl"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-end gap-1">
Return URL
<Tooltip>
<TooltipTrigger>
<InfoCircledIcon width={13} />
</TooltipTrigger>
<TooltipContent className="bg-white dark:bg-black">
Where should users be redirected after form submission?
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Input placeholder="http://..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<DialogFooter>
<Button
onClick={form.handleSubmit(onSubmit)}
className="mt-2 w-full"
>
Create Form Endpoint
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Form>
);
}
30 changes: 30 additions & 0 deletions apps/web/src/app/(main)/onboarding/form/create-form-step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CopyButton } from '~/components/copy-button';

import { CreateFormDialog } from './create-form-dialog';

type CreateFormStepProps = {
formId: string | null;
};

export const CreateFormStep = ({ formId }: CreateFormStepProps) => {
return (
<div className="-mt-0.5">
<h2 className="text-xl font-semibold">Add a new form endpoint</h2>
<div className="text-gray-600 dark:text-muted-foreground mt-2"></div>
<div className="space-y-4">
<p>Use the new endpoint to recieve submissions</p>

{formId === null ? (
<CreateFormDialog />
) : (
<div>
<pre className="rounded-lg border flex justify-between items-center text-white/90 dark:text-white bg-black p-4 w-[500px]">
<>{`https://formbase.dev/s/${formId}`}</>
<CopyButton text={formId} className="text-white" />
</pre>
</div>
)}
</div>
</div>
);
};
Loading
Loading