Add snapshot of frontend
This commit is contained in:
parent
e68badbe5b
commit
045e45fe63
Binary file not shown.
After Width: | Height: | Size: 348 KiB |
|
@ -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.
|
||||||
|
|
181
src/App.css
181
src/App.css
|
@ -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
|
||||||
|
*/
|
||||||
|
|
71
src/App.js
71
src/App.js
|
@ -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;
|
||||||
|
|
|
@ -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};
|
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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};
|
|
@ -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};
|
Binary file not shown.
After Width: | Height: | Size: 362 KiB |
Loading…
Reference in New Issue