Compare commits

..

7 Commits

Author SHA1 Message Date
dfc2d315c4 Improve "More Infos" visibility on mobile 2022-10-11 21:23:18 +02:00
b891091d1b Add page content, Add icons 2022-10-11 18:35:16 +02:00
316c92ecd0 Fix overlap, Add memes 2022-10-11 15:39:19 +02:00
822f97de4d Fix regex 2022-10-11 14:00:26 +02:00
ac757fd322 Enter matrix 2022-10-11 13:49:15 +02:00
1046ff09e0 Fix build failing in CI due to warning 2022-10-10 20:33:36 +02:00
00a421f5d4 Change deploy path 2022-10-10 20:24:58 +02:00
14 changed files with 17417 additions and 6203 deletions

View File

@@ -14,7 +14,7 @@ steps:
ssh_key: ssh_key:
from_secret: rsync_key from_secret: rsync_key
source: build/ 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: trigger:
branch: branch:

View File

@@ -4,27 +4,9 @@ The API documentation is available [here](https://party.leafbla.de/api/docs)
# CI Setup # CI Setup
To enable automatic CI deployment for this repository, you need to do the following: [![Build Status](https://drone.eterna.leafbla.de/api/badges/partypages/lan-2022-10/status.svg)](https://drone.eterna.leafbla.de/partypages/lan-2022-10)
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.
This Repository is configured to automatically deploy to [lan.party.leafbla.de](https://lan.party.leafbla.de).
# Getting Started with Create React App # Getting Started with Create React App

23086
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,26 @@
"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/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^16.18.126", "@types/node": "^16.11.64",
"@types/react": "19.2.7", "@types/react": "^18.0.21",
"@types/react-dom": "19.2.3", "@types/react-dom": "^18.0.6",
"react": "19.2.3", "react": "^18.2.0",
"react-dom": "19.2.3", "react-dom": "^18.2.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "^4.9.5", "typescript": "^4.8.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {

View File

@@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Party Invitation" content="Web site created using create-react-app"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
@@ -24,7 +24,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>Party Invitation</title> <title>React App</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>

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,17 +1,15 @@
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react'; import React, { createContext, useEffect, useMemo, useState } from 'react';
import { getPartyStatusRequest, getSelfStatusRequest, modifySelfRequest, parseURI } from './partyApi'; import { getPartyStatusRequest, getSelfStatusRequest, parseURI } from './partyApi';
import './PartyPage.css'; import './PartyPage.css';
export const PartyContext = createContext<PartyContextType>({ export const PartyContext = createContext<PartyContextType>({
party: { definitely_coming: 0, maybe_coming: 0 }, 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 = { export type PartyContextType = {
party: PartyStatus, party: PartyStatus,
self: SelfStatus, self: SelfStatus,
update: (u: UpdatableSelfStatus) => void,
} }
export type PartyStatus = { export type PartyStatus = {
@@ -22,22 +20,15 @@ export type PartyStatus = {
export type SelfStatus = { export type SelfStatus = {
token: string, token: string,
name: string, name: string,
coming: "yes" | "no" | "maybe" | null, coming: "yes" | "no" | "maybe",
"grammatical_gender": "m" | "f" | "d", "grammatical_gender": "m" | "f" | "d",
extra?: SelfStatusExtraData, extra?: SelfStatusExtraData,
} }
export type UpdatableSelfStatus = {
coming?: "yes" | "no" | "maybe" | null,
extra?: SelfStatusExtraData,
}
export type APIEndPoint = { partyName: string, token: string }; export type APIEndPoint = { partyName: string, token: string };
// Adapt this type to your desires // Adapt this type to your desires
export type SelfStatusExtraData = { export type SelfStatusExtraData = {
/* Example type: */
/* plusone: string; */
}; };
export const PartyContextProvider: React.FC<{ children: React.ReactNode }> = (props) => { export const PartyContextProvider: React.FC<{ children: React.ReactNode }> = (props) => {
@@ -52,27 +43,22 @@ export const PartyContextProvider: React.FC<{ children: React.ReactNode }> = (pr
return p; return p;
}, []); }, []);
const loadData = useCallback(async () => { const loadData = async () => {
if (partyContext !== undefined) return; if (partyContext !== undefined) return;
const selfStatus = await getSelfStatusRequest(apiEndpoint); const selfStatus = await getSelfStatusRequest(apiEndpoint);
const partyStatus = await getPartyStatusRequest(apiEndpoint); const partyStatus = await getPartyStatusRequest(apiEndpoint);
const update = async (newData: UpdatableSelfStatus) => { const ctx = { party: partyStatus, self: selfStatus };
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); setPartyContext(ctx);
}, [apiEndpoint, partyContext]); };
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [apiEndpoint, loadData]); // eslint-disable-next-line
}, [apiEndpoint]);
return partyContext ? return partyContext ?
<PartyContext.Provider value={partyContext}> <PartyContext.Provider value={partyContext}>
{props.children} {props.children}
</PartyContext.Provider> </PartyContext.Provider>
: <div className="loading" />; : <div className="loading" />;
}; };

View File

@@ -1,12 +1,56 @@
.root { .loading {
margin: auto; height: 100vh;
width: 100vw;
background-color: black;
} }
.coming-yes button[data-coming="yes"]{
background-color: green; .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;
} }
.coming-maybe button[data-coming="maybe"]{
background-color: yellow; .container {
max-width: 1024px;
} }
.coming-no button[data-coming="no"]{
background-color: red; .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;
}

View File

@@ -1,52 +1,93 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import './PartyPage.css'; import './PartyPage.css';
import { PartyContext } from './PartyContext'; import { PartyContext } from './PartyContext';
import MatrixBackground from './MatrixBackground';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleDown, faCalendarDays, faLocationDot } from '@fortawesome/free-solid-svg-icons';
const myDear = {
"m": "lieber",
"f": "liebe",
"d": "liebes",
};
export const PartyPage: React.FC = () => { export const PartyPage: React.FC = () => {
const {self: guest, update: updateGuest} = useContext(PartyContext); const partyContext = useContext(PartyContext);
const dear = myDear[partyContext.self.grammatical_gender];
const name = partyContext.self.name;
const party = partyContext.party;
return <div> let coming: string;
<p> if (party.maybe_coming === 0) {
<span>Hello {guest.name}</span> // exact number
</p> if (party.definitely_coming === 0) {
<p> coming = "Bisher hat noch niemand zugesagt."
Please come to the party! } else if (party.definitely_coming === 1) {
</p> coming = "Bisher hat ein Gast zugesagt."
<p> } else {
Are you coming? coming = `Es haben schon ${party.definitely_coming} Gäste zugesagt.`
<div className={`coming-${guest.coming}`}> }
<button data-coming={"yes"} onClick={() => updateGuest({coming: "yes"})}> } else {
Yes // inexact
</button> if (party.definitely_coming === 0 && party.maybe_coming === 1) {
<button data-coming={"maybe"} onClick={() => updateGuest({coming: "maybe"})}> coming = "Bisher hat ein Gast vorläufig zugesagt."
Maybe } else if (party.definitely_coming === 0) {
</button> coming = `Bisher haben ${party.maybe_coming} Gäste vorläufig zugesagt.`
<button data-coming={"no"} onClick={() => updateGuest({coming: "no"})}> } else {
No coming = `Nach den bisherigen Zusagen kommen ${party.definitely_coming} bis ${party.definitely_coming + party.maybe_coming} Gäste.`
</button> }
}
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'>
<button>Auf gehts</button>
<button>Hmm vielleicht</button>
<button>Nee sorry</button>
</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} /> <strong>Mainzer Str. 28</strong>, 66111 Saarbrücken
</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>
</div> </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>
*/}
</div> </div>
}; };

View File

@@ -1,7 +1,4 @@
body { body {
height: 100%;
width: 100%;
display: flex;
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -13,4 +10,4 @@ body {
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }

1
src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,4 +1,4 @@
import { APIEndPoint, PartyStatus, SelfStatus, UpdatableSelfStatus } from "./PartyContext"; import { APIEndPoint, PartyStatus, SelfStatus } from "./PartyContext";
export const parseURI = (uri: string): APIEndPoint | undefined => { export const parseURI = (uri: string): APIEndPoint | undefined => {
const x = uri.match(/https?:\/\/(?<partyName>\w+)\.(?<host>.+)\/(?<token>.+)/); const x = uri.match(/https?:\/\/(?<partyName>\w+)\.(?<host>.+)\/(?<token>.+)/);
@@ -22,14 +22,7 @@ export const getSelfStatusRequest = async (apiEndpoint: APIEndPoint): Promise<Se
export const getPartyStatusRequest = async (apiEndpoint: APIEndPoint): Promise<PartyStatus> => { export const getPartyStatusRequest = async (apiEndpoint: APIEndPoint): Promise<PartyStatus> => {
const result = await fetch(`${apiUrl(apiEndpoint)}/status`); const result = await fetch(`${apiUrl(apiEndpoint)}/status`);
if (!result.ok) throw new Error("Error sending getPartyRequest"); if (!result.ok) throw new Error("Error sending getSelfRequest");
const data = await result.json(); const data = await result.json();
return data as PartyStatus; return data as PartyStatus;
}; };
export const modifySelfRequest = async (apiEndpoint: APIEndPoint, payload: UpdatableSelfStatus): Promise<SelfStatus> => {
const result = await fetch(`${apiUrl(apiEndpoint)}/me`, { method: "PATCH", body: JSON.stringify(payload), headers: { "Content-Type": "application/json" } });
if (!result.ok) throw new Error("Error sending modifySelfRequest");
const data = await result.json();
return data as SelfStatus;
};

15
src/reportWebVitals.ts Normal file
View File

@@ -0,0 +1,15 @@
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;

5
src/setupTests.ts Normal file
View File

@@ -0,0 +1,5 @@
// 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';