Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db3a8d6b15 | |||
| 88d806c0e4 | |||
| 52ca7a2b0f | |||
| f942c73b02 | |||
| 07507d46e0 | |||
| e116fee53a | |||
| a313e09410 | |||
| e2e6799f49 | |||
| 41598ffa32 | |||
| 3df1360c66 | |||
| 95ed928f18 | |||
| 9f643fae78 | |||
| 8262e1fa5b | |||
| 171d710bac | |||
|
|
2e1f63c5f2 | ||
|
|
9a227a1e47 | ||
| 59901b4c92 | |||
| 9285d46840 | |||
| 21faf7b043 | |||
| c25d746ecb | |||
| 8a5a7cc732 | |||
| 303aa556cf | |||
|
|
45f6c958fe |
16692
package-lock.json
generated
@ -3,6 +3,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"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/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@ -10,9 +13,11 @@
|
|||||||
"@types/node": "^16.11.64",
|
"@types/node": "^16.11.64",
|
||||||
"@types/react": "^18.0.21",
|
"@types/react": "^18.0.21",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"fortawesome": "^0.0.1-security",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"serve": "^14.2.1",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
public/background.gif
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/background.mp4
Normal file
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
@ -3,11 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-react-app"
|
content="Weihnachtsfeier 2025"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<!--
|
<!--
|
||||||
@ -24,7 +25,7 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
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`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>Weihnachtsfeier 2025</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 45 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "Weihnachtsfeier",
|
||||||
"name": "Create React App Sample",
|
"name": "Weihnachtsfeier 2023",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
|||||||
@ -41,8 +41,7 @@ export const PartyContextProvider: React.FC<{ children: React.ReactNode }> = (pr
|
|||||||
const [partyContext, setPartyContext] = useState<PartyContextType>();
|
const [partyContext, setPartyContext] = useState<PartyContextType>();
|
||||||
|
|
||||||
const apiEndpoint = useMemo<APIEndPoint>(() => {
|
const apiEndpoint = useMemo<APIEndPoint>(() => {
|
||||||
// eslint-disable-next-line no-restricted-globals
|
const href = window.location.href;
|
||||||
const href = location.href;
|
|
||||||
const p = parseURI(href);
|
const p = parseURI(href);
|
||||||
if (!p) return { partyName: "error", token: "" }
|
if (!p) return { partyName: "error", token: "" }
|
||||||
return p;
|
return p;
|
||||||
|
|||||||
@ -4,41 +4,107 @@
|
|||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.App {
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1224px;
|
||||||
|
margin-left: 3ch;
|
||||||
|
margin-right: 3ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-logo {
|
input[type="radio"] {
|
||||||
height: 40vmin;
|
display: none;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
input[type="radio"]+label {
|
||||||
.App-logo {
|
font-size: larger;
|
||||||
animation: App-logo-spin infinite 20s linear;
|
cursor: pointer;
|
||||||
}
|
padding: 0 1em 0 1em;
|
||||||
|
border-right: 0.1em solid white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-header {
|
input[type="radio"]+label:hover {
|
||||||
background-color: #282c34;
|
text-shadow: 0 0 1em white;
|
||||||
min-height: 100vh;
|
}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
input[type="radio"]+label:last-of-type {
|
||||||
align-items: center;
|
border-right: none;
|
||||||
justify-content: center;
|
}
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
|
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;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,27 +1,128 @@
|
|||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React, { ChangeEvent, useContext, useRef, useState } from 'react';
|
||||||
import logo from './logo.svg';
|
|
||||||
import './PartyPage.css';
|
import './PartyPage.css';
|
||||||
import { PartyContext } from './PartyContext';
|
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.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const PartyPage: React.FC = () => {
|
export const PartyPage: React.FC = () => {
|
||||||
const partyContext = useContext(PartyContext);
|
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">
|
return <div className="App">
|
||||||
<header className="App-header" >
|
<div className='container'>
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
<div className='hero fullheight'>
|
||||||
|
<div className='hero-outer'></div>
|
||||||
|
<h1>Hallo {dear} {name},</h1>
|
||||||
<p>
|
<p>
|
||||||
Edit <code> src/PartyPage.tsx </code> and save to reload.
|
wir laden {isMehrzahl ? 'euch' : 'dich'} am <strong>Freitag, den 19. Dezember</strong> herzlich ein, mit uns zu feiern!
|
||||||
</p>
|
</p>
|
||||||
<a
|
<p>
|
||||||
className="App-link"
|
Ab <strong>19:00</strong> veranstalten wir nämlich wieder unsere alljährliche Weihnachtsparty!
|
||||||
href="https://reactjs.org"
|
</p>
|
||||||
target="_blank"
|
<p>
|
||||||
rel="noopener noreferrer"
|
Wir würden uns sehr freuen, wenn auch {isMehrzahl ? 'ihr' : 'du'}, {dear} {name}, {isMehrzahl ? 'dabei seid.' : 'dabei bist :)'}
|
||||||
>
|
</p>
|
||||||
Learn React
|
<div className='feedback'>
|
||||||
</a>
|
<input type="radio" id="coming-yes" name="coming" value="yes" checked={comingState === "yes"} onChange={handleSelect} />
|
||||||
<span>Hello {partyContext.self.name}</span>
|
<label htmlFor='coming-yes'>Ja</label>
|
||||||
</header>
|
<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> 19. Dezember, ab 19:00</strong>. Bitte kommt nicht all zu spät.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FontAwesomeIcon icon={faLocationDot} /> <strong> Gebäude E1 1, Raum 407</strong>, Universität des Saarlandes
|
||||||
|
</p>
|
||||||
|
<h2>Was ist geplant?</h2>
|
||||||
|
<p>
|
||||||
|
Wir (Iona, Sebastian & 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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
BIN
src/background.gif
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
@ -11,3 +11,14 @@ code {
|
|||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-image: url('./background.gif');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
padding:0;
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
import { APIEndPoint, PartyStatus, SelfStatus, UpdatableSelfStatus } from "./PartyContext";
|
import { APIEndPoint, PartyStatus, SelfStatus, UpdatableSelfStatus } from "./PartyContext";
|
||||||
|
|
||||||
export const parseURI = (uri: string): APIEndPoint | undefined => {
|
export const parseURI = (uri: string): APIEndPoint | undefined => {
|
||||||
const x = uri.match(/https?:\/\/(?<partyName>.+)\.party\.leafbla\.de\/(?<token>.+)/);
|
// const x = uri.match(/https?:\/\/(?<partyName>.+)\.party\.leafbla\.de\/(?<token>.+)/);
|
||||||
|
const x = uri.match(/https?:\/\/[^/]+\/(?<token>.+)/);
|
||||||
if (x === null || x.groups === undefined) return;
|
if (x === null || x.groups === undefined) return;
|
||||||
const partyName = x.groups["partyName"];
|
// const partyName = x.groups["partyName"];
|
||||||
|
const partyName = "xmas";
|
||||||
const token = x.groups["token"];
|
const token = x.groups["token"];
|
||||||
if (!partyName || !token) return;
|
if (!partyName || !token) return;
|
||||||
return { partyName, token };
|
return { partyName, token };
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiUrl = (apiEndPoint : APIEndPoint): string => {
|
const apiUrl = (apiEndPoint : APIEndPoint): string => {
|
||||||
return `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`;
|
|
||||||
|
let a = `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`;
|
||||||
|
console.log(a);
|
||||||
|
return a;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSelfStatusRequest = async (apiEndpoint: APIEndPoint): Promise<SelfStatus> => {
|
export const getSelfStatusRequest = async (apiEndpoint: APIEndPoint): Promise<SelfStatus> => {
|
||||||
|
|||||||