First Commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async () => {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/auth/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import BlurredImageBG from "@/src/components/general/blured-image-bg";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function AuthPage() {
|
||||
return (
|
||||
<>
|
||||
<Section className="hero-section">
|
||||
<BlurredImageBG src="/images/children-school-1.webp" />
|
||||
<Container className="relative z-10">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<Span className="htag">Admin Section</Span>
|
||||
<H1 className="text-center font-bold">
|
||||
Redirecting you to login ...
|
||||
</H1>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Admin Login | ${SiteData["SiteName"]}`,
|
||||
description: `Admin section access. Redirecting to login.`,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { ApiReqParams, LoginFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function submitLoginForm({
|
||||
setLoading,
|
||||
setStatus,
|
||||
form,
|
||||
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/auth/get-login-code`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
login: {
|
||||
email_or_username: form.username_or_email,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.pathname = `/auth/sso`;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Login Failed",
|
||||
});
|
||||
console.log("res", res);
|
||||
console.log("form", form);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import type { ApiReqParams, GoogleWindow, LoginFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
|
||||
const GoogleScriptSelector =
|
||||
'script[src="https://accounts.google.com/gsi/client"]';
|
||||
|
||||
export default function useGoogleLoginInit({
|
||||
loading,
|
||||
setStatus,
|
||||
status,
|
||||
setLoading,
|
||||
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const googleButtonRef = useRef<HTMLDivElement>(null);
|
||||
const googleInitializedRef = useRef(false);
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||
const [buttonWidth, setButtonWidth] = useState(0);
|
||||
const [googleReady, setGoogleReady] = useState(false);
|
||||
|
||||
const googleClientId = pageProps?.envs?.GOOGLE_CLIENT_ID;
|
||||
|
||||
const submitGoogleLogin = async (google_token: string) => {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/auth/google-login`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
google_token,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.pathname = `/admin`;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Google Login Failed",
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!googleClientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientWindow = window as GoogleWindow;
|
||||
|
||||
if (clientWindow.google?.accounts?.id) {
|
||||
setScriptLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingScript = document.querySelector(
|
||||
GoogleScriptSelector,
|
||||
) as HTMLScriptElement | null;
|
||||
|
||||
const handleLoad = () => {
|
||||
setScriptLoaded(true);
|
||||
setStatus(undefined);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Couldn't load Google login.",
|
||||
});
|
||||
};
|
||||
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener("load", handleLoad);
|
||||
existingScript.addEventListener("error", handleError);
|
||||
|
||||
return () => {
|
||||
existingScript.removeEventListener("load", handleLoad);
|
||||
existingScript.removeEventListener("error", handleError);
|
||||
};
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://accounts.google.com/gsi/client";
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.addEventListener("load", handleLoad);
|
||||
script.addEventListener("error", handleError);
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
script.removeEventListener("load", handleLoad);
|
||||
script.removeEventListener("error", handleError);
|
||||
};
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!googleButtonRef.current || typeof ResizeObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
const nextWidth = Math.round(entries[0]?.contentRect.width || 0);
|
||||
|
||||
if (!nextWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
setButtonWidth((prev) => (prev === nextWidth ? prev : nextWidth));
|
||||
});
|
||||
|
||||
resizeObserver.observe(googleButtonRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!googleClientId ||
|
||||
!scriptLoaded ||
|
||||
!googleButtonRef.current ||
|
||||
!buttonWidth
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const googleId = (window as GoogleWindow).google?.accounts?.id;
|
||||
|
||||
if (!googleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!googleInitializedRef.current) {
|
||||
googleId.initialize({
|
||||
client_id: googleClientId,
|
||||
callback: async ({ credential }) => {
|
||||
if (!credential) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Google login did not return a token.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await submitGoogleLogin(credential);
|
||||
},
|
||||
});
|
||||
|
||||
googleInitializedRef.current = true;
|
||||
}
|
||||
|
||||
googleButtonRef.current.innerHTML = "";
|
||||
googleId.renderButton(googleButtonRef.current, {
|
||||
theme: "outline",
|
||||
size: "large",
|
||||
text: "signin_with",
|
||||
shape: "rectangular",
|
||||
width: Math.min(buttonWidth, 400),
|
||||
});
|
||||
setGoogleReady(true);
|
||||
}, [buttonWidth, scriptLoaded, status, submitGoogleLogin]);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!googleClientId) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Google login is not configured yet.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!googleInitializedRef.current) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Google login is still loading. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
handleButtonClick,
|
||||
googleClientId,
|
||||
googleButtonRef,
|
||||
googleReady,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import Img from "@/src/components/twui/layout/Img";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { LoginFormObject } from "@/src/types";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import useGoogleLoginInit from "../../(hooks)/use-google-login-init";
|
||||
|
||||
export default function GoogleLogin(
|
||||
init: ReturnType<typeof useFormInit<LoginFormObject>>,
|
||||
) {
|
||||
const {
|
||||
loading,
|
||||
handleButtonClick,
|
||||
googleClientId,
|
||||
googleButtonRef,
|
||||
googleReady,
|
||||
} = useGoogleLoginInit(init);
|
||||
|
||||
return (
|
||||
<Row className="w-full relative justify-center">
|
||||
<Button
|
||||
title="Login with Google"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className="w-full py-3 relative"
|
||||
loading={loading}
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<Img
|
||||
alt="Google Logo Icon"
|
||||
src={`/icons/google.png`}
|
||||
size={17}
|
||||
/>
|
||||
<Row>
|
||||
<span>Login with Google</span>
|
||||
</Row>
|
||||
</Button>
|
||||
{googleClientId ? (
|
||||
<div
|
||||
ref={googleButtonRef}
|
||||
aria-hidden
|
||||
className={twMerge(
|
||||
"absolute inset-0 z-10 opacity-0",
|
||||
googleReady && !loading
|
||||
? "pointer-events-auto"
|
||||
: "pointer-events-none",
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Form from "@/src/components/twui/form/Form";
|
||||
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Divider from "@/src/components/twui/layout/Divider";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type LoginFormObject } from "@/src/types";
|
||||
import LoginShell from "@/src/components/general/login-shell";
|
||||
import GoogleLogin from "./google-login";
|
||||
import LoginFormEmailUsername from "./login-form-email-username";
|
||||
import submitLoginForm from "../../(functions)/submit-login-form";
|
||||
import LoginFormAction from "./login-form-action";
|
||||
|
||||
export default function LoginForm() {
|
||||
const init = useFormInit<LoginFormObject>();
|
||||
const { loading, status } = init;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GoogleLogin {...init} />
|
||||
<Row className="w-full flex-nowrap gap-4 -my-2">
|
||||
<Divider />
|
||||
<Span className="text-sm font-semibold opacity-50">OR</Span>
|
||||
<Divider />
|
||||
</Row>
|
||||
<Form
|
||||
className="w-full flex items-center justify-center relative gap-4"
|
||||
onSubmit={() => {
|
||||
submitLoginForm(init);
|
||||
}}
|
||||
>
|
||||
{loading && <LoadingOverlay />}
|
||||
<LoginFormEmailUsername {...init} />
|
||||
<LoginFormAction {...init} />
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type LoginFormObject } from "@/src/types";
|
||||
|
||||
export default function LoginFormAction({}: ReturnType<
|
||||
typeof useFormInit<LoginFormObject>
|
||||
>) {
|
||||
return (
|
||||
<>
|
||||
<Button title="Submit Login Form" type="submit" className="w-full">
|
||||
Get Login Code
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type LoginFormObject } from "@/src/types";
|
||||
|
||||
export default function LoginFormEmailUsername({
|
||||
setForm,
|
||||
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
||||
return (
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Email or Username"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
username_or_email: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import LoginShell from "@/src/components/general/login-shell";
|
||||
|
||||
export default function Hero() {
|
||||
// return <LoginShell></LoginShell>;
|
||||
return (
|
||||
<Section className="">
|
||||
<Container className="relative z-10">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<Span className="htag">Welcome Back Admin</Span>
|
||||
<H1 className="text-center font-bold">
|
||||
Login to your account
|
||||
</H1>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import LoginForm from "../(partials)/login-form";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import ArrowedLink from "@/src/components/twui/layout/ArrowedLink";
|
||||
|
||||
export default function LoginFormSection() {
|
||||
return (
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<LoginForm />
|
||||
<Stack className="gap-2 items-center">
|
||||
<Span variant="faded">Received an SSO code already?</Span>
|
||||
<ArrowedLink
|
||||
link={{
|
||||
title: "Complete Login",
|
||||
url: "/auth/sso",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
const users_res = await grabUsers();
|
||||
|
||||
if (!users_res.singleRes?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/signup`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import Hero from "./(sections)/hero";
|
||||
import LoginFormSection from "./(sections)/login-form-section";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<>
|
||||
{/* <Hero /> */}
|
||||
<LoginFormSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { deleteCookies } from "@/src/utils/cookies";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async ({ req }) => {
|
||||
return {
|
||||
res_transform(res) {
|
||||
const new_res = res.clone();
|
||||
deleteCookies(new_res, [
|
||||
{
|
||||
name: AppData["AuthKeyCookieName"],
|
||||
httpOnly: true,
|
||||
},
|
||||
{
|
||||
name: AppData["AuthCSRFCookieName"],
|
||||
httpOnly: true,
|
||||
},
|
||||
{
|
||||
name: AppData["SSOAuthCookieName"],
|
||||
httpOnly: true,
|
||||
},
|
||||
]);
|
||||
return new_res;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Loading from "@/src/components/twui/elements/Loading";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import { useEffect } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Logo from "@/src/components/general/logo";
|
||||
import Paper from "@/src/components/twui/elements/Paper";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Center from "@/src/components/twui/layout/Center";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function LogoutPage() {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
window.location.pathname = "/";
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Section
|
||||
className={twMerge(
|
||||
"h-screen w-screen px-0 items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<Center>
|
||||
<Paper className="w-auto p-10 items-center">
|
||||
<Logo
|
||||
text_props={{
|
||||
className: "text-dark",
|
||||
}}
|
||||
/>
|
||||
<Row>
|
||||
<Loading />
|
||||
<Span>Logging out ...</Span>
|
||||
</Row>
|
||||
</Paper>
|
||||
</Center>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Logging Out | ${SiteData["SiteName"]}`,
|
||||
description: `You are being logged out and redirected to the homepage.`,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (ctx) => {
|
||||
if (ctx.props.user?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/admin`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const users_res = await grabUsers();
|
||||
|
||||
if (users_res.singleRes?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/login`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import UserForm from "@/src/components/general/user-form";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
|
||||
export default function LogoutPage() {
|
||||
return (
|
||||
<Stack className="items-center gap-10">
|
||||
<H1 className="text-center text-xl!">Create Admin User</H1>
|
||||
<UserForm is_first_user />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Create Super Admin Account | ${SiteData["SiteName"]}`,
|
||||
description: `Create Super Admin Account`,
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import Form from "@/src/components/twui/form/Form";
|
||||
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import { type ApiReqParams, type SSOFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import LoginShell from "@/src/components/general/login-shell";
|
||||
|
||||
export default function SSOForm() {
|
||||
const init = useFormInit<SSOFormObject>();
|
||||
const { form, loading, setForm, setLoading, status, setStatus } = init;
|
||||
|
||||
const submitSSOLoginForm = async () => {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/auth/login`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
sso_code: form.code,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.pathname = `/admin`;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Login Failed",
|
||||
});
|
||||
console.log("res", res);
|
||||
console.log("form", form);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoginShell status={status}>
|
||||
<Form
|
||||
className="w-full flex items-center justify-center relative gap-4"
|
||||
onSubmit={submitSSOLoginForm}
|
||||
>
|
||||
{loading && <LoadingOverlay />}
|
||||
|
||||
<Input
|
||||
placeholder="SSO Code"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
code: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
title="Submit Login Form"
|
||||
type="submit"
|
||||
className="w-full"
|
||||
>
|
||||
Complete Login
|
||||
</Button>
|
||||
</Form>
|
||||
</LoginShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<Section className="hero-section">
|
||||
<Container className="relative z-10">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<Span className="htag">SSO Login</Span>
|
||||
<H1 className="text-center font-bold">
|
||||
Complete your login
|
||||
</H1>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import SSOForm from "../(partials)/sso-form";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import ArrowedLink from "@/src/components/twui/layout/ArrowedLink";
|
||||
|
||||
export default function SSOFormSection() {
|
||||
return (
|
||||
<Section className="py-0!">
|
||||
<Container className="relative z-10 py-20 -mt-28">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<SSOForm />
|
||||
<Stack className="gap-2 items-center">
|
||||
<Span variant="faded">
|
||||
Login Failed or Code Expired?
|
||||
</Span>
|
||||
<ArrowedLink
|
||||
link={{
|
||||
title: "Login Again",
|
||||
url: "/auth/login",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Hero from "./(sections)/hero";
|
||||
import SSOFormSection from "./(sections)/sso-form-section";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function SSOPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<SSOFormSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Sign In | ${SiteData["SiteName"]}`,
|
||||
description: `Sign in to your account to access the admin dashboard.`,
|
||||
};
|
||||
Reference in New Issue
Block a user