Compare commits

..

3 Commits
main ... main

Author SHA1 Message Date
Dominic Zimmer
ba64b97136 Add extraData example 2026-01-03 19:06:19 +01:00
Dominic Zimmer
e6319ec3cf Migrate template to react19 and new API 2026-01-02 21:23:02 +01:00
Dominic Zimmer
f105e36f15 Update party template 2023-03-23 11:08:50 +01:00
19 changed files with 6201 additions and 26404 deletions

23026
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,28 +3,19 @@
"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",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.64",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"fortawesome": "^0.0.1-security",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@types/node": "^16.18.126",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-scripts": "5.0.1",
"serve": "^14.2.1",
"typescript": "^4.8.4",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -3,12 +3,11 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Weihnachtsfeier 2025"
content="Party Invitation"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
@ -25,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Weihnachtsfeier 2025</title>
<title>Party Invitation</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,6 +1,6 @@
{
"short_name": "Weihnachtsfeier",
"name": "Weihnachtsfeier 2023",
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",

View File

@ -1,15 +1,17 @@
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react';
import { getPartyStatusRequest, getSelfStatusRequest, parseURI } from './partyApi';
import { getPartyStatusRequest, getSelfStatusRequest, modifySelfRequest, parseURI } from './partyApi';
import './PartyPage.css';
export const PartyContext = createContext<PartyContextType>({
party: { definitely_coming: 0, maybe_coming: 0 },
self: { token: "", name: "", coming: "yes", grammatical_gender: "m" }
self: { token: "", name: "", coming: "yes", grammatical_gender: "m" },
update: () => {},
});
export type PartyContextType = {
party: PartyStatus,
self: SelfStatus,
update: (u: UpdatableSelfStatus) => void,
}
export type PartyStatus = {
@ -34,6 +36,8 @@ export type APIEndPoint = { partyName: string, token: string };
// Adapt this type to your desires
export type SelfStatusExtraData = {
/* Example type: */
/* plusone: string; */
};
export const PartyContextProvider: React.FC<{ children: React.ReactNode }> = (props) => {
@ -41,7 +45,8 @@ export const PartyContextProvider: React.FC<{ children: React.ReactNode }> = (pr
const [partyContext, setPartyContext] = useState<PartyContextType>();
const apiEndpoint = useMemo<APIEndPoint>(() => {
const href = window.location.href;
// eslint-disable-next-line no-restricted-globals
const href = location.href;
const p = parseURI(href);
if (!p) return { partyName: "error", token: "" }
return p;
@ -51,7 +56,13 @@ export const PartyContextProvider: React.FC<{ children: React.ReactNode }> = (pr
if (partyContext !== undefined) return;
const selfStatus = await getSelfStatusRequest(apiEndpoint);
const partyStatus = await getPartyStatusRequest(apiEndpoint);
const ctx = { party: partyStatus, self: selfStatus };
const update = async (newData: UpdatableSelfStatus) => {
const reply = await modifySelfRequest(apiEndpoint, newData);
const ctx = { party: partyStatus, self: reply, update: update };
setPartyContext(ctx);
return partyStatus;
};
const ctx = { party: partyStatus, self: selfStatus, update: update };
setPartyContext(ctx);
}, [apiEndpoint, partyContext]);

View File

@ -1,110 +1,12 @@
.loading {
height: 100vh;
width: 100vw;
background-color: black;
.root {
margin: auto;
}
.App {
color: white;
font-size: calc(7px + 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;
.coming-yes button[data-coming="yes"]{
background-color: green;
}
.container {
max-width: 1224px;
margin-left: 3ch;
margin-right: 3ch;
.coming-maybe button[data-coming="maybe"]{
background-color: yellow;
}
.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;
}
input[type="radio"] {
display: none;
}
input[type="radio"]+label {
font-size: larger;
cursor: pointer;
padding: 0 1em 0 1em;
border-right: 0.1em solid white;
}
input[type="radio"]+label:hover {
text-shadow: 0 0 1em white;
}
input[type="radio"]+label:last-of-type {
border-right: none;
}
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;
}
.hooverdam:hover {
text-shadow: 0 0 1em white;
}
a {
color: white;
.coming-no button[data-coming="no"]{
background-color: red;
}

View File

@ -1,128 +1,52 @@
import React, { ChangeEvent, useContext, useRef, useState } from 'react';
import React, { useContext } from 'react';
import './PartyPage.css';
import { APIEndPoint, PartyContext, PartyStatus } 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": "",
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getComingString(party: PartyStatus): string {
if (party.maybe_coming === 0) {
// exact number
if (party.definitely_coming === 0) {
return "Bisher hat noch niemand zugesagt."
} else if (party.definitely_coming === 1) {
return "Bisher hat ein Gast zugesagt."
} else {
return `Es haben schon ${party.definitely_coming} Gäste zugesagt.`
}
} else {
// inexact
if (party.definitely_coming === 0 && party.maybe_coming === 1) {
return "Bisher hat ein Gast vorläufig zugesagt."
} else if (party.definitely_coming === 0) {
return `Bisher haben ${party.maybe_coming} Gäste vorläufig zugesagt.`
} else {
return `Nach den bisherigen Zusagen kommen ${party.definitely_coming} bis ${party.definitely_coming + party.maybe_coming} Gäste.`
}
}
}
import { PartyContext } from './PartyContext';
export const PartyPage: React.FC = () => {
const partyContext = useContext(PartyContext);
const dear = myDear[partyContext.self.grammatical_gender];
const name = partyContext.self.name;
const isMehrzahl = partyContext.self.grammatical_gender === "d";
const wannUndWoRef = useRef<HTMLDivElement>(null)
const executeScroll = () => {
wannUndWoRef.current!.scrollIntoView({ behavior: 'smooth' })
}
const [comingState, setComingState] = useState(partyContext.self.coming);
// SAFETY: If this is undefined, the contextProvider already fails
const endpoint = parseURI(window.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);
}
return <div className="App">
<div className='container'>
<div className='hero fullheight'>
<div className='hero-outer'></div>
<h1>Hallo {dear} {name},</h1>
<p>
wir laden {isMehrzahl ? 'euch' : 'dich'} am <strong>Freitag, den 19. Dezember</strong> herzlich ein, mit uns zu feiern!
</p>
<p>
Ab <strong>19:00</strong> veranstalten wir nämlich wieder unsere alljährliche Weihnachtsparty!
</p>
<p>
Wir würden uns sehr freuen, wenn auch {isMehrzahl ? 'ihr' : 'du'}, {dear} {name}, {isMehrzahl ? 'dabei seid.' : 'dabei bist :)'}
</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' >
<span className='hooverdam' onClick={executeScroll}>
<p>Mehr Infos</p>
<FontAwesomeIcon icon={faAngleDown} />
</span>
</div>
</div>
<div className='hero fullheight' ref={wannUndWoRef}>
<h2>Wann und Wo?</h2>
<p>
<FontAwesomeIcon icon={faCalendarDays} /> <strong>&nbsp;19. Dezember, ab 19:00</strong>. Bitte kommt nicht all zu spät.
</p>
<p>
<FontAwesomeIcon icon={faLocationDot} /> <strong>&nbsp;Gebäude E1 1, Raum 407</strong>, Universität des Saarlandes
</p>
<h2>Was ist geplant?</h2>
<p>
Wir (Iona, Sebastian &amp; Simon) wollen uns mit euch auf Weihnachten einstimmen.
Wir planen ein weihnachtliches Programm mit Geschichten, Liedern, Essen und der einen oder anderen Überraschung. Falls du ein Instrument spielst, bring es auch gerne mit.
</p>
<p>
Natürlich haben wir auch genügend Zeit, uns gemütlich bei einem Heißgetränk zu unterhalten.
</p>
<h2>Was gibt es zu Essen?</h2>
<p>
Wir planen ein Potluck-Event. Damit sollte für alle genug Essen dabei sein.
Getränke, insbesondere Glühwein, Kinderpunsch und Ähnliches, wird von uns organisiert.
</p>
<h2>Was soll ich mitbringen?</h2>
<p>
Es wäre super, wenn du zum Potluck ein Gericht mitbringen kannst. Für Inspirationen kannst du uns natürlich gerne fragen, oder schon einmal <a href="https://www.tasteofhome.com/collection/vegetarian-potluck-recipes/">hier</a> vorbeischauen.
Bitte informiere uns kurz, was du gerne mitbringen würdest, damit wir besser kalkulieren können.
Ansonsten darfst du auch sehr gerne Plätzchen, Weihnachtsdeko oder Ähnliches mitbringen.
</p>
<p>
Falls du ansonsten Wünsche oder Anregungen für einen gelungenen Abend hast, teil uns diese gerne mit!
</p>
const {self: guest, update: updateGuest} = useContext(PartyContext);
return <div>
<p>
<span>Hello {guest.name}</span>
</p>
<p>
Please come to the party!
</p>
<p>
Are you coming?
<div className={`coming-${guest.coming}`}>
<button data-coming={"yes"} onClick={() => updateGuest({coming: "yes"})}>
Yes
</button>
<button data-coming={"maybe"} onClick={() => updateGuest({coming: "maybe"})}>
Maybe
</button>
<button data-coming={"no"} onClick={() => updateGuest({coming: "no"})}>
No
</button>
</div>
</p>
{/*
Example usage of extraData:
<div>
<button onClick={() => {
const lastNum = parseInt(selfStatus.extra?.["plusone"] ?? "NaN");
if (isNaN(lastNum) || lastNum <= 0) return;
update({extra:{plusone: "" + (lastNum - 1)}});
}}>
-1
</button>
<button onClick={() => update({extra:{plusone: "1"}})}>
Du bringst {selfStatus.extra?.["plusone"] ?? "undefined"} Plus-Eins
</button>
<button onClick={() => {
const lastNum = parseInt(selfStatus.extra?.["plusone"] ?? "NaN");
if (isNaN(lastNum)) return;
update({extra:{plusone: "" + (lastNum + 1)}});
}}>
+1
</button>
</div>
*/}
</div>
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -1,4 +1,7 @@
body {
height: 100%;
width: 100%;
display: flex;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@ -11,14 +14,3 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
body {
background-image: url('./background.gif');
background-size: cover;
background-position: center;
background-attachment: fixed;
height: 100vh;
padding:0;
margin:0;
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,21 +1,16 @@
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?:\/\/[^/]+\/(?<token>.+)/);
const x = uri.match(/https?:\/\/(?<partyName>\w+)\.(?<host>.+)\/(?<token>.+)/);
if (x === null || x.groups === undefined) return;
// const partyName = x.groups["partyName"];
const partyName = "xmas";
const partyName = x.groups["partyName"];
const token = x.groups["token"];
if (!partyName || !token) return;
return { partyName, token };
};
const apiUrl = (apiEndPoint : APIEndPoint): string => {
let a = `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`;
console.log(a);
return a;
const apiUrl = (apiEndPoint: APIEndPoint): string => {
return `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`;
};
export const getSelfStatusRequest = async (apiEndpoint: APIEndPoint): Promise<SelfStatus> => {

View File

@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

9200
yarn.lock

File diff suppressed because it is too large Load Diff