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

update

parent 47585ca4
No related branches found
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