New color selector

- Use headless-ui radio menu for the selector
- Use enum for colors
- Design is WIP
This commit is contained in:
Hauke Tönjes 2021-07-28 14:28:45 +02:00
parent a970c1be15
commit 11d739b473
No known key found for this signature in database
GPG Key ID: 0BF2BC96C9FAAE9E
10 changed files with 133 additions and 67 deletions

View File

@ -0,0 +1,34 @@
import React, {useState} from 'react'
import {RadioGroup} from '@headlessui/react'
import ColorOption from "./color_selector/ColorOption";
import {COLORS} from "../src/colors";
interface ColorSelectorProps {
initialValue: COLORS,
onChange(value: COLORS): void;
}
function ColorSelector(props: ColorSelectorProps): JSX.Element {
let [color, setColor] = useState(props.initialValue)
return (
<RadioGroup<"div", COLORS> className="flex items-center flex-wrap" value={color}
onChange={function (value) {
setColor(value);
props.onChange(value);
}}>
<ColorOption color={COLORS.WHITE}/>
<ColorOption color={COLORS.BLACK}/>
<ColorOption color={COLORS.GREY}/>
<ColorOption color={COLORS.GREEN}/>
<ColorOption color={COLORS.INDIGO}/>
<ColorOption color={COLORS.BLUE}/>
<ColorOption color={COLORS.PURPLE}/>
<ColorOption color={COLORS.TEAL}/>
</RadioGroup>
)
}
export default ColorSelector

View File

