1
0
Fork 0
mirror of https://github.com/voltbonn/qrcode.volt.link.git synced 2024-05-11 07:21:27 +00:00

added basic react stuff

This commit is contained in:
thomasrosen 2021-06-30 02:23:43 +02:00
parent 9016ee1943
commit 687bcad53d
34 changed files with 1423 additions and 0 deletions

View file

@ -0,0 +1,50 @@
# This is a workflow to deploy the react code to GitHub Pages.
name: Deploy to Uberspace via GitHub Pages
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ main ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
name: Build and Deploy
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '12'
check-latest: true
- name: Install Dependencies
run: yarn
- name: Add gh-pages as a dev-dependency
run: yarn add --dev gh-pages
- name: Build ReactJS App
run: yarn build
- name: Deploy to GitHub Pages
uses: Cecilapp/GitHub-Pages-deploy@3.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
email: thomas.rosen@me.com
build_dir: build
cname: ""
- name: run deploy commands on uberspace
uses: garygrossgarten/github-action-ssh@release
with:
command: |
cd ~/qrcode.volt.link/ && git checkout gh-pages && git pull && supervisorctl restart qrcode_volt_link;
host: maury.uberspace.de
username: volteu
password: ${{ secrets.UBERSPACE_PASSWORD_VOLTEU }}

70
README-react.md Normal file
View file

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

53
package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "qrcode.volt.link",
"version": "0.1.0",
"private": true,
"dependencies": {
"@fluent/bundle": "^0.16.1",
"@fluent/langneg": "^0.5.2",
"@fluent/react": "^0.13.1",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"intl-pluralrules": "^1.2.2",
"iso-639-1": "^2.1.9",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.2",
"react-inlinesvg": "^2.3.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"uuid": "^8.3.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"source-map-explorer": "^2.5.2"
}
}

21
public/Ubuntu/index.css Normal file
View file

