Compare commits

...

12 Commits
main ... main

Author SHA1 Message Date
Dominic Zimmer
3c4448a151 Implement timeline UI
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-27 13:23:51 +02:00
929a7080b8 Add Parking paragraph and OpenStreetMap links
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-27 00:05:04 +02:00
d5b0a772dd Update selection style, Add Brückentag text
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 22:47:44 +02:00
0b5111dfea Implement feedback buttons
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 22:29:36 +02:00
6120b43b55 Merge branch 'main' of https://git.leafbla.de/partypages/party-template
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 21:38:03 +02:00
dfc2d315c4 Improve "More Infos" visibility on mobile
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 21:23:18 +02:00
b891091d1b Add page content, Add icons
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 18:35:16 +02:00
316c92ecd0 Fix overlap, Add memes
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 15:39:19 +02:00
822f97de4d
Fix regex
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 14:00:26 +02:00
ac757fd322
Enter matrix
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-11 13:49:15 +02:00
1046ff09e0
Fix build failing in CI due to warning
All checks were successful
continuous-integration/drone/push Build is passing
2022-10-10 20:33:36 +02:00
00a421f5d4
Change deploy path
Some checks failed
continuous-integration/drone/push Build is failing
2022-10-10 20:24:58 +02:00
11 changed files with 17299 additions and 117 deletions

View File

@ -14,7 +14,7 @@ steps:
ssh_key:
from_secret: rsync_key
source: build/
destination: pelipper@oreburgh.leafbla.de:/srv/docker/party/nginx/html/${DRONE_REPO_NAME}/
destination: pelipper@oreburgh.leafbla.de:/srv/docker/party/nginx/html/lan/
trigger:
branch:

View File