@ -1,6 +1,6 @@
import {saveAs} from 'file-saver';
import React, {FormEvent, useEffect, useRef, useState} from "react";
import {BrowserQRCodeReader} from "@zxing/browser";
import {BrowserQRCodeReader, IScannerControls} from "@zxing/browser";
import {Result} from "@zxing/library";
import {useTranslation} from 'next-i18next';
import Link from 'next/link';
@ -11,15 +11,20 @@ import Check from './Check';
import {PayloadBody} from "../src/payload";
import {getPayloadBodyFromFile, getPayloadBodyFromQR} from "../src/process";
import {PassData} from "../src/pass";
import ColorSelector from "./ColorSelector";
import {COLORS} from "../src/colors";
function Form(): JSX.Element {
const { t } = useTranslation(['index', 'errors', 'common']);
const {t} = useTranslation(['index', 'errors', 'common']);
// Whether camera is open or not
const [isCameraOpen, setIsCameraOpen] = useState<boolean>(false);
// Currently selected color
const [selectedColor, setSelectedColor] = useState<COLORS>(COLORS.WHITE);
// Global camera controls
const [globalControls, setGlobalControls] = useState(undefined);
const [globalControls, setGlobalControls] = useState<IScannerControls>(undefined);
// Currently selected QR Code / File. Only one of them is set.
const [qrCode, setQrCode] = useState<Result>(undefined);
@ -82,7 +87,7 @@ function Form(): JSX.Element {
setErrorMessage('noCameraAccess');
return;
}
// Check if camera device is present
if (deviceList.length == 0) {
setErrorMessage("noCameraFound");
@ -123,7 +128,7 @@ function Form(): JSX.Element {
event.preventDefault();
setLoading(true);
if(navigator.userAgent.match('CriOS')) {
if (navigator.userAgent.match('CriOS')) {
setErrorMessage('safariSupportOnly');
setLoading(false);
return;
@ -135,7 +140,7 @@ function Form(): JSX.Element {
return;
}
const color = (document.getElementById('color') as HTMLSelectElement).value;
const color = selectedColor;
let payloadBody: PayloadBody;
try {
@ -208,24 +213,7 @@ function Form(): JSX.Element {
<div className="space-y-5">
<p>{t('index:pickColorDescription')}</p>
<div className="relative inline-block w-full">
<select name="color" id="color"
className="bg-gray-200 dark:bg-gray-900 focus:outline-none w-full h-10 pl-3 pr-6 text-base rounded-md appearance-none cursor-pointer">
<option value="white">{t('index:colorWhite')}</option>
<option value="black">{t('index:colorBlack')}</option>
<option value="grey">{t('index:colorGrey')}</option>
<option value="green">{t('index:colorGreen')}</option>
<option value="indigo">{t('index:colorIndigo')}</option>
<option value="blue">{t('index:colorBlue')}</option>
<option value="purple">{t('index:colorPurple')}</option>
<option value="teal">{t('index:colorTeal')}</option>
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-5 h-5 fill-current" viewBox="0 0 20 20">
<path
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd" fillRule="evenodd"/>
</svg>
</div>
<ColorSelector onChange={setSelectedColor} initialValue={selectedColor}/>
</div>
</div>
}/>
@ -241,9 +229,9 @@ function Form(): JSX.Element {
</p>
<div>
<ul className="list-none">
<Check text={t('createdOnDevice')}></Check>
<Check text={t('openSourceTransparent')}></Check>
<Check text={t('hostedInEU')}></Check>
<Check text={t('createdOnDevice')}/>
<Check text={t('openSourceTransparent')}/>
<Check text={t('hostedInEU')}/>
</ul>
</div>
<label htmlFor="privacy" className="flex flex-row space-x-4 items-center">

View File

@ -0,0 +1,31 @@
import {RadioGroup} from '@headlessui/react'
import {COLORS, rgbToHex} from "../../src/colors";
interface ColorOptionProps {
color: COLORS,
}
function ColorOption(props: ColorOptionProps): JSX.Element {
let colorIsDark = props.color != COLORS.WHITE;
return (
<RadioGroup.Option
value={props.color}>
{({checked}) => (
<div
className={`${colorIsDark ? 'border-white' : 'border-black'} border-2 cursor-pointer rounded-full w-9 h-9 flex items-center justify-center m-1`}
style={{backgroundColor: rgbToHex(props.color)}}>
{checked &&
<svg className={`${colorIsDark ? 'text-white' : 'text-black'} h-5 w-5`} fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"/>
</svg>
}
</div>
)}
</RadioGroup.Option>
)
}
export default ColorOption

View File

@ -9,6 +9,7 @@
"start": "next start"
},
"dependencies": {
"@headlessui/react": "^1.3.0",
"@zxing/browser": "^0.0.9",
"@zxing/library": "^0.18.6",
"base45": "^3.0.0",

22
src/colors.ts Normal file
View File

@ -0,0 +1,22 @@
export enum COLORS {
WHITE = 'rgb(255, 255, 255)',
BLACK = 'rgb(0, 0, 0)',
GREY = 'rgb(33, 33, 33)',
GREEN = 'rgb(27, 94, 32)',
INDIGO = 'rgb(26, 35, 126)',
BLUE = 'rgb(1, 87, 155)',
PURPLE = 'rgb(74, 20, 140)',
TEAL = 'rgb(0, 77, 64)',
}
export function rgbToHex(rgbString: string) {
let a = rgbString.split("(")[1].split(")")[0];
let values = a.split(",");
let b = values.map(function (value) {
let x = parseInt(value).toString(16);
return (x.length == 1) ? "0" + x : x;
})
return "#" + b.join("");
}

View File

@ -3,17 +3,6 @@ export class Constants {
public static PASS_IDENTIFIER = 'pass.de.marvinsextro.covidpass'
public static TEAM_IDENTIFIER = 'X8Q7Q2RLTD'
public static COLORS = {
white: 'rgb(255, 255, 255)',
black: 'rgb(0, 0, 0)',
grey: 'rgb(33, 33, 33)',
green: 'rgb(27, 94, 32)',
indigo: 'rgb(26, 35, 126)',
blue: 'rgb(1, 87, 155)',
purple: 'rgb(74, 20, 140)',
teal: 'rgb(0, 77, 64)',
}
public static img1xBlack: Buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAABU0lEQVR4AWIYaWAUiAExoB06gGggDOMwripJthAgGwBoCGYDBIDIgAFBwDAABoJjhBgEAEChBBBaAA0JyeKAqR0hmnWx3p5o8MHdvfd9Z7SHH8Dr723iCpdoYBOZtoJ9XOALYghxjj0sw1k7OEEAiekVxyjBShto4h6SUg8N5KGqhCHEshdsI3FdiCM3SNwnxA1uKxKXZm3QfJCPQ3RmYVAfW5j2YH+QfkweQ1uDviEmdNHBR8SYddxCDOC2ojeI4RlL+K2Kd8UYcFvRE8TQxyKmVdFLOAbcVnQNMeEUCzCKPQbcVnQEiRilGQNuK9qFRI1SjAG3Fa0iiDh8hgPcQWIKwG1dHsQyD+qKCCGWhCgiVZ7T7yhagw9JyQe37FTGCKI0QhlWq2GiGDNBDU6qYwyJaYw6nFbBABJhgAoyKYc2QoghRBs5ZF4BLTz+aaGAef+nHwt5/579e2c2AAAAAElFTkSuQmCC', 'base64');
public static img2xBlack: Buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACZ0lEQVR4Ae3TA8xcURRF4dq2ozqqbRtxUjeq7Zi127C2bXvSsLZtu/PP6ardjs577765K/li7mQnizGbzWaz2Wx50BXjMRYdkR0JXRq0xVq8g/ziDZaiGVIiYSqLybgPCdMtjEZpP1+oP45CYnQYPZDd7xeKnPMX1L+QAoULmnUhX12wVrwupHjBKnC8tFgEMcRcpIFjrYQYZhkcqQXEUM2h3haIoTZDvRsQQ92AeiGIoUJQTwxnB7ID2YHsQHYgO5B7zmMlztiBfhbCCKQAJUuOYbADIYRe+FP7TB1IfxyiUaYOpD8O0TJzB9IfpyqCZg6kP05ZPIBESL0gJAIBVENONMRJF8cJQr1nkDBdRWb8WBYEHB8HeAb1bkPCNB5E/xlJfRzgNtQ7CQnTWNB/R9IfBzgJ9TZCwnQJGcMYSX8cYCPUmw6JwCqkwt9K5cg4wHSo1x0SoZX/GUJ/HKA71KsAURhJdxygAtRLg1cKI2mP8wpp4EibIQoj6YwDbIZj9YIojKQzDtALjlUESZAYrEN2fK2u4jhJKAJH2wmJ0UOsRQBJECU74XjtIYZoD8dLi1sQj7uFtHClIRCPGwLXyox7EI+6h8xwtR4Qj+oB10uFExCPOYFU8ERVEIR4RBBV4KlGQTxiFDxXWgQgLgsgLTxZQdyBuOQOCsLTVcELiMNeoAqMqBHeQhzyFo1gVC3wCqLsFVrAyGrgMUTJY9SA0RXDMYVxjqEYfFEGzITEyUxkgO9qhEuQKF1CI/i69BiCB5AwPcAQpEfClBUDcR7yF+cxEFmR0NXDVFz5YirqwWaz2Ww2W9R9AE/cBAw+cEeMAAAAAElFTkSuQmCC', 'base64')
public static img1xWhite: Buffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAABUUlEQVRIx+XWP0tCURjH8cf+IBLaELSEvoIcWkRHX0AQbm4tbQ7uTi4XhIbAxfdQQ9HqkK05RINIgZtUgoOQ6A3026Sc9D73jzblb73P/XAOnPM8R2RTwyFF7rmjwMGqxC5n3PLNLDY3nLITDDnhih5O+eCSpB9inyLPeKVJgZgbk+QTv3nnWIcaBMmDDo0DQWMdCpj/A3W4oLo+9MqRiAgv60EzJmaeNB2aGr82qPK1wOzxaFRMdag/L3pjW4QMA5WBvg61ja1siYiQoakw0NahulFWI2R8WWagrkPlX4VzypGBsg5lF0prhFQGsjoUXmpn15zz5Mj0CLt1JMv3/bDcO2QC2xdjk/BqttYfrEdEhAgdT6ZDxM8ASDF0ZYak/I6jHBOVmZALMtnyjByZEfmgkzZNd4npkl5laEepGIfBpkJ09UdEnBItWpSIb+xL6gcK3j+JspcAUAAAAABJRU5ErkJggg==', 'base64')

View File

@ -1,5 +1,6 @@
import {ValueSets} from "./value_sets";
import {Constants} from "./constants";
import {COLORS} from "./colors";
enum CertificateType {
Vaccine = 'Vaccine',
@ -27,7 +28,7 @@ export interface PassDictionary {
}
export interface PayloadBody {
color: string;
color: COLORS;
rawData: string;
decodedData: Uint8Array;
}
@ -48,13 +49,7 @@ export class Payload {
constructor(body: PayloadBody, valueSets: ValueSets) {
let colors = Constants.COLORS;
if (!(body.color in colors)) {
throw new Error('invalidColor');
}
const dark = body.color != 'white'
const dark = body.color != COLORS.WHITE;
const healthCertificate = body.decodedData['-260'];
const covidCertificate = healthCertificate['1']; // Version number subject to change
@ -76,13 +71,13 @@ export class Payload {
const firstName = nameInformation['gn'];
const lastName = nameInformation['fn'];
const transliteratedFirstName = nameInformation['gnt'].replaceAll('<', ' ');
const transliteratedLastName = nameInformation['fnt'].replaceAll('<', ' ');
// Check if name contains non-latin characters
const nameRegex = new RegExp('^[\\p{Script=Latin}\\p{P}\\p{M}\\p{Z}]+$', 'u');
let name: string;
if (nameRegex.test(firstName) && nameRegex.test(lastName)) {
@ -155,9 +150,9 @@ export class Payload {
// Set Values
this.rawData = body.rawData;
this.backgroundColor = dark ? colors[body.color] : colors.white
this.labelColor = dark ? colors.white : colors.grey
this.foregroundColor = dark ? colors.white : colors.black
this.backgroundColor = dark ? body.color : COLORS.WHITE
this.labelColor = dark ? COLORS.WHITE : COLORS.GREY
this.foregroundColor = dark ? COLORS.WHITE : COLORS.BLACK
this.img1x = dark ? Constants.img1xWhite : Constants.img1xBlack
this.img2x = dark ? Constants.img2xWhite : Constants.img2xBlack
this.dark = dark;
@ -188,7 +183,7 @@ export class Payload {
key: "dose",
label: "Dose",
value: dose
},
},
{
key: "dov",
label: "Date of Vaccination",
@ -232,7 +227,7 @@ export class Payload {
const testDateTimeString = properties['sc'];
const testResultKey = properties['tr'];
const testingCentre = properties['tc'];
if (!(testResultKey in valueSets.testResults)) {
throw new Error('invalidTestResult');
}
@ -244,7 +239,7 @@ export class Payload {
const testType = valueSets.testTypes[testTypeKey].display;
const testTime = testDateTimeString.replace(/.*T/, '').replace('Z', ' ') + 'UTC';
const testDate = testDateTimeString.replace(/T.*/,'');
const testDate = testDateTimeString.replace(/T.*/, '');
data.secondaryFields.push(...[
{
@ -343,7 +338,7 @@ export class Payload {
default:
throw new Error('certificateType');
}
return data;
}
}

View File

@ -4,10 +4,11 @@ import * as PdfJS from 'pdfjs-dist'
import jsQR, {QRCode} from "jsqr";
import {decodeData} from "./decode";
import {Result} from "@zxing/library";
import {COLORS} from "./colors";
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
export async function getPayloadBodyFromFile(file: File, color: string): Promise<PayloadBody> {
export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise<PayloadBody> {
// Read file
const fileBuffer = await file.arrayBuffer();
@ -59,7 +60,7 @@ export async function getPayloadBodyFromFile(file: File, color: string): Promise
}
}
export async function getPayloadBodyFromQR(qrCodeResult: Result, color: string): Promise<PayloadBody> {
export async function getPayloadBodyFromQR(qrCodeResult: Result, color: COLORS): Promise<PayloadBody> {
// Get raw data
let rawData = qrCodeResult.getText();

View File

@ -1,12 +1,12 @@
module.exports = {
mode: 'jit',
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: 'media',
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
mode: 'jit',
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: 'media',
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

View File

@ -80,6 +80,11 @@
resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.0.tgz"
integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
"@headlessui/react@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.3.0.tgz#62287c92604923e5dfb394483e5ec2463e1baea6"
integrity sha512-2gqTO6BQ3Jr8vDX1B67n1gl6MGKTt6DBmR+H0qxwj0gTMnR2+Qpktj8alRWxsZBODyOiBb77QSQpE/6gG3MX4Q==
"@next/env@11.0.1":
version "11.0.1"
resolved "https://registry.npmjs.org/@next/env/-/env-11.0.1.tgz"