updates
All checks were successful
Deploy / deploy (ubuntu-latest, 2.44.0) (push) Successful in 1m39s
@ -31,7 +31,6 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-social-icons": "^6.18.0",
|
||||
"react-turnstile": "^1.1.3",
|
||||
"sharp": "^0.33.5",
|
||||
"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 { motion } from "framer-motion";
|
||||
import Greeting from "@/components/auth/greeting";
|
||||
import Footer from "@/components/auth/footer";
|
||||
|
||||
/**
|
||||
* The page to authenticate with.
|
||||
@ -29,7 +28,6 @@ const AuthPage = (): ReactElement => (
|
||||
<Separator className="w-28" />
|
||||
</div>
|
||||
<AuthForm />
|
||||
<Footer />
|
||||
</motion.div>
|
||||
</main>
|
||||
);
|
||||
@ -40,7 +38,7 @@ const AuthPage = (): ReactElement => (
|
||||
* @return the providers jsx
|
||||
*/
|
||||
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="Google" link="#" />
|
||||
</div>
|
||||
|
@ -14,7 +14,10 @@ const inter: NextFont = Inter({ subsets: ["latin"] });
|
||||
* The metadata for this app.
|
||||
*/
|
||||
export const metadata: Metadata = {
|
||||
title: "Pulse App",
|
||||
title: {
|
||||
default: "Pulse App",
|
||||
template: "%s • Pulse App",
|
||||
},
|
||||
description:
|
||||
"A lightweight service monitoring solution for tracking the availability of whatever service your heart desires!",
|
||||
openGraph: {
|
||||
@ -31,7 +34,7 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#DC2626",
|
||||
themeColor: "#A855F7",
|
||||
};
|
||||
|
||||
/**
|
||||
@ -48,7 +51,7 @@ const RootLayout = ({
|
||||
<div
|
||||
style={{
|
||||
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>
|
||||
|
@ -74,7 +74,7 @@ const UserProvider = ({ children }: { children: ReactNode }) => {
|
||||
) {
|
||||
router.push("/dashboard/onboarding");
|
||||
}
|
||||
}, [cookies, router]);
|
||||
}, [cookies, router, path]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser();
|
||||
|
@ -50,9 +50,9 @@ body {
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary: 271 91% 65%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary: 272 72% 47%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.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,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { apiRequest } from "@/lib/api";
|
||||
import { Session } from "@/app/types/user/session";
|
||||
import { Cookies, useCookies } from "next-client-cookies";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
import { motion } from "framer-motion";
|
||||
import Turnstile, { useTurnstile } from "react-turnstile";
|
||||
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.
|
||||
@ -28,24 +31,31 @@ import { TurnstileObject } from "turnstile-types";
|
||||
const buildEmailInput = (allowEmpty: boolean) =>
|
||||
z
|
||||
.string()
|
||||
.email("Invalid email address")
|
||||
.email("That email address is invalid.")
|
||||
.refine(
|
||||
(val) => {
|
||||
return !allowEmpty || val.length > 0;
|
||||
},
|
||||
{ message: "Email is required" }
|
||||
{ message: "An email address is required." }
|
||||
);
|
||||
|
||||
const EmailSchema = z.object({
|
||||
email: buildEmailInput(false),
|
||||
});
|
||||
|
||||
const RegisterSchema = z.object({
|
||||
email: buildEmailInput(true),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
passwordConfirmation: z.string(),
|
||||
});
|
||||
const RegisterSchema = z
|
||||
.object({
|
||||
email: buildEmailInput(true),
|
||||
username: z
|
||||
.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({
|
||||
email: buildEmailInput(true),
|
||||
@ -108,7 +118,7 @@ const AuthForm = (): ReactElement => {
|
||||
setStage(data?.exists ? "login" : "register");
|
||||
} else {
|
||||
const registering: boolean = stage === "register";
|
||||
const { data, error } = await apiRequest<Session>({
|
||||
const { data, error } = await apiRequest<UserAuthResponse>({
|
||||
endpoint: `/auth/${registering ? "register" : "login"}`,
|
||||
method: "POST",
|
||||
body: registering
|
||||
@ -132,13 +142,18 @@ const AuthForm = (): ReactElement => {
|
||||
turnstile.reset();
|
||||
} else {
|
||||
// Otherwise store the session and redirect to the dashboard
|
||||
cookies.set("session", JSON.stringify(data), {
|
||||
cookies.set("session", JSON.stringify(data?.session), {
|
||||
expires:
|
||||
((data?.expires as number) - Date.now()) / 86_400_000,
|
||||
((data?.session.expires as number) - Date.now()) /
|
||||
86_400_000,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
});
|
||||
router.push("/dashboard");
|
||||
router.push(
|
||||
hasFlag(data?.user as User, UserFlag.COMPLETED_ONBOARDING)
|
||||
? "/dashboard"
|
||||
: "/dashboard/onboarding"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -238,7 +253,7 @@ const AuthForm = (): ReactElement => {
|
||||
|
||||
{/* Submit Form */}
|
||||
<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"
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import { SocialIcon } from "react-social-icons";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
/**
|
||||
* The props for this oauth provider.
|
||||
@ -26,14 +26,14 @@ type OAuthProviderProps = {
|
||||
const OAuthProvider = ({ name, link }: OAuthProviderProps): ReactElement => (
|
||||
<Link className="cursor-not-allowed" href={link}>
|
||||
<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
|
||||
>
|
||||
<SocialIcon
|
||||
className="w-10 h-10"
|
||||
as="div"
|
||||
bgColor="transparent"
|
||||
network={name.toLowerCase()}
|
||||
<Image
|
||||
src={`/media/platforms/${name.toLowerCase()}.svg`}
|
||||
alt={`${name}'s Logo`}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{name}</span>
|
||||
</Button>
|
||||
|
@ -1,7 +1,11 @@
|
||||
"use client";
|
||||
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -21,21 +25,24 @@ import { User } from "@/app/types/user/user";
|
||||
* Define the various stages of onboarding.
|
||||
*/
|
||||
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[] = [
|
||||
{
|
||||
name: "Onboarding",
|
||||
name: "Create a new organization",
|
||||
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({
|
||||
organizationName,
|
||||
organizationSlug,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Status Page",
|
||||
name: "Create a new status page",
|
||||
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({
|
||||
organizationName,
|
||||
organizationSlug,
|
||||
statusPageName: z
|
||||
.string()
|
||||
.min(2, "You need a longer status page name!!!"),
|
||||
@ -64,15 +71,28 @@ const OnboardingForm = (): ReactElement => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
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.
|
||||
*/
|
||||
const onSubmit = async ({ organizationName, statusPageName }: any) => {
|
||||
const onSubmit = async ({
|
||||
organizationName,
|
||||
organizationSlug,
|
||||
statusPageName,
|
||||
}: any) => {
|
||||
// Completed onboarding
|
||||
if (stage === stages[stages.length - 1]) {
|
||||
const { data, error } = await apiRequest<void>({
|
||||
@ -81,6 +101,7 @@ const OnboardingForm = (): ReactElement => {
|
||||
session,
|
||||
body: {
|
||||
organizationName,
|
||||
organizationSlug,
|
||||
statusPageName,
|
||||
},
|
||||
});
|
||||
@ -103,64 +124,98 @@ const OnboardingForm = (): ReactElement => {
|
||||
return (
|
||||
<motion.div
|
||||
key={stage.name}
|
||||
className="flex flex-col gap-3"
|
||||
className="w-96 flex flex-col gap-2.5"
|
||||
initial={{ x: -50, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
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
|
||||
className="flex flex-col gap-2"
|
||||
className="flex flex-col gap-0.5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-1 select-none pointer-events-none">
|
||||
<h1 className="text-3xl font-bold">{stage.name}</h1>
|
||||
<p className="max-w-[20rem] opacity-65">
|
||||
{stage.description}
|
||||
<div className="my-3 p-6 pb-3.5 flex flex-col gap-3.5 justify-center bg-zinc-900 rounded-lg">
|
||||
{/* Organization Name */}
|
||||
{stage === stages[0] && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<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>
|
||||
</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 */}
|
||||
<div className="mt-1.5 flex justify-between">
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
className="bg-white"
|
||||
className="text-white"
|
||||
type="button"
|
||||
color={stage === stages[0] ? "secondary" : "primary"}
|
||||
disabled={stage === stages[0]}
|
||||
onClick={() =>
|
||||
setStage(stages[stages.indexOf(stage) - 1])
|
||||
@ -168,7 +223,7 @@ const OnboardingForm = (): ReactElement => {
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button className="bg-white" type="submit">
|
||||
<Button className="text-white" type="submit">
|
||||
{stage === stages[stages.length - 1]
|
||||
? "Finish"
|
||||
: "Next"}
|
||||
@ -178,4 +233,12 @@ const OnboardingForm = (): ReactElement => {
|
||||
</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;
|
||||
|
@ -4,6 +4,11 @@ import Link from "next/link";
|
||||
import { ReactElement } from "react";
|
||||
import Branding from "@/components/branding";
|
||||
|
||||
/**
|
||||
* The greeting for the landing page.
|
||||
*
|
||||
* @return the greeting jsx
|
||||
*/
|
||||
const Greeting = (): ReactElement => (
|
||||
<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">
|
||||
@ -11,7 +16,7 @@ const Greeting = (): ReactElement => (
|
||||
<Branding className="animate-pulse" size="lg" />
|
||||
|
||||
{/* 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">
|
||||
A lightweight service monitoring solution for tracking the
|
||||
availability of whatever service your heart desires!
|
||||
@ -27,7 +32,7 @@ const Greeting = (): ReactElement => (
|
||||
variant="ghost"
|
||||
>
|
||||
<Image
|
||||
src="/media/github.png"
|
||||
src="/media/platforms/github.svg"
|
||||
alt="GitHub Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
|
@ -10,7 +10,7 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
"bg-primary/75 text-primary-foreground shadow hover:bg-primary/60",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
|