Add snapshot of frontend

This commit is contained in:
Peter J. Holzer 2022-02-26 22:32:58 +01:00 committed by Peter J. Holzer
parent e68badbe5b
commit 045e45fe63
18 changed files with 471 additions and 32 deletions

BIN
public/guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -4,17 +4,6 @@
<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="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.

View File

@ -1,5 +1,19 @@
import Background from './guitar.png'
.App { .App {
text-align: center; }
cite {
display: block;
}
cite::before {
content: "—";
}
th {
text-align: right;
} }
.App-logo { .App-logo {
@ -36,3 +50,168 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.App {
display: grid;
}
#the-quote {
grid-column: 2;
}
.intro {
grid-column: 1;
grid-row: 1;
}
#country-list {
grid-column: 1 / 3;
grid-row: 2;
}
#permalink {
grid-column: 2;
grid-row: 1;
margin-left: auto;
}
#summary {
grid-column: 1;
grid-row: 1;
}
@media (max-width: 40rem) {
#the-quote {
grid-column: 2;
}
.intro {
grid-column: 1 / 3;
grid-row: 2;
}
#country-list {
grid-column: 1 / 3;
grid-row: 3;
}
#permalink {
grid-column: 2;
grid-row: 1;
margin-left: auto;
}
#summary {
grid-column: 1 /3;
grid-row: 2;
}
}
.intro {
margin: 3em;
font-family: MontserratAlternates;
font-weight: 300;
font-size: 1.2rem;
line-height: 1.7rem;
}
#country-list input {
margin: 0.2em 0.5em;
}
input:focus {
box-shadow: 0 0 0.2em 0.2em hsl(240 100% 70%);
}
@font-face {
font-family: Philosopher;
src: url(Philosopher-Regular.ttf);
}
@font-face {
font-family: Philosopher;
src: url(Philosopher-Bold.ttf);
}
@font-face {
font-family: MontserratAlternates;
src: url(MontserratAlternates-Light.ttf);
font-weight: 300;
}
@font-face {
font-family: MontserratAlternates;
src: url(MontserratAlternates-Regular.ttf);
font-weight: 400;
}
@font-face {
font-family: MontserratAlternates;
src: url(MontserratAlternates-Medium.ttf);
font-weight: 500;
}
@font-face {
font-family: MontserratAlternates;
src: url(MontserratAlternates-LightItalic.ttf);
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: MontserratAlternates;
src: url(MontserratAlternates-MediumItalic.ttf);
font-weight: 500;
font-style: italic;
}
th {
vertical-align: baseline;
font-family: MontserratAlternates;
font-weight: 400;
font-color: #444;
}
td {
vertical-align: baseline;
}
body {
background-image: url("guitar.png");
background-size: contain;
background-repeat: no-repeat;
background-attachment: fixed;
text-shadow: 0 0 1.0em #FFFFFF,
0 0 0.2em #FFFFFF;
font-family: MontserratAlternates;
font-weight: 300;
}
.band {
font-family: MontserratAlternates;
font-weight: 300;
white-space: nowrap;
}
.band.by_user {
font-weight: 500;
font-style: italic;
}
table {
border-spacing: 1em 0.3em;
}
#permalink a {
text-decoration: none;
color: #444;
padding: 0.5em;
border: 1px dotted black;
}
#summary {
margin-left: 3em;
}
/*
vim: sw=4
*/

View File

@ -1,25 +1,62 @@
import logo from './logo.svg'; import React, {Component} from 'react'
import {v4 as uuidv4} from 'uuid';
import './App.css'; import './App.css';
function App() { import { Questionaire } from "./Questionaire.js";
import { Stats } from "./Stats.js";
const apiBase = "http://localhost:8000";
class App extends Component {
state = {
screen: "questionaire",
uid: uuidv4(),
lang: this.getPreferredLanguage(),
};
render() {
console.log(this.state.screen);
let screen = false;
if (this.state.screen == "questionaire") {
screen = <Questionaire uid={this.state.uid} lang={this.state.lang}
show={this.showStats.bind(this)}
apiBase={apiBase} />
} else if (this.state.screen == "stats") {
screen = <Stats puid={this.state.uid.substr(24)} lang={this.state.lang} apiBase={apiBase} />
} else {
screen = <div className="error">Impossible</div>
}
return ( return (
<div className="App"> <div className="App">
<header className="App-header"> {screen}
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div> </div>
); )
}
getPreferredLanguage() {
const availableLanguages = ["en", "de"];
for (let lng of navigator.languages) {
const preferredLanguage = availableLanguages.find(x => lng.startsWith(x));
if (preferredLanguage) {
return preferredLanguage;
}
}
return availableLanguages[0];
}
showStats() {
console.log("in showStats");
this.setState({screen: "stats"});
}
componentDidMount() {
console.log("in componentDidMount");
console.log("window.location", window.location);
const pm = window.location.pathname.match("^/(stats)/([0-9a-f]+)");
if (pm) {
this.setState({screen: pm[1], uid: "00000000-0000-0000-0000-" + pm[2]})
}
}
} }
export default App; export default App;

45
src/CountryInput.js Normal file
View File