@ -0,0 +1,21 @@
/* ubuntu-regular - latin */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local(''),
url('./ubuntu-v15-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('./ubuntu-v15-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* ubuntu-700 - latin */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 700;
font-display: swap;
src: local(''),
url('./ubuntu-v15-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('./ubuntu-v15-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

39
public/index.html Normal file
View file

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<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="#502379" />
<meta
name="description"
content="Generate QR-Codes for Volt Europa."
/>
<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.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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>Volt QR-Code Generator</title>
<link rel="stylesheet" href="%PUBLIC_URL%/Ubuntu/index.css" type="text/css" />
<link rel="preload" href="%PUBLIC_URL%/Ubuntu/ubuntu-v15-latin-regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="%PUBLIC_URL%/Ubuntu/ubuntu-v15-latin-700.woff2" as="font" type="font/woff2" crossorigin />
<script async defer data-website-id="1c499366-299a-410b-9dea-ee9ee12977a2" src="https://umami.qiekub.org/umami.js" data-domains="qrcode.volt.link"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

25
public/manifest.json Normal file
View file

@ -0,0 +1,25 @@
{
"short_name": "QR-Code",
"name": "Volt QR-Code Generator",
"icons": [
{
"src": "volt-logo-white-64.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/png"
},
{
"src": "volt-logo-white-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "volt-logo-white-512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "browser",
"theme_color": "#502379",
"background_color": "#502379"
}

3
public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,006 B

15
src/components/Header.js Normal file
View file

@ -0,0 +1,15 @@
import classes from './Header.module.css'
export default function Header({ title, rightActions, notificationBanner }) {
return <header className={classes.header}>
<div className={classes.headerBar}>
<h2>{title}</h2>
{rightActions}
</div>
{
!!notificationBanner
? <div className={classes.notificationBanner}>{notificationBanner}</div>
: null
}
</header>
}

View file

@ -0,0 +1,44 @@
.header {
z-index: 9999;
position: sticky;
--minus_basis_x4: calc(-1 * var(--basis_x4));
margin: var(--minus_basis_x4) var(--minus_basis_x4) var(--basis_x4) var(--minus_basis_x4);
top: 0;
background: var(--background);
color: var(--on-background);
}
@supports(display: grid) {
.header {
background: rgba(var(--background-rgb), var(--alpha-more));
backdrop-filter: blur(var(--blur));
}
}
.header .headerBar {
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
padding: var(--basis_x2) var(--basis_x4);
gap: var(--basis_x4);
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.header .headerBar h2{
flex-grow: 1;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
}
.header .headerBar button{
margin-top: 0;
margin-bottom: 0;
}
.notificationBanner {
padding: var(--basis_x2);
}

View file

@ -0,0 +1,71 @@
import { useState, useCallback, useRef } from 'react'
import ISO6391 from 'iso-639-1'
const locales = ISO6391.getLanguages('en de es fr it nl pt'.split(' '))
function InputWithLocal({ locale, defaultValue, children, style, onChange, ...props }) {
const wrapperDiv = useRef(null)
const [changedLocale, setChangedLocale] = useState(locale)
const [changedValue, setChangedValue] = useState(defaultValue)
const handleLocaleChange = useCallback((event) => {
setChangedLocale(event.target.value)
if (onChange) {
const target = wrapperDiv.current
target.value = {
locale: event.target.value,
value: changedValue,
}
onChange({ target })
}
}, [setChangedLocale, onChange, changedValue])
const handleTextChange = useCallback((event) => {
setChangedValue(event.target.value)
if (onChange) {
const target = wrapperDiv.current
target.value = {
locale: changedLocale,
value: event.target.value,
}
onChange({ target })
}
}, [setChangedValue, onChange, changedLocale])
return <div
ref={wrapperDiv}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
maxWidth: '100%',
...style
}}
{...props}
>
<select
onChange={handleLocaleChange}
defaultValue={locale}
style={{
margin: '0 var(--basis) 0 0'
}}
>
{locales.map(({ code, nativeName }) => <option key={code} value={code}>{nativeName}</option>) }
</select>
{
!!children
? children({
onChange: handleTextChange,
defaultValue: defaultValue,
style: {
flexGrow: '1',
}
})
: null
}
</div>
}
export default InputWithLocal

View file

@ -0,0 +1,45 @@
import { useState, useCallback, useEffect } from 'react'
function MultiButton({ ariaLabel, items, defaultValue, onChange, style, className }) {
const [choosen, setChoosen] = useState()
useEffect(() => setChoosen(defaultValue), [defaultValue, setChoosen])
const handleClick = useCallback(event => {
const newValue = event.target.dataset.value
setChoosen(newValue)
if (onChange) {
onChange(newValue)
}
}, [setChoosen, onChange])
return <div
aria-label={ariaLabel}
className={'buttonRow ' + (className || '')}
style={{
display: 'inline-block',
...style
}}
>
{
items.map(item => {
const value = item.value
const title = item.title
const icon = item.icon || null
return <button
key={value}
className={`${choosen === value ? 'choosen' : ''} ${!!icon ? 'hasIcon' : ''}`}
onClick={handleClick}
data-value={value}
>
<span style={{pointerEvents: 'none'}}>
{!!icon ? icon : null}
<span style={{verticalAlign: 'middle'}}>{title}</span>
</span>
</button>
})
}
</div>
}
export default MultiButton

View file

@ -0,0 +1,21 @@
/* THIS IS NOT IN USE. THE STYLES ARE IN index.css */
.buttonRow{
margin-top: var(--basis);
}
.buttonRow button{
margin: var(--basis) var(--basis) 0 0;
}
.buttonRow button:laft-of-type{
margin-right: 0;
}
.buttonRow button span {
vertical-align: middle;
}
.buttonRow button.hasIcon{
padding-left: var(--basis);
}
.buttonRow button.hasIcon span {
margin-left: var(--basis_x2);
}

View file

@ -0,0 +1,69 @@
import { useState, useCallback, useRef } from 'react'
function PermissionInput({ role, defaultValue, children, style, onChange, ariaLabel, placeholder, ...props }) {
const wrapperDiv = useRef(null)
const [changedRole, setChangedRole] = useState(role || 'editor')
const [changedValue, setChangedValue] = useState(defaultValue)
const handleRoleChange = useCallback((event) => {
setChangedRole(event.target.value)
if (onChange) {
const target = wrapperDiv.current
target.value = {
role: event.target.value,
value: changedValue,
}
onChange({ target })
}
}, [setChangedRole, onChange, changedValue])
const handleTextChange = useCallback((event) => {
setChangedValue(event.target.value)
if (onChange) {
const target = wrapperDiv.current
target.value = {
role: changedRole,
value: event.target.value,
}
onChange({ target })
}
}, [setChangedValue, onChange, changedRole])
return <div
ref={wrapperDiv}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
maxWidth: '100%',
...style
}}
{...props}
>
{
!!children
? children({
onChange: handleTextChange,
defaultValue: defaultValue,
style: {
flexGrow: '1',
}
})
: null
}
<select
onChange={handleRoleChange}
defaultValue={role}
style={{
margin: '0 0 0 var(--basis)',
display: 'none',
}}
>
<option key="editor" value="editor">Editor</option>
<option key="viewer" value="viewer">Viewer</option>
</select>
</div>
}
export default PermissionInput

190
src/components/Repeater.js Normal file
View file

@ -0,0 +1,190 @@
import { useState, useCallback, useEffect } from 'react'
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'
import classes from './Repeater.module.css'
function reorder(list, startIndex, endIndex) {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result
}
function Repeater({ defaultValue, addDefaultValue, addButtonText, reorderLabel = 'Reorder', render, style, onChange, prependNewItems, isReorderable }) {
if (!(!!addButtonText)) {
addButtonText = 'Add Row'
}
const [rows, setRows] = useState([])
useEffect(() => {
let tmp_defaultValue = defaultValue
if (!Array.isArray(defaultValue) || defaultValue.length === 0) {
tmp_defaultValue = [addDefaultValue()]
}
setRows(tmp_defaultValue)
}, [defaultValue, addDefaultValue, setRows])
const handleRemoveRow = useCallback(event => {
const index = event.target.dataset.index
let new_rows = [...rows]
new_rows.splice(index, 1)
const rows_from_onChange = onChange(new_rows)
if (Array.isArray(rows_from_onChange)) {
new_rows = rows_from_onChange
}
setRows(new_rows)
}, [rows, setRows, onChange])
const handleRowChange = useCallback(event => {
const index = event.target.dataset.index
const _id = event.target.dataset.id
const newValue = event.target.value
const new_rows = [...rows]
if (typeof newValue !== 'object' || Array.isArray(newValue)) {
new_rows[index] = {
_id,
value: newValue
}
} else if (typeof newValue === 'object') {
new_rows[index] = {
_id,
...newValue
}
}
setRows(new_rows)
onChange(new_rows)
}, [rows, setRows, onChange])
const handleAddRow = useCallback(event => {
const newValue = addDefaultValue()
let index = null
if (!!event.target.dataset.index) {
index = parseInt(event.target.dataset.index)
if (isNaN(index)) {
index = null
}
}
let new_rows = null
if (index !== null && index >= 0) {
new_rows = [...rows]
new_rows.splice(index, 0, newValue)
} else {
if (prependNewItems === true) {
new_rows = [newValue, ...rows]
} else {
new_rows = [...rows, newValue]
}
}
setRows(new_rows)
onChange(new_rows)
}, [rows, addDefaultValue, setRows, onChange, prependNewItems])
function onDragEnd(result) {
if (!result.destination) {
return
}
if (result.destination.index === result.source.index) {
return
}
const new_rows = reorder(
rows,
result.source.index,
result.destination.index
)
setRows(new_rows)
onChange(new_rows)
}
const hasOnlyOneRow = rows.length === 1
const addButton = <div style={{ textAlign: 'right' }}>
<button className={`green ${classes.addRowButton}`} onClick={handleAddRow}>{addButtonText}</button>
</div>
return <div style={style}>
{!hasOnlyOneRow && prependNewItems ? <div style={{ marginBottom: 'var(--basis)' }}>{addButton}</div> : null}
<DragDropContext onDragEnd={onDragEnd}>
<Droppable
droppableId="list"
isDropDisabled={isReorderable === true ? false : true}
>
{provided => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{
rows
.filter(subDefaultValue => subDefaultValue._id)
.map(
(subDefaultValue, index) => <Draggable
key={subDefaultValue._id}
draggableId={subDefaultValue._id}
index={index}
isDragDisabled={isReorderable === true ? false : true}
disableInteractiveElementBlocking={true}
>
{provided => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
key={subDefaultValue._id}
className={classes.row}
>
{
index !== 0
? <div className={classes.middleActions}>
<div className={classes.trigger}></div>
<div className={classes.content}>
<button className={`green ${classes.inlineRowButton}`} data-index={index} onClick={handleAddRow}>+</button>
</div>
</div>
: null
}
<div className={classes.form}>
{
isReorderable === true
? <button aria-label={reorderLabel} className={`text ${classes.inlineRowButton}`} {...provided.dragHandleProps}></button>
: null
}
{
render({
key: subDefaultValue._id,
defaultValue: subDefaultValue,
className: classes.item,
'data-index': index,
'data-id': subDefaultValue._id,
onChange: handleRowChange
})
}
{
hasOnlyOneRow
? <button className={`green ${classes.inlineRowButton}`} onClick={handleAddRow}>+</button>
: <button className={`red ${classes.inlineRowButton}`} data-index={index} onClick={handleRemoveRow}></button>
}
</div>
</div>
)}
</Draggable>
)
}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
{!hasOnlyOneRow && !prependNewItems ? addButton : null}
</div>
}
export default Repeater

View file

@ -0,0 +1,67 @@
.row {
margin-top: var(--basis);
display: flex;
flex-direction: column;
position: relative;
}
.row:first-of-type {
margin-top: 0;
}
.row .middleActions{
position: absolute;
display: block;
top: -6px;
right: 0;
left: 0;
height: 6px;
}
.row .middleActions .trigger{
height: 6px;
}
.row .middleActions .content{
position: absolute;
left: 50%;
transform: translate(-50%, calc(-50% - 3px));
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.row .middleActions:hover .content{
opacity: 1;
pointer-events: all;
}
.row .form{
display: flex;
flex-direction: row;
align-items: flex-start;
}
.row .item {
flex-grow: 1;
}
.row .inlineRowButton{
flex-shrink: 0;
margin-top: 0;
margin-bottom: 0;
text-align: center;
}
.row .inlineRowButton:first-child {
margin-left: 0;
}
.row .inlineRowButton:last-child {
margin-right: 0;
}
.addRowButton{
/* width: 100%; */
margin-right: 0;
margin-bottom: 0;
margin-left: 0;
}

83
src/fluent/Localized.js Normal file
View file

@ -0,0 +1,83 @@
import React from 'react'
import {
Localized as LocalizedOriginal,
// withLocalization,
} from '@fluent/react'
import { negotiateLanguages } from '@fluent/langneg'
import { FluentContext } from '../../node_modules/@fluent/react/esm/context.js'
const Localized = props => (
<LocalizedOriginal
key={props.id}
{...props}
elems={{
br: <br />,
...props.elems,
}}
>
<React.Fragment>{props.children}</React.Fragment>
</LocalizedOriginal>
)
// A custom withLocalization to have an empty fallback.
// It is nearly identical to the original.
function withLocalization(Inner) {
function WithLocalization(props) {
const l10n = React.useContext(FluentContext)
const getString = (id, args, fallback) => l10n.getString(id, args, fallback || ' ')
const fluentByObject = (object, fallback) => {
if (!(!!fallback)) {
fallback = null
}
if (!!object) {
const globalSupportedLocales = l10n.supportedLocales || []
const thisSupportedLocales = Object.keys(object).filter(locale => globalSupportedLocales.includes(locale))
const currentLocales = negotiateLanguages(
l10n.userLocales,
thisSupportedLocales,
{ defaultLocale: l10n.defaultLocale }
)
for (const locale of currentLocales) {
if (!!object[locale]) {
return object[locale]
}
}
return fallback
}
return fallback
}
return React.createElement(Inner, { fluentByObject, getString, ...props })
}
return WithLocalization
}
export {
withLocalization,
Localized,
Localized as default,
}
/*
import { Localized, withLocalization } from '../Localized/'
<Localized id="translation_id" />
export default withLocalization(componentName)
import Localized from '../Localized/'
<Localized id="translation_id" />
import { withLocalization } from '@fluent/react'
export default withLocalization(componentName)
*/

94
src/fluent/l10n.js Normal file
View file

@ -0,0 +1,94 @@
import React, { useEffect, useState } from 'react'
// https://projectfluent.org/play/
// import { LocalizationProvider, Localized } from '@fluent/react' // '@fluent/react/compat'
import { ReactLocalization, LocalizationProvider } from '@fluent/react'
import { FluentBundle, FluentResource } from '@fluent/bundle'
import { negotiateLanguages } from '@fluent/langneg'
export const locales = {
de: 'Deutsch',
en: 'English',
// es: 'Español',
// pt: 'Português',
// fr: 'Français',
// it: 'Italiano',
// nl: 'Dutch',
// pl: 'Polska',
// ru: 'Pусский',
}
const _supportedLocales_ = Object.keys(locales)
const _defaultLocale_ = 'en'
async function fetchMessages(locale) {
const path = await import('../locales/' + locale + '.ftl')
const response = await fetch(path.default)
const messages = await response.text()
return { [locale]: new FluentResource(messages) }
}
function getDefaultBundles() {
const bundle = new FluentBundle('')
bundle.addResource(new FluentResource(''))
return new ReactLocalization([bundle])
}
async function createMessagesGenerator(currentLocales) {
const fetched = await Promise.all(
currentLocales.map(fetchMessages)
)
const messages = fetched.reduce(
(obj, cur) => Object.assign(obj, cur)
)
return function* generateBundles() {
for (const locale of currentLocales) {
const bundle = new FluentBundle(locale)
bundle.addResource(messages[locale])
yield bundle
}
}
}
export function AppLocalizationProvider({ userLocales, children, onLocaleChange }) {
const [bundles, setBundles] = useState(getDefaultBundles())
useEffect(() => {
async function loadBundles() {
const currentLocales = negotiateLanguages(
userLocales,
_supportedLocales_,
{ defaultLocale: _defaultLocale_ }
)
if (!!onLocaleChange) {
onLocaleChange(currentLocales)
}
const generateBundles = await createMessagesGenerator(currentLocales)
const new_bundles = new ReactLocalization(generateBundles())
new_bundles.userLocales = userLocales
new_bundles.defaultLocale = _defaultLocale_
new_bundles.supportedLocales = _supportedLocales_
setBundles(new_bundles)
}
loadBundles()
}, [userLocales, onLocaleChange])
if (!bundles) {
// Show a loader.
return <div>Loading texts</div>
}
return <LocalizationProvider l10n={bundles}>
{children}
</LocalizationProvider>
}

325
src/index.css Normal file
View file

@ -0,0 +1,325 @@
:root {
--white: #fff;
--white-rgb: 255, 255, 255;
--grey: #ede8f1;
--grey-rgb: 237, 232, 241;
--dark-grey: #aa94c0;
--dark-grey-rgb: 170, 148, 192;
--purple: #502379;
--purple-rgb: 80, 35, 121;
--purple-dark: #140022;
--purple-dark-rgb: 20, 0, 34;
--alpha-more: 0.8;
--alpha-less: 0.12;
--blur: 20px;
--yellow: #FDC220;
--on-yellow: #fff;
--green: #1BBE6F;
--on-green: #fff;
--blue: #82D0F4;
--on-blue: #fff;
--red: #E63E12;
--on-red: #fff;
--basis: 0.4rem;
--basis_x0_2: calc(0.2 * var(--basis));
--basis_x0_5: calc(0.5 * var(--basis));
--basis_x2: calc(2 * var(--basis));
--basis_x4: calc(4 * var(--basis));
--basis_x8: calc(8 * var(--basis));
--basis_x16: calc(16 * var(--basis));
/* --basis_x32: calc(32 * var(--basis)); */
--basis_x64: calc(64 * var(--basis));
--font-add: 1rem;
}
* {
margin: 0;
padding: 0;
font-family: 'Ubuntu', 'Noto Kufi Arabic', 'Geeza Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
/*
Ubuntu = Volt (It includes latin and arabic characters.)
Noto Kufi Arabic = A Noto font from Google (maybe it's installed)
Geeza Pro = macOS Arabic Default
-apple-system, BlinkMacSystemFont = macOS Default
Helvetica Neue = Old macOS Default
Segoe UI = Windows Default
Roboto = Android Default
Fira Sans = Firefox OS Default
Oxygen, Cantarell, Droid Sans = Linux
sans-serif = Fallback
*/
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
--background: var(--white);
--background-rgb: var(--white-rgb);
--background-contrast: var(--grey);
--background-contrast-rgb: var(--grey-rgb);
--on-background: var(--purple);
--on-background-contrast: var(--purple-dark);
--input-background: var(--background);
--constant-button-color: var(--background);
}
@media (prefers-color-scheme: dark) {
html {
--background: var(--purple-dark);
--background-rgb: var(--purple-dark-rgb);
--background-contrast: var(--purple);
--background-contrast-rgb: var(--purple-rgb);
--on-background: var(--white);
--on-background-contrast: var(--dark-grey);
--input-background: var(--background);
--constant-button-color: var(--background-contrast);
}
}
html[lang="ar"] { /* for arabic */
letter-spacing: 0 !important;
}
body {
background: var(--background);
color: var(--on-background);
--body-font-size: calc(var(--font-add) + var(--basis));
--body2-font-size: calc(var(--font-add) + var(--basis_x0_5));
font-size: var(--body-font-size);
}
code {
font-family: 'Ubuntu Mono', source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
code.filled{
padding: var(--basis_x0_2);
margin: 0 var(--basis_x0_2);
border-radius: var(--basis_x0_5);
background: var(--on-background);
color: var(--background);
}
a,
a:visited {
color: var(--on-background);
}
a:hover,
a:focus {
color: var(--on-background-contrast);
}
footer {
padding: var(--basis_x4);
text-align: center;
}
h1 {
font-size: calc(var(--font-add) + var(--basis_x4));
line-height: 0.9;
/* margin: 0 0 var(--basis) 0; */
margin: var(--basis_x4) 0 var(--basis) 0;
text-decoration: inherit;
}
h2 {
font-size: calc(var(--font-add) + var(--basis_x2));
margin: var(--basis_x4) 0 var(--basis) 0;
text-decoration: inherit;
}
h3 {
font-size: calc(var(--font-add) + var(--basis));
margin: var(--basis_x4) 0 var(--basis) 0;
text-decoration: inherit;
}
h1.yellow,
h2.yellow,
h3.yellow {
color: var(--yellow);
}
h1.green,
h2.green,
h3.green {
color: var(--green);
}
h1.blue,
h2.blue,
h3.blue {
color: var(--blue);
}
h1.red,
h2.red,
h3.red {
color: var(--red);
}
p {
/* width: calc(var(--basis_x16) + var(--basis_x8)); */
max-width: 100%;
margin: var(--basis) 0;
text-decoration: inherit;
}
ul {
margin-inline-start: var(--basis_x4);
max-width: calc(100% - var(--basis_x4));
}
.body2 {
font-size: var(--body2-font-size);
}
button,
textarea,
input[type="text"],
select {
min-width: 0px;
outline: none;
border: none;
margin: var(--basis);
font-size: var(--body-font-size);
}
textarea,
input[type="text"] {
padding: var(--basis);
background: var(--input-background);
color: var(--on-background);
box-shadow: inset 0 0 0 calc(0.2 * var(--basis)) var(--on-background);
}
textarea.inverted,
input[type="text"].inverted {
background: var(--on-background);
color: var(--input-background);
box-shadow: inset 0 0 0 calc(0.2 * var(--basis)) var(--on-background);
}
textarea:focus,
input[type="text"]:focus {
background: var(--input-background);
color: var(--on-background);
box-shadow: inset 0 0 0 calc(0.2 * var(--basis)) var(--on-background-contrast);
}
textarea {
resize: vertical;
padding-bottom: var(--basis_x2);
min-height: 20px; /* TODO: Find out why this is value works. */
}
button,
select {
font-weight: bold;
transition: transform 0.2s ease;
position: relative;
cursor: pointer;
padding: var(--basis) var(--basis_x2);
--button-background: var(--on-background);
--button-color: var(--constant-button-color);
background: var(--button-background);
color: var(--button-color);
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-position-x: calc(100% - 6px);
background-position-y: 50%;
padding: var(--basis) calc(var(--basis_x2) + 14px) var(--basis) var(--basis_x2);
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
}
@media (prefers-color-scheme: dark) {
select {
background-image: url("data:image/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
}
}
button.yellow {
--button-background: var(--yellow);
--button-color: var(--on-yellow);
}
button.green {
--button-background: var(--green);
--button-color: var(--on-green);
}
button.blue {
--button-background: var(--blue);
--button-color: var(--on-blue);
}
button.red {
--button-background: var(--red);
--button-color: var(--on-red);
}
button.text {
--button-background: transparent;
--button-color: var(--on-background);
}
button:focus {
--button-background: var(--background-contrast);
--button-color: var(--on-background);
}
button:hover{
transform-origin: center center;
transform: scale(1.05);
}
button.choosen:after {
content: "";
position: absolute;
top: calc(-1 * var(--basis_x0_5));
right: calc(-1 * var(--basis_x0_5));
bottom: calc(-1 * var(--basis_x0_5));
left: calc(-1 * var(--basis_x0_5));
box-shadow: 0 0 0 var(--basis_x0_5) var(--on-background);
}
button:focus.choosen:after {
box-shadow: 0 0 0 var(--basis_x0_5) var(--on-background-contrast);
}
.buttonRow button {
--margin-right: calc(var(--basis) * 1.5);
margin: var(--basis) var(--margin-right) calc(var(--basis) * .5) 0;
}
.buttonRow button:last-of-type {
margin-right: 0;
}
.buttonRow.usesLinks button:last-of-type {
margin-right: var(--margin-right);
}
.buttonRow.usesLinks a:last-of-type button {
margin-right: 0;
}
.buttonRow button.hasIcon{
padding-left: var(--basis);
}
.buttonRow button.hasIcon span {
margin-left: var(--basis_x2);
}
hr {
margin: var(--basis_x4) 0;
border: none;
height: var(--basis_x0_2);
background: var(--on-background);
}
@media (max-width: 1000px) {
.hideOnSmallScreen {
display: none;
}
}

69
src/index.js Normal file
View file

@ -0,0 +1,69 @@
import React, { useState, useEffect, useCallback } from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './pages/App.js'
// import reportWebVitals from './reportWebVitals'
import { BrowserRouter as Router } from 'react-router-dom'
import 'intl-pluralrules'
import { AppLocalizationProvider, locales } from './fluent/l10n.js'
window.domains = {
frontend: 'https://qrcode.volt.link',
// frontend: 'http://localhost:3000/',
backend: 'https://volt.link/',
// backend: 'http://localhost:4000/',
}
function AppLanguageWrapper() {
// const [userLocales, setUserLocales] = useState(['de'])
const [userLocales, setUserLocales] = useState(navigator.languages)
const [currentLocale, setCurrentLocale] = useState(null)
useEffect(() => {
if (!!window.umami) {
let systemLocales = navigator.languages
if (!!systemLocales || Array.isArray(systemLocales)) {
for (let locale of systemLocales) {
locale = locale.toLowerCase() // Not really correct but the system locales sadly don't conform to the standard.
const language = locale.split('-')[0]
if (language !== locale) {
window.umami.trackEvent('L: ' + language) // Log just the language.
}
window.umami.trackEvent('L: ' + locale) // Log the full locale.
}
}
}
}, [])
const handleLanguageChange = useCallback(event => {
setUserLocales([event.target.dataset.locale])
}, [setUserLocales])
const handleCurrentLocalesChange = useCallback(currentLocales => {
setCurrentLocale(currentLocales.length > 0 ? currentLocales[0] : '')
}, [setCurrentLocale])
return <AppLocalizationProvider
key="AppLocalizationProvider"
userLocales={userLocales}
onLocaleChange={handleCurrentLocalesChange}
>
<App locales={locales} currentLocale={currentLocale} onLanguageChange={handleLanguageChange} />
</AppLocalizationProvider>
}
ReactDOM.render(
<React.StrictMode>
<Router>
<AppLanguageWrapper />
</Router>
</React.StrictMode>,
document.getElementById('root')
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
// reportWebVitals()

4
src/locales/de.ftl Normal file
View file

@ -0,0 +1,4 @@
default_locale = de
contact = Kontakt
source_code = Quellcode

4
src/locales/en.ftl Normal file
View file

@ -0,0 +1,4 @@
default_locale = en
contact = Contact
source_code = Source Code

22
src/pages/App.js Normal file
View file

@ -0,0 +1,22 @@
import classes from './App.module.css'
import {
Switch,
Route,
} from 'react-router-dom'
import Localized from '../fluent/Localized.js'
function App() {
return (<>
<div className={classes.app}>
</div>
<footer>
<a href="mailto:thomas.rosen@volteuropa.org"><Localized id="contact" /></a>
&nbsp; &nbsp;
<a href="https://github.com/voltbonn/qrcode.volt.link" target="_blank" rel="noreferrer"><Localized id="source_code" /></a>
</footer>
</>)
}
export default App

13
src/pages/App.module.css Normal file
View file

@ -0,0 +1,13 @@
.app {
display: flex;
flex-direction: column;
align-items: left;
justify-content: flex-start;
/* text-align: center; */
padding: var(--basis_x4);
min-height: 80vh;
background: var(--background);
color: var(--on-background);
}

8
src/pages/App.test.js Normal file
View file

@ -0,0 +1,8 @@
// import { render, screen } from '@testing-library/react'
// import App from './App'
//
// test('renders learn react link', () => {
// render(<App />)
// const linkElement = screen.getByText(/learn react/i)
// expect(linkElement).toBeInTheDocument()
// })

13
src/reportWebVitals.js Normal file
View file

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
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.js 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';