@@ -8,7 +8,10 @@ import { BrowserRouter, Route, Routes, useLocation } from "react-router";
|
||||
import "App.css";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import ConsoleLayout from "./layouts/ConsoleLayout";
|
||||
import AuthLayout from "./layouts/AuthLayout";
|
||||
import { RelayEnvironment } from "./RelayEnvironment";
|
||||
import { AuthProvider } from "./contexts/AuthContext";
|
||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||
|
||||
posthog.init(process.env.POSTHOG_KEY!, {
|
||||
api_host: process.env.POSTHOG_HOST,
|
||||
@@ -29,11 +32,12 @@ const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage"));
|
||||
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
||||
const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage"));
|
||||
const FrameworkOverviewPage = lazy(
|
||||
() => import("./pages/FrameworkOverviewPage"),
|
||||
() => import("./pages/FrameworkOverviewPage")
|
||||
);
|
||||
const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage"));
|
||||
const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage"));
|
||||
|
||||
const LoginPage = lazy(() => import("./pages/LoginPage"));
|
||||
const RegisterPage = lazy(() => import("./pages/RegisterPage"));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -43,125 +47,171 @@ function App() {
|
||||
<RelayEnvironmentProvider environment={RelayEnvironment}>
|
||||
<HelmetProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ErrorBoundaryWithLocation>
|
||||
<ConsoleLayout />
|
||||
</ErrorBoundaryWithLocation>
|
||||
}
|
||||
>
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
{/* Authentication Routes - Accessible without login */}
|
||||
<Route
|
||||
index
|
||||
path="/login"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<AuthLayout />
|
||||
</ErrorBoundaryWithLocation>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<LoginPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="/register"
|
||||
element={
|
||||
<ErrorBoundaryWithLocation>
|
||||
<AuthLayout />
|
||||
</ErrorBoundaryWithLocation>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<RegisterPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
{/* Protected Routes - Require authentication */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ErrorBoundaryWithLocation>
|
||||
<HomePage />
|
||||
<ConsoleLayout />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="peoples"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<PeopleListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="peoples/create"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<CreatePeoplePage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="peoples/:peopleId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<PeopleOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="vendors"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<VendorListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks/:frameworkId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks/:frameworkId/controls/:controlId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<ControlOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="vendors/:vendorId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<VendorOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<SettingsPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Suspense>
|
||||
<NotFoundPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<HomePage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="peoples"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<PeopleListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="peoples/create"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<CreatePeoplePage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="peoples/:peopleId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<PeopleOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="vendors"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<VendorListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkListPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks/:frameworkId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<FrameworkOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="frameworks/:frameworkId/controls/:controlId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<ControlOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="vendors/:vendorId"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<VendorOverviewPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
<Suspense>
|
||||
<ErrorBoundaryWithLocation>
|
||||
<SettingsPage />
|
||||
</ErrorBoundaryWithLocation>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Suspense>
|
||||
<NotFoundPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</HelmetProvider>
|
||||
</RelayEnvironmentProvider>
|
||||
|
||||
20
apps/console/src/components/ProtectedRoute.tsx
Normal file
20
apps/console/src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Navigate, Outlet, useLocation } from "react-router";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
||||
export function ProtectedRoute() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
// Show nothing while checking authentication
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace state={{ from: location }} />;
|
||||
}
|
||||
|
||||
// Render the protected content
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<578d66fa0d0065452d9fa64611b7cfda>>
|
||||
* @generated SignedSource<<717a5c6c939c65b7e0790f84e2c41bcd>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -72,7 +72,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
@@ -109,7 +109,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
|
||||
78
apps/console/src/contexts/AuthContext.tsx
Normal file
78
apps/console/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { buildEndpoint } from "@/utils";
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
checkAuth: () => Promise<boolean>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const checkAuth = async (): Promise<boolean> => {
|
||||
try {
|
||||
// Make a request to an endpoint that requires authentication
|
||||
const response = await fetch(buildEndpoint("/console/v1/query"), {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operationName: "CheckAuth",
|
||||
query: `query CheckAuth { viewer { id } }`,
|
||||
variables: {},
|
||||
}),
|
||||
});
|
||||
|
||||
const authenticated =
|
||||
response.ok && !response.headers.get("WWW-Authenticate");
|
||||
setIsAuthenticated(authenticated);
|
||||
return authenticated;
|
||||
} catch (error) {
|
||||
setIsAuthenticated(false);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async (): Promise<void> => {
|
||||
try {
|
||||
await fetch(buildEndpoint("/console/v1/auth/logout"), {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
} finally {
|
||||
setIsAuthenticated(false);
|
||||
navigate("/login");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ isAuthenticated, isLoading, checkAuth, logout }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
40
apps/console/src/layouts/AuthLayout.tsx
Normal file
40
apps/console/src/layouts/AuthLayout.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Outlet } from "react-router";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
export default function AuthLayout() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<div className="flex flex-1">
|
||||
<div className="flex flex-1 flex-col bg-muted/40">
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<main className="w-full max-w-md p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden flex-1 bg-gradient-to-br from-gray-900 to-gray-950 lg:block">
|
||||
<div className="flex h-full items-center justify-center p-8">
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute right-0 top-1/4 z-10">
|
||||
<img
|
||||
src="/assets/android-chrome-512x512.png"
|
||||
alt="Probo Mascot"
|
||||
className="h-auto w-96"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute left-8 top-1/3 z-0 text-white">
|
||||
<h1 className="text-4xl font-bold leading-tight">
|
||||
Navigate compliance with
|
||||
<br />
|
||||
confidence thanks to{" "}
|
||||
<span className="text-lime-400">probo</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
apps/console/src/pages/LoginPage.tsx
Normal file
153
apps/console/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { buildEndpoint } from "@/utils";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { checkAuth } = useAuth();
|
||||
|
||||
// Get the redirect path from location state or default to home
|
||||
const from = location.state?.from?.pathname || "/";
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email || !password) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Email and password are required",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(buildEndpoint("/console/v1/auth/login"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Login failed");
|
||||
}
|
||||
|
||||
// Check authentication status after login
|
||||
await checkAuth();
|
||||
|
||||
// Redirect to the original page or home after successful login
|
||||
navigate(from, { replace: true });
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : "Login failed",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Log in - Probo</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">Log in</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Enter your credentials to access your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Logging in..." : "Log in with email"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Don't have an account?{" "}
|
||||
<Link to="/register" className="underline">
|
||||
Sign up here
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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" className="w-full" disabled={isLoading}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-4 w-4"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M17.13 17.13v-4.26l-3.2 3.2a4.33 4.33 0 0 1-6.13-6.13l3.2-3.2h-4.26" />
|
||||
</svg>
|
||||
Log in with Google
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
155
apps/console/src/pages/RegisterPage.tsx
Normal file
155
apps/console/src/pages/RegisterPage.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { buildEndpoint } from "@/utils";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { checkAuth } = useAuth();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email || !password) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Email and password are required",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(buildEndpoint("/console/v1/auth/register"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Registration failed");
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Account created successfully",
|
||||
});
|
||||
|
||||
// Check authentication status after registration
|
||||
await checkAuth();
|
||||
|
||||
// Redirect to home page after successful registration
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
error instanceof Error ? error.message : "Registration failed",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Sign up - Probo</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">Sign up</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Enter your information to create an account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Creating account..." : "Sign up with email"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Already have an account?{" "}
|
||||
<Link to="/login" className="underline">
|
||||
Log in here
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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" className="w-full" disabled={isLoading}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-4 w-4"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M17.13 17.13v-4.26l-3.2 3.2a4.33 4.33 0 0 1-6.13-6.13l3.2-3.2h-4.26" />
|
||||
</svg>
|
||||
Sign up with Google
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<25866c1f4e6fb640f91b8d05651df9f4>>
|
||||
* @generated SignedSource<<bcc0482212bee6fc9682545cc8255139>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -36,7 +36,7 @@ v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<b79fe4cf20a9bc930e7bd90ccc2daf03>>
|
||||
* @generated SignedSource<<903f19e5b3c193151107de5969fe5259>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -163,7 +163,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
@@ -197,7 +197,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<d57e3b87db862a1cfa086ea4b410d567>>
|
||||
* @generated SignedSource<<7ca2f2186b657853d9fce4c7a21a7db1>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -615,7 +615,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
@@ -654,7 +654,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<dde370798e95a61988dd69cdde3acaad>>
|
||||
* @generated SignedSource<<fd54290261cf29d9b3cf42b70a68f398>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -94,7 +94,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
@@ -137,7 +137,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<fab1b817b8d6876b5bf4dc9320c4deef>>
|
||||
* @generated SignedSource<<0b5e0bcf4f6519e1ccb1d7ffa3e488cb>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -56,7 +56,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
@@ -91,7 +91,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<4b2d9a3c1a701053fe929da59580308b>>
|
||||
* @generated SignedSource<<42f5a79d59c05d56dc664163fd507fed>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -94,7 +94,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
@@ -137,7 +137,7 @@ return {
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Viewer",
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
|
||||
Reference in New Issue
Block a user