@ -0,0 +1,45 @@
import {Component} from 'react'
class CountryInput extends Component {
state = {
bands: []
};
render() {
let bandInputs = [];
for (let i = 0; i <= this.state.bands.length; i++) { // sic!
const id = `${this.props.country.code}/${i}`;
const value = this.state.bands[i] || "";
bandInputs[i] = <input key={i} id={id} value={value}
onChange={this.handleChange}
onBlur={this.handleBlur} />
}
return (
<tr key={this.props.country.code}>
<th> {this.props.country.name} </th>
<td> {bandInputs} </td>
</tr>
);
}
handleChange = (e) => {
const id = e.target.id;
const i = +id.split("/")[1];
let newBands = [];
newBands[i] = e.target.value;
for (let j = 0; j < this.state.bands.length; j++) {
if (i !== j) {
newBands[j] = this.state.bands[j];
}
}
this.setState({bands: newBands});
}
handleBlur = (e) => {
console.log(e);
this.props.updateCountryBands(this.props.country.code, this.state.bands);
}
}
export {CountryInput};

13
src/Intro.js Normal file
View File

@ -0,0 +1,13 @@
import {Component} from 'react'
class Intro extends Component {
render() {
const intro = {
de: "Alle Länder dieser Erde — welche Bands oder Solo-Musiker aus diesen Ländern kennst Du?",
en: "All the countries in the world — which bands or solo musicians from these countries do you know?"
}
return <div className="intro">{intro[this.props.lang]}</div>
}
}
export {Intro};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/Philosopher-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
src/Philosopher-Italic.ttf Normal file

Binary file not shown.

BIN
src/Philosopher-Regular.ttf Normal file

Binary file not shown.

101
src/Questionaire.js Normal file
View File

@ -0,0 +1,101 @@
import {Component} from 'react'
import { CountryInput } from "./CountryInput.js";
import { Intro } from "./Intro.js";
class Questionaire extends Component {
state = {
countries: [
{ code: "ch", name: "China" },
{ code: "in", name: "Indien" },
{ code: "us", name: "Vereinigte Staaten von Amerika" },
{ code: "id", name: "Indonesien" },
{ code: "br", name: "Brasilien" },
{ code: "pk", name: "Pakistan" },
{ code: "ng", name: "Nigeria" },
{ code: "bg", name: "Bangladesch" },
{ code: "ru", name: "Russland" },
{ code: "mx", name: "Mexiko" },
],
};
render() {
const rows = this.state.countries.map(
(row, index) => {
return (
<CountryInput key={row.code} country={row}
updateCountryBands={this.updateCountryBands.bind(this)} />
);
}
);
const showTexts = {
"en": "Show",
"de": "Aufdecken",
};
const showText = showTexts[this.props.lang];
return (
<>
<blockquote id="the-quote">
I don't need a rationale<br/>
to sing the Internationale
<cite>
They might be giants
</cite>
</blockquote>
<Intro lang={this.props.lang}/>
<table id="country-list">
<tbody>
{rows}
</tbody>
</table>
<button onClick={this.props.show}>
{showText}
</button>
</>
);
}
updateCountryBands(code, bands) {
console.log(this);
this.setState({
bands: {...this.state.bands, [code]: bands}
});
fetch(this.props.apiBase + "/update", {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({uuid: this.props.uid, country: code, bands: bands}),
})
.then((result) => result.json())
.then((result) => {
this.setState({apiResult: result });
})
.catch((error) => {
console.log(error);
this.setState({apiResult: error });
});
}
getCountries() {
fetch(this.props.apiBase + "/countries/" + this.props.lang, {
headers: {
"Content-Type": "application/json;charset=utf-8",
},
}
)
.then((result) => result.json())
.then((result) => {
this.setState({countries: result});
})
.catch((error) => {
this.setState({apiResult: error });
})
}
componentDidMount() {
this.getCountries();
}
}
export {Questionaire};

75
src/Stats.js Normal file
View File

@ -0,0 +1,75 @@
import {Component} from 'react'
class Stats extends Component {
state = {
stats: {
countries: []
}
};
render() {
console.log("in Stats.render");
console.log("in Stats.render: stats = ", this.state.stats);
let rows = [];
for (let country of this.state.stats.countries) {
let bands = []
let k=0
for (let band of country.bands) {
k++;
let n;
if (band.by_user) {
n = <span key={k} className="band by_user">{band.name}</span>
} else {
n = <span key={k} className="band">{band.name}</span>
}
if (bands.length > 0) {
bands.push(", ")
}
bands.push(n)
}
rows.push(<tr key={country.code}><th>{country.name}</th><td>{bands}</td></tr>);
}
let summary;
if (this.props.lang == "de") {
summary = (
<p id="summary">
Du hast Bands aus {this.state.stats.country_count} Ländern genannt,
Damit bist Du auf Platz {this.state.stats.country_rank} von {this.state.stats.user_count}.
</p>
);
} else if (this.props.lang == "en") {
summary = (
<p id="summary">
You submitted bands from {this.state.stats.country_count} countries,
which puts you at place {this.state.stats.country_rank} of {this.state.stats.user_count}.
</p>
);
}
const linkSymbol = "\u{1F517}\u{FE0E}"
return [
<div id="permalink"><a href={"/stats/" + this.props.puid}>{linkSymbol} Permalink</a></div>,
summary,
<table id="country-list">{rows}</table>
];
}
componentDidMount() {
console.log("in componentDidMount")
fetch(this.props.apiBase + `/stats/${this.props.puid}/${this.props.lang}`, {
method: "GET",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
})
.then((result) => result.json())
.then((result) => {
this.setState({stats: result });
})
.catch((error) => {
console.log(error);
this.setState({apiResult: error });
});
}
}
export {Stats};

BIN
src/guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB