Files
plunk/packages/dashboard/src/pages/onboarding/actions.tsx
T

906 lines
28 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { ActionSchemas, EventSchemas, TemplateSchemas } from "@plunk/shared";
import type { Template } from "@prisma/client";
import { AnimatePresence, motion } from "framer-motion";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { type FieldError, useForm } from "react-hook-form";
import { toast } from "sonner";
import {
CodeBlock,
Dropdown,
Editor,
FullscreenLoader,
Modal,
MultiselectDropdown,
Toggle,
Tooltip,
} from "../../components";
import { API_URI } from "../../lib/constants";
import { useActions } from "../../lib/hooks/actions";
import { useEvents } from "../../lib/hooks/events";
import { useActiveProject } from "../../lib/hooks/projects";
import { useTemplates } from "../../lib/hooks/templates";
import { useUser } from "../../lib/hooks/users";
import { network } from "../../lib/network";
interface EventValues {
event: string;
}
interface TemplateValues {
subject: string;
body: string;
type: "MARKETING" | "TRANSACTIONAL";
style: "PLUNK" | "HTML";
}
interface ActionValues {
name: string;
runOnce: boolean;
delay: number;
template: string;
events: string[];
notevents: string[];
}
/**
*
*/
export default function Index() {
const router = useRouter();
const activeProject = useActiveProject();
const { data: user } = useUser();
const { data: events, mutate: eventMutate } = useEvents();
const { data: templates, mutate: templateMutate } = useTemplates();
const { data: actions, mutate: actionMutate } = useActions();
const [eventModal, setEventModal] = useState(false);
const [advancedSettings, setAdvancedSettings] = useState(false);
const [step, setStep] = useState<0 | 1 | 2 | 3>(0);
const [language, setLanguage] = useState<"javascript" | "python" | "curl" | "PHP" | "ruby">("curl");
const [delay, setDelay] = useState<{
delay: number;
unit: "MINUTES" | "HOURS" | "DAYS";
}>({
delay: 0,
unit: "MINUTES",
});
useEffect(() => {
switch (delay.unit) {
case "MINUTES":
actionSetValue("delay", delay.delay);
break;
case "HOURS":
actionSetValue("delay", delay.delay * 60);
break;
case "DAYS":
actionSetValue("delay", delay.delay * 24 * 60);
break;
}
}, [delay]);
const {
register: eventRegister,
handleSubmit: eventHandleSubmit,
formState: { errors: eventErrors },
getValues: eventGetValues,
} = useForm<EventValues>({
resolver: zodResolver(EventSchemas.post.pick({ event: true })),
});
const {
register: templateRegister,
handleSubmit: templateHandleSubmit,
formState: { errors: templateErrors },
setValue: templateSetValue,
watch: templateWatch,
} = useForm<TemplateValues>({
resolver: zodResolver(TemplateSchemas.create),
defaultValues: {
type: "MARKETING",
style: "PLUNK",
subject: "Welcome to Plunk!",
body:
'<h1 id="welcome-to-plunk-">Welcome to Plunk!</h1>\n' +
"<p>Writing emails in Plunk is super easy, anyone do it! \n" +
'They also support all sorts of cool stuff like <code>code blocks</code>, <strong>bold text</strong>, and <a href="https://www.useplunk.com">links</a>.</p><p><strong>Highlight this text</strong> and see what is possible!</p>',
},
});
const {
register: actionRegister,
handleSubmit: actionHandleSubmit,
formState: { errors: actionErrors },
setValue: actionSetValue,
watch: actionWatch,
} = useForm<ActionValues>({
resolver: zodResolver(ActionSchemas.create),
defaultValues: {
template: "No template selected",
events: [],
notevents: [],
runOnce: false,
},
});
if (!activeProject || !events || !user || !actions || !templates) {
return <FullscreenLoader />;
}
const triggerEvent = (data: EventValues) => {
toast.promise(
network.mock<boolean, typeof EventSchemas.post>(activeProject.secret, "POST", "/v1/track", {
event: data.event,
email: user.email,
subscribed: true,
}),
{
loading: "Sending your event",
success: () => {
setEventModal(false);
void eventMutate();
return `${data.event} delivered`;
},
error: "Having trouble sending your event, we will try again later!",
},
);
};
const createTemplate = (data: TemplateValues) => {
toast.promise(
network.mock<Template, typeof TemplateSchemas.create>(activeProject.secret, "POST", "/v1/templates", {
...data,
}),
{
loading: "Creating new template",
success: () => {
void templateMutate();
setStep(3);
return "Created your template";
},
error: "Could not create new email!",
},
);
};
const createAction = (data: ActionValues) => {
toast.promise(
Promise.all([
fetch("/api/plunk", {
method: "POST",
body: JSON.stringify({
event: "onboarding-completed",
email: user.email,
data: {
project: activeProject.name,
firstEvent: events.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0].name,
},
}),
headers: { "Content-Type": "application/json" },
}),
network.mock<Template, typeof ActionSchemas.create>(activeProject.secret, "POST", "/v1/actions", {
...data,
}),
]),
{
loading: "Creating new action",
success: () => {
void actionMutate();
return "Created your action";
},
error: "Could not create new action!",
},
);
};
const renderStep = () => {
switch (step) {
case 0:
return (
<>
<motion.div
key={"welcome"}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
className={"flex h-96 flex-col items-center justify-center text-center"}
>
<motion.span
animate={{
rotate: [0, 35, 0],
}}
transition={{ repeat: 5, duration: 1 }}
className={"text-6xl"}
>
👋
</motion.span>
<h2 className={"my-4 text-4xl font-bold"}>Let's get started!</h2>
<div className={"max-w-2xl font-medium text-neutral-500"}>
<p>Are you ready to give Plunk Actions a spin?</p>
<p>
In this 3 step tutorial, we'll help you set up your first email action so that you have an example on hand when
you are ready to start building your own.
</p>
</div>
<motion.button
whileTap={{ scale: 0.9 }}
whileHover={{ scale: 1.05 }}
onClick={() => setStep(1)}
className={"mt-6 rounded bg-neutral-800 px-12 py-4 text-sm font-medium text-white"}
>
Let's get started!
</motion.button>
</motion.div>
</>
);
case 1:
return events.length > 0 ? (
<>
<motion.div
key={"event-success"}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
className={"flex h-96 flex-col items-center justify-center text-center"}
>
<motion.span
animate={{
rotate: [0, -180, 0],
}}
transition={{ repeat: 1, duration: 1 }}
className={"text-6xl"}
>
🎉
</motion.span>
<h2 className={"my-4 text-2xl font-bold"}>Your event has successfully arrived</h2>
<p className={"font-medium text-neutral-500 sm:w-1/2"}>
We have received your event{" "}
<span className={"rounded bg-neutral-50 px-2 py-0.5 font-mono text-neutral-600"}>
{events.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0].name}
</span>
, you are now ready to create your first email template!
</p>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }}
onClick={() => setStep(2)}
className={"mt-4 rounded-md bg-neutral-800 px-10 py-3 text-sm font-medium text-white"}
>
Design an email
</motion.button>
</motion.div>
</>
) : (
<>
<motion.div
key={"event"}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
>
<div className={"mx-auto my-6 max-w-xl text-center"}>
<h2 className={"my-2 text-2xl font-bold"}>Track your first event</h2>
<p className={"font-medium text-neutral-500"}>
Actions start from events. You can call them whatever you want and send them from anywhere using an API call.
</p>
</div>
<div className={"mt-8 grid gap-6 sm:grid-cols-3"}>
<div className={"border-b border-neutral-100 p-4 sm:col-span-2 sm:border-b-0 sm:border-r-2"}>
<h3 className={"text-center font-semibold text-neutral-800"}>From your application</h3>
<div className={"mt-3 space-y-6"}>
<div>
<Dropdown
onChange={(e) => setLanguage(e as "javascript" | "python" | "curl")}
values={[
{ value: "curl", name: "cURL" },
{ name: "JavaScript", value: "javascript" },
{ value: "python", name: "Python" },
{ value: "PHP", name: "PHP" },
{ value: "ruby", name: "Ruby" },
]}
selectedValue={language}
/>
</div>
<CodeBlock
style={{
fontSize: "0.9rem",
borderRadius: "0.5rem",
padding: "1rem",
}}
language={language}
code={
{
javascript: `await fetch('${API_URI}/v1/track', {
method: 'POST',
body: JSON.stringify({
event: "my-new-event",
email: "${user.email}"
}),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${activeProject.secret}',
},
});`,
python: `import requests
requests.post(
"${API_URI}/v1/track",
headers={"Content-Type": "application/json", "Authorization": "Bearer ${activeProject.secret}"},
json={
"event": "my-new-event",
"user": "${user.email}",
},
)`,
curl: `curl --location --request POST '${API_URI}/v1/track' \\
--header 'Authorization: Bearer ${activeProject.secret}' \\
--header 'Content-Type: application/json' \\
--data-raw '{"email": "[email protected]", "event": "new-project"}'`,
PHP: `<?php
$client = new Client();
$request = new Request('POST', '${API_URI}/v1/track', ['Authorization' => 'Bearer ${activeProject.secret}', 'Content-Type' => 'application/json'], '{
"event": "my-new-event",
"email": "${user.email}"
}');
$res = $client->sendAsync($request)->wait();`,
ruby: `require "uri"
require "json"
require "net/http"
url = URI("${API_URI}/v1/track")
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = "Bearer ${activeProject.secret}"
request["Content-Type"] = "application/json"
request.body = JSON.dump({
"event": "my-new-event",
"email": "${user.email}"
})
response = https.request(request)`,
}[language]
}
/>
</div>
</div>
<div className={"flex flex-col items-center justify-center p-4"}>
<h3 className={"text-center font-semibold text-neutral-800"}>From Plunk</h3>
<div className={"flex flex-1 flex-col items-center justify-center"}>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }}
onClick={() => setEventModal(true)}
className={"mt-6 rounded-md bg-neutral-800 px-10 py-4 text-sm font-medium text-white"}
>
Trigger a demo event
</motion.button>
</div>
</div>
</div>
</motion.div>
</>
);
case 2:
return (
<>
<motion.div
key={"template"}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
>
<div className={"mx-auto my-6 max-w-4xl text-center"}>
<h2 className={"my-2 text-2xl font-bold"}>Design an email</h2>
<p className={"font-medium text-neutral-500"}>
Our templates are easy to write and automatically transformed into HTML that email clients understand.
</p>
</div>
<form onSubmit={templateHandleSubmit(createTemplate)} className={"grid gap-6 sm:grid-cols-6"}>
<div className={"sm:col-span-4"}>
<label htmlFor={"subject"} className="block text-sm font-medium text-neutral-700">
Subject
</label>
<div className="mt-1">
<input
autoComplete={"off"}
type={"text"}
className={
"block w-full rounded-md border-neutral-300 transition ease-in-out focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
}
placeholder={"The subject of the email"}
{...templateRegister("subject")}
/>
</div>
<AnimatePresence>
{templateErrors.subject?.message && (
<motion.p
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="mt-1 text-xs text-red-500"
>
{templateErrors.subject.message}
</motion.p>
)}
</AnimatePresence>
</div>
<div className={"sm:col-span-2"}>
<label htmlFor={"type"} className="block 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) => templateSetValue("type", t as "MARKETING" | "TRANSACTIONAL")}
values={[
{ name: "Marketing", value: "MARKETING" },
{ name: "Transactional", value: "TRANSACTIONAL" },
]}
selectedValue={templateWatch("type")}
/>
<AnimatePresence>
{templateErrors.type?.message && (
<motion.p
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="mt-1 text-xs text-red-500"
>
{templateErrors.type.message}
</motion.p>
)}
</AnimatePresence>
</div>
<div className={"sm:col-span-6"}>
<Editor value={templateWatch("body")} mode={"PLUNK"} onChange={(val) => templateSetValue("body", val)} />
<AnimatePresence>
{templateErrors.body?.message && (
<motion.p
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="mt-1 text-xs text-red-500"
>
{templateErrors.body.message}
</motion.p>
)}
</AnimatePresence>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }}
className={
"my-6 ml-auto flex items-center gap-x-0.5 rounded-md bg-neutral-800 px-10 py-2.5 text-center text-sm font-medium text-white sm:col-span-6"
}
>
<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>
</form>
</motion.div>
</>
);
case 3:
return actions.length > 0 ? (
<>
<motion.div
key={"action-success"}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
className={"flex h-96 flex-col items-center justify-center text-center"}
>
<motion.span
animate={{
x: [0, -20, 20, 0],
}}
transition={{ repeat: 1, duration: 1 }}
className={"text-6xl"}
>
🏎
</motion.span>
<h2 className={"my-4 text-2xl font-bold"}>Your action has been created</h2>
<p className={"w-1/2 font-medium text-neutral-500"}>
Users will now automatically start to receive emails when they complete your event{" "}
<span className={"rounded-md bg-neutral-50 px-2 py-0.5 font-mono text-neutral-500"}>
{events.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0].name}
</span>
. There is loads more to discover in Plunk but let's try out your action first!
</p>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }}
className={"mt-9 rounded-md bg-neutral-800 px-24 py-3 text-sm font-medium text-white"}
onClick={async () => {
toast.promise(
network.mock<boolean, typeof EventSchemas.post>(activeProject.secret, "POST", "/v1/track", {
event: eventGetValues("event"),
email: user.email,
}),
{
loading: "Sending your event",
success: `${eventGetValues("event")} delivered`,
error: "Could not deliver your event!",
},
);
await router.push("/");
}}
>
Try it out!
</motion.button>
</motion.div>
</>
) : (
<>
<motion.div
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
key={"action"}
>
<div className={"mx-auto my-6 max-w-2xl text-center"}>
<h2 className={"my-2 text-2xl font-bold"}>Creating your first action</h2>
<p className={"font-medium text-neutral-500"}>
Actions tie together events and templates, they automate your email workflows.
</p>
</div>
<form onSubmit={actionHandleSubmit(createAction)} className="grid gap-4 space-y-6 pb-6 sm:grid-cols-2">
<div className={"sm:col-span-2"}>
<label htmlFor={"name"} className="block text-sm font-medium text-neutral-700">
Name
</label>
<div className="mt-1">
<input
type={"text"}
autoComplete={"off"}
className={
"block w-full rounded-md border-neutral-300 transition ease-in-out focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
}
placeholder={"Welcome flow"}
{...actionRegister("name")}
/>
</div>
<AnimatePresence>
{actionErrors.name?.message && (
<motion.p
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="mt-1 text-xs text-red-500"
>
{actionErrors.name.message}
</motion.p>
)}
</AnimatePresence>
</div>
<div>
<label htmlFor={"events"} className="block text-sm font-medium text-neutral-700">
Events that need to be triggered
</label>
<MultiselectDropdown
onChange={(e) => actionSetValue("events", e)}
values={events.map((e) => {
return { name: e.name, value: e.id };
})}
selectedValues={actionWatch("events")}
/>
<AnimatePresence>
{(actionErrors.events as FieldError | undefined)?.message && (
<motion.p
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="mt-1 text-xs text-red-500"
>
{(actionErrors.events as FieldError | undefined)?.message}
</motion.p>
)}
</AnimatePresence>
</div>
<div>
<label htmlFor={"template"} className="block text-sm font-medium text-neutral-700">
Template that will be sent
</label>
<Dropdown
onChange={(t) => actionSetValue("template", t)}
values={templates.map((t) => {
return { name: t.subject, value: t.id };
})}
selectedValue={actionWatch("template")}
/>
<AnimatePresence>
{actionErrors.template?.message && (
<motion.p
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="mt-1 text-xs text-red-500"
>
{actionErrors.template.message}
</motion.p>
)}
</AnimatePresence>
</div>
{
<div className={"w-full text-center sm:col-span-2"}>
<button
onClick={(e) => {
e.preventDefault();
setAdvancedSettings(!advancedSettings);
}}
className={"text-sm font-medium text-neutral-500 transition hover:text-neutral-700"}
>
{advancedSettings ? "Hide advanced settings" : "Show advanced settings"}
</button>
</div>
}
<AnimatePresence>
{advancedSettings && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
key={"advanced"}
className={"sm:col-span-2"}
>
<div>
<label htmlFor={"template"} className="block text-sm font-medium text-neutral-700">
Delay before sending
</label>
<div className={"grid grid-cols-2 gap-4"}>
<div className={"mt-1"}>
<input
type={"number"}
autoComplete={"off"}
min={0}
className={
"block w-full rounded-md border-neutral-300 transition ease-in-out focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
}
placeholder={"0"}
value={delay.delay}
onChange={(e) =>
setDelay({
...delay,
delay: Number.parseInt(e.target.value),
})
}
/>
</div>
<div>
<Dropdown
onChange={(t) =>
setDelay({
...delay,
unit: t as "MINUTES" | "HOURS" | "DAYS",
})
}
values={[
{ name: "Minutes", value: "MINUTES" },
{ name: "Hours", value: "HOURS" },
{ name: "Days", value: "DAYS" },
]}
selectedValue={delay.unit}
/>
</div>
</div>
</div>
<div className={"mt-8 sm:col-span-2"}>
<Toggle
title={"Run once"}
description={
actionWatch("runOnce")
? "This action will only run once for each contact."
: "This action will run each time the required events are triggered."
}
toggled={actionWatch("runOnce")}
onToggle={() => actionSetValue("runOnce", !actionWatch("runOnce"))}
/>
</div>
</motion.div>
)}
</AnimatePresence>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }}
className={
"ml-auto flex items-center gap-x-0.5 rounded-md bg-neutral-800 px-10 py-2.5 text-center text-sm font-medium text-white sm:col-span-2"
}
>
<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>
</form>
</motion.div>
</>
);
}
};
return (
<>
<Modal
isOpen={eventModal}
onToggle={() => setEventModal(!eventModal)}
onAction={eventHandleSubmit(triggerEvent)}
type={"info"}
action={"Trigger"}
title={"Trigger an event"}
description={"Trigger an event to use in your actions"}
>
<div>
<label htmlFor={"event"} className="block text-sm font-medium text-neutral-700">
Event
</label>
<div className="mt-1">
<input
type={"text"}
autoComplete={"off"}
className={
"block w-full rounded-md border-neutral-300 transition ease-in-out focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
}
placeholder={"My-new-event"}
{...eventRegister("event")}
/>
</div>
<AnimatePresence>
{eventErrors.event?.message && (
<motion.p
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="mt-1 text-xs text-red-500"
>
{eventErrors.event.message}
</motion.p>
)}
</AnimatePresence>
</div>
</Modal>
<main>
<div className={"mx-auto max-w-7xl"}>
<div className={"pt-8"}>
<nav aria-label="Progress">
<ol className="space-y-4 md:flex md:space-x-8 md:space-y-0">
<li className="md:flex-1">
<span
className={`${
step >= 0 ? "border-neutral-800" : "border-neutral-200"
} group flex flex-col border-l-4 py-2 pl-4 transition md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4`}
>
<span className="text-sm font-medium">Welcome</span>
</span>
</li>
<li className="md:flex-1">
<span
className={`${
step >= 1 ? "border-neutral-800" : "border-neutral-200"
} group flex flex-col border-l-4 py-2 pl-4 transition md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4`}
>
<span className="text-sm font-medium">Track an event</span>
</span>
</li>
<li className="md:flex-1">
<span
className={`${
step >= 2 ? "border-neutral-800" : "border-neutral-200"
} group flex flex-col border-l-4 py-2 pl-4 transition md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4`}
>
<span className="text-sm font-medium">Design an email</span>
</span>
</li>
<li className="md:flex-1">
<span
className={`${
step >= 3 ? "border-neutral-800" : "border-neutral-200"
} group flex flex-col border-l-4 py-2 pl-4 transition md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4`}
>
<span className="text-sm font-medium">Create an action</span>
</span>
</li>
</ol>
</nav>
</div>
<div className={"mx-auto flex h-full flex-col items-center pt-16"}>{renderStep()}</div>
</div>
{step === 0 && (
<div className={"fixed bottom-3 w-full bg-white text-center"}>
<span
className={"cursor-pointer text-sm text-neutral-500 transition ease-in-out hover:text-neutral-700"}
onClick={async () => {
await router.push("/onboarding");
}}
>
Go back
</span>
</div>
)}
</main>
</>
);
}