@ -4,27 +4,9 @@ The API documentation is available [here](https://party.leafbla.de/api/docs)
# CI Setup
To enable automatic CI deployment for this repository, you need to do the following:
1. Adapt `.drone.yml` such that the output is deployed to where you want to.
By default, this is `<repo-name>.party.leafbla.de`.
1. Enable CI for the repository in [drone](https://drone.eterna.leafbla.de/).
Search the repository name in the search bar (you might need to click the "sync" button to make it show up) and click "activate repository"
1. Add the required secrets so that the CI can deploy to oreburgh
- Secret Name: `rsync_key`
Secret Value: Find "pelipper ssh key" in Bitwarden and paste the content of `id_rsa`
- Secret Name: `docker_config`
Secret Value: Find "Docker registry credentials" in Bitwarden and paste the content of the note.
The next time you push to the repository, the pipeline should run.
[![Build Status](https://drone.eterna.leafbla.de/api/badges/partypages/lan-2022-10/status.svg)](https://drone.eterna.leafbla.de/partypages/lan-2022-10)
This Repository is configured to automatically deploy to [lan.party.leafbla.de](https://lan.party.leafbla.de).
# Getting Started with Create React App

16812
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",

227
src/MatrixBackground.tsx Normal file
View File

@ -0,0 +1,227 @@
import React, { useEffect, useRef } from "react";
const CHAR_WIDTH = 10; // canvas pixels
const CHAR_HEIGHT = 12; // canvas pixels
const ROWS = 50;
const COLS = 50;
const TRAIL_COUNT = 50;
const ALPHABET = (() => {
let alphabet = [];
// 0 - 9
for (let charCode = "0".charCodeAt(0); charCode <= "9".charCodeAt(0); ++charCode) {
alphabet.push(String.fromCharCode(charCode));
}
// A - Z
for (let charCode = "A".charCodeAt(0); charCode <= "Z".charCodeAt(0); ++charCode) {
alphabet.push(String.fromCharCode(charCode));
}
// https://en.wikipedia.org/wiki/Half-width_kana
for (let codePoint = 0xFF71; codePoint <= 0xFF9D; ++codePoint) {
alphabet.push(String.fromCodePoint(codePoint));
}
return alphabet;
})();
// TODO add more
const MEMES = [
"ABFAHRT",
"BALLERN",
"NO SLEEP",
"ZOCKEN ALTER",
"MEGA KRASSE LAN",
"アヤヤアヤヤ", // Ayaya Ayaya
"オマエハモウシンテイル", // Omae wa mou shindeiru
]
const NORMAL_STYLE = "#020";
const MEME_STYLE = "#050";
const HEAD_STYLE = "#555";
/**
* Likelihood of a meme to appear after each character.
*
* 0.1 means there is one meme approximately every 10 chars.
*/
const MEME_PROBABILITY = 0.02;
type Trail = {
/** Column in which the trail falls. Should be between 0 and `COLS` */
col: number,
/** Row where the head (lowest character) of the trail is. May be negative or above `ROWS`. */
head: number,
/** Number of characters to display above, including the head */
length: number,
/**
* Vertical segments of text to show in the trail body
*
* The sum of each `chars.length` should be at least `ROWS`, but may be greater.
*/
content: {
chars: string,
fillStyle: string, // TODO is there a better type for this?
}[],
}
function randomTrail(): Trail {
let content = []
let current = "";
let totalLength = 0;
while (totalLength < ROWS) {
if (Math.random() < MEME_PROBABILITY) {
// Commit the current non-meme chars
if (current.length > 0) {
content.push({
chars: current,
fillStyle: NORMAL_STYLE,
});
current = "";
}
// Pick and add a random meme
const meme = choice(MEMES);
// If the meme has spaces, replace them with random characters
// This also places a random character after the meme, ensuring
// that two memes don't come immediately after each other
for (let chunk of meme.split(" ")) {
content.push({
chars: chunk,
fillStyle: MEME_STYLE,
});
content.push({
chars: choice(ALPHABET),
fillStyle: NORMAL_STYLE,
});
}
totalLength += meme.length + 1;
} else {
// No meme, just add one random character
current += choice(ALPHABET);
totalLength += 1;
}
}
if (current.length > 0) {
content.push({
chars: current,
fillStyle: NORMAL_STYLE,
});
}
return {
col: randRange(0, COLS),
head: randRange(-1 * COLS, 0), // spawn above the screen initially
length: randRange(5, 20),
content: content
};
}
function randRange(from: number, to: number) {
return from + Math.floor((to - from + 1) * Math.random());
}
function choice<T>(candidates: T[]): T {
return candidates[randRange(0, candidates.length - 1)];
}
const MatrixBackground = () => {
const canvasRef: React.MutableRefObject<HTMLCanvasElement | null> = useRef(null);
useEffect(() => {
const canvas = canvasRef.current
if (canvas === null) return;
const ctx = canvas.getContext("2d");
if (ctx === null) throw new Error('getContext("2d") returned null!');
ctx.font = '10pt monospace';
const width = canvas.width;
const height = canvas.height;
const trails: Trail[] = new Array(TRAIL_COUNT).fill(0).map((_) => randomTrail());
const matrixEffect = () => {
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
// draw all trails
for (let trail of trails) {
const x = trail.col * CHAR_WIDTH;
let idx = 0;
// TODO this can probably be optimized
outer:
for (let item of trail.content) {
for (let char of item.chars) {
// stop drawing characters after reaching the head
if (idx > trail.head) break outer;
// skip characters behind the trail end
if (idx < trail.head - trail.length) {
idx += 1;
continue;
}
// draw the character, overwriting any that are already there
const y = idx * CHAR_HEIGHT;
ctx.fillStyle = "#000";
ctx.fillRect(x, y - CHAR_HEIGHT, CHAR_WIDTH, CHAR_HEIGHT);
ctx.fillStyle = (idx === trail.head ? HEAD_STYLE : item.fillStyle);
ctx.fillText(char, x, y);
idx += 1;
}
}
}
// update all trails
for (let i in trails) {
trails[i].head += 1;
if (trails[i].head - trails[i].length > ROWS) {
// trail is completely off-screen, generate a new one
trails[i] = randomTrail();
trails[i].head = 0;
}
}
}
const interval = setInterval(matrixEffect, 100);
return () => {
clearInterval(interval);
}
}, [canvasRef]);
return (
<canvas
ref={canvasRef}
style={{
background: '#000000',
position: 'fixed',
width: '100%',
height: '100%',
zIndex: -1,
top: '0',
left: '0',
objectFit: 'cover',
imageRendering: 'crisp-edges',
}}
width={COLS * CHAR_WIDTH}
height={ROWS * CHAR_HEIGHT}
/>
)
};
export default MatrixBackground;

View File

@ -1,44 +1,98 @@
.loading {
height: 100vh;
width: 100vw;
background-color: black;
height: 100vh;
width: 100vw;
background-color: black;
}
.App {
color: white;
font-size: calc(10px + 2vmin);
text-shadow:
-1px -1px 0.2em #000,
1px -1px 0.2em #000,
-1px 1px 0.2em #000,
1px 1px 0.2em #000;
text-align: center;
display: flex;
justify-content: center;
}
.container {
max-width: 1024px;
}
.fullheight {
min-height: 100vh;
}
.hero {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.hero-outer {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-bottom: 0.5em;
}
@media (min-width: 1024px) {
.hero-outer {
justify-content: flex-end;
}
}
h1,
h2 {
margin: 1em 0 0.1em 0;
}
p {
margin: 0.3em 0 0.3em 0;
}
.feedback {
line-height: 3em;
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
input[type="radio"] {
display: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
input[type="radio"]+label {
font-size: larger;
cursor: pointer;
padding: 0 1em 0 1em;
border-right: 0.1em solid white;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
input[type="radio"]+label:hover {
text-shadow: 0 0 1em white;
}
.App-link {
color: #61dafb;
input[type="radio"]+label:last-of-type {
border-right: none;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
input[type="radio"]:checked+label {
color: var(--selected-color);
text-shadow: 0 0 1em var(--selected-color);
text-decoration: underline;
}
#coming-yes+label {
--selected-color: #0f0;
}
#coming-maybe+label {
--selected-color: #fc0;
}
#coming-no+label {
--selected-color: #f00;
}

View File

@ -1,27 +1,122 @@
import React, { useContext } from 'react';
import logo from './logo.svg';
import React, { ChangeEvent, useContext, useState } from 'react';
import './PartyPage.css';
import { PartyContext } from './PartyContext';
import { APIEndPoint, PartyContext } from './PartyContext';
import MatrixBackground from './MatrixBackground';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleDown, faCalendarDays, faLocationDot } from '@fortawesome/free-solid-svg-icons';
import { modifySelfRequest, parseURI } from './partyApi';
const myDear = {
"m": "lieber",
"f": "liebe",
"d": "liebes",
};
export const PartyPage: React.FC = () => {
const partyContext = useContext(PartyContext);
return <div className="App" >
<header className="App-header" >
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code> src/PartyPage.tsx </code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<span>Hello {partyContext.self.name}</span>
</header>
const dear = myDear[partyContext.self.grammatical_gender];
const name = partyContext.self.name;
const party = partyContext.party;
const [comingState, setComingState] = useState(partyContext.self.coming);
// SAFETY: If this is undefined, the contextProvider already fails
// eslint-disable-next-line no-restricted-globals
const endpoint = parseURI(location.href) as APIEndPoint;
const handleSelect = async (e: ChangeEvent) => {
const value = (e.target as HTMLInputElement).value;
if (value !== "yes" && value !== "no" && value !== "maybe") {
throw new Error("received invalid value?");
}
const status = await modifySelfRequest(endpoint, { coming: value });
setComingState(status.coming);
}
let coming: string;
if (party.maybe_coming === 0) {
// exact number
if (party.definitely_coming === 0) {
coming = "Bisher hat noch niemand zugesagt."
} else if (party.definitely_coming === 1) {
coming = "Bisher hat ein Gast zugesagt."
} else {
coming = `Es haben schon ${party.definitely_coming} Gäste zugesagt.`
}
} else {
// inexact
if (party.definitely_coming === 0 && party.maybe_coming === 1) {
coming = "Bisher hat ein Gast vorläufig zugesagt."
} else if (party.definitely_coming === 0) {
coming = `Bisher haben ${party.maybe_coming} Gäste vorläufig zugesagt.`
} else {
coming = `Nach den bisherigen Zusagen kommen ${party.definitely_coming} bis ${party.definitely_coming + party.maybe_coming} Gäste.`
}
}
return <div className="App">
<MatrixBackground />
<div className='container'>
<div className='hero fullheight'>
<div className='hero-outer'></div>
<h1>Hallo {dear} {name},</h1>
<p>
am <strong> 30. Oktober </strong> wird die Uhr wieder auf Winterzeit umgestellt.
Das heißt, theoretisch könnten wir <strong> 25 Stunden an einem Tag zocken!</strong>
</p>
<p>
Das wollen wir (Kai, Dominic, Jesko) uns natürlich nicht entgehen lassen.
Also veranstalten wir eine <strong> mega krasse LAN-Party. </strong>
{coming}
</p>
<p>
Wir würden uns sehr freuen, wenn auch du, {dear} {name}, am Start wärst :)
</p>
<div className='feedback'>
<input type="radio" id="coming-yes" name="coming" value="yes" checked={comingState === "yes"} onChange={handleSelect} />
<label htmlFor='coming-yes'>Ja</label>
<input type="radio" id="coming-maybe" name="coming" value="maybe" checked={comingState === "maybe"} onChange={handleSelect} />
<label htmlFor='coming-maybe'>Vielleicht</label>
<input type="radio" id="coming-no" name="coming" value="no" checked={comingState === "no"} onChange={handleSelect} />
<label htmlFor='coming-no'>Nein</label>
</div>
<div className='hero-outer'>
Mehr Infos
<FontAwesomeIcon icon={faAngleDown} />
</div>
</div>
<div className='hero fullheight'>
<h2>Wann und Wo?</h2>
<p>
<FontAwesomeIcon icon={faCalendarDays} /> <strong>29. Oktober, 19:00</strong> bis <strong>31. Oktober</strong> irgendwann.
</p>
<p>
<FontAwesomeIcon icon={faLocationDot} /> <a href="https://www.openstreetmap.org/way/213757745"><strong>Mainzer Str. 28</strong>, 66111 Saarbrücken</a>
</p>
<p>
Ja, der 30. ist ein Sonntag.
Am 1. 11. ist aber Allerheiligen, also bietet es sich an am 31. nen Brückentag zu machen.
So hat man dann auch genug Zeit um nach den 25+ Stunden ordentlich auszuschlafen.
</p>
<h2>Alter ernsthaft 25 Stunden?</h2>
<p>
Prinzipiell ja.
Wer möchte kann sich aber gerne auf eine unserer beiden Couches zurückziehen,
oder eine Luftmatratze mitbringen, oder vorher gehen, oder später dazukommen,
oder zwischendurch nach Hause fahren...
</p>
<h2>Und was wenn ich Hunger bekomme?</h2>
<p>
Wir werden ein Curry, Chili o.Ä. kochen.
Bring aber auch gerne Snacks, Getränke, Knoblauchdip oder Kuchen mit :)
</p>
<h2>Aber wo soll ich mein Auto hinstellen?</h2>
<p>
Am Waldhaus gibt es einen kostenlosen <a href="https://www.openstreetmap.org/way/111250120">Parkplatz</a>.
Wenn du mit dem Auto kommst sag Bescheid, wir planen uns dort zu treffen und dann mit einem Auto auf Kai's Premium-Parkplatz direkt neben der WG fahren.
</p>
</div>
</div>
</div>
};

View File

@ -1,7 +1,7 @@
import { APIEndPoint, PartyStatus, SelfStatus, UpdatableSelfStatus } from "./PartyContext";
export const parseURI = (uri: string): APIEndPoint | undefined => {
const x = uri.match(/https?:\/\/(?<partyName>.+)\.party\.leafbla\.de\/(?<token>.+)/);
const x = uri.match(/https?:\/\/(?<partyName>\w+)\.(?<host>.+)\/(?<token>.+)/);
if (x === null || x.groups === undefined) return;
const partyName = x.groups["partyName"];
const token = x.groups["token"];
@ -9,7 +9,7 @@ export const parseURI = (uri: string): APIEndPoint | undefined => {
return { partyName, token };
};
const apiUrl = (apiEndPoint : APIEndPoint): string => {
const apiUrl = (apiEndPoint: APIEndPoint): string => {
return `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`;
};

32
timeline/style.css Normal file
View File

@ -0,0 +1,32 @@
:root {
--fg: red;
--bg: black;
--accent: white;
--mute: gray;
}
svg {
background-color: var(--bg);
}
.timeline {
stroke: var(--fg);
fill: none;
stroke-width: 1px;
stroke-dasharray: 314px 314px;
stroke-dashoffset: -314px;
}
.time {
stroke: var(--accent);
fill: none;
stroke-width: 0.1px;
}
.h0 {
stroke-width: 1px;
}
.h3 {
stroke-width: 0.4px;
}
.timeline-mute {
stroke: var(--mute);
fill: none;
stroke-width: 0.5px;
}

38
timeline/timeline.html Normal file
View File

@ -0,0 +1,38 @@
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<svg viewBox="0 0 260 35">
<path class="timeline-mute" d="M 5 25 L 245 25"/>
<path id="timeline" class="timeline" d="M 5 25 L 25 25 A 5 5 90 0 0 25 15 A 5 5 90 0 0 25 25 L 255 25" />
<path class="time h0" d="M 5 24 L 5 26"/>
<path class="time " d="M 15 24 L 15 26"/>
<path class="time " d="M 25 24 L 25 26"/>
<path class="time h3" d="M 35 24 L 35 26"/>
<path class="time " d="M 45 24 L 45 26"/>
<path class="time " d="M 55 24 L 55 26"/>
<path class="time h3" d="M 65 24 L 65 26"/>
<path class="time " d="M 75 24 L 75 26"/>
<path class="time " d="M 85 24 L 85 26"/>
<path class="time h3" d="M 95 24 L 95 26"/>
<path class="time " d="M 105 24 L 105 26"/>
<path class="time " d="M 115 24 L 115 26"/>
<path class="time h0" d="M 125 24 L 125 26"/>
<path class="time " d="M 135 24 L 135 26"/>
<path class="time " d="M 145 24 L 145 26"/>
<path class="time h3" d="M 155 24 L 155 26"/>
<path class="time " d="M 165 24 L 165 26"/>
<path class="time " d="M 175 24 L 175 26"/>
<path class="time h3" d="M 185 24 L 185 26"/>
<path class="time " d="M 195 24 L 195 26"/>
<path class="time " d="M 205 24 L 205 26"/>
<path class="time h3" d="M 215 24 L 215 26"/>
<path class="time " d="M 225 24 L 225 26"/>
<path class="time " d="M 235 24 L 235 26"/>
<path class="time h0" d="M 245 24 L 245 26"/>
</svg>
<script src="timeline.js" >
</script>
</body>
</html>

31
timeline/timeline.js Normal file
View File

@ -0,0 +1,31 @@
let overridetime = undefined;
//overridetime = Date.now();
// Saturday at 23:00 UTC, the event starts
const startTime = overridetime ?? Date.parse('Sat, 29 Oct 2022 23:00:00');
const timeToOffset = (hours) => {
const speed1 = -10;
const speed2 = -31;
if (hours <= 0) return 0;
if (hours >= 25) return 24*speed1 + speed2;
if (hours <= 2) {
return hours * speed1;
} else if (hours > 2 && hours < 3) {
return 2*speed1 + (hours-2) * speed2;
} else {
return 2*speed1 + speed2 + (hours-3) * speed1;
}
};
const update = () => {
const now = Date.now();
const hoursPassed = (now-startTime) / 1000 / 3600;
// test speed here
const offset = timeToOffset(hoursPassed);
const timeline = document.getElementById("timeline");
timeline.style.strokeDashoffset = 314 + offset;
};
update();
setInterval(update, 50);