Skip to content
Snippets Groups Projects
Commit 566c4be2 authored by Aaron Dötsch's avatar Aaron Dötsch
Browse files

update

parent 47585ca4
Branches
No related tags found
No related merge requests found
# Credentials for the database
POSTGRES_USER=postgres POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres POSTGRES_DB=postgres
POSTGRES_HOST=localhost POSTGRES_HOST=localhost
POSTGRES_PORT=5432 POSTGRES_PORT=5432
AUTH_SECRET= # some random key, e.g. openssl rand -base64 32 # Some random key, e.g. openssl rand -base64 32
AUTH_SECRET=
# The scopes that the keycloak client should request
KEYCLOAK_SCOPES=openid profile email KEYCLOAK_SCOPES=openid profile email
# The issuer of the keycloak realm
KEYCLOAK_ISSUER=https://keycloak.example.com/realms/esag KEYCLOAK_ISSUER=https://keycloak.example.com/realms/esag
KEYCLOAK_ID= # Add your client id here # The client id of the keycloak client
KEYCLOAK_SECRET= # Add your secret here KEYCLOAK_ID=
# The client secret of the keycloak client
KEYCLOAK_SECRET=
# The group that the user must be in to be allowed to access the application
# If this is not set, no group is required
REQUIRED_GROUP=esag REQUIRED_GROUP=esag
# Information for sending mails
MAIL_HOST=mail.example.com MAIL_HOST=mail.example.com
MAIL_PORT=25 MAIL_PORT=25
MAIL_FROM=esag@example.com MAIL_FROM=esag@example.com
DOMAIN=example.com # The domain of the application # The domain of the application
DOMAIN=example.com
# The maximum size of the request body in bytes
# This only affects the node server itself. If you are using a reverse proxy you may need
# to configure it there as well.
BODY_SIZE_LIMIT=Infinity BODY_SIZE_LIMIT=Infinity
# create-svelte # ESA-Website
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). Diese Website dient der Verwaltung aller Erstsemesterangelegenheiten der Fachschaft Mathematik/Physik/Informatik der RWTH Aachen.
## Creating a project ## Installation
If you're seeing this, you've probably already done this step. Congrats! 1. Docker und Docker Compose installieren
2. Repository klonen
3. `.env.example` in `.env` umbenennen und anpassen
4. `docker compose up` ausführen
```bash ## Verwendung
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: ## Entwicklung
```bash ### Voraussetzungen
npm run dev
# or start the server and open the app in a new browser tab 1. NodeJS und npm installiert
npm run dev -- --open 2. Repository klonen
``` 3. `npm install --legacy-peer-deps` ausführen
4. PostgreSQL Server aufsetzen
5. `.env.example` in `.env` umbenennen und anpassen
6. `npm run dev` ausführen
## Building ## Lizenz
To create a production version of your app: [MIT](https://choosealicense.com/licenses/mit/)
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
This diff is collapsed.
<script lang="ts">
import MarkdownIt from "markdown-it";
import type { HTMLAttributes } from "svelte/elements";
type Props = {
md: string;
allowHtml?: boolean;
} & Partial<HTMLAttributes<HTMLDivElement>>;
let { md, allowHtml = false, ...restProps }: Props = $props();
const markdown = new MarkdownIt({
breaks: true,
linkify: true,
quotes: "“”‘’",
html: allowHtml,
});
</script>
<style>
/* add styles again bc tailwind removes them all :( */
div :global(h1) {
font-size: 2rem;
font-weight: 700;
padding-bottom: .3em;
}
div :global(h2) {
font-size: 1.75rem;
font-weight: 600;
padding-bottom: .3em;
}
div :global(h3) {
font-size: 1.5rem;
font-weight: 600;
padding-bottom: .3em;
}
div :global(h4) {
font-size: 1.25rem;
font-weight: 600;
padding-bottom: .3em;
}
div :global(h5) {
font-size: 1rem;
font-weight: 600;
padding-bottom: .3em;
}
div :global(h6) {
font-size: .875rem;
font-weight: 600;
padding-bottom: .3em;
}
div :global(p) {
padding-bottom: .75em;
}
div :global(blockquote) {
margin: 0 0 .5em 0;
padding: 0 .5em;
border-left: 4px solid #d2d6dc;
}
div :global(.dark blockquote) {
border-left-color: #4b5563;
}
div :global(blockquote p) {
padding: .2em 0;
}
div :global(blockquote blockquote){
margin-bottom: 0;
}
div :global(pre) {
padding: 1em;
background-color: #f3f4f6;
border-radius: .5em;
margin-bottom: .5em;
overflow-x: auto;
}
div :global(.dark pre) {
background-color: #31384f;
}
div :global(code) {
background-color: #f3f4f6;
padding: .1em .3em;
border-radius: .3em;
}
div :global(.dark code) {
background-color: #31384f;
}
div :global(pre code){
background-color: transparent;
padding: 0;
border-radius: 0;
}
div :global(a) {
color: #2563EB;
text-decoration: underline;
}
div :global(.dark a) {
color: #90CDF4;
}
</style>
<div {...restProps}>
{@html markdown.render(md)}
</div>
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import de from "./de"; import de from "./de";
import en from "./en"; import en from "./en";
import { browser } from "$app/environment"; import { browser, dev } from "$app/environment";
export type LocalizedString = string; export type LocalizedString = string;
export type TranslationFunction = ((...args: unknown[])=>LocalizedString&{raw:string}); export type TranslationFunction = ((...args: unknown[])=>LocalizedString&{raw:string});
...@@ -108,7 +108,7 @@ function formatLocaleString(input: string, locale: Locale = currentLocale, ...ar ...@@ -108,7 +108,7 @@ function formatLocaleString(input: string, locale: Locale = currentLocale, ...ar
} }
const nothingProxy: ()=>()=>string = (path: string)=>new Proxy(()=>{ const nothingProxy: ()=>()=>string = (path: string)=>new Proxy(()=>{
console.trace("Trying to call non-existing translation function", path); if(dev) console.trace("Trying to call non-existing translation function", path);
return ""; return "";
}, { }, {
get: (_, prop: string)=>nothingProxy(path+"."+prop), get: (_, prop: string)=>nothingProxy(path+"."+prop),
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import type { MrXData } from "./data/+server.js"; import type { MrXData } from "./data/+server.js";
import { Card, P, Span } from "flowbite-svelte"; import { Card, P, Span } from "flowbite-svelte";
import Image from "$lib/components/Image.svelte"; import Image from "$lib/components/Image.svelte";
import MarkdownIt from "markdown-it"; import Markdown from "$lib/components/Markdown.svelte";
let { data } = $props(); let { data } = $props();
...@@ -23,96 +23,8 @@ ...@@ -23,96 +23,8 @@
clearInterval(interval); clearInterval(interval);
}; };
}); });
const md = new MarkdownIt({
breaks: true,
linkify: true,
quotes: "“”‘’",
html: true,
});
</script> </script>
<style>
/* add styles again bc tailwind removes them all :( */
:global(.md-wrapper h1) {
font-size: 2rem;
font-weight: 700;
padding-bottom: .3em;
}
:global(.md-wrapper h2) {
font-size: 1.75rem;
font-weight: 600;
padding-bottom: .3em;
}
:global(.md-wrapper h3) {
font-size: 1.5rem;
font-weight: 600;
padding-bottom: .3em;
}
:global(.md-wrapper h4) {
font-size: 1.25rem;
font-weight: 600;
padding-bottom: .3em;
}
:global(.md-wrapper h5) {
font-size: 1rem;
font-weight: 600;
padding-bottom: .3em;
}
:global(.md-wrapper h6) {
font-size: .875rem;
font-weight: 600;
padding-bottom: .3em;
}
:global(.md-wrapper p) {
padding-bottom: .75em;
}
:global(.md-wrapper blockquote) {
margin: 0 0 .5em 0;
padding: 0 .5em;
border-left: 4px solid #d2d6dc;
}
:global(.dark .md-wrapper blockquote) {
border-left-color: #4b5563;
}
:global(.md-wrapper blockquote p) {
padding: .2em 0;
}
:global(.md-wrapper blockquote blockquote){
margin-bottom: 0;
}
:global(.md-wrapper pre) {
padding: 1em;
background-color: #f3f4f6;
border-radius: .5em;
margin-bottom: .5em;
overflow-x: auto;
}
:global(.dark .md-wrapper pre) {
background-color: #31384f;
}
:global(.md-wrapper code) {
background-color: #f3f4f6;
padding: .1em .3em;
border-radius: .3em;
}
:global(.dark .md-wrapper code) {
background-color: #31384f;
}
:global(.md-wrapper pre code){
background-color: transparent;
padding: 0;
border-radius: 0;
}
:global(.md-wrapper a) {
color: #2563EB;
text-decoration: underline;
}
:global(.dark .md-wrapper a) {
color: #90CDF4;
}
</style>
<H1>{mrX.name}</H1> <H1>{mrX.name}</H1>
<div class="w-full flex flex-col items-center gap-3"> <div class="w-full flex flex-col items-center gap-3">
...@@ -120,9 +32,7 @@ ...@@ -120,9 +32,7 @@
<Card class="w-full max-w-2xl relative"> <Card class="w-full max-w-2xl relative">
<Span class="relative -top-3 -left-2 -mb-2 text-sm font-normal text-gray-600 dark:text-gray-400">{entry.time.toLocaleString()}</Span> <Span class="relative -top-3 -left-2 -mb-2 text-sm font-normal text-gray-600 dark:text-gray-400">{entry.time.toLocaleString()}</Span>
{#if entry.text} {#if entry.text}
<div class="text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal {entry.image ? "mb-2" : "-mb-4"} md-wrapper"> <Markdown allowHtml md={entry.text} class="text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal {entry.image ? "mb-2" : "-mb-4"} md-wrapper" />
{@html md.render(entry.text)}
</div>
{/if} {/if}
{#if entry.image} {#if entry.image}
<div class="flex justify-center max-h-svh"> <div class="flex justify-center max-h-svh">
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
let { data } = $props(); let { data } = $props();
// TODO rename OpeningStatus - has nothing to do with the opening hours but whether the discount is valid
function getValidityStatus(startDate: string, endDate: string, currentDate: number): number{ function getValidityStatus(startDate: string, endDate: string, currentDate: number): number{
if(startDate && endDate){ if(startDate && endDate){
let started = Date.parse(startDate) <= currentDate; let started = Date.parse(startDate) <= currentDate;
...@@ -35,19 +36,34 @@ ...@@ -35,19 +36,34 @@
)) ))
.sort((a,b)=>{ .sort((a,b)=>{
if(a.status === b.status){ if(a.status === b.status){
switch(a.status){ if(a.status === OpeningStatus.VALID){
case OpeningStatus.VALID: if(a.isOpen === b.isOpen){
case OpeningStatus.STARTING_SOON: return a.title.localeCompare(b.title);
if(a.startDate === b.startDate){ }else if(a.isOpen){
return -1;
}else if(b.isOpen){
return 1;
}else if(a.openingSoon === b.openingSoon){
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
}else if(a.openingSoon){
return -1;
}else if(b.openingSoon){
return 1;
}else{ }else{
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} }
case OpeningStatus.ENDED: }else if(a.status === OpeningStatus.STARTING_SOON){
if(a.startDate === b.startDate){
if(a.endDate === b.endDate){
return a.title.localeCompare(b.title);
}else{
return b.endDate.localeCompare(a.endDate); return b.endDate.localeCompare(a.endDate);
default: // should never happen }
console.warn(`Unknown OpeningStatus ${a.status}`); }else{
return 0; return a.startDate.localeCompare(b.startDate);
}
}else{
return a.title.localeCompare(b.title);
} }
}else{ }else{
return a.status - b.status; return a.status - b.status;
......
<script lang="ts"> <script lang="ts">
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import Markdown from "$lib/components/Markdown.svelte";
import { addMessage } from "$lib/messages"; import { addMessage } from "$lib/messages";
import { Button, Heading, Input, Label, P, Select } from "flowbite-svelte"; import { Button, Heading, Input, Label, Select } from "flowbite-svelte";
import MarkdownIt from "markdown-it";
let { data } = $props(); let { data } = $props();
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
breaks: true,
});
</script> </script>
<style>
.description :global(p) {
margin-bottom: 0.7rem;
}
</style>
<Heading customSize="text-4xl font-bold" class="mb-6 text-center">{data.rallyStation.name}</Heading> <Heading customSize="text-4xl font-bold" class="mb-6 text-center">{data.rallyStation.name}</Heading>
<div class="description text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal mb-6">{@html md.render(data.rallyStation.description)}</div> <Markdown md={data.rallyStation.description} allowHtml class="text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal mb-6" />
<form method="post" action="?/setPoints" use:enhance={()=>({result, update})=>{ <form method="post" action="?/setPoints" use:enhance={()=>({result, update})=>{
if(result.type === "success"){ if(result.type === "success"){
......
...@@ -2,7 +2,7 @@ import "$lib/polyfill"; ...@@ -2,7 +2,7 @@ import "$lib/polyfill";
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import { createCanvas } from 'canvas'; import { createCanvas } from 'canvas';
import * as pdfjs from "pdfjs-dist"; import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs"; // legacy is required for pdfjs to use node-canvas
import { getImages, uploadImage } from "$lib/server/images"; import { getImages, uploadImage } from "$lib/server/images";
export const load = (async () => { export const load = (async () => {
...@@ -33,20 +33,25 @@ async function renderFlyerImages(filename: string){ ...@@ -33,20 +33,25 @@ async function renderFlyerImages(filename: string){
url: `static/flyer/${filename}.pdf`, url: `static/flyer/${filename}.pdf`,
standardFontDataUrl: "node_modules/pdfjs-dist/standard_fonts/", standardFontDataUrl: "node_modules/pdfjs-dist/standard_fonts/",
}).promise; }).promise;
const page = await file.getPage(1); return renderPage(file, 1080, 1080);
return renderPage(page, 1080, 1080);
} }
async function renderPage(page: pdfjs.PDFPageProxy, minWidth: number, minHeight: number): Promise<Buffer> { async function renderPage(document: pdfjs.PDFDocumentProxy, minWidth: number, minHeight: number): Promise<Buffer> {
const page = await document.getPage(1);
const unscaledViewport = page.getViewport({ scale: 1 }); const unscaledViewport = page.getViewport({ scale: 1 });
const scale = Math.max(minWidth / unscaledViewport.width, minHeight / unscaledViewport.height); const scale = Math.max(minWidth / unscaledViewport.width, minHeight / unscaledViewport.height);
const viewport = page.getViewport({ scale }); const viewport = page.getViewport({ scale });
const canvas = createCanvas(viewport.width, viewport.height); /*const canvas = createCanvas(viewport.width, viewport.height);
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
await page.render({ await page.render({
// @ts-expect-error ctx is a valid canvas context, but from a different library // @ts-expect-error ctx is a valid canvas context, but from a different library
canvasContext: ctx, canvasContext: ctx,
viewport, viewport,
}).promise; }).promise;*/
return Buffer.from(canvas.toDataURL("image/jpeg").split(",")[1], "base64"); //return Buffer.from(canvas.toDataURL("image/jpeg").split(",")[1], "base64");
const canvasAndContext = document.canvasFactory.create(viewport.width, viewport.height);
await page.render({ canvasContext: canvasAndContext.context, viewport }).promise;
const buffer = canvasAndContext.canvas.toBuffer();
page.cleanup();
return buffer;
} }
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
let { data } = $props(); let { data } = $props();
// TODO REMOVE, DO NOT COMMIT
data.tutorials = Array.from({ length: 20 }).map((_, i) => ({ id: i, name: `Tutorium ${i+1}`, rallyQuestionnairePoints: 0 }));
data.stations = [...data.stations, ...data.stations, ...data.stations, ...data.stations, ...data.stations];
// TODO REMOVE, DO NOT COMMIT
let tutorialIds = data.tutorials.map(tutorial => tutorial.id); let tutorialIds = data.tutorials.map(tutorial => tutorial.id);
let stationIds = data.stations.map(station => station.id); let stationIds = data.stations.map(station => station.id);
let points = Array.from({ length: stationIds.length }).map(()=>Array.from({ length: tutorialIds.length }).map(()=>null as {points: number, bribe: number}|null)); let points = Array.from({ length: stationIds.length }).map(()=>Array.from({ length: tutorialIds.length }).map(()=>null as {points: number, bribe: number}|null));
...@@ -41,6 +46,7 @@ ...@@ -41,6 +46,7 @@
} }
function evaluate(){ function evaluate(){
if(!data.tutorials.length || !data.stations.length) return [];
const normalized = normalizeStations(); const normalized = normalizeStations();
const transposed = normalized[0].map((_, i) => normalized.map(row => row[i])); const transposed = normalized[0].map((_, i) => normalized.map(row => row[i]));
const numberOfStationsToGrade = Math.min(6, stationIds.length); const numberOfStationsToGrade = Math.min(6, stationIds.length);
...@@ -69,9 +75,23 @@ ...@@ -69,9 +75,23 @@
let showModal = $state(false); let showModal = $state(false);
let ranking = $derived(showModal ? evaluate() : []); let ranking = $derived(showModal ? evaluate() : []);
$inspect(ranking); let headElement: HTMLTableRowElement | null = $state(null);
</script> </script>
<svelte:window onscroll={e=>{
if(!headElement) return;
// position sticky does not work with overflow-x-auto, so we have to do it manually
// I would prefer a solution that just changes the table position to fixed once a certrain scroll
// position is reached, but that breaks the table layout
const table = headElement.closest("table")!;
const tableOffset = Math.floor(table.getBoundingClientRect().top + window.scrollY + 1);
if(window.scrollY > tableOffset){
headElement.style.transform = `translateY(${window.scrollY - tableOffset}px)`;
}else{
headElement.style.transform = "";
}
}} />
<Breadcrumb class="mb-4"> <Breadcrumb class="mb-4">
<BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem>
<BreadcrumbItem href="/admin/rallye">Rallye</BreadcrumbItem> <BreadcrumbItem href="/admin/rallye">Rallye</BreadcrumbItem>
...@@ -114,18 +134,20 @@ ...@@ -114,18 +134,20 @@
<Button on:click={() => showModal = true} class="mb-4">Gewinner auswerten</Button> <Button on:click={() => showModal = true} class="mb-4">Gewinner auswerten</Button>
<Table> <Table divClass="relative overflow-x-auto overflow-y-hidden">
<TableHead> <TableHead defaultRow={false}>
<TableHeadCell></TableHeadCell> <tr class="bg-inherit z-20 relative" bind:this={headElement}>
<TableHeadCell class="bg-inherit z-10 sticky left-0"></TableHeadCell>
{#each data.tutorials as tutorial} {#each data.tutorials as tutorial}
<TableHeadCell>{tutorial.name}</TableHeadCell> <TableHeadCell class="bg-inherit">{tutorial.name}</TableHeadCell>
{/each} {/each}
</tr>
</TableHead> </TableHead>
<TableBody> <TableBody>
{#each data.stations as station, stationIndex} {#each data.stations as station, stationIndex}
{@const normalized = normalizeStation(stationIndex)} {@const normalized = normalizeStation(stationIndex)}
<TableBodyRow> <TableBodyRow>
<TableBodyCell>{station.name}</TableBodyCell> <TableBodyCell class="bg-inherit z-10 sticky left-0">{station.name}</TableBodyCell>
{#each data.tutorials as tutorial, tutorialIndex} {#each data.tutorials as tutorial, tutorialIndex}
<TableBodyCell> <TableBodyCell>
<Label> <Label>
...@@ -141,7 +163,7 @@ ...@@ -141,7 +163,7 @@
</TableBodyRow> </TableBodyRow>
{/each} {/each}
<TableBodyRow> <TableBodyRow>
<TableBodyCell>Laufzettel</TableBodyCell> <TableBodyCell class="bg-inherit z-10 sticky left-0">Laufzettel</TableBodyCell>
{#each data.tutorials as tutorial} {#each data.tutorials as tutorial}
<TableBodyCell> <TableBodyCell>
<Label> <Label>
......
<script lang="ts"> <script lang="ts">
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import Markdown from "$lib/components/Markdown.svelte";
import { addMessage } from "$lib/messages"; import { addMessage } from "$lib/messages";
import { Permission } from "$lib/perms.js"; import { Permission } from "$lib/perms.js";
import { A, Breadcrumb, BreadcrumbItem, Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; import { A, Breadcrumb, BreadcrumbItem, Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
import MarkdownIt from "markdown-it";
let { data } = $props(); let { data } = $props();
const md = new MarkdownIt({
breaks: true,
linkify: true,
html: true,
typographer: true,
});
let canUpdate = data.user.permissions.has(Permission.UPDATE_RALLY_STATIONS); let canUpdate = data.user.permissions.has(Permission.UPDATE_RALLY_STATIONS);
</script> </script>
...@@ -54,7 +47,7 @@ ...@@ -54,7 +47,7 @@
<A href="/rallye/{station.id}">{station.name}</A> <A href="/rallye/{station.id}">{station.name}</A>
</TableBodyCell> </TableBodyCell>
<TableBodyCell> <TableBodyCell>
<div class="text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal">{@html md.render(station.description)}</div> <Markdown allowHtml md={station.description} class="text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal max-h-44 overflow-y-auto" />
</TableBodyCell> </TableBodyCell>
{#if canUpdate} {#if canUpdate}
<TableBodyCell> <TableBodyCell>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment