mirror of
https://github.com/Xevion/linkpulse.git
synced 2025-12-10 02:07:41 -06:00
Add /register route, split up authentication page
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -37,7 +37,8 @@
|
|||||||
"timestamper",
|
"timestamper",
|
||||||
"tseslint",
|
"tseslint",
|
||||||
"vitest",
|
"vitest",
|
||||||
"xdist"
|
"xdist",
|
||||||
|
"Zustand's"
|
||||||
],
|
],
|
||||||
"python.analysis.extraPaths": ["./backend/"],
|
"python.analysis.extraPaths": ["./backend/"],
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Icons } from "@/components/icons";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { HTMLAttributes, useState } from "react";
|
|
||||||
|
|
||||||
interface UserAuthFormProps extends HTMLAttributes<HTMLDivElement> {}
|
|
||||||
|
|
||||||
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
async function onSubmit(event: React.SyntheticEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("grid gap-6", className)} {...props}>
|
|
||||||
<form onSubmit={onSubmit}>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="sr-only" htmlFor="email">
|
|
||||||
Email
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
placeholder="name@example.com"
|
|
||||||
type="email"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="email"
|
|
||||||
autoCorrect="off"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button disabled={isLoading}>
|
|
||||||
{isLoading && (
|
|
||||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
)}
|
|
||||||
Sign In with Email
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<span className="w-full border-t" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
|
||||||
<span className="bg-background px-2 text-muted-foreground">
|
|
||||||
Or continue with
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" type="button" disabled={isLoading}>
|
|
||||||
{isLoading ? (
|
|
||||||
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Icons.gitHub className="mr-2 h-4 w-4" />
|
|
||||||
)}{" "}
|
|
||||||
GitHub
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
108
frontend/src/components/auth/form.tsx
Normal file
108
frontend/src/components/auth/form.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Icons } from "@/components/icons";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { HTMLAttributes, useState } from "react";
|
||||||
|
|
||||||
|
interface UserAuthFormProps extends HTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
|
export function RegisterForm({ className, ...props }: UserAuthFormProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
async function onSubmit(event: React.SyntheticEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("grid gap-6", className)} {...props}>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="sr-only" htmlFor="email">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
type="email"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="email"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button disabled={isLoading}>
|
||||||
|
{isLoading && (
|
||||||
|
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
Sign In with Email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginForm({ className, ...props }: UserAuthFormProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
async function onSubmit(event: React.SyntheticEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("grid gap-6", className)} {...props}>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="sr-only" htmlFor="email">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
type="email"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="email"
|
||||||
|
autoCorrect="off"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button disabled={isLoading}>
|
||||||
|
{isLoading && (
|
||||||
|
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
|
<Link
|
||||||
|
href="/terms"
|
||||||
|
className="underline underline-offset-4 hover:text-primary"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Link>{" "}
|
||||||
|
or{" "}
|
||||||
|
<Link
|
||||||
|
href="/forgot-password"
|
||||||
|
className="underline underline-offset-4 hover:text-primary"
|
||||||
|
>
|
||||||
|
Forgot Password
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
frontend/src/components/pages/authentication.tsx
Normal file
67
frontend/src/components/pages/authentication.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { Icons } from "@/components/icons";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export function AgreementText() {
|
||||||
|
return (
|
||||||
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
|
By clicking continue, you agree to our{" "}
|
||||||
|
<Link
|
||||||
|
href="/terms"
|
||||||
|
className="underline underline-offset-4 hover:text-primary"
|
||||||
|
>
|
||||||
|
Terms of Service
|
||||||
|
</Link>{" "}
|
||||||
|
and{" "}
|
||||||
|
<Link
|
||||||
|
href="/privacy"
|
||||||
|
className="underline underline-offset-4 hover:text-primary"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AuthenticationPage({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="container relative h-[100vh] flex-col items-center grid w-screen lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"absolute right-4 top-4 md:right-8 md:top-8",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Linkpulse
|
||||||
|
</Link>
|
||||||
|
<div className="relative hidden h-full flex-col grow bg-muted p-10 text-white dark:border-r lg:flex">
|
||||||
|
<div className="absolute inset-0 bg-zinc-900" />
|
||||||
|
<div className="relative z-20 flex items-center text-lg font-medium">
|
||||||
|
<Icons.linkpulse className="mr-2 h-6 w-6 text-white" />
|
||||||
|
Linkpulse
|
||||||
|
</div>
|
||||||
|
<div className="z-20 mt-auto space-y-2">
|
||||||
|
{/* <blockquote className="space-y-2">
|
||||||
|
<p className="text-lg">
|
||||||
|
“This library has saved me countless hours of work and
|
||||||
|
helped me deliver stunning designs to my clients faster than
|
||||||
|
ever before.”
|
||||||
|
</p>
|
||||||
|
<footer className="text-sm">Sofia Davis</footer>
|
||||||
|
</blockquote> */}
|
||||||
|
{/* <p className="text-lg"></p>
|
||||||
|
<footer className="text-sm"></footer> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-6">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
|
import { Route as RegisterImport } from './routes/register'
|
||||||
import { Route as LoginImport } from './routes/login'
|
import { Route as LoginImport } from './routes/login'
|
||||||
import { Route as DashboardImport } from './routes/dashboard'
|
import { Route as DashboardImport } from './routes/dashboard'
|
||||||
|
|
||||||
@@ -22,6 +23,12 @@ const IndexLazyImport = createFileRoute('/')()
|
|||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const RegisterRoute = RegisterImport.update({
|
||||||
|
id: '/register',
|
||||||
|
path: '/register',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const LoginRoute = LoginImport.update({
|
const LoginRoute = LoginImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@@ -65,6 +72,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof LoginImport
|
preLoaderRoute: typeof LoginImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/register': {
|
||||||
|
id: '/register'
|
||||||
|
path: '/register'
|
||||||
|
fullPath: '/register'
|
||||||
|
preLoaderRoute: typeof RegisterImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,12 +88,14 @@ export interface FileRoutesByFullPath {
|
|||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/register': typeof RegisterRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/register': typeof RegisterRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -87,14 +103,15 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/register': typeof RegisterRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/dashboard' | '/login'
|
fullPaths: '/' | '/dashboard' | '/login' | '/register'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/dashboard' | '/login'
|
to: '/' | '/dashboard' | '/login' | '/register'
|
||||||
id: '__root__' | '/' | '/dashboard' | '/login'
|
id: '__root__' | '/' | '/dashboard' | '/login' | '/register'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,12 +119,14 @@ export interface RootRouteChildren {
|
|||||||
IndexLazyRoute: typeof IndexLazyRoute
|
IndexLazyRoute: typeof IndexLazyRoute
|
||||||
DashboardRoute: typeof DashboardRoute
|
DashboardRoute: typeof DashboardRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
|
RegisterRoute: typeof RegisterRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexLazyRoute: IndexLazyRoute,
|
IndexLazyRoute: IndexLazyRoute,
|
||||||
DashboardRoute: DashboardRoute,
|
DashboardRoute: DashboardRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
|
RegisterRoute: RegisterRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@@ -122,7 +141,8 @@ export const routeTree = rootRoute
|
|||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/dashboard",
|
"/dashboard",
|
||||||
"/login"
|
"/login",
|
||||||
|
"/register"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@@ -133,6 +153,9 @@ export const routeTree = rootRoute
|
|||||||
},
|
},
|
||||||
"/login": {
|
"/login": {
|
||||||
"filePath": "login.tsx"
|
"filePath": "login.tsx"
|
||||||
|
},
|
||||||
|
"/register": {
|
||||||
|
"filePath": "register.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
||||||
import { useUserStore } from "@/lib/state";
|
import { useUserStore } from "@/lib/state";
|
||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { LoginForm } from "@/components/auth/form";
|
||||||
import { cn } from "@/lib/utils";
|
import AuthenticationPage from "@/components/pages/authentication";
|
||||||
import { UserAuthForm } from "@/components/auth/UserAuthForm";
|
|
||||||
import { Icons } from "@/components/icons";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/login")({
|
export const Route = createFileRoute("/login")({
|
||||||
beforeLoad: async ({ location }) => {
|
beforeLoad: async ({ location }) => {
|
||||||
@@ -22,67 +20,16 @@ export const Route = createFileRoute("/login")({
|
|||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
return (
|
return (
|
||||||
<>
|
<AuthenticationPage>
|
||||||
<div className="container relative h-[100vh] flex-col items-center grid w-screen lg:max-w-none lg:grid-cols-2 lg:px-0">
|
<div className="mx-auto flex w-full flex-col space-y-6 sm:w-[350px]">
|
||||||
<a
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
href="/"
|
<h1 className="text-2xl font-semibold tracking-tight">Login</h1>
|
||||||
className={cn(
|
<p className="text-sm text-muted-foreground">
|
||||||
buttonVariants({ variant: "ghost" }),
|
Enter your email below to login
|
||||||
"absolute right-4 top-4 md:right-8 md:top-8",
|
</p>
|
||||||
)}
|
|
||||||
>
|
|
||||||
Linkpulse
|
|
||||||
</a>
|
|
||||||
<div className="relative hidden h-full flex-col grow bg-muted p-10 text-white dark:border-r lg:flex">
|
|
||||||
<div className="absolute inset-0 bg-zinc-900" />
|
|
||||||
<div className="relative z-20 flex items-center text-lg font-medium">
|
|
||||||
<Icons.linkpulse className="mr-2 h-6 w-6 text-white" />
|
|
||||||
Linkpulse
|
|
||||||
</div>
|
|
||||||
<div className="z-20 mt-auto space-y-2">
|
|
||||||
{/* <blockquote className="space-y-2">
|
|
||||||
<p className="text-lg">
|
|
||||||
“This library has saved me countless hours of work and
|
|
||||||
helped me deliver stunning designs to my clients faster than
|
|
||||||
ever before.”
|
|
||||||
</p>
|
|
||||||
<footer className="text-sm">Sofia Davis</footer>
|
|
||||||
</blockquote> */}
|
|
||||||
{/* <p className="text-lg"></p>
|
|
||||||
<footer className="text-sm"></footer> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-6">
|
|
||||||
<div className="mx-auto flex w-full flex-col space-y-6 sm:w-[350px]">
|
|
||||||
<div className="flex flex-col space-y-2 text-center">
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
|
||||||
Create an account
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Enter your email below to create your account
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<UserAuthForm />
|
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
|
||||||
By clicking continue, you agree to our{" "}
|
|
||||||
<a
|
|
||||||
href="/terms"
|
|
||||||
className="underline underline-offset-4 hover:text-primary"
|
|
||||||
>
|
|
||||||
Terms of Service
|
|
||||||
</a>{" "}
|
|
||||||
and{" "}
|
|
||||||
<a
|
|
||||||
href="/privacy"
|
|
||||||
className="underline underline-offset-4 hover:text-primary"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</AuthenticationPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
40
frontend/src/routes/register.tsx
Normal file
40
frontend/src/routes/register.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useUserStore } from "@/lib/state";
|
||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { RegisterForm } from "@/components/auth/form";
|
||||||
|
import AuthenticationPage, {
|
||||||
|
AgreementText,
|
||||||
|
} from "@/components/pages/authentication";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/register")({
|
||||||
|
beforeLoad: async ({ location }) => {
|
||||||
|
const isLoggedIn = useUserStore.getState().user !== null;
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return redirect({
|
||||||
|
to: "/dashboard",
|
||||||
|
search: { redirect: location.href },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: Register,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Register() {
|
||||||
|
return (
|
||||||
|
<AuthenticationPage>
|
||||||
|
<div className="mx-auto flex w-full flex-col space-y-6 sm:w-[350px]">
|
||||||
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Create an account
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Enter your email below to create your account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<RegisterForm />
|
||||||
|
<AgreementText />
|
||||||
|
</div>
|
||||||
|
</AuthenticationPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user