inital commit

This commit is contained in:
Valentin Kolb 2024-03-26 17:07:08 +01:00
commit 253bb24224
29 changed files with 2621 additions and 0 deletions

18
.eslintrc.cjs Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

54
README.md Normal file
View File

@ -0,0 +1,54 @@
# StuVe IT Frontend
## Übersicht
Dieses Repository enthält den Quellcode für die StuVe IT Frontend Webseite.
### Verwendete Technologien
- [React ❤️](https://reactjs.org/) [&]() [Typescript](https://www.typescriptlang.org/) - Code Library
- [Vite](https://vitejs.dev/) - Build Tool
- [Mantine](https://mantine.dev/) [&]() [Postcss](https://postcss.org/) - UI Library
- [Tabler Icons](https://tablericons.com/) - Icon Library
- [Pocketbase js](https://github.com/pocketbase/js-sdk) - SDK für die Pocketbase API (Backend)
- [React Router](https://reactrouter.com/) - Routing
- [React Query](https://react-query.tanstack.com/) - Data Fetching
- [Docker](https://www.docker.com/) - Containerization
## Backend
Als Backend wird ein selbst erweitertes Pocketbase verwendet. Dieses ist in diesem [Repo](https://gitlab.uni-ulm.de/stuve-it/it-tools/backend/)
zu finden.
## Projekt Struktur
```
├── index.html # html Datei die den React Code in die Seite einbindet
├── tsconfig.json # Typescript Config Datei
├── vite.config.ts # Vite Config Datei
├── package.json # Package Datei (Node's Package Manager)
├── public/ # Öffentliche Dateien (favicon, logo, ...)
├── src/ # Source Code
│ ├── main.tsx # Main React Datei
│ ├── Router.tsx # React Router Datei
│ ├── components/ # Wiederfendbare React Komponenten
│ ├── lib/ # React Hooks etc.
│ ├── pages/ # React Seiten (der Dateibaum entspricht der URL)
│ └── models/ # Types für die Pocketbase API (Datenbank Modelle)
└── README.md # Diese Datei
```
## Code Patterns
### Pocketbase API
Um die Kommunikation mit dem Backend zu erleichtern ist in der Datei "src/lib/pocketbase.ts" ein Pocketbase Client implementiert.
Dieser Client ist ein React Hook und kann in jeder React Komponente verwendet werden.
```typescript
import {usePB} from "/lib/pocketbase";
const {pb} = usePB();
```
#### Pocketbase Modell Types

51
config.ts Normal file
View File

@ -0,0 +1,51 @@
/**
* @description Global configuration file for the application
*/
import {IconHome, IconInfoCircle, IconShovel, TablerIconsProps} from "@tabler/icons-react";
import {ReactNode} from "react";
// POCKETBASE
export const PB_USER_COLLECTION = "ldap_users"
export const PB_BASE_URL = "https://stuve.uni-ulm.de"
export const PB_STORAGE_KEY = "stuve-it-ldap-login"
// Navigation
export const NAV_ITEMS = [
{
section: "General",
items: [
{
title: "Home",
icon: IconHome,
description: "Home",
link: "/"
}
]
},
{
section: "Events",
items: [
{
title: "Übersicht",
icon: IconInfoCircle,
description: "Administration für StuVe Events. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,",
link: "/events"
},
{
title: "Listen",
icon: IconShovel,
description: "Administration für StuVe Events. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,",
link: "/events/lists"
}
]
}
] as {
section: string,
items: {
title: string,
icon: (props: TablerIconsProps) => ReactNode,
description: string,
link: string
}[]
}[]

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="icon" href="/public/stuve-logo.svg"/>
<title>StuVe IT</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/fira-code": "^5.0.15",
"@fontsource/overpass": "^5.0.15",
"@mantine/core": "^7.1.5",
"@mantine/form": "^7.1.5",
"@mantine/hooks": "^7.1.5",
"@mantine/notifications": "^7.1.5",
"@tabler/icons-react": "^2.39.0",
"@tanstack/react-query": "^5.0.5",
"jwt-decode": "^3.1.2",
"ms": "^2.1.3",
"pocketbase": "^0.19.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0"
},
"devDependencies": {
"@types/ms": "^0.7.33",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.31",
"postcss-preset-mantine": "^1.9.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

14
postcss.config.cjs Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};

22
public/stuve-logo.svg Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.02041,0,0,1.02041,-197.959,0)">
<rect id="Artboard1" x="194" y="0" width="98" height="98" style="fill:none;"/>
<g id="Artboard11">
<g transform="matrix(0.129488,0,0,0.128633,216.823,22.7241)">
<g transform="matrix(7.56828,0,0,7.61857,-176.258,-188.086)">
<path d="M95.493,95.155L4.5,95.155C4.5,95.155 4.726,76.243 5.56,72.26C5.961,70.348 5.798,55.477 5.798,55.477C5.798,55.477 5.802,55.187 5.8,55.048C5.795,54.633 6.268,53.615 6.72,53.294C7.011,53.086 7.343,52.821 7.343,52.419C7.345,49.181 7.341,34.535 7.341,34.535L7.339,34.302C7.329,33.742 7.68,32.975 8.093,32.614C8.55,32.214 9.207,31.923 9.441,31.656C9.589,31.487 9.709,31.294 9.815,30.999C9.9,30.761 16.837,4.845 16.837,4.845C16.837,4.845 23.776,30.762 23.861,30.999C23.967,31.294 24.087,31.487 24.234,31.656C24.469,31.923 25.126,32.214 25.583,32.614C25.996,32.975 26.347,33.742 26.337,34.302L26.334,34.535C26.334,34.535 26.331,49.181 26.333,52.419C26.333,52.821 26.665,53.086 26.956,53.294C27.408,53.615 27.88,54.633 27.876,55.048C27.874,55.187 27.878,55.477 27.878,55.477L27.863,65.838L73.37,65.833L76.83,49.964L78.56,57.898L80.294,49.942L83.757,65.832L83.757,71.587L90.119,71.584C90.119,71.584 91.628,71.592 91.636,71.588C92.215,71.34 94.358,74.803 94.912,76.282C95.606,78.131 95.493,95.155 95.493,95.155Z" style="fill:none;"/>
</g>
<g transform="matrix(7.56828,0,0,7.61857,-176.28,-188.086)">
<path d="M94.912,76.282C95.606,78.131 95.5,98.155 95.5,98.155L4.5,98.155C4.5,98.155 4.726,76.243 5.56,72.26C5.961,70.348 5.798,55.477 5.798,55.477C5.798,55.477 5.802,55.187 5.8,55.048C5.795,54.633 6.268,53.615 6.72,53.294C7.011,53.086 7.343,52.821 7.343,52.419C7.345,49.181 7.341,34.535 7.341,34.535L7.339,34.302C7.329,33.742 7.68,32.975 8.093,32.614C8.55,32.214 9.207,31.923 9.441,31.656C9.589,31.487 9.709,31.294 9.815,30.999C9.9,30.761 16.837,4.845 16.837,4.845C16.837,4.845 23.776,30.762 23.861,30.999C23.967,31.294 24.087,31.487 24.234,31.656C24.469,31.923 25.126,32.214 25.583,32.614C25.996,32.975 26.347,33.742 26.337,34.302L26.334,34.535C26.334,34.535 26.331,49.181 26.333,52.419C26.333,52.821 26.665,53.086 26.956,53.294C27.408,53.615 27.88,54.633 27.876,55.048C27.874,55.187 27.878,55.477 27.878,55.477L27.863,65.838L73.37,65.833L76.83,49.964L78.56,57.898L80.294,49.942L83.757,65.832L83.757,71.587L90.119,71.584C90.119,71.584 91.628,71.592 91.636,71.588C92.215,71.34 94.358,74.803 94.912,76.282Z" style="fill:rgb(34,139,230);"/>
</g>
<g transform="matrix(3.31017,0,0,3.33216,-123.695,-80.204)">
<path d="M47.904,125.072L82.509,185.184L13.3,185.184L47.904,125.072Z" style="fill:rgb(231,245,255);"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

31
src/Router.tsx Normal file
View File

@ -0,0 +1,31 @@
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import HomePage from "./pages/home";
import NotFound from "./pages/not-found";
import Layout from "./components/layout";
const router = createBrowserRouter([
{
path: "/",
element: <Layout/>,
children: [
{
index: true,
element: <HomePage/>
},
{
path: "*",
element: <NotFound/>
}
]
},
])
function Router() {
return (
<RouterProvider router={router}/>
)
}
export default Router

View File

@ -0,0 +1,14 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
position: fixed;
overflow: hidden;
}
.content {
padding: var(--padding);
overflow: auto;
height: 100%;
}

View File

@ -0,0 +1,14 @@
import NavBar from "../nav";
import {Outlet} from "react-router-dom";
import classes from "./index.module.css";
export default function Layout() {
return <div className={classes.container}>
<NavBar/>
<div className={`${classes.content} scrollbar`}>
<Outlet/>
</div>
</div>
}

View File

@ -0,0 +1,76 @@
import {IconExclamationCircle, IconLogin} from "@tabler/icons-react";
import {ActionIcon, Alert, Button, Modal, PasswordInput, TextInput, Title} from "@mantine/core";
import {useDisclosure} from "@mantine/hooks";
import {usePB} from "../../lib/pocketbase.tsx";
import {useForm} from "@mantine/form";
import {useMutation} from "@tanstack/react-query";
import classes from "./index.module.css";
/**
* This component renders a login button and a login modal.
*/
export default function Login() {
const [opened, {open, close}] = useDisclosure(false)
const {ldapLogin} = usePB()
const formValues = useForm({
initialValues: {
username: "",
password: ""
}
})
const loginMutation = useMutation({
mutationFn: async () => {
await ldapLogin(formValues.values.username, formValues.values.password)
}
})
return <>
<Modal opened={opened} onClose={close} withCloseButton={false} size={"sm"}>
<form
className={classes.stack}
onSubmit={formValues.onSubmit(() => loginMutation.mutate())}
>
<Title order={3}>Login mit StuVe IT Account</Title>
<TextInput
label={"Anmeldename"}
placeholder={"vorname.nachname"}
{...formValues.getInputProps("username")}
/>
<PasswordInput
label={"Passwort"}
placeholder={"Passwort"}
{...formValues.getInputProps("password")}
/>
{loginMutation.error && (
<Alert variant="transparent" color="red" title="Fehler" icon={<IconExclamationCircle/>}>
{loginMutation.error.message}
</Alert>
)}
<Button
loading={loginMutation.isPending}
disabled={formValues.values.username === "" || formValues.values.password === ""}
type={"submit"}
>
Einloggen
</Button>
</form>
</Modal>
<ActionIcon
variant={"transparent"}
color={"gray"}
aria-label={"Login"}
onClick={open}
>
<IconLogin/>
</ActionIcon>
</>
}

View File

@ -0,0 +1,34 @@
import {Menu, rem} from "@mantine/core";
import {NAV_ITEMS} from "../../../config.ts";
import {NavLink} from "react-router-dom";
import {Fragment} from "react";
export default function MenuItems() {
return <>
{NAV_ITEMS.map((section, index) => (
<Fragment key={index + section.section}>
<Menu.Label>
{section.section}
</Menu.Label>
{
section.items.map((item, index) => {
return (
<Menu.Item
key={index + item.title}
leftSection={<item.icon style={{width: rem(14), height: rem(14)}}/>}
component={NavLink}
to={item.link}
aria-label={item.description}
>
{item.title}
</Menu.Item>
)
})
}
</Fragment>
))}
</>
}

View File

@ -0,0 +1,132 @@
import {ActionIcon, Button, Divider, List, Modal, rem, Text, ThemeIcon, Title} from "@mantine/core";
import {
IconBalloon,
IconCalendar,
IconId,
IconLogout,
IconServer,
IconServerOff,
IconUsersGroup
} from "@tabler/icons-react";
import {useDisclosure} from "@mantine/hooks";
import {usePB} from "../../lib/pocketbase.tsx";
import classes from "./index.module.css";
/**
* This component renders a user menu button and a user menu modal with user information.
*/
export default function UserMenu() {
const [opened, {open, close}] = useDisclosure(false)
const {user, logout, apiIsHealthy} = usePB()
return <>
<Modal opened={opened} onClose={close} withCloseButton={false} size={"sm"}>
<div className={classes.stack}>
<Title order={3}>Hallo {user!.username}</Title>
<div className={classes.row}>
<ThemeIcon
variant={"transparent"}
size={"xl"}
>
<IconId/>
</ThemeIcon>
<Text>
ID {user?.uidNumber}
</Text>
</div>
<div className={classes.row}>
<ThemeIcon
variant={"transparent"}
size={"xl"}
>
<IconCalendar/>
</ThemeIcon>
<Text>
{user?.accountExpires ? (
user?.accountExpires?.getTime() > Date.now() ? (
"Account ist aktiv und läuft am " + user?.accountExpires?.toLocaleDateString() + " ab"
) : (
"Account ist abgelaufen"
)
) : (
"Dein Account läuft nicht ab"
)}
</Text>
</div>
<div className={classes.row}>
{apiIsHealthy ? (
<ThemeIcon
variant={"transparent"}
color={"green"}
size={"xl"}
>
<IconServer/>
</ThemeIcon>
) : (
<ThemeIcon
variant={"transparent"}
color={"red"}
size={"xl"}
>
<IconServerOff/>
</ThemeIcon>
)}
<Text>
{apiIsHealthy ? "API ist erreichbar" : "API ist nicht erreichbar"}
</Text>
</div>
{user?.memberOf.length && <Divider/>}
{user?.memberOf.length && <Title order={5}>Deine Gruppen</Title>}
<List
spacing="xs"
size="sm"
center
icon={
<ThemeIcon color="teal" size={24} radius="xl">
<IconUsersGroup style={{width: rem(16), height: rem(16)}}/>
</ThemeIcon>
}
>
{(user?.expand.memberOf || []).map((group) => (
<List.Item key={group.id}>
{group.cn}
</List.Item>
)
)}
</List>
{user?.memberOf.length && <Divider/>}
<Button
leftSection={<IconLogout/>}
color={"orange"}
onClick={logout}
>
Ausloggen
</Button>
</div>
</Modal>
<ActionIcon
variant={"transparent"}
color={"gray"}
aria-label={"User Menu"}
onClick={open}
>
<IconBalloon/>
</ActionIcon>
</>
}

View File

@ -0,0 +1,45 @@
.navbar {
border-bottom: 1px solid var(--mantine-color-gray-4);
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 var(--padding);
}
.logo {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
gap: var(--mantine-spacing-xs);
}
.title {
font-size: var(--mantine-h2-font-size);
font-weight: bold;
}
.actionIcons {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
gap: var(--gap);
}
.stack {
display: flex;
flex-direction: column;
gap: var(--gap);
}
.row {
display: flex;
align-items: center;
flex-direction: row;
gap: var(--gap);
}

View File

@ -0,0 +1,62 @@
import {usePB} from "../../lib/pocketbase.tsx";
import classes from "./index.module.css";
import {ActionIcon, Image, Menu, ThemeIcon, useMantineColorScheme} from "@mantine/core";
import {IconChevronDown, IconMoon, IconSun} from "@tabler/icons-react";
import UserMenu from "./UserMenu.tsx";
import Login from "./Login.tsx";
import MenuItems from "./MenuItems.tsx";
export default function NavBar() {
const {user} = usePB()
const {colorScheme, toggleColorScheme} = useMantineColorScheme()
return <nav className={classes.navbar}>
<Menu
trigger="hover"
closeDelay={400}
position="bottom-start"
shadow="md"
width={200}
>
<Menu.Target>
<div className={classes.logo}>
<Image
h={30}
w={30}
src={"/public/stuve-logo.svg"}
alt={"StuVe IT Logo"}
/>
<div className={classes.title}>
StuVe IT
</div>
<ThemeIcon variant={"transparent"} size={"sm"}>
<IconChevronDown/>
</ThemeIcon>
</div>
</Menu.Target>
<Menu.Dropdown>
<MenuItems/>
</Menu.Dropdown>
</Menu>
<div className={classes.actionIcons}>
<ActionIcon
variant={"transparent"}
color={"gray"}
onClick={toggleColorScheme}
>
{colorScheme === "dark" ?
<IconSun/>
:
<IconMoon/>
}
</ActionIcon>
{user ? <UserMenu/> : <Login/>}
</div>
</nav>
}

45
src/global.css Normal file
View File

@ -0,0 +1,45 @@
:root {
--gap: var(--mantine-spacing-sm);
--padding: var(--mantine-spacing-md);
--border-radius: var(--mantine-radius-md);
}
.scrollbar {
scrollbar-color: var(--mantine-color-blue-5) var(--mantine-color-body);
scrollbar-width: thin;
&::-webkit-scrollbar {
display: block;
width: 5px;
}
/* Track */
&::-webkit-scrollbar-track {
background: unset;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: var(--mantine-color-gray-3);
border-radius: var(--mantine-radius-md);
}
/* Handle on hover */
&::-webkit-scrollbar-thumb:hover {
background: var(--mantine-color-blue-5);
}
}
a {
color: inherit; /* Inherits the color from the parent element */
text-decoration: none; /* Removes underline */
}
a:hover, a:active, a:visited {
color: inherit; /* Inherits the color from the parent element */
text-decoration: none; /* Removes underline */
}

107
src/lib/pocketbase.tsx Normal file
View File

@ -0,0 +1,107 @@
import {createContext, DependencyList, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from "react"
import PocketBase, {LocalAuthStore, RecordAuthResponse, RecordSubscription} from 'pocketbase'
import ms from "ms";
import {useInterval} from "@mantine/hooks";
import {useQuery} from "@tanstack/react-query";
import {TypedPocketBase} from "../models";
import {PB_BASE_URL, PB_STORAGE_KEY, PB_USER_COLLECTION} from "../../config.ts";
import {LdapUser} from "../models/AuthTypes.ts";
const oneMinuteInMs = ms("1 minute");
const PocketContext = createContext({})
const PocketData = () => {
const pb = useMemo(() =>
new PocketBase(
PB_BASE_URL,
new LocalAuthStore(PB_STORAGE_KEY)
) as TypedPocketBase,
[])
const apiIsHealthyQuery = useQuery({
queryKey: ["apiHealthCheck"],
queryFn: async () => {
const res = await pb.health.check()
return res.code === 200
},
refetchInterval: oneMinuteInMs
})
const [user, setUser] = useState(pb.authStore.model)
pb.authStore.onChange((_, userRecord) => {
setUser(userRecord)
})
const refreshUser = useCallback(async () => {
await pb.collection(PB_USER_COLLECTION).authRefresh({
expand: "memberOf"
}).catch(() => {
pb.authStore.clear()
})
}, [pb])
const ldapLogin = useCallback(async (usernameOrCN: string, password: string) => {
const res = await pb.send<RecordAuthResponse>("/api/ldap/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username: usernameOrCN,
password: password
})
})
pb.authStore.save(res.token, res.record)
}, [pb])
const logout = useCallback(async () => {
pb.authStore.clear();
}, [pb.authStore])
const useSubscription = <T, >({idOrName, topic = "*", callback}: {
idOrName: string,
topic?: string,
callback: (data: RecordSubscription<T>) => void
}, deps?: DependencyList | undefined) => useEffect(() => {
pb.collection(idOrName).subscribe<T>(topic, callback)
return () => {
pb.collection(idOrName).unsubscribe(topic)
}
}, deps ? deps : []);
useInterval(refreshUser, oneMinuteInMs)
return {
ldapLogin,
logout,
user: user as LdapUser | null,
pb,
refreshUser,
useSubscription,
apiIsHealthy: apiIsHealthyQuery.data ?? false
}
}
export const PocketBaseProvider = ({children}: {
children: ReactNode,
userCollection?: string
}) => {
const data = PocketData()
return (
<PocketContext.Provider
value={data}
>
{children}
</PocketContext.Provider>
)
}
export const usePB = () => {
const pb = useContext(PocketContext)
if (!pb) {
throw new Error("usePB must be used inside PocketBaseProvider")
}
return pb as ReturnType<typeof PocketData>
}

36
src/main.tsx Normal file
View File

@ -0,0 +1,36 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import Router from './Router.tsx'
import '@mantine/core/styles.css';
import {createTheme, DEFAULT_THEME, MantineProvider, mergeMantineTheme} from "@mantine/core";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {PocketBaseProvider} from "./lib/pocketbase.tsx";
import "./global.css";
// fonts
import "@fontsource/overpass"
import "@fontsource/fira-code"
const queryClient = new QueryClient()
const themeOverride = createTheme({
fontFamilyMonospace: 'Fira Code VF, monospace',
fontFamily: 'Overpass, sans-serif',
headings: {
fontFamily: 'Overpass, sans-serif'
},
});
export const theme = mergeMantineTheme(DEFAULT_THEME, themeOverride);
ReactDOM.createRoot(document.getElementById('root')!).render(
<MantineProvider theme={theme} >
<QueryClientProvider client={queryClient}>
<PocketBaseProvider>
<React.StrictMode>
<Router/>
</React.StrictMode>
</PocketBaseProvider>
</QueryClientProvider>
</MantineProvider>
)

42
src/models/AuthTypes.ts Normal file
View File

@ -0,0 +1,42 @@
import {RecordModel} from "pocketbase";
export type LdapUser = {
username: string;
email: string;
cn: string;
dn: string;
uidNumber: string;
sn: string;
givenName: string;
accountExpires: Date | null;
memberOf: string[];
expand: {
memberOf: LdapGroup[]
}
} & RecordModel
export type LdapGroup = {
gidNumber: string;
description: string;
cn: string;
dn: string;
memberOf: string[];
expand: {
memberOf: LdapGroup[]
}
} & RecordModel
export type LdapSyncLog = {
usersFound: number;
usersSynced: number;
usersRemoved: number;
userSyncErrors: string[] | null;
groupsFound: number;
groupsSynced: number;
groupsRemoved: number;
groupSyncErrors: string[] | null;
} & RecordModel

12
src/models/index.ts Normal file
View File

@ -0,0 +1,12 @@
import PocketBase, {RecordService} from "pocketbase";
import {LdapGroup, LdapSyncLog, LdapUser} from "./AuthTypes.ts";
export interface TypedPocketBase extends PocketBase {
collection(idOrName: string): RecordService // default fallback for any other collection
collection(idOrName: 'ldap_users'): RecordService<LdapUser>
collection(idOrName: 'ldap_groups'): RecordService<LdapGroup>
collection(idOrName: 'ldap_sync_logs'): RecordService<LdapSyncLog>
}

22
src/pages/home/index.tsx Normal file
View File

@ -0,0 +1,22 @@
export default function HomePage() {
return (
<div>
<h1>Home Page</h1>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum. Quisquam, voluptatum. Quisquam,
</div>
)
}

View File

@ -0,0 +1,23 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--gap);
width: min-content;
text-align: center;
margin: auto;
}
.status {
font-size: 10rem;
@media (max-width: $mantine-breakpoint-xs) {
font-size: 7rem;
}
font-weight: 700;
color: var(--mantine-primary-color-filled);
margin: 0;
}

View File

@ -0,0 +1,21 @@
import classes from "./index.module.css"
export default function NotFound() {
return (
<div className={classes.container}>
<h1 className={classes.status}>404</h1>
<div>
<p>
Oops! In deiner Timeline existiert die gesuchte Seite leider nicht.
</p>
<p>
Nutze die Navigation, um zurück in vertraute Dimensionen zu gelangen.
</p>
</div>
</div>
)
}

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

@ -0,0 +1,3 @@
/// <reference types="vite/client" />
declare module "*.module.css";
declare module "*.css";

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

8
vite.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

1619
yarn.lock Normal file

File diff suppressed because it is too large Load Diff