Initial Commit

This commit is contained in:
Stavros
2025-01-19 13:40:06 +02:00
commit c0e085ea10
34 changed files with 3195 additions and 0 deletions

17
site/src/App.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { Navigate } from "react-router";
import { useUserContext } from "./context/user-context";
import { LogoutPage } from "./pages/logout-page";
export const App = () => {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri");
const { isLoggedIn } = useUserContext();
if (!isLoggedIn) {
return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
}
return <LogoutPage />;
};

View File

@@ -0,0 +1,12 @@
import { Center, Flex } from "@mantine/core";
import { ReactNode } from "react";
export const Layout = ({ children }: { children: ReactNode }) => {
return (
<Center style={{ minHeight: "100vh" }}>
<Flex direction="column" flex="1" maw={350}>
{children}
</Flex>
</Center>
);
};

View File

@@ -0,0 +1,42 @@
import { useQuery } from "@tanstack/react-query";
import React, { createContext, useContext } from "react";
import { UserContextSchemaType } from "../schemas/user-context-schema";
import axios from "axios";
const UserContext = createContext<UserContextSchemaType | null>(null);
export const UserContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const {
data: userContext,
isLoading,
error,
} = useQuery({
queryKey: ["isLoggedIn"],
queryFn: async () => {
const res = await axios.get("/api/status");
return res.data;
},
});
if (error && !isLoading) {
throw error;
}
return (
<UserContext.Provider value={userContext}>{children}</UserContext.Provider>
);
};
export const useUserContext = () => {
const context = useContext(UserContext);
if (context === null) {
throw new Error("useUserContext must be used within a UserContextProvider");
}
return context;
};

44
site/src/main.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App.tsx";
import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Route } from "react-router";
import { Routes } from "react-router";
import { UserContextProvider } from "./context/user-context.tsx";
import { LoginPage } from "./pages/login-page.tsx";
import { LogoutPage } from "./pages/logout-page.tsx";
import { ContinuePage } from "./pages/continue-page.tsx";
import { NotFoundPage } from "./pages/not-found-page.tsx";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
},
},
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<MantineProvider forceColorScheme="dark">
<QueryClientProvider client={queryClient}>
<Notifications />
<UserContextProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path="/continue" element={<ContinuePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
</UserContextProvider>
</QueryClientProvider>
</MantineProvider>
</StrictMode>,
);

View File

@@ -0,0 +1,53 @@
import { Button, Paper, Text } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { Navigate } from "react-router";
import { useUserContext } from "../context/user-context";
import { Layout } from "../components/layouts/layout";
export const ContinuePage = () => {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri");
const { isLoggedIn } = useUserContext();
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
const redirect = () => {
notifications.show({
title: "Redirecting",
message: "You should be redirected to the app soon",
color: "blue",
});
setTimeout(() => {
window.location.replace(redirectUri!);
}, 500);
};
return (
<Layout>
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
{redirectUri ? (
<>
<Text size="xl" fw={700}>
Continue
</Text>
<Text>Click the button to continue to your app.</Text>
<Button fullWidth mt="xl" onClick={redirect}>
Continue
</Button>
</>
) : (
<>
<Text size="xl" fw={700}>
Logged in
</Text>
<Text>You are now signed in and can use your apps.</Text>
</>
)}
</Paper>
</Layout>
);
};

View File

@@ -0,0 +1,99 @@
import { Button, Paper, PasswordInput, TextInput, Title } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import { notifications } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { z } from "zod";
import { useUserContext } from "../context/user-context";
import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout";
export const LoginPage = () => {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri");
const { isLoggedIn } = useUserContext();
if (isLoggedIn) {
return <Navigate to="/logout" />;
}
const schema = z.object({
email: z.string().email({ message: "Invalid email" }),
password: z.string(),
});
type FormValues = z.infer<typeof schema>;
const form = useForm({
mode: "uncontrolled",
initialValues: {
email: "",
password: "",
},
validate: zodResolver(schema),
});
const loginMutation = useMutation({
mutationFn: (login: FormValues) => {
return axios.post("/api/login", login);
},
onError: () => {
notifications.show({
title: "Failed to login",
message: "Check your email and password",
color: "red",
});
},
onSuccess: () => {
notifications.show({
title: "Logged in",
message: "Welcome back!",
color: "green",
});
setTimeout(() => {
window.location.replace(`/continue?redirect_uri=${redirectUri}`);
});
},
});
const handleSubmit = (values: FormValues) => {
loginMutation.mutate(values);
};
return (
<Layout>
<Title ta="center">Welcome back!</Title>
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
label="Email"
placeholder="you@example.com"
required
disabled={loginMutation.isLoading}
key={form.key("email")}
{...form.getInputProps("email")}
/>
<PasswordInput
label="Password"
placeholder="password"
required
mt="md"
disabled={loginMutation.isLoading}
key={form.key("password")}
{...form.getInputProps("password")}
/>
<Button
fullWidth
mt="xl"
type="submit"
loading={loginMutation.isLoading}
>
Sign in
</Button>
</form>
</Paper>
</Layout>
);
};

View File

@@ -0,0 +1,59 @@
import { Button, Paper, Text } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { useUserContext } from "../context/user-context";
import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout";
export const LogoutPage = () => {
const { isLoggedIn } = useUserContext();
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
const logoutMutation = useMutation({
mutationFn: () => {
return axios.post("/api/logout");
},
onError: () => {
notifications.show({
title: "Failed to logout",
message: "Please try again",
color: "red",
});
},
onSuccess: () => {
notifications.show({
title: "Logged out",
message: "Goodbye!",
color: "green",
});
setTimeout(() => {
window.location.reload();
}, 500);
},
});
return (
<Layout>
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
<Text size="xl" fw={700}>
Logout
</Text>
<Text>
You are already logged in, click the button below to log out.
</Text>
<Button
fullWidth
mt="xl"
onClick={() => logoutMutation.mutate()}
loading={logoutMutation.isLoading}
>
Logout
</Button>
</Paper>
</Layout>
);
};

View File

@@ -0,0 +1,18 @@
import { Button, Paper, Text } from "@mantine/core";
import { Layout } from "../components/layouts/layout";
export const NotFoundPage = () => {
return (
<Layout>
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
<Text size="xl" fw={700}>
Not found
</Text>
<Text>The page you are looking for does not exist.</Text>
<Button fullWidth mt="xl" onClick={() => window.location.replace("/")}>
Go home
</Button>
</Paper>
</Layout>
);
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const userContextSchema = z.object({
isLoggedIn: z.boolean(),
});
export type UserContextSchemaType = z.infer<typeof userContextSchema>;

1
site/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />