Initial Commit
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TemplateSchemas, type UtilitySchemas } from "@plunk/shared";
|
||||
import type { Template } from "@prisma/client";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Save } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Card,
|
||||
Dropdown,
|
||||
Editor,
|
||||
FullscreenLoader,
|
||||
Input,
|
||||
Tooltip,
|
||||
} from "../../components";
|
||||
import { Dashboard } from "../../layouts";
|
||||
import { useActiveProject } from "../../lib/hooks/projects";
|
||||
import { useTemplate, useTemplates } from "../../lib/hooks/templates";
|
||||
import { network } from "../../lib/network";
|
||||
|
||||
interface TemplateValues {
|
||||
subject: string;
|
||||
body: string;
|
||||
type: "MARKETING" | "TRANSACTIONAL";
|
||||
style: "PLUNK" | "HTML";
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
|
||||
if (!router.isReady) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
|
||||
const project = useActiveProject();
|
||||
const { mutate } = useTemplates();
|
||||
const { data: template } = useTemplate(router.query.id as string);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
} = useForm<TemplateValues>({
|
||||
resolver: zodResolver(TemplateSchemas.update),
|
||||
defaultValues: {
|
||||
body: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!template) {
|
||||
return;
|
||||
}
|
||||
|
||||
reset(template);
|
||||
}, [reset, template]);
|
||||
|
||||
if (
|
||||
!project ||
|
||||
!template ||
|
||||
(watch("body") as string | undefined) === undefined
|
||||
) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
|
||||
const update = (data: TemplateValues) => {
|
||||
toast.promise(
|
||||
network.mock<Template, typeof TemplateSchemas.update>(
|
||||
project.secret,
|
||||
"PUT",
|
||||
"/v1/templates",
|
||||
{
|
||||
id: template.id,
|
||||
...data,
|
||||
},
|
||||
),
|
||||
{
|
||||
loading: "Saving your template",
|
||||
success: () => {
|
||||
void mutate();
|
||||
|
||||
return "Saved your template";
|
||||
},
|
||||
error: "Could not save your template!",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const duplicate = async (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
toast.promise(
|
||||
network.mock<Template, typeof UtilitySchemas.id>(
|
||||
project.secret,
|
||||
"POST",
|
||||
"/v1/templates/duplicate",
|
||||
{
|
||||
id: template.id,
|
||||
},
|
||||
),
|
||||
{
|
||||
loading: "Duplicating your template",
|
||||
success: () => {
|
||||
void mutate();
|
||||
return "Duplicated your template";
|
||||
},
|
||||
error: "Could not duplicate your template!",
|
||||
},
|
||||
);
|
||||
|
||||
await router.push("/templates");
|
||||
};
|
||||
|
||||
const remove = async (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (template.actions.length > 0) {
|
||||
return toast.error(
|
||||
"You cannot delete a template that is linked to an action!",
|
||||
);
|
||||
}
|
||||
|
||||
toast.promise(
|
||||
network.mock<Template, typeof UtilitySchemas.id>(
|
||||
project.secret,
|
||||
"DELETE",
|
||||
"/v1/templates",
|
||||
{
|
||||
id: template.id,
|
||||
},
|
||||
),
|
||||
{
|
||||
loading: "Deleting your template",
|
||||
success: () => {
|
||||
void mutate();
|
||||
return "Deleted your template";
|
||||
},
|
||||
error: "Could not delete your template!",
|
||||
},
|
||||
);
|
||||
|
||||
await router.push("/templates");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dashboard>
|
||||
<Card
|
||||
title={"Update your template"}
|
||||
options={
|
||||
<>
|
||||
<button
|
||||
onClick={duplicate}
|
||||
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-neutral-700 transition hover:bg-neutral-100"
|
||||
role="menuitem"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M6.5 15.25V15.25C5.5335 15.25 4.75 14.4665 4.75 13.5V6.75C4.75 5.64543 5.64543 4.75 6.75 4.75H13.5C14.4665 4.75 15.25 5.5335 15.25 6.5V6.5"
|
||||
/>
|
||||
<rect
|
||||
width="10.5"
|
||||
height="10.5"
|
||||
x="8.75"
|
||||
y="8.75"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
rx="2"
|
||||
/>
|
||||
</svg>
|
||||
Duplicate
|
||||
</button>
|
||||
<button
|
||||
onClick={remove}
|
||||
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-neutral-700 transition hover:bg-neutral-100"
|
||||
role="menuitem"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M6.75 7.75L7.59115 17.4233C7.68102 18.4568 8.54622 19.25 9.58363 19.25H14.4164C15.4538 19.25 16.319 18.4568 16.4088 17.4233L17.25 7.75"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M9.75 7.5V6.75C9.75 5.64543 10.6454 4.75 11.75 4.75H12.25C13.3546 4.75 14.25 5.64543 14.25 6.75V7.5"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M5 7.75H19"
|
||||
/>
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(update)}
|
||||
className="grid gap-6 sm:grid-cols-6"
|
||||
>
|
||||
<Input
|
||||
className={"sm:col-span-4"}
|
||||
label={"Subject"}
|
||||
placeholder={`Welcome to ${project.name}!`}
|
||||
register={register("subject")}
|
||||
error={errors.subject}
|
||||
/>
|
||||
|
||||
<div className={"sm:col-span-2"}>
|
||||
<label
|
||||
htmlFor={"type"}
|
||||
className="flex items-center text-sm font-medium text-neutral-700"
|
||||
>
|
||||
Type
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<p className={"mb-2 text-base font-semibold"}>
|
||||
What type of email is this?
|
||||
</p>
|
||||
<ul className={"list-inside"}>
|
||||
<li className={"mb-6"}>
|
||||
<span className={"font-semibold"}>Marketing</span>
|
||||
<br />
|
||||
Promotional emails with a Plunk-hosted unsubscribe
|
||||
link
|
||||
<br />
|
||||
<span className={"text-neutral-400"}>
|
||||
(e.g. welcome emails, promotions)
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={"font-semibold"}>Transactional</span>
|
||||
<br />
|
||||
Mission critical emails <br />
|
||||
<span className={"text-neutral-400"}>
|
||||
{" "}
|
||||
(e.g. email verification, password reset)
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
icon={
|
||||
<>
|
||||
<path d="M12 16v.01" />
|
||||
<path d="M12 13a2.003 2.003 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Dropdown
|
||||
onChange={(t) =>
|
||||
setValue("type", t as "MARKETING" | "TRANSACTIONAL")
|
||||
}
|
||||
values={[
|
||||
{ name: "Marketing", value: "MARKETING" },
|
||||
{ name: "Transactional", value: "TRANSACTIONAL" },
|
||||
]}
|
||||
selectedValue={watch("type")}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{errors.type?.message && (
|
||||
<motion.p
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: "auto" }}
|
||||
exit={{ height: 0 }}
|
||||
className="mt-1 text-xs text-red-500"
|
||||
>
|
||||
{errors.type.message}
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className={"sm:col-span-6"}>
|
||||
<Editor
|
||||
value={watch("body")}
|
||||
mode={watch("style")}
|
||||
modeSwitcher
|
||||
onChange={(value, type) => {
|
||||
setValue("style", type);
|
||||
setValue("body", value);
|
||||
}}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{errors.body?.message && (
|
||||
<motion.p
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: "auto" }}
|
||||
exit={{ height: 0 }}
|
||||
className="mt-1 text-xs text-red-500"
|
||||
>
|
||||
{errors.body.message}
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className={"flex justify-end gap-3 sm:col-span-6"}>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
return router.push("/templates");
|
||||
}}
|
||||
className={
|
||||
"flex w-full justify-center rounded border border-neutral-300 bg-white px-6 py-2 text-base font-medium text-neutral-700 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm"
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={
|
||||
"flex items-center gap-x-2 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white"
|
||||
}
|
||||
>
|
||||
<Save strokeWidth={1.5} size={18} />
|
||||
Save
|
||||
</motion.button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</Dashboard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import dayjs from "dayjs";
|
||||
import { motion } from "framer-motion";
|
||||
import { LayoutTemplate, Plus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { Alert, Badge, Card, Empty, Skeleton } from "../../components";
|
||||
import { Dashboard } from "../../layouts";
|
||||
import { useTemplates } from "../../lib/hooks/templates";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Index() {
|
||||
const { data: templates } = useTemplates();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dashboard>
|
||||
{templates?.length === 0 && (
|
||||
<Alert type={"info"} title={"Need a hand?"}>
|
||||
<div className={"mt-3 grid items-center sm:grid-cols-4"}>
|
||||
<p className={"sm:col-span-3"}>
|
||||
Want us to help you get started? We can help you build your
|
||||
first action in less than 5 minutes.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href={"/onboarding/actions"}
|
||||
className={
|
||||
"inline-block rounded bg-neutral-800 px-6 py-2 text-center text-sm font-medium text-white sm:col-span-1"
|
||||
}
|
||||
>
|
||||
Build an action
|
||||
</Link>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Card
|
||||
title={"Templates"}
|
||||
description={"Reusable blueprints of your emails"}
|
||||
actions={
|
||||
<>
|
||||
<Link href={"templates/new"} passHref>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={
|
||||
"flex items-center gap-x-1 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white"
|
||||
}
|
||||
>
|
||||
<Plus strokeWidth={1.5} size={18} />
|
||||
New
|
||||
</motion.button>
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{templates ? (
|
||||
templates.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
"grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"
|
||||
}
|
||||
>
|
||||
{templates
|
||||
.sort((a, b) => {
|
||||
if (a.actions.length > 0 && b.actions.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
if (a.actions.length === 0 && b.actions.length > 0) {
|
||||
return 1;
|
||||
}
|
||||
if (a.subject < b.subject) {
|
||||
return -1;
|
||||
}
|
||||
if (a.subject > b.subject) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map((t) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="col-span-1 divide-y divide-neutral-200 rounded border border-neutral-200 bg-white"
|
||||
key={t.id}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between space-x-6 p-6">
|
||||
<span className="inline-flex rounded bg-neutral-100 p-3 text-neutral-800 ring-4 ring-white">
|
||||
<LayoutTemplate size={20} />
|
||||
</span>
|
||||
<div className="flex-1 truncate">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="truncate text-sm font-medium text-neutral-800">
|
||||
{t.subject}
|
||||
</h3>
|
||||
{t.actions.length > 0 && (
|
||||
<Badge type={"success"}>Active</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 truncate text-sm text-neutral-500">
|
||||
Last edited {dayjs().to(t.updatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="-mt-px flex divide-x divide-neutral-200">
|
||||
<div className="flex w-0 flex-1">
|
||||
<Link
|
||||
href={`/templates/${t.id}`}
|
||||
className="relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl border border-transparent py-4 text-sm font-medium text-neutral-800 transition hover:bg-neutral-50 hover:text-neutral-700"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M4.75 19.25L9 18.25L18.2929 8.95711C18.6834 8.56658 18.6834 7.93342 18.2929 7.54289L16.4571 5.70711C16.0666 5.31658 15.4334 5.31658 15.0429 5.70711L5.75 15L4.75 19.25Z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M19.25 19.25H13.75"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="ml-3">Edit</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Empty
|
||||
title={"No templates here"}
|
||||
description={
|
||||
"Try creating a new email blueprint for your actions"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<Skeleton type={"table"} />
|
||||
)}
|
||||
</Card>
|
||||
</Dashboard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { TemplateSchemas } from "@plunk/shared";
|
||||
import type { Template } from "@prisma/client";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Card,
|
||||
Dropdown,
|
||||
Editor,
|
||||
FullscreenLoader,
|
||||
Input,
|
||||
Tooltip,
|
||||
} from "../../components";
|
||||
import { Dashboard } from "../../layouts";
|
||||
import { useActiveProject } from "../../lib/hooks/projects";
|
||||
import { useTemplates } from "../../lib/hooks/templates";
|
||||
import { network } from "../../lib/network";
|
||||
|
||||
interface TemplateValues {
|
||||
subject: string;
|
||||
body: string;
|
||||
type: "MARKETING" | "TRANSACTIONAL";
|
||||
style: "PLUNK" | "HTML";
|
||||
}
|
||||
|
||||
const templates = {
|
||||
blank: {
|
||||
subject: "",
|
||||
body: "",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
|
||||
const project = useActiveProject();
|
||||
const { mutate } = useTemplates();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<TemplateValues>({
|
||||
resolver: zodResolver(TemplateSchemas.create),
|
||||
defaultValues: {
|
||||
...templates.blank,
|
||||
type: "MARKETING",
|
||||
style: "PLUNK",
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return <FullscreenLoader />;
|
||||
}
|
||||
|
||||
const create = async (data: TemplateValues) => {
|
||||
toast.promise(
|
||||
network.mock<Template, typeof TemplateSchemas.create>(
|
||||
project.secret,
|
||||
"POST",
|
||||
"/v1/templates",
|
||||
{
|
||||
...data,
|
||||
},
|
||||
),
|
||||
{
|
||||
loading: "Creating new template",
|
||||
success: () => {
|
||||
void mutate();
|
||||
|
||||
return "Created new template!";
|
||||
},
|
||||
error: "Could not create new template!",
|
||||
},
|
||||
);
|
||||
|
||||
await router.push("/templates");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dashboard>
|
||||
<Card
|
||||
title={"Create a new template"}
|
||||
description={"Reusable blueprints of your emails"}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(create)}
|
||||
className="grid gap-6 sm:grid-cols-6"
|
||||
>
|
||||
<Input
|
||||
className={"sm:col-span-4"}
|
||||
label={"Subject"}
|
||||
placeholder={`Welcome to ${project.name}!`}
|
||||
register={register("subject")}
|
||||
error={errors.subject}
|
||||
/>
|
||||
|
||||
<div className={"sm:col-span-2"}>
|
||||
<label
|
||||
htmlFor={"type"}
|
||||
className="flex items-center text-sm font-medium text-neutral-700"
|
||||
>
|
||||
Type
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<p className={"mb-2 text-base font-semibold"}>
|
||||
What type of email is this?
|
||||
</p>
|
||||
<ul className={"list-inside"}>
|
||||
<li className={"mb-6"}>
|
||||
<span className={"font-semibold"}>Marketing</span>
|
||||
<br />
|
||||
Promotional emails with a Plunk-hosted unsubscribe
|
||||
link
|
||||
<br />
|
||||
<span className={"text-neutral-400"}>
|
||||
(e.g. welcome emails, promotions)
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={"font-semibold"}>Transactional</span>
|
||||
<br />
|
||||
Mission critical emails <br />
|
||||
<span className={"text-neutral-400"}>
|
||||
{" "}
|
||||
(e.g. email verification, password reset)
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
icon={
|
||||
<>
|
||||
<path d="M12 16v.01" />
|
||||
<path d="M12 13a2.003 2.003 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Dropdown
|
||||
onChange={(t) =>
|
||||
setValue("type", t as "MARKETING" | "TRANSACTIONAL")
|
||||
}
|
||||
values={[
|
||||
{ name: "Marketing", value: "MARKETING" },
|
||||
{ name: "Transactional", value: "TRANSACTIONAL" },
|
||||
]}
|
||||
selectedValue={watch("type")}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{errors.type?.message && (
|
||||
<motion.p
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: "auto" }}
|
||||
exit={{ height: 0 }}
|
||||
className="mt-1 text-xs text-red-500"
|
||||
>
|
||||
{errors.type.message}
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className={"sm:col-span-6"}>
|
||||
<Editor
|
||||
value={watch("body")}
|
||||
mode={watch("style")}
|
||||
modeSwitcher
|
||||
onChange={(value, type) => {
|
||||
setValue("body", value);
|
||||
setValue("style", type);
|
||||
}}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{errors.body?.message && (
|
||||
<motion.p
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: "auto" }}
|
||||
exit={{ height: 0 }}
|
||||
className="mt-1 text-xs text-red-500"
|
||||
>
|
||||
{errors.body.message}
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className={"flex justify-end gap-3 sm:col-span-6"}>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
return router.push("/templates");
|
||||
}}
|
||||
className={
|
||||
"flex w-full justify-center rounded border border-neutral-300 bg-white px-6 py-2 text-base font-medium text-neutral-700 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm"
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={
|
||||
"flex items-center gap-x-0.5 rounded bg-neutral-800 px-8 py-2 text-center text-sm font-medium text-white"
|
||||
}
|
||||
>
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M12 5.75V18.25"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M18.25 12L5.75 12"
|
||||
/>
|
||||
</svg>
|
||||
Create
|
||||
</motion.button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</Dashboard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user