updates
All checks were successful
Deploy / deploy (ubuntu-latest, 2.44.0) (push) Successful in 1m39s
@ -31,7 +31,6 @@
|
|||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-social-icons": "^6.18.0",
|
|
||||||
"react-turnstile": "^1.1.3",
|
"react-turnstile": "^1.1.3",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.3 KiB |
5
public/media/platforms/github.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||||
|
fill="#fff"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 986 B |
16
public/media/platforms/google.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64"
|
||||||
|
height="64">
|
||||||
|
<defs>
|
||||||
|
<path id="A"
|
||||||
|
d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/>
|
||||||
|
</defs>
|
||||||
|
<clipPath id="B">
|
||||||
|
<use xlink:href="#A"/>
|
||||||
|
</clipPath>
|
||||||
|
<g transform="matrix(.727273 0 0 .727273 -.954545 -1.45455)">
|
||||||
|
<path d="M0 37V11l17 13z" clip-path="url(#B)" fill="#fbbc05"/>
|
||||||
|
<path d="M0 11l17 13 7-6.1L48 14V0H0z" clip-path="url(#B)" fill="#ea4335"/>
|
||||||
|
<path d="M0 37l30-23 7.9 1L48 0v48H0z" clip-path="url(#B)" fill="#34a853"/>
|
||||||
|
<path d="M48 48L17 24l-4-3 35-10z" clip-path="url(#B)" fill="#4285f4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 861 B |
@ -6,7 +6,6 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import AuthForm from "@/components/auth/auth-form";
|
import AuthForm from "@/components/auth/auth-form";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Greeting from "@/components/auth/greeting";
|
import Greeting from "@/components/auth/greeting";
|
||||||
import Footer from "@/components/auth/footer";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page to authenticate with.
|
* The page to authenticate with.
|
||||||
@ -29,7 +28,6 @@ const AuthPage = (): ReactElement => (
|
|||||||
<Separator className="w-28" />
|
<Separator className="w-28" />
|
||||||
</div>
|
</div>
|
||||||
<AuthForm />
|
<AuthForm />
|
||||||
<Footer />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
@ -40,7 +38,7 @@ const AuthPage = (): ReactElement => (
|
|||||||
* @return the providers jsx
|
* @return the providers jsx
|
||||||
*/
|
*/
|
||||||
const OAuthProviders = (): ReactElement => (
|
const OAuthProviders = (): ReactElement => (
|
||||||
<div className="mt-1 flex gap-2.5">
|
<div className="mt-1 flex gap-2.5 justify-center">
|
||||||
<OAuthProvider name="GitHub" link="#" />
|
<OAuthProvider name="GitHub" link="#" />
|
||||||
<OAuthProvider name="Google" link="#" />
|
<OAuthProvider name="Google" link="#" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,7 +14,10 @@ const inter: NextFont = Inter({ subsets: ["latin"] });
|
|||||||
* The metadata for this app.
|
* The metadata for this app.
|
||||||
*/
|
*/
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Pulse App",
|
title: {
|
||||||
|
default: "Pulse App",
|
||||||
|
template: "%s • Pulse App",
|
||||||
|
},
|
||||||
description:
|
description:
|
||||||
"A lightweight service monitoring solution for tracking the availability of whatever service your heart desires!",
|
"A lightweight service monitoring solution for tracking the availability of whatever service your heart desires!",
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@ -31,7 +34,7 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
themeColor: "#DC2626",
|
themeColor: "#A855F7",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,7 +51,7 @@ const RootLayout = ({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"linear-gradient(to top, hsla(240, 6%, 10%, 0.7), hsl(var(--background)))",
|
"linear-gradient(to top, hsl(240, 6%, 10%), hsl(var(--background)))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
|
@ -74,7 +74,7 @@ const UserProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
) {
|
) {
|
||||||
router.push("/dashboard/onboarding");
|
router.push("/dashboard/onboarding");
|
||||||
}
|
}
|
||||||
}, [cookies, router]);
|
}, [cookies, router, path]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUser();
|
fetchUser();
|
||||||
|
@ -50,9 +50,9 @@ body {
|
|||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 240 10% 3.9%;
|
--popover: 240 10% 3.9%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 0 0% 98%;
|
--primary: 271 91% 65%;
|
||||||
--primary-foreground: 240 5.9% 10%;
|
--primary-foreground: 240 5.9% 10%;
|
||||||
--secondary: 240 3.7% 15.9%;
|
--secondary: 272 72% 47%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 240 3.7% 15.9%;
|
--muted: 240 3.7% 15.9%;
|
||||||
--muted-foreground: 240 5% 64.9%;
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
17
src/app/types/user/response/user-auth-response.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { User } from "@/app/types/user/user";
|
||||||
|
import { Session } from "@/app/types/user/session";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response for successfully logging in.
|
||||||
|
*/
|
||||||
|
export type UserAuthResponse = {
|
||||||
|
/**
|
||||||
|
* The created session for the user.
|
||||||
|
*/
|
||||||
|
session: Session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user logging in.
|
||||||
|
*/
|
||||||
|
user: User;
|
||||||
|
};
|
@ -14,13 +14,16 @@ import {
|
|||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { apiRequest } from "@/lib/api";
|
import { apiRequest } from "@/lib/api";
|
||||||
import { Session } from "@/app/types/user/session";
|
|
||||||
import { Cookies, useCookies } from "next-client-cookies";
|
import { Cookies, useCookies } from "next-client-cookies";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Turnstile, { useTurnstile } from "react-turnstile";
|
import Turnstile, { useTurnstile } from "react-turnstile";
|
||||||
import { TurnstileObject } from "turnstile-types";
|
import { TurnstileObject } from "turnstile-types";
|
||||||
|
import { UserAuthResponse } from "@/app/types/user/response/user-auth-response";
|
||||||
|
import { hasFlag } from "@/lib/user";
|
||||||
|
import { UserFlag } from "@/app/types/user/user-flag";
|
||||||
|
import { User } from "@/app/types/user/user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the form schemas for the various stages.
|
* Define the form schemas for the various stages.
|
||||||
@ -28,24 +31,31 @@ import { TurnstileObject } from "turnstile-types";
|
|||||||
const buildEmailInput = (allowEmpty: boolean) =>
|
const buildEmailInput = (allowEmpty: boolean) =>
|
||||||
z
|
z
|
||||||
.string()
|
.string()
|
||||||
.email("Invalid email address")
|
.email("That email address is invalid.")
|
||||||
.refine(
|
.refine(
|
||||||
(val) => {
|
(val) => {
|
||||||
return !allowEmpty || val.length > 0;
|
return !allowEmpty || val.length > 0;
|
||||||
},
|
},
|
||||||
{ message: "Email is required" }
|
{ message: "An email address is required." }
|
||||||
);
|
);
|
||||||
|
|
||||||
const EmailSchema = z.object({
|
const EmailSchema = z.object({
|
||||||
email: buildEmailInput(false),
|
email: buildEmailInput(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
const RegisterSchema = z.object({
|
const RegisterSchema = z
|
||||||
email: buildEmailInput(true),
|
.object({
|
||||||
username: z.string(),
|
email: buildEmailInput(true),
|
||||||
password: z.string(),
|
username: z
|
||||||
passwordConfirmation: z.string(),
|
.string()
|
||||||
});
|
.regex(/^[a-z0-9_.]*$/, "That username is invalid."),
|
||||||
|
password: z.string(),
|
||||||
|
passwordConfirmation: z.string(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.passwordConfirmation, {
|
||||||
|
path: ["passwordConfirmation"],
|
||||||
|
message: "Your passwords do not match.",
|
||||||
|
});
|
||||||
|
|
||||||
const LoginSchema = z.object({
|
const LoginSchema = z.object({
|
||||||
email: buildEmailInput(true),
|
email: buildEmailInput(true),
|
||||||
@ -108,7 +118,7 @@ const AuthForm = (): ReactElement => {
|
|||||||
setStage(data?.exists ? "login" : "register");
|
setStage(data?.exists ? "login" : "register");
|
||||||
} else {
|
} else {
|
||||||
const registering: boolean = stage === "register";
|
const registering: boolean = stage === "register";
|
||||||
const { data, error } = await apiRequest<Session>({
|
const { data, error } = await apiRequest<UserAuthResponse>({
|
||||||
endpoint: `/auth/${registering ? "register" : "login"}`,
|
endpoint: `/auth/${registering ? "register" : "login"}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: registering
|
body: registering
|
||||||
@ -132,13 +142,18 @@ const AuthForm = (): ReactElement => {
|
|||||||
turnstile.reset();
|
turnstile.reset();
|
||||||
} else {
|
} else {
|
||||||
// Otherwise store the session and redirect to the dashboard
|
// Otherwise store the session and redirect to the dashboard
|
||||||
cookies.set("session", JSON.stringify(data), {
|
cookies.set("session", JSON.stringify(data?.session), {
|
||||||
expires:
|
expires:
|
||||||
((data?.expires as number) - Date.now()) / 86_400_000,
|
((data?.session.expires as number) - Date.now()) /
|
||||||
|
86_400_000,
|
||||||
secure: true,
|
secure: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
});
|
});
|
||||||
router.push("/dashboard");
|
router.push(
|
||||||
|
hasFlag(data?.user as User, UserFlag.COMPLETED_ONBOARDING)
|
||||||
|
? "/dashboard"
|
||||||
|
: "/dashboard/onboarding"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,7 +253,7 @@ const AuthForm = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Submit Form */}
|
{/* Submit Form */}
|
||||||
<Button
|
<Button
|
||||||
className="h-11 flex gap-2.5 items-center bg-zinc-800/75 text-white border border-zinc-700/35 hover:bg-zinc-800/75 hover:opacity-75 transition-all transform-gpu group"
|
className="h-11 flex gap-2.5 items-center text-white border border-secondary transition-all transform-gpu group"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { ReactElement } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The auth footer.
|
|
||||||
*
|
|
||||||
* @return the footer jsx
|
|
||||||
*/
|
|
||||||
const Footer = (): ReactElement => (
|
|
||||||
<footer className="flex justify-center text-center">
|
|
||||||
<p className="max-w-[17rem] opacity-95 select-none">
|
|
||||||
By registering you agree to our{" "}
|
|
||||||
<DocumentLink name="Terms and Conditions" link="/legal/terms" /> and
|
|
||||||
our <DocumentLink name="Privacy Policy" link="/legal/privacy" />.
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
|
|
||||||
const DocumentLink = ({ name, link }: { name: string; link: string }) => (
|
|
||||||
<Link
|
|
||||||
className="text-red-500 hover:opacity-85 transition-all transform-gpu"
|
|
||||||
href={link}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Footer;
|
|
@ -1,7 +1,7 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SocialIcon } from "react-social-icons";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The props for this oauth provider.
|
* The props for this oauth provider.
|
||||||
@ -26,14 +26,14 @@ type OAuthProviderProps = {
|
|||||||
const OAuthProvider = ({ name, link }: OAuthProviderProps): ReactElement => (
|
const OAuthProvider = ({ name, link }: OAuthProviderProps): ReactElement => (
|
||||||
<Link className="cursor-not-allowed" href={link}>
|
<Link className="cursor-not-allowed" href={link}>
|
||||||
<Button
|
<Button
|
||||||
className="h-12 bg-zinc-800/85 text-white border border-zinc-700/35 hover:bg-muted hover:opacity-75 transition-all transform-gpu"
|
className="w-32 h-12 flex gap-2.5 bg-zinc-800/85 text-white border border-zinc-700/35 hover:bg-muted hover:opacity-75 transition-all transform-gpu"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<SocialIcon
|
<Image
|
||||||
className="w-10 h-10"
|
src={`/media/platforms/${name.toLowerCase()}.svg`}
|
||||||
as="div"
|
alt={`${name}'s Logo`}
|
||||||
bgColor="transparent"
|
width={24}
|
||||||
network={name.toLowerCase()}
|
height={24}
|
||||||
/>
|
/>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ReactElement, useState } from "react";
|
import { ReactElement, useState } from "react";
|
||||||
import { BriefcaseIcon, ClipboardIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
BriefcaseIcon,
|
||||||
|
ClipboardIcon,
|
||||||
|
LinkIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@ -21,21 +25,24 @@ import { User } from "@/app/types/user/user";
|
|||||||
* Define the various stages of onboarding.
|
* Define the various stages of onboarding.
|
||||||
*/
|
*/
|
||||||
const organizationName = z.string().min(2, "You need a longer org name!!!");
|
const organizationName = z.string().min(2, "You need a longer org name!!!");
|
||||||
|
const organizationSlug = z.string().min(2, "You need a longer org slug!!!");
|
||||||
const stages: OnboardingStage[] = [
|
const stages: OnboardingStage[] = [
|
||||||
{
|
{
|
||||||
name: "Onboarding",
|
name: "Create a new organization",
|
||||||
description:
|
description:
|
||||||
"Welcome to Pulse App! To get started, first create your organization!",
|
"First create your organization! Organizations are used to manage your status pages.",
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
organizationName,
|
organizationName,
|
||||||
|
organizationSlug,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Status Page",
|
name: "Create a new status page",
|
||||||
description:
|
description:
|
||||||
"Next, create your status page and jump right into the app!",
|
"Thanks! Next, create your status page and jump right into the app!",
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
organizationName,
|
organizationName,
|
||||||
|
organizationSlug,
|
||||||
statusPageName: z
|
statusPageName: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, "You need a longer status page name!!!"),
|
.min(2, "You need a longer status page name!!!"),
|
||||||
@ -64,15 +71,28 @@ const OnboardingForm = (): ReactElement => {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(stage.schema),
|
resolver: zodResolver(stage.schema),
|
||||||
});
|
});
|
||||||
|
const organizationName: string | undefined = watch(
|
||||||
|
"organizationName",
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const defaultOrgSlug: string = `${user?.username}'s Cool Org`;
|
||||||
|
const organizationSlugPreview = buildOrgSlugPreview(
|
||||||
|
organizationName || defaultOrgSlug
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle submitting the form.
|
* Handle submitting the form.
|
||||||
*/
|
*/
|
||||||
const onSubmit = async ({ organizationName, statusPageName }: any) => {
|
const onSubmit = async ({
|
||||||
|
organizationName,
|
||||||
|
organizationSlug,
|
||||||
|
statusPageName,
|
||||||
|
}: any) => {
|
||||||
// Completed onboarding
|
// Completed onboarding
|
||||||
if (stage === stages[stages.length - 1]) {
|
if (stage === stages[stages.length - 1]) {
|
||||||
const { data, error } = await apiRequest<void>({
|
const { data, error } = await apiRequest<void>({
|
||||||
@ -81,6 +101,7 @@ const OnboardingForm = (): ReactElement => {
|
|||||||
session,
|
session,
|
||||||
body: {
|
body: {
|
||||||
organizationName,
|
organizationName,
|
||||||
|
organizationSlug,
|
||||||
statusPageName,
|
statusPageName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -103,64 +124,98 @@ const OnboardingForm = (): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={stage.name}
|
key={stage.name}
|
||||||
className="flex flex-col gap-3"
|
className="w-96 flex flex-col gap-2.5"
|
||||||
initial={{ x: -50, opacity: 0 }}
|
initial={{ x: -50, opacity: 0 }}
|
||||||
animate={{ x: 0, opacity: 1 }}
|
animate={{ x: 0, opacity: 1 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
>
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col gap-1 text-center items-center select-none pointer-events-none">
|
||||||
|
<h1 className="text-3xl font-bold">{stage.name}</h1>
|
||||||
|
<p className="max-w-[25rem] opacity-65">{stage.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className="flex flex-col gap-2"
|
className="flex flex-col gap-0.5"
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
<div className="my-3 p-6 pb-3.5 flex flex-col gap-3.5 justify-center bg-zinc-900 rounded-lg">
|
||||||
<div className="flex flex-col gap-1 select-none pointer-events-none">
|
{/* Organization Name */}
|
||||||
<h1 className="text-3xl font-bold">{stage.name}</h1>
|
{stage === stages[0] && (
|
||||||
<p className="max-w-[20rem] opacity-65">
|
<div className="flex flex-col gap-1.5">
|
||||||
{stage.description}
|
<p className="text-sm font-medium">
|
||||||
|
Organization Name
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<BriefcaseIcon className="absolute left-2 top-[0.6rem] w-[1.15rem] h-[1.15rem]" />
|
||||||
|
<Input
|
||||||
|
className="pl-8 rounded-lg"
|
||||||
|
defaultValue={defaultOrgSlug}
|
||||||
|
{...register("organizationName")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Organization Slug */}
|
||||||
|
{stage === stages[0] && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Organization Slug
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute left-2 top-[0.5rem] flex gap-1 items-center">
|
||||||
|
<LinkIcon className="w-[1.15rem] h-[1.15rem]" />
|
||||||
|
<p className="text-sm opacity-60">
|
||||||
|
pulseapp.cc/
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className="pl-[7.25rem] rounded-lg"
|
||||||
|
placeholder={organizationSlugPreview}
|
||||||
|
defaultValue={organizationSlugPreview}
|
||||||
|
{...register("organizationSlug")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Page Name */}
|
||||||
|
{stage === stages[1] && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Status Page Name
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<ClipboardIcon className="absolute left-2 top-[0.6rem] w-[1.15rem] h-[1.15rem]" />
|
||||||
|
<Input
|
||||||
|
className="pl-8 rounded-lg"
|
||||||
|
defaultValue={`${user?.username}'s Status Page`}
|
||||||
|
{...register("statusPageName")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Display the global error if it exists, otherwise show the first field error */}
|
||||||
|
<p className="text-red-500">
|
||||||
|
{error
|
||||||
|
? error
|
||||||
|
: Object.values(errors).find(
|
||||||
|
(err: any) => err?.message
|
||||||
|
) &&
|
||||||
|
Object.values(errors)
|
||||||
|
.find((err: any) => err?.message)
|
||||||
|
?.message?.toString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Organization Name */}
|
|
||||||
{stage === stages[0] && (
|
|
||||||
<div className="relative">
|
|
||||||
<BriefcaseIcon className="absolute left-2 top-[0.6rem] w-[1.15rem] h-[1.15rem]" />
|
|
||||||
<Input
|
|
||||||
className="pl-8 rounded-lg"
|
|
||||||
placeholder="Organization Name"
|
|
||||||
{...register("organizationName")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Status Page Name */}
|
|
||||||
{stage === stages[1] && (
|
|
||||||
<div className="relative">
|
|
||||||
<ClipboardIcon className="absolute left-2 top-[0.6rem] w-[1.15rem] h-[1.15rem]" />
|
|
||||||
<Input
|
|
||||||
className="pl-8 rounded-lg"
|
|
||||||
placeholder="Status Page Name"
|
|
||||||
{...register("statusPageName")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Display the global error if it exists, otherwise show the first field error */}
|
|
||||||
<p className="text-red-500">
|
|
||||||
{error
|
|
||||||
? error
|
|
||||||
: Object.values(errors).find(
|
|
||||||
(err: any) => err?.message
|
|
||||||
) &&
|
|
||||||
Object.values(errors)
|
|
||||||
.find((err: any) => err?.message)
|
|
||||||
?.message?.toString()}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Back/Next Buttons */}
|
{/* Back/Next Buttons */}
|
||||||
<div className="mt-1.5 flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Button
|
<Button
|
||||||
className="bg-white"
|
className="text-white"
|
||||||
type="button"
|
type="button"
|
||||||
|
color={stage === stages[0] ? "secondary" : "primary"}
|
||||||
disabled={stage === stages[0]}
|
disabled={stage === stages[0]}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setStage(stages[stages.indexOf(stage) - 1])
|
setStage(stages[stages.indexOf(stage) - 1])
|
||||||
@ -168,7 +223,7 @@ const OnboardingForm = (): ReactElement => {
|
|||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-white" type="submit">
|
<Button className="text-white" type="submit">
|
||||||
{stage === stages[stages.length - 1]
|
{stage === stages[stages.length - 1]
|
||||||
? "Finish"
|
? "Finish"
|
||||||
: "Next"}
|
: "Next"}
|
||||||
@ -178,4 +233,12 @@ const OnboardingForm = (): ReactElement => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildOrgSlugPreview = (organizationName: string): string =>
|
||||||
|
organizationName
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
||||||
|
.replace(/[^a-z0-9-]/g, ""); // Remove special characters (keeping hyphens)
|
||||||
|
|
||||||
export default OnboardingForm;
|
export default OnboardingForm;
|
||||||
|
@ -4,6 +4,11 @@ import Link from "next/link";
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import Branding from "@/components/branding";
|
import Branding from "@/components/branding";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The greeting for the landing page.
|
||||||
|
*
|
||||||
|
* @return the greeting jsx
|
||||||
|
*/
|
||||||
const Greeting = (): ReactElement => (
|
const Greeting = (): ReactElement => (
|
||||||
<div className="h-screen flex flex-col gap-4 justify-center text-center items-center">
|
<div className="h-screen flex flex-col gap-4 justify-center text-center items-center">
|
||||||
<div className="flex flex-col gap-2 items-center select-none pointer-events-none">
|
<div className="flex flex-col gap-2 items-center select-none pointer-events-none">
|
||||||
@ -11,7 +16,7 @@ const Greeting = (): ReactElement => (
|
|||||||
<Branding className="animate-pulse" size="lg" />
|
<Branding className="animate-pulse" size="lg" />
|
||||||
|
|
||||||
{/* Greeting */}
|
{/* Greeting */}
|
||||||
<h1 className="text-3xl text-red-500 font-bold">Pulse App</h1>
|
<h1 className="text-3xl text-primary font-bold">Pulse App</h1>
|
||||||
<p className="max-w-[30rem] text-center opacity-75">
|
<p className="max-w-[30rem] text-center opacity-75">
|
||||||
A lightweight service monitoring solution for tracking the
|
A lightweight service monitoring solution for tracking the
|
||||||
availability of whatever service your heart desires!
|
availability of whatever service your heart desires!
|
||||||
@ -27,7 +32,7 @@ const Greeting = (): ReactElement => (
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/media/github.png"
|
src="/media/platforms/github.svg"
|
||||||
alt="GitHub Logo"
|
alt="GitHub Logo"
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
|
@ -10,7 +10,7 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
"bg-primary/75 text-primary-foreground shadow hover:bg-primary/60",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
|