Compare commits
39 Commits
47cd07a59d
...
d173ed8a16
Author | SHA1 | Date |
---|---|---|
Marvin Sextro | d173ed8a16 | |
Marvin Sextro | 685bbcb19b | |
Marvin Sextro | 7d930105d2 | |
Marvin Sextro | d205444659 | |
Marvin Sextro | 822e8fcd4c | |
Marvin Sextro | 53c81eb9e5 | |
Marvin Sextro | 2a9bdd86d7 | |
po | 87a990e146 | |
Marvin Sextro | b8eae1fb54 | |
Marvin Sextro | 931b0806f9 | |
Marvin Sextro | 4f30b9b326 | |
Marvin Sextro | 3b04e24a6b | |
Marvin Sextro | 64f473d214 | |
Marvin Sextro | 1cbaccdf82 | |
Marvin Sextro | d1da98a9e4 | |
Marvin Sextro | 1eb6bea1a6 | |
Marvin Sextro | c106be369f | |
Marvin Sextro | bc3efc4728 | |
Marvin Sextro | b349653721 | |
Marvin Sextro | 59e079f27e | |
Marvin Sextro | eeed18bbc4 | |
Julien Blatecky | 674d325502 | |
Hauke Tönjes | 48028bb211 | |
Marvin Sextro | 4f80dcf33c | |
Marvin Sextro | 8746d3c400 | |
Marvin Sextro | c12e0f5d2e | |
Marvin Sextro | be72026d54 | |
Marvin Sextro | 240f73ab5f | |
Marvin Sextro | d07c6ae232 | |
Marvin Sextro | 22c00b4052 | |
Marvin Sextro | 790bdf4494 | |
Marvin Sextro | 1aee8b8d86 | |
Marvin Sextro | 50d3830fec | |
Hauke Tönjes | 586811c55c | |
Marvin Sextro | 4f749442d6 | |
Hauke Tönjes | cc7fddb2af | |
Hauke Tönjes | ecaa891a40 | |
iBobo | 1949060709 | |
Hauke Tönjes | 03a00ea5cb |
|
@ -1,5 +1,5 @@
|
||||||
# Install dependencies only when needed
|
# Install dependencies only when needed
|
||||||
FROM node:14-alpine AS deps
|
FROM node:16-alpine AS deps
|
||||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -7,14 +7,14 @@ COPY package.json ./
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
# Rebuild the source code only when needed
|
||||||
FROM node:14-alpine AS builder
|
FROM node:16-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# Production image, copy all the files and run next
|
# Production image, copy all the files and run next
|
||||||
FROM node:14-alpine AS runner
|
FROM node:16-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import {useTranslation} from 'next-i18next';
|
|
||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
errorMessage: string;
|
message: string;
|
||||||
|
isWarning: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Alert(props: AlertProps): JSX.Element {
|
function Alert(props: AlertProps): JSX.Element {
|
||||||
const { t } = useTranslation(['index', 'errors']);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mt-5 rounded relative" role="alert">
|
<div className={`${props.isWarning ? "bg-yellow-100 border border-yellow-400 text-yellow-700" : "bg-red-100 border border-red-400 text-red-700"} px-4 py-3 mt-5 rounded-md relative flex justify-between items-center`} role="alert">
|
||||||
<span className="block sm:inline pr-6" id="message">{props.errorMessage}</span>
|
<span className={`${props.isWarning ? "" : "pr-7"} block sm:inline text-lg`} id="message">{props.message}</span>
|
||||||
<span className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={props.onClose}>
|
{
|
||||||
<svg className="fill-current h-6 w-6 text-red-500" role="button" xmlns="http://www.w3.org/2000/svg"
|
!props.isWarning &&
|
||||||
viewBox="0 0 20 20">
|
<span className="absolute top-0 right-0 p-1 m-2" onClick={props.onClose}>
|
||||||
<title>{t('index:errorClose')}</title>
|
<svg className="text-red-500 fill-current h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path fill="currentColor"
|
||||||
d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/>
|
d="M6.2253 4.81108C5.83477 4.42056 5.20161 4.42056 4.81108 4.81108C4.42056 5.20161 4.42056 5.83477 4.81108 6.2253L10.5858 12L4.81114 17.7747C4.42062 18.1652 4.42062 18.7984 4.81114 19.1889C5.20167 19.5794 5.83483 19.5794 6.22535 19.1889L12 13.4142L17.7747 19.1889C18.1652 19.5794 18.7984 19.5794 19.1889 19.1889C19.5794 18.7984 19.5794 18.1652 19.1889 17.7747L13.4142 12L19.189 6.2253C19.5795 5.83477 19.5795 5.20161 19.189 4.81108C18.7985 4.42056 18.1653 4.42056 17.7748 4.81108L12 10.5858L6.2253 4.81108Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
interface ButtonProps {
|
||||||
|
text?: string,
|
||||||
|
icon?: string,
|
||||||
|
loading?: boolean,
|
||||||
|
onClick: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
function Button(props: ButtonProps): JSX.Element {
|
||||||
|
|
||||||
|
function handleTouchEnd(event: React.TouchEvent<HTMLButtonElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
props.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={props.onClick}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
className="bg-gray-400 dark:bg-gray-600 hover:bg-gray-500 relative focus:outline-none h-20 text-white font-semibold rounded-md items-center flex justify-center">
|
||||||
|
{
|
||||||
|
props.icon && <img src={props.icon} className="w-12 h-12 mr-2 -ml-4" />
|
||||||
|
}
|
||||||
|
{props.text}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
|
@ -1,20 +1,16 @@
|
||||||
import {useTranslation} from 'next-i18next';
|
|
||||||
|
|
||||||
interface CheckProps {
|
interface CheckProps {
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Check(props: CheckProps): JSX.Element {
|
function Check(props: CheckProps): JSX.Element {
|
||||||
const { t } = useTranslation(["index"]);
|
return (
|
||||||
|
<li className="flex flex-row space-x-4 items-center">
|
||||||
return (
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mx-2 fill-current text-green-500" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<li className="flex flex-row space-x-4 items-center">
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mx-2 fill-current text-green-500" viewBox="0 0 20 20" fill="currentColor">
|
</svg>
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
{props.text}
|
||||||
</svg>
|
</li>
|
||||||
{props.text}
|
)
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Check;
|
export default Check;
|
|
@ -13,6 +13,7 @@ import {getPayloadBodyFromFile, getPayloadBodyFromQR} from "../src/process";
|
||||||
import {PassData} from "../src/pass";
|
import {PassData} from "../src/pass";
|
||||||
import {COLORS} from "../src/colors";
|
import {COLORS} from "../src/colors";
|
||||||
import Colors from './Colors';
|
import Colors from './Colors';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
function Form(): JSX.Element {
|
function Form(): JSX.Element {
|
||||||
const {t} = useTranslation(['index', 'errors', 'common']);
|
const {t} = useTranslation(['index', 'errors', 'common']);
|
||||||
|
@ -59,6 +60,44 @@ function Form(): JSX.Element {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [inputFile])
|
}, [inputFile])
|
||||||
|
|
||||||
|
// Whether Safari is used or not
|
||||||
|
let [isSafari, setIsSafari] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// Check if Safari is used
|
||||||
|
useEffect(() => {
|
||||||
|
const navigator = window.navigator;
|
||||||
|
setIsSafari(
|
||||||
|
navigator.vendor &&
|
||||||
|
navigator.vendor.indexOf('Apple') > -1 &&
|
||||||
|
navigator.userAgent &&
|
||||||
|
navigator.userAgent.indexOf('CriOS') == -1 &&
|
||||||
|
navigator.userAgent.indexOf('FxiOS') == -1
|
||||||
|
)
|
||||||
|
}, [isSafari]);
|
||||||
|
|
||||||
|
// Whether Safari is used or not
|
||||||
|
let [isShareDialogAvailable, setIsShareDialogAvailable] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Check if share dialog is available
|
||||||
|
useEffect(() => {
|
||||||
|
setIsShareDialogAvailable(window.navigator && window.navigator.share !== undefined);
|
||||||
|
}, [isShareDialogAvailable]);
|
||||||
|
|
||||||
|
// Open share dialog
|
||||||
|
async function showShareDialog() {
|
||||||
|
const shareData = {
|
||||||
|
title: document.title,
|
||||||
|
text: t('common:title') + ' – ' + t('common:subtitle'),
|
||||||
|
url: window.location.protocol + "//" + window.location.host,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.navigator.share(shareData);
|
||||||
|
} catch(error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show file Dialog
|
// Show file Dialog
|
||||||
async function showFileDialog() {
|
async function showFileDialog() {
|
||||||
|
@ -102,7 +141,7 @@ function Form(): JSX.Element {
|
||||||
// Start decoding from video device
|
// Start decoding from video device
|
||||||
await codeReader.decodeFromVideoDevice(undefined,
|
await codeReader.decodeFromVideoDevice(undefined,
|
||||||
previewElem,
|
previewElem,
|
||||||
(result, error, controls) => {
|
(result, _error, controls) => {
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
setQrCode(result);
|
setQrCode(result);
|
||||||
setFile(undefined);
|
setFile(undefined);
|
||||||
|
@ -113,9 +152,6 @@ function Form(): JSX.Element {
|
||||||
setGlobalControls(undefined);
|
setGlobalControls(undefined);
|
||||||
setIsCameraOpen(false);
|
setIsCameraOpen(false);
|
||||||
}
|
}
|
||||||
if (error !== undefined) {
|
|
||||||
setErrorMessage(error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -155,6 +191,9 @@ function Form(): JSX.Element {
|
||||||
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
|
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
|
||||||
saveAs(passBlob, 'covid.pkpass');
|
saveAs(passBlob, 'covid.pkpass');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
var scrollingElement = (document.scrollingElement || document.body);
|
||||||
|
scrollingElement.scrollTop = scrollingElement.scrollHeight;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErrorMessage(e.message);
|
setErrorMessage(e.message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -164,29 +203,22 @@ function Form(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form className="space-y-5" id="form" onSubmit={addToWallet}>
|
<form className="space-y-5" id="form" onSubmit={addToWallet}>
|
||||||
|
{
|
||||||
|
!isSafari && <Alert isWarning={true} message={t('iosHint')} onClose={() => {}}/>
|
||||||
|
}
|
||||||
<Card step="1" heading={t('index:selectCertificate')} content={
|
<Card step="1" heading={t('index:selectCertificate')} content={
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<p>{t('index:selectCertificateDescription')}</p>
|
<p>{t('index:selectCertificateDescription')}</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
<button
|
<Button text={isCameraOpen ? t('index:stopCamera') : t('index:startCamera')} onClick={isCameraOpen ? hideCameraView : showCameraView} />
|
||||||
type="button"
|
<Button text={t('index:openFile')} onClick={showFileDialog} />
|
||||||
onClick={isCameraOpen ? hideCameraView : showCameraView}
|
|
||||||
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
|
|
||||||
{isCameraOpen ? t('index:stopCamera') : t('index:startCamera')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={showFileDialog}
|
|
||||||
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
|
|
||||||
{t('index:openFile')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<video id="cameraPreview"
|
<video id="cameraPreview"
|
||||||
className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
|
className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
|
||||||
<input type='file'
|
<input type='file'
|
||||||
id='file'
|
id='file'
|
||||||
accept="application/pdf,image/png"
|
accept="application/pdf,image/png,image/jpeg,image/webp,image/gif"
|
||||||
ref={inputFile}
|
ref={inputFile}
|
||||||
style={{display: 'none'}}
|
style={{display: 'none'}}
|
||||||
/>
|
/>
|
||||||
|
@ -195,7 +227,7 @@ function Form(): JSX.Element {
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<svg className="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24"
|
<svg className="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24"
|
||||||
stroke="currentColor">
|
stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"/>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M9 5l7 7-7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="w-full truncate">
|
<span className="w-full truncate">
|
||||||
{
|
{
|
||||||
|
@ -245,27 +277,38 @@ function Form(): JSX.Element {
|
||||||
</Link>.
|
</Link>.
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-row items-center justify-start">
|
<div className="grid grid-cols-1">
|
||||||
<button id="download" type="submit"
|
<button
|
||||||
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
|
type="submit"
|
||||||
|
className="bg-green-600 hover:bg-green-700 relative focus:outline-none h-20 text-white font-semibold rounded-md items-center flex justify-center">
|
||||||
|
<div id="spin" className={`${loading ? undefined : "hidden"} absolute left-2`}>
|
||||||
|
<svg className="animate-spin h-5 w-5 ml-4" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor"
|
||||||
|
strokeWidth="4"/>
|
||||||
|
<path className="opacity-80" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
{t('index:addToWallet')}
|
{t('index:addToWallet')}
|
||||||
</button>
|
</button>
|
||||||
<div id="spin" className={loading ? undefined : "hidden"}>
|
|
||||||
<svg className="animate-spin h-5 w-5 ml-4" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor"
|
|
||||||
strokeWidth="4"/>
|
|
||||||
<path className="opacity-75" fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}/>
|
}/>
|
||||||
|
{
|
||||||
|
errorMessage && <Alert isWarning={false} message={errorMessage} onClose={() => setErrorMessage(undefined)}/>
|
||||||
|
}
|
||||||
|
<Card content={
|
||||||
|
<div className={`${isShareDialogAvailable ? "md:grid-cols-2": ""} grid-cols-1 grid gap-5`}>
|
||||||
|
{
|
||||||
|
isShareDialogAvailable && <Button text={t('index:share')} onClick={showShareDialog} />
|
||||||
|
}
|
||||||
|
<Button icon="kofi.png" text={t('common:donate')} onClick={() => {
|
||||||
|
window.open('https://ko-fi.com/marvinsxtr', '_blank');
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
}/>
|
||||||
</form>
|
</form>
|
||||||
<canvas id="canvas" style={{display: "none"}}/>
|
<canvas id="canvas" style={{display: "none"}}/>
|
||||||
{
|
|
||||||
errorMessage && <Alert errorMessage={errorMessage} onClose={() => setErrorMessage(undefined)}/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ function Page(props: PageProps): JSX.Element {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:w-2/3 xl:w-2/5 md:mx-auto flex flex-col min-h-screen justify-center px-5 py-12">
|
<div className="md:w-2/3 xl:w-2/5 md:mx-auto flex flex-col min-h-screen justify-center px-5 pt-12 pb-16">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t('common:title')}</title>
|
<title>{t('common:title')}</title>
|
||||||
<link rel="icon" href="/favicon.ico"/>
|
<link rel="icon" href="/favicon.ico"/>
|
||||||
|
@ -24,8 +24,7 @@ function Page(props: PageProps): JSX.Element {
|
||||||
{props.content}
|
{props.content}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<nav className="nav flex pt-4 flex-row space-x-4 justify-center text-md font-bold flex-wrap">
|
<nav className="nav flex flex-row space-x-4 justify-center text-md font-bold flex-wrap">
|
||||||
<a href="https://ko-fi.com/marvinsxtr" className="hover:underline">{t('common:donate')}</a>
|
|
||||||
<a href="https://github.com/marvinsxtr/covidpass" className="hover:underline">{t('common:gitHub')}</a>
|
<a href="https://github.com/marvinsxtr/covidpass" className="hover:underline">{t('common:gitHub')}</a>
|
||||||
<Link href="/privacy"><a className="hover:underline">{t('common:privacyPolicy')}</a></Link>
|
<Link href="/privacy"><a className="hover:underline">{t('common:privacyPolicy')}</a></Link>
|
||||||
<Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link>
|
<Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link>
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/types/global" />
|
/// <reference types="next/types/global" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
|
|
@ -12,7 +12,8 @@ module.exports = {
|
||||||
'es', 'es-ES',
|
'es', 'es-ES',
|
||||||
'no', 'no-NO',
|
'no', 'no-NO',
|
||||||
'nb', 'nb-NO',
|
'nb', 'nb-NO',
|
||||||
'sv', 'sv-SE', 'sv-FI'
|
'sv', 'sv-SE', 'sv-FI',
|
||||||
|
'ro', 'ro-RO', 'ro-MD'
|
||||||
],
|
],
|
||||||
localeExtension: 'yml',
|
localeExtension: 'yml',
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,4 +2,12 @@ const {i18n} = require('./next-i18next.config');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
i18n,
|
i18n,
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/pass/note',
|
||||||
|
destination: '/pass'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
};
|
};
|
|
@ -16,16 +16,15 @@
|
||||||
"cbor-js": "^0.1.0",
|
"cbor-js": "^0.1.0",
|
||||||
"do-not-zip": "^1.0.0",
|
"do-not-zip": "^1.0.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jpeg-js": "^0.4.3",
|
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"next": "latest",
|
"next": "^11.1.0",
|
||||||
"next-i18next": "^8.5.1",
|
"next-i18next": "^8.5.1",
|
||||||
"next-seo": "^4.26.0",
|
"next-seo": "^4.26.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"pdfjs-dist": "^2.5.207",
|
"pdfjs-dist": "^2.5.207",
|
||||||
"pngjs": "^6.0.0",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-qr-code": "^2.0.3",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webpack": "^5.0.0",
|
"webpack": "^5.0.0",
|
||||||
"worker-loader": "^3.0.7"
|
"worker-loader": "^3.0.7"
|
||||||
|
|
|
@ -14,12 +14,12 @@ function Imprint(): JSX.Element {
|
||||||
<p className="font-bold">{t('imprint:heading')}</p>
|
<p className="font-bold">{t('imprint:heading')}</p>
|
||||||
<p>
|
<p>
|
||||||
Marvin Sextro<br />
|
Marvin Sextro<br />
|
||||||
Wilhelm-Busch-Str. 8A<br />
|
Kopenhagener Straße 45<br />
|
||||||
30167 Hannover<br />
|
10437 Berlin
|
||||||
</p>
|
</p>
|
||||||
<p className="font-bold">{t('imprint:contact')}</p>
|
<p className="font-bold">{t('imprint:contact')}</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="mailto:marvin.sextro@gmail.com" className="underline">marvin.sextro@gmail.com</a>
|
<a href="mailto:covidpass@marvinsextro.de" className="underline">covidpass@marvinsextro.de</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="font-bold">{t('imprint:euDisputeResolution')}</p>
|
<p className="font-bold">{t('imprint:euDisputeResolution')}</p>
|
||||||
<p>{t('imprint:euDisputeResolutionParagraph')}</p>
|
<p>{t('imprint:euDisputeResolutionParagraph')}</p>
|
||||||
|
|
|
@ -40,9 +40,8 @@ function Index(): JSX.Element {
|
||||||
<Page content={
|
<Page content={
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Card content={
|
<Card content={
|
||||||
<p>{t('common:subtitle')} {t('index:iosHint')}</p>
|
<p>{t('common:subtitle')}</p>
|
||||||
}/>
|
}/>
|
||||||
|
|
||||||
<Form/>
|
<Form/>
|
||||||
</div>
|
</div>
|
||||||
}/>
|
}/>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {useTranslation} from 'next-i18next';
|
||||||
|
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
|
||||||
|
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import QRCode from "react-qr-code";
|
||||||
|
|
||||||
|
import Alert from '../components/Alert';
|
||||||
|
import Card from '../components/Card';
|
||||||
|
import Logo from "../components/Logo";
|
||||||
|
|
||||||
|
function Pass(): JSX.Element {
|
||||||
|
const { t } = useTranslation(['common', 'index']);
|
||||||
|
|
||||||
|
const [fragment, setFragment] = useState<string>(undefined);
|
||||||
|
const [view, setView] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const rawFragment = window.location.hash.substring(1);
|
||||||
|
|
||||||
|
if (!rawFragment) {
|
||||||
|
setView(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeTimeout = window.setTimeout(() => {
|
||||||
|
if (rawFragment) {
|
||||||
|
window.location.replace('/pass/note');
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(resizeTimeout);
|
||||||
|
const decodedFragment = Buffer.from(rawFragment, 'base64').toString();
|
||||||
|
setFragment(decodedFragment);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-5 flex flex-col space-y-5 md:w-2/3 xl:w-2/5 md:mx-auto items-center justify-center px-5">
|
||||||
|
<Logo/>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
{
|
||||||
|
fragment &&
|
||||||
|
<Card content={
|
||||||
|
<div className="p-2 bg-white rounded-md">
|
||||||
|
<QRCode value={fragment} size={280} level="L" />
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!view &&
|
||||||
|
<Alert isWarning={true} message={t('index:viewerNote')} onClose={undefined} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticProps({ locale }) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...(await serverSideTranslations(locale, ['index', 'common'])),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Pass;
|
|
@ -49,12 +49,11 @@ function Privacy(): JSX.Element {
|
||||||
</div>
|
</div>
|
||||||
<p className="font-bold">{t('privacy:contact')}</p>
|
<p className="font-bold">{t('privacy:contact')}</p>
|
||||||
<p>
|
<p>
|
||||||
Marvin Sextro<br/>
|
Marvin Sextro
|
||||||
Wilhelm-Busch-Str. 8A<br/>
|
<br/>
|
||||||
30167 Hannover<br/>
|
|
||||||
{t('privacy:email')}:
|
{t('privacy:email')}:
|
||||||
|
|
||||||
<a href="mailto:marvin.sextro@gmail.com">marvin.sextro@gmail.com</a>
|
<a href="mailto:covidpass@marvinsextro.de" className="underline">covidpass@marvinsextro.de</a>
|
||||||
<br/>
|
<br/>
|
||||||
{t('privacy:website')}:
|
{t('privacy:website')}:
|
||||||
|
|
||||||
|
@ -137,9 +136,9 @@ function Privacy(): JSX.Element {
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
PayPal:
|
Ko-fi:
|
||||||
|
|
||||||
<a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full?locale.x=en_EN" className="underline">
|
<a href="https://more.ko-fi.com/privacy" className="underline">
|
||||||
{t('common:privacyPolicy')}
|
{t('common:privacyPolicy')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -153,7 +152,7 @@ function Privacy(): JSX.Element {
|
||||||
<li>
|
<li>
|
||||||
{t('privacy:appleSync')}:
|
{t('privacy:appleSync')}:
|
||||||
|
|
||||||
<a href="https://www.apple.com/legal/privacy/en-ww/privacy.tsx" className="underline">
|
<a href="https://www.apple.com/legal/privacy/en-ww/" className="underline">
|
||||||
{t('common:privacyPolicy')}
|
{t('common:privacyPolicy')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -1,9 +1,7 @@
|
||||||
iosHint: Bitte verwende unter iOS den Safari Browser.
|
iosHint: Bitte verwende unter iOS den Safari Browser.
|
||||||
errorClose: Schließen
|
errorClose: Schließen
|
||||||
selectCertificate: Zertifikat auswählen
|
selectCertificate: Zertifikat auswählen
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Scanne den QR-Code auf Deinem Zertifikat oder wähle einen Screenshot oder eine PDF-Datei mit dem QR-Code.
|
||||||
Scanne den QR-Code auf Deinem Zertifikat oder wähle einen Screenshot oder eine PDF-Datei mit dem QR-Code.
|
|
||||||
Bitte beachte, dass die Auswahl einer Datei direkt von der Kamera nicht unterstützt wird.
|
|
||||||
stopCamera: Kamera stoppen
|
stopCamera: Kamera stoppen
|
||||||
startCamera: Kamera starten
|
startCamera: Kamera starten
|
||||||
openFile: Datei auswählen
|
openFile: Datei auswählen
|
||||||
|
@ -26,4 +24,6 @@ iAcceptThe: Ich akzeptiere die
|
||||||
privacyPolicy: Datenschutzerklärung
|
privacyPolicy: Datenschutzerklärung
|
||||||
createdOnDevice: Auf Deinem Gerät erstellt
|
createdOnDevice: Auf Deinem Gerät erstellt
|
||||||
openSourceTransparent: Open Source und transparent
|
openSourceTransparent: Open Source und transparent
|
||||||
hostedInEU: In der EU gehostet
|
hostedInEU: In der EU gehostet
|
||||||
|
share: Weiterempfehlen
|
||||||
|
viewerNote: Bitte drücke und halte den Link auf der Rückseite des Passes, um den QR Code unter iOS vergrößert anzuzeigen.
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: Στο iOS, παρακαλώ χρησιμοποιήστε τον περιηγητή Safari.
|
iosHint: Στο iOS, παρακαλώ χρησιμοποιήστε τον περιηγητή Safari.
|
||||||
errorClose: Κλείσιμο
|
errorClose: Κλείσιμο
|
||||||
selectCertificate: Επιλογή Πιστοποιητικού
|
selectCertificate: Επιλογή Πιστοποιητικού
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Παρακαλώ σαρώστε τον κωδικό QR του πιστοποιητικού σας ή επιλέξτε ένα στιγμιότυπο οθόνης ή την σελίδα PDF με τον κωδικό QR.
|
||||||
Παρακαλώ σαρώστε τον κωδικό QR του πιστοποιητικού σας ή επιλέξτε ένα στιγμιότυπο οθόνης ή την σελίδα PDF με τον κωδικό QR.
|
|
||||||
Λάβετε υπόψη πως η απευθείας επιλογή κάποιου αρχείου μέσω της κάμερας, δεν υποστηρίζεται.
|
|
||||||
stopCamera: Τερματισμός Κάμερας
|
stopCamera: Τερματισμός Κάμερας
|
||||||
startCamera: Εκκίνηση Κάμερας
|
startCamera: Εκκίνηση Κάμερας
|
||||||
openFile: Επιλογή Αρχείου
|
openFile: Επιλογή Αρχείου
|
||||||
|
@ -27,3 +25,4 @@ privacyPolicy: Πολιτική Απορρήτου
|
||||||
createdOnDevice: Δημιουργείται στη συσκευή σας
|
createdOnDevice: Δημιουργείται στη συσκευή σας
|
||||||
openSourceTransparent: Ανοιχτού κώδικα και διαφανής
|
openSourceTransparent: Ανοιχτού κώδικα και διαφανής
|
||||||
hostedInEU: Φιλοξενείται στην ΕΕ
|
hostedInEU: Φιλοξενείται στην ΕΕ
|
||||||
|
share: Κοινοποιήστε το
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: On iOS, please use the Safari Browser.
|
iosHint: On iOS, please use the Safari Browser.
|
||||||
errorClose: Close
|
errorClose: Close
|
||||||
selectCertificate: Select Certificate
|
selectCertificate: Select Certificate
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Please scan the QR code on your certificate or select a screenshot or PDF page with the QR code.
|
||||||
Please scan the QR code on your certificate or select a screenshot or PDF page with the QR code.
|
|
||||||
Note that selecting a file directly from camera is not supported.
|
|
||||||
stopCamera: Stop Camera
|
stopCamera: Stop Camera
|
||||||
startCamera: Start Camera
|
startCamera: Start Camera
|
||||||
openFile: Select File
|
openFile: Select File
|
||||||
|
@ -26,4 +24,6 @@ iAcceptThe: I accept the
|
||||||
privacyPolicy: Privacy Policy
|
privacyPolicy: Privacy Policy
|
||||||
createdOnDevice: Created on your device
|
createdOnDevice: Created on your device
|
||||||
openSourceTransparent: Open source and transparent
|
openSourceTransparent: Open source and transparent
|
||||||
hostedInEU: Hosted in the EU
|
hostedInEU: Hosted in the EU
|
||||||
|
share: Share
|
||||||
|
viewerNote: Please press and hold the link on the back of the pass in order to enlarge the QR code on iOS.
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: En iOS, Por favor use el navegador Safari.
|
iosHint: En iOS, Por favor use el navegador Safari.
|
||||||
errorClose: Cerrar
|
errorClose: Cerrar
|
||||||
selectCertificate: Seleccione Certificado
|
selectCertificate: Seleccione Certificado
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Escanee el código QR de su certificado, o seleccione una captura de pantalla o un PDF que contenga el código QR.
|
||||||
Escanee el código QR de su certificado, o seleccione una captura de pantalla o un PDF que contenga el código QR.
|
|
||||||
Tenga en cuenta que no se admite la selección de un archivo directamente desde la cámara.
|
|
||||||
stopCamera: Detener Cámara
|
stopCamera: Detener Cámara
|
||||||
startCamera: Iniciar Cámara
|
startCamera: Iniciar Cámara
|
||||||
openFile: Seleccione archivo
|
openFile: Seleccione archivo
|
||||||
|
@ -26,4 +24,5 @@ iAcceptThe: Acepto la
|
||||||
privacyPolicy: Política de Privacidad
|
privacyPolicy: Política de Privacidad
|
||||||
createdOnDevice: Creado en su dispositivo
|
createdOnDevice: Creado en su dispositivo
|
||||||
openSourceTransparent: Open source y transparente
|
openSourceTransparent: Open source y transparente
|
||||||
hostedInEU: Alojado en la UE
|
hostedInEU: Alojado en la UE
|
||||||
|
share: Recomendar
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: Käytä iOS:ssä Safari-selainta.
|
iosHint: Käytä iOS:ssä Safari-selainta.
|
||||||
errorClose: Sulje
|
errorClose: Sulje
|
||||||
selectCertificate: Valitse todistus
|
selectCertificate: Valitse todistus
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Skannaa todistuksessa oleva QR-koodi tai valitse kuvakaappaus tai PDF-sivu, jossa on QR-koodi.
|
||||||
Skannaa todistuksessa oleva QR-koodi tai valitse kuvakaappaus tai PDF-sivu, jossa on QR-koodi.
|
|
||||||
Huomaa, että tiedoston valitsemista suoraan kamerasta ei tueta.
|
|
||||||
stopCamera: Lopeta Kamera
|
stopCamera: Lopeta Kamera
|
||||||
startCamera: Käynnistä Kamera
|
startCamera: Käynnistä Kamera
|
||||||
openFile: Valitse Tiedosto
|
openFile: Valitse Tiedosto
|
||||||
|
@ -26,4 +24,5 @@ iAcceptThe: Hyväksyn
|
||||||
privacyPolicy: Tietosuojaselosteen
|
privacyPolicy: Tietosuojaselosteen
|
||||||
createdOnDevice: Luotu laitteellasi
|
createdOnDevice: Luotu laitteellasi
|
||||||
openSourceTransparent: Avoin lähdekoodi ja läpinäkyvä
|
openSourceTransparent: Avoin lähdekoodi ja läpinäkyvä
|
||||||
hostedInEU: Isännöidään EU:ssa.
|
hostedInEU: Isännöidään EU:ssa.
|
||||||
|
share: Suosittele
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: Sur iOS, veuillez utiliser le navigateur Safari.
|
iosHint: Sur iOS, veuillez utiliser le navigateur Safari.
|
||||||
errorClose: Fermez
|
errorClose: Fermez
|
||||||
selectCertificate: Sélectionner le certificat
|
selectCertificate: Sélectionner le certificat
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Veuillez scanner le QR Code de votre certificat ou sélectionner une capture d'écran ou une page PDF avec le QR Code.
|
||||||
Veuillez scanner le QR Code de votre certificat ou sélectionner une capture d'écran ou une page PDF avec le QR Code.
|
|
||||||
Notez que la sélection d'un fichier directement à partir de l'appareil photo n'est pas prise en charge.
|
|
||||||
stopCamera: Arrêter l'appareil photo
|
stopCamera: Arrêter l'appareil photo
|
||||||
startCamera: Démarrer l'appareil photo
|
startCamera: Démarrer l'appareil photo
|
||||||
openFile: Sélectionner un fichier
|
openFile: Sélectionner un fichier
|
||||||
|
@ -26,4 +24,5 @@ iAcceptThe: J'accepte la
|
||||||
privacyPolicy: Politique de confidentialité
|
privacyPolicy: Politique de confidentialité
|
||||||
createdOnDevice : Créé sur votre appareil
|
createdOnDevice : Créé sur votre appareil
|
||||||
openSourceTransparent : Open source et transparent
|
openSourceTransparent : Open source et transparent
|
||||||
hostedInEU : Hébergé dans l'UE
|
hostedInEU : Hébergé dans l'UE
|
||||||
|
share: Recommander
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: Su iOS, si prega di utilizzare il browser Safari.
|
iosHint: Su iOS, si prega di utilizzare il browser Safari.
|
||||||
errorClose: Chiudi
|
errorClose: Chiudi
|
||||||
selectCertificate: Seleziona il certificato
|
selectCertificate: Seleziona il certificato
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Scannerizza il codice QR sul tuo certificato o seleziona uno screenshot o una pagina PDF con il codice QR.
|
||||||
Scannerizza il codice QR sul tuo certificato o seleziona uno screenshot o una pagina PDF con il codice QR.
|
|
||||||
Nota che la selezione di un file direttamente dalla fotocamera non è supportata.
|
|
||||||
stopCamera: Blocca Fotocamera
|
stopCamera: Blocca Fotocamera
|
||||||
startCamera: Avvia Fotocamera
|
startCamera: Avvia Fotocamera
|
||||||
openFile: Seleziona un File
|
openFile: Seleziona un File
|
||||||
|
@ -27,3 +25,4 @@ privacyPolicy: Privacy Policy
|
||||||
createdOnDevice: Creato sul tuo dispositivo
|
createdOnDevice: Creato sul tuo dispositivo
|
||||||
openSourceTransparent: Open source e trasparente
|
openSourceTransparent: Open source e trasparente
|
||||||
hostedInEU: Server nell'UE
|
hostedInEU: Server nell'UE
|
||||||
|
share: Raccomandare
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: På iOS, vennligst bruk Safari nettleseren.
|
iosHint: På iOS, vennligst bruk Safari nettleseren.
|
||||||
errorClose: Lukk
|
errorClose: Lukk
|
||||||
selectCertificate: Velg Sertifikat
|
selectCertificate: Velg Sertifikat
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Skann QR-koden på sertifikatet ditt, eller velg et skjermbilde eller en PDF med QR-koden.
|
||||||
Skann QR-koden på sertifikatet ditt, eller velg et skjermbilde eller en PDF med QR-koden.
|
|
||||||
Vær oppmerksom på at det ikke støttes å velge en fil direkte fra kameraet.
|
|
||||||
stopCamera: Stopp Kamera
|
stopCamera: Stopp Kamera
|
||||||
startCamera: Start Kamera
|
startCamera: Start Kamera
|
||||||
openFile: Velg Fil
|
openFile: Velg Fil
|
||||||
|
@ -27,3 +25,4 @@ privacyPolicy: Personvernerklæring
|
||||||
createdOnDevice: Laget på enheten din
|
createdOnDevice: Laget på enheten din
|
||||||
openSourceTransparent: Åpen kildekode og gjennomsiktig
|
openSourceTransparent: Åpen kildekode og gjennomsiktig
|
||||||
hostedInEU: Driftet i EU
|
hostedInEU: Driftet i EU
|
||||||
|
share: Anbefale
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: Gebruik op iOS de Safari-browser.
|
iosHint: Gebruik op iOS de Safari-browser.
|
||||||
errorClose: Sluiten
|
errorClose: Sluiten
|
||||||
selectCertificate: Selecteer Certificaat
|
selectCertificate: Selecteer Certificaat
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Scan de QR-code op uw certificaat of selecteer een screenshot of pdf-pagina met de QR-code.
|
||||||
Scan de QR-code op uw certificaat of selecteer een screenshot of pdf-pagina met de QR-code.
|
|
||||||
Merk op dat het rechtstreeks vanaf de camera selecteren van een bestand niet wordt ondersteund.
|
|
||||||
stopCamera: Stop Camera
|
stopCamera: Stop Camera
|
||||||
startCamera: Start Camera
|
startCamera: Start Camera
|
||||||
openFile: Selecteer Bestand
|
openFile: Selecteer Bestand
|
||||||
|
@ -26,4 +24,5 @@ iAcceptThe: Ik accepteer het
|
||||||
privacyPolicy: Privacybeleid
|
privacyPolicy: Privacybeleid
|
||||||
createdOnDevice: Aangemaakt op uw apparaat
|
createdOnDevice: Aangemaakt op uw apparaat
|
||||||
openSourceTransparent: Open source en transparant
|
openSourceTransparent: Open source en transparant
|
||||||
hostedInEU: Gehost in de EU
|
hostedInEU: Gehost in de EU
|
||||||
|
share: Aanbevelen
|
|
@ -0,0 +1 @@
|
||||||
|
ro
|
|
@ -0,0 +1 @@
|
||||||
|
ro
|
|
@ -0,0 +1,6 @@
|
||||||
|
title: CovidPass
|
||||||
|
subtitle: Adaugă-ți Certificatul UE Covid in aplicația ta preferată de wallet.
|
||||||
|
privacyPolicy: Politica de Confidențialitate
|
||||||
|
donate: Donează
|
||||||
|
gitHub: GitHub
|
||||||
|
imprint: Mențiuni Legale
|
|
@ -0,0 +1,20 @@
|
||||||
|
noFileOrQrCode: Te rog scaneaza un cod QR, sau alege un fisier
|
||||||
|
signatureFailed: Eroare in timpul semnarii certificatului pe server
|
||||||
|
decodingFailed: Imposibil de decodat continutul codului QR
|
||||||
|
invalidColor: Culoare Invalida
|
||||||
|
certificateData: Eroare in citirea datelor certificatului
|
||||||
|
nameMissing: Eroare in citirea numelui
|
||||||
|
dobMissing: Eroare in citirea datei de nastere
|
||||||
|
invalidMedicalProduct: Produs medical (Vaccin) invalid
|
||||||
|
invalidCountryCode: Codul tarii este invalid
|
||||||
|
invalidManufacturer: Fabricant invalid
|
||||||
|
invalidFileType: Tipul fisierului invalid
|
||||||
|
couldNotDecode: Eroare in decodarea codului QR din fisier
|
||||||
|
couldNotFindQrCode: Nu s-a gasit niciun cod QR valid in fisierul selectat
|
||||||
|
invalidQrCode: Cod QR invalid
|
||||||
|
certificateType: Tipul certificatului este invalid
|
||||||
|
invalidTestResult: Rezultatul testului este invalid
|
||||||
|
invalidTestType: Tipul testului este invalid
|
||||||
|
noCameraAccess: Nu s-a putut accesa camera. Schimbati permisiunile in Setari > Safari > Camera.
|
||||||
|
noCameraFound: Nu s-a putut accesa camera.
|
||||||
|
safariSupportOnly: in iOS, va rog sa folositi doar browser-ul safari.
|
|
@ -0,0 +1,27 @@
|
||||||
|
heading: Informații conform articolului 5 TMG
|
||||||
|
contact: Contact
|
||||||
|
euDisputeResolution: Soluționarea Disputelor UE
|
||||||
|
euDisputeResolutionParagraph: |
|
||||||
|
Comisia Europeana dispune de o platforma pentru soluționarea disputelor online (OS) https://ec.europa.eu/consumers/odr.
|
||||||
|
Ne puteți găsi adresa de email in datele de mai sus.
|
||||||
|
consumerDisputeResolution: Rezolvarea litigiilor consumatorilor / comisia universală de arbitraj
|
||||||
|
consumerDisputeResolutionParagraph: Nu suntem dispuși sau obligați să participăm la procedurile de soluționare a litigiilor în fața unei comisii de arbitraj pentru consumatori.
|
||||||
|
liabilityForContents: Raspunderea pentru continut
|
||||||
|
liabilityForContentsParagraph: |
|
||||||
|
În calitate de furnizori de servicii, suntem responsabili pentru conținutul propriu în conformitate cu articolul 7 paragraful 1 TMG în conformitate cu legile generale.
|
||||||
|
Conform secțiunilor 8 până la 10, nu suntem obligați să monitorizăm informațiile transmise sau stocate sau să investigăm circumstanțe care indică o activitate ilegală.
|
||||||
|
Obligațiile de a elimina sau de a bloca utilizarea informațiilor conform legilor generale rămân neafectate.
|
||||||
|
Cu toate acestea, răspunderea în această privința este posibilă numai din momentul în care devine cunoscută o încălcare concretă a legii.
|
||||||
|
Dacă aflăm de existența unor asemenea încălcări, vom elimina imediat conținutul relevant.
|
||||||
|
liabilityForLinks: Raspunderea pentru linkuri
|
||||||
|
liabilityForLinksParagraph: |
|
||||||
|
Oferta noastră conține link-uri către site-uri externe ale terților, asupra cărora nu avem nicio influență.
|
||||||
|
Prin urmare, nu ne putem asuma nicio răspundere pentru aceste conținuturi externe.
|
||||||
|
Furnizorul sau operatorul respectiv al site-urilor este întotdeauna responsabil pentru conținutul site-urilor mentionate.
|
||||||
|
Paginile conectate au fost verificate pentru posibile încălcări legale la momentul punerii link-ului.
|
||||||
|
Conținuturile ilegale nu erau recunoscute în momentul punerii link-urilor.
|
||||||
|
Cu toate acestea, un control permanent al conținutului paginilor linkate nu este rezonabil fără dovezi concrete ale unei încălcări a legii.
|
||||||
|
Dacă aflăm că există încălcări, vom elimina imediat astfel de legături.
|
||||||
|
credits: Credite
|
||||||
|
creditsSource: Cu extrase din https://www.e-recht24.de/impressum-generator.html
|
||||||
|
creditsTranslation: Tradus cu https://www.DeepL.com/Translator (free version)
|
|
@ -0,0 +1,28 @@
|
||||||
|
iosHint: In iOS, te rog foloseste browser-ul Safari.
|
||||||
|
errorClose: Inchide
|
||||||
|
selectCertificate: Alege certificatul
|
||||||
|
selectCertificateDescription: Te rog scaneaza codul QR de pe certificatul tau sau alege un screenshot/PDF cu codul QR
|
||||||
|
stopCamera: Opreste Camera
|
||||||
|
startCamera: Porneste Camera
|
||||||
|
openFile: Selecteaza fisierul
|
||||||
|
foundQrCode: Cod QR detectat!
|
||||||
|
pickColor: Alege o culoare
|
||||||
|
pickColorDescription: Alege o culoare de fundal pentru certificatul tau.
|
||||||
|
colorWhite: alb
|
||||||
|
colorBlack: negru
|
||||||
|
colorGrey: gri
|
||||||
|
colorGreen: verde
|
||||||
|
colorIndigo: indigo
|
||||||
|
colorBlue: albastru
|
||||||
|
colorPurple: mov
|
||||||
|
colorTeal: teal
|
||||||
|
addToWallet: Adauga in Wallet
|
||||||
|
dataPrivacyDescription: |
|
||||||
|
Confidentialitatea datelor este de o importanta speciala cand vine vorba de date medicale.
|
||||||
|
Pentru a face o alegere informata, va rugam cititi
|
||||||
|
iAcceptThe: Sunt de acord cu
|
||||||
|
privacyPolicy: Politica de Confidentialitate
|
||||||
|
createdOnDevice: Creat pe dispozitivul tau
|
||||||
|
openSourceTransparent: Open source si transparent
|
||||||
|
hostedInEU: Gazduit in UE
|
||||||
|
share: Distribuie
|
|
@ -0,0 +1,57 @@
|
||||||
|
gdprNotice: |
|
||||||
|
Politica noastră de confidențialitate se bazează pe termenii folosiți de legiuitorul european
|
||||||
|
pentru adoptarea Regulamentului general privind protecția datelor (GDPR).
|
||||||
|
generalInfo: Informații Generale
|
||||||
|
generalInfoProcess: |
|
||||||
|
Întregul proces de generare a fișierului de Wallet are loc local în browserul dvs.
|
||||||
|
Pentru pasul de semnare (certificare), doar o reprezentare hashed (codata ireversibil) a datelor dvs. este trimisă către server.
|
||||||
|
generalInfoStoring: Datele dumneavoastră nu sunt stocate dincolo de sesiunea activă a browserului, iar site-ul nu utilizează cookie-uri.
|
||||||
|
generalInfoThirdParties: Nu sunt trimise date catre părți terțe.
|
||||||
|
generalInfoHttps: Datele sunt transmise în mod securizat prin https.
|
||||||
|
generalInfoLocation: Serverele noastre sunt găzduite in Nuremberg, Germania.
|
||||||
|
generalInfoGitHub: Codul sursă al acestui site este accesibil pe
|
||||||
|
generalInfoLockScreen: În mod normal, cardurile din Apple Wallet sunt accesibile de pe lock screen. Puteți schimba acest lucru in
|
||||||
|
settings: setări
|
||||||
|
generalInfoProvider: |
|
||||||
|
Furnizorul de server prelucrează date pentru a furniza acest site.
|
||||||
|
Pentru a înțelege mai bine ce măsuri iau aceștia pentru a vă proteja datele, vă rugăm să citiți și pe partea lor
|
||||||
|
privacyPolicy: Politica de Confidențialitate
|
||||||
|
andThe: și
|
||||||
|
dataPrivacyFaq: Întrebari puse des despre confidențialitate
|
||||||
|
contact: Contact
|
||||||
|
email: Email
|
||||||
|
website: Website
|
||||||
|
process: Explicație simplificata a procesului
|
||||||
|
processFirst: În primul rând, următorii pași au loc local în browserul dvs
|
||||||
|
processSecond: În al doilea rând, următorii pași au loc pe serverul nostru
|
||||||
|
processThird: În cele din urmă, următorii pași au loc local în browserul dvs
|
||||||
|
processRecognizing: Recunoașterea și extragerea datelor codului QR din certificatul selectat
|
||||||
|
processDecoding: Decodificarea datelor dvs. personale și medicale din încărcarea codului QR
|
||||||
|
processAssembling: Asamblarea unui fișier de Wallet incomplet din datele dvs
|
||||||
|
processGenerating: Generarea unui fișier care conține hash-uri ale datelor stocate în fișierul wallet
|
||||||
|
processSending: Trimiterea doar fișierul care conține hash-urile către serverul nostru
|
||||||
|
processReceiving: Primirea și verificarea hashurilor care au fost generate local
|
||||||
|
processSigning: Semnarea fișierului care conține hashurile
|
||||||
|
processSendingBack: Trimiterea semnăturii înapoi
|
||||||
|
processCompleting: Asamblarea fișierului wallet semnat din fișierul incomplet generat local și semnătura
|
||||||
|
processSaving: Salvarea fisierului pe dispozitiv
|
||||||
|
locallyProcessedData: Date procesate local
|
||||||
|
the:
|
||||||
|
schema: Schema Certificatului Digital Covid
|
||||||
|
specification: contine o schema complete cu datele care sunt continute de codul QR al certificatului covid si care vor fi procesate in browser.
|
||||||
|
serverProvider: Furnizorul de Servere
|
||||||
|
serverProviderIs: Furnizorul nostru de servere este
|
||||||
|
logFiles: Următoarele date pot fi colectate și stocate în fișierele jurnal ale serverului
|
||||||
|
logFilesBrowser: Tipurile și versiunile de browser utilizate
|
||||||
|
logFilesOs: Sistemul de operare utilizat de sistemul de accesare
|
||||||
|
logFilesReferrer: Site-ul web de pe care un sistem de accesare ajunge pe site-ul nostru web (așa-numiții recomandanți)
|
||||||
|
logFilesTime: Data și ora accesului
|
||||||
|
logFilesIpAddress: Adresele IP pseudonimizate
|
||||||
|
rights: Drepturile tale
|
||||||
|
rightsGranted: În conformitate cu GDPR, aveți următoarele drepturi
|
||||||
|
rightsAccess: Dreptul de acces la datele dumneavoastră; Ai dreptul să știi ce date au fost colectate despre tine și cum au fost prelucrate.
|
||||||
|
rightsErasure: Dreptul de a fi uitat; Ștergerea datelor dumneavoastră personale.
|
||||||
|
rightsRectification: Dreptul de rectificare; Aveți dreptul de a corecta datele eronate.
|
||||||
|
rightsPortability: Dreptul la portabilitatea datelor; Aveți dreptul să vă transferați datele dintr-un sistem de procesare în altul.
|
||||||
|
thirdParties: Părți terțe
|
||||||
|
appleSync: Este posibil ca Apple să vă sincronizeze cardurile wallet prin iCloud
|
|
@ -1,9 +1,7 @@
|
||||||
iosHint: På iOS, vänligen använd Safari som webbläsare.
|
iosHint: På iOS, vänligen använd Safari som webbläsare.
|
||||||
errorClose: Stäng
|
errorClose: Stäng
|
||||||
selectCertificate: Välj certifikat
|
selectCertificate: Välj certifikat
|
||||||
selectCertificateDescription: |
|
selectCertificateDescription: Vänligen skanna QR-koden på ditt certifikat eller välj en skärmdump eller PDF-fil som innehåller QR-koden.
|
||||||
Vänligen skanna QR-koden på ditt certifikat eller välj en skärmdump eller PDF-fil som innehåller QR-koden.
|
|
||||||
Observera att det saknas stöd för att välja en fil direkt från kameran.
|
|
||||||
stopCamera: Stäng kamera
|
stopCamera: Stäng kamera
|
||||||
startCamera: Öppna kamera
|
startCamera: Öppna kamera
|
||||||
openFile: Välj fil
|
openFile: Välj fil
|
||||||
|
@ -26,4 +24,5 @@ iAcceptThe: Jag godtar
|
||||||
privacyPolicy: integritetspolicyn
|
privacyPolicy: integritetspolicyn
|
||||||
createdOnDevice: Skapad på din enhet
|
createdOnDevice: Skapad på din enhet
|
||||||
openSourceTransparent: Öppen källkod och transparent
|
openSourceTransparent: Öppen källkod och transparent
|
||||||
hostedInEU: Drivs från EU
|
hostedInEU: Drivs från EU
|
||||||
|
share: Rekommendera
|
|
@ -0,0 +1,3 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /pass
|
||||||
|
Disallow: /pass/note
|
|
@ -20,6 +20,10 @@ export function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeData(data: string): Object {
|
export function decodeData(data: string): Object {
|
||||||
|
if (data.startsWith('https://')) {
|
||||||
|
var url = new URL(data);
|
||||||
|
data = decodeURIComponent(url.hash.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
if (data.startsWith('HC1')) {
|
if (data.startsWith('HC1')) {
|
||||||
data = data.substring(3);
|
data = data.substring(3);
|
||||||
|
@ -45,4 +49,4 @@ export function decodeData(data: string): Object {
|
||||||
var decoded: Object = cbor.decode(typedArrayToBufferSliced(plaintext));
|
var decoded: Object = cbor.decode(typedArrayToBufferSliced(plaintext));
|
||||||
|
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ interface QrCode {
|
||||||
message: string;
|
message: string;
|
||||||
format: QrFormat;
|
format: QrFormat;
|
||||||
messageEncoding: Encoding;
|
messageEncoding: Encoding;
|
||||||
|
altText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SignData {
|
interface SignData {
|
||||||
|
@ -83,6 +84,7 @@ export class PassData {
|
||||||
message: payload.rawData,
|
message: payload.rawData,
|
||||||
format: QrFormat.PKBarcodeFormatQR,
|
format: QrFormat.PKBarcodeFormatQR,
|
||||||
messageEncoding: Encoding.utf8,
|
messageEncoding: Encoding.utf8,
|
||||||
|
altText: 'SCAN TO VERIFY',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create pass data
|
// Create pass data
|
||||||
|
|
|
@ -3,9 +3,9 @@ import {Constants} from "./constants";
|
||||||
import {COLORS} from "./colors";
|
import {COLORS} from "./colors";
|
||||||
|
|
||||||
enum CertificateType {
|
enum CertificateType {
|
||||||
Vaccination = 'Vaccination Card',
|
Vaccination = 'Vaccination Pass',
|
||||||
Test = 'Test Certificate',
|
Test = 'Test Pass',
|
||||||
Recovery = 'Recovery Certificate',
|
Recovery = 'Recovery Pass',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TextAlignment {
|
enum TextAlignment {
|
||||||
|
@ -116,6 +116,11 @@ export class Payload {
|
||||||
|
|
||||||
const country = valueSets.countryCodes[countryCode].display;
|
const country = valueSets.countryCodes[countryCode].display;
|
||||||
|
|
||||||
|
// Encode raw data and get url
|
||||||
|
const encodedData = Buffer.from(body.rawData).toString('base64');
|
||||||
|
const url = window.location.protocol + "//" + window.location.host;
|
||||||
|
|
||||||
|
|
||||||
const generic: PassDictionary = {
|
const generic: PassDictionary = {
|
||||||
headerFields: [
|
headerFields: [
|
||||||
{
|
{
|
||||||
|
@ -134,6 +139,11 @@ export class Payload {
|
||||||
secondaryFields: [],
|
secondaryFields: [],
|
||||||
auxiliaryFields: [],
|
auxiliaryFields: [],
|
||||||
backFields: [
|
backFields: [
|
||||||
|
{
|
||||||
|
key: "enlarge",
|
||||||
|
label: "Enlarging the QR Code",
|
||||||
|
value: `Inside the Wallet app on iOS, press and hold the link below. This does not work when accessing the Wallet by double-clicking the side button.\n<a href='${url}/pass#${encodedData}'>Enlarge QR Code</a>`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "uvci",
|
key: "uvci",
|
||||||
label: "Unique Certificate Identifier (UVCI)",
|
label: "Unique Certificate Identifier (UVCI)",
|
||||||
|
@ -157,10 +167,27 @@ export class Payload {
|
||||||
this.img2x = dark ? Constants.img2xWhite : Constants.img2xBlack
|
this.img2x = dark ? Constants.img2xWhite : Constants.img2xBlack
|
||||||
this.dark = dark;
|
this.dark = dark;
|
||||||
|
|
||||||
this.generic = Payload.fillPassData(this.certificateType, generic, properties, valueSets, country, dateOfBirth);
|
this.generic = Payload.fillPassData(
|
||||||
|
this.certificateType,
|
||||||
|
generic,
|
||||||
|
properties,
|
||||||
|
valueSets,
|
||||||
|
country,
|
||||||
|
dateOfBirth,
|
||||||
|
url
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fillPassData(type: CertificateType, data: PassDictionary, properties: Object, valueSets: ValueSets, country: string, dateOfBirth: string): PassDictionary {
|
static fillPassData(
|
||||||
|
type: CertificateType,
|
||||||
|
data: PassDictionary,
|
||||||
|
properties: Object,
|
||||||
|
valueSets: ValueSets,
|
||||||
|
country: string,
|
||||||
|
dateOfBirth: string,
|
||||||
|
url: string
|
||||||
|
): PassDictionary {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CertificateType.Vaccination:
|
case CertificateType.Vaccination:
|
||||||
const dose = `${properties['dn']}/${properties['sd']}`;
|
const dose = `${properties['dn']}/${properties['sd']}`;
|
||||||
|
@ -235,7 +262,17 @@ export class Payload {
|
||||||
throw new Error('invalidTestType')
|
throw new Error('invalidTestType')
|
||||||
}
|
}
|
||||||
|
|
||||||
const testResult = valueSets.testResults[testResultKey].display;
|
let testResult = valueSets.testResults[testResultKey].display;
|
||||||
|
|
||||||
|
switch (testResult) {
|
||||||
|
case 'Not detected':
|
||||||
|
testResult = 'Negative';
|
||||||
|
break;
|
||||||
|
case 'Detected':
|
||||||
|
testResult = 'Positive';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const testType = valueSets.testTypes[testTypeKey].display;
|
const testType = valueSets.testTypes[testTypeKey].display;
|
||||||
|
|
||||||
const testTime = testDateTimeString.replace(/.*T/, '').replace('Z', ' ') + 'UTC';
|
const testTime = testDateTimeString.replace(/.*T/, '').replace('Z', ' ') + 'UTC';
|
||||||
|
@ -339,6 +376,14 @@ export class Payload {
|
||||||
throw new Error('certificateType');
|
throw new Error('certificateType');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.backFields.push(...[
|
||||||
|
{
|
||||||
|
key: "credits",
|
||||||
|
label: "",
|
||||||
|
value: `Created with <a href='${url}'>CovidPass</a>`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import {PayloadBody} from "./payload";
|
import {PayloadBody} from "./payload";
|
||||||
import {PNG} from 'pngjs'
|
|
||||||
import * as PdfJS from 'pdfjs-dist'
|
import * as PdfJS from 'pdfjs-dist'
|
||||||
import jsQR, {QRCode} from "jsqr";
|
import jsQR, {QRCode} from "jsqr";
|
||||||
import {decodeData} from "./decode";
|
import {decodeData} from "./decode";
|
||||||
|
@ -9,19 +8,19 @@ import {COLORS} from "./colors";
|
||||||
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
|
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
|
||||||
|
|
||||||
export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise<PayloadBody> {
|
export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise<PayloadBody> {
|
||||||
// Read file
|
|
||||||
const fileBuffer = await file.arrayBuffer();
|
|
||||||
|
|
||||||
let imageData: ImageData;
|
let imageData: ImageData;
|
||||||
|
|
||||||
switch (file.type) {
|
switch (file.type) {
|
||||||
case 'application/pdf':
|
case 'application/pdf':
|
||||||
console.log('pdf')
|
// Read file
|
||||||
|
const fileBuffer = await file.arrayBuffer();
|
||||||
imageData = await getImageDataFromPdf(fileBuffer)
|
imageData = await getImageDataFromPdf(fileBuffer)
|
||||||
break
|
break
|
||||||
case 'image/png':
|
case 'image/png':
|
||||||
console.log('png')
|
case 'image/jpeg':
|
||||||
imageData = await getImageDataFromPng(fileBuffer)
|
case 'image/webp':
|
||||||
|
case 'image/gif':
|
||||||
|
imageData = await getImageDataFromImage(file)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw Error('invalidFileType')
|
throw Error('invalidFileType')
|
||||||
|
@ -81,18 +80,48 @@ export async function getPayloadBodyFromQR(qrCodeResult: Result, color: COLORS):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getImageDataFromPng(fileBuffer: ArrayBuffer): Promise<ImageData> {
|
function getImageDataFromImage(file: File): Promise<ImageData> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let png = new PNG({filterType: 4})
|
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
|
||||||
|
const canvasContext = canvas.getContext('2d');
|
||||||
|
|
||||||
png.parse(fileBuffer, (error, data) => {
|
// create Image object
|
||||||
if (error) {
|
const img = new Image();
|
||||||
reject();
|
|
||||||
|
img.onload = () => {
|
||||||
|
// constrain image to 2 Mpx
|
||||||
|
const maxPx = 2000000;
|
||||||
|
let width: number;
|
||||||
|
let height: number;
|
||||||
|
if (img.naturalWidth * img.naturalHeight > maxPx) {
|
||||||
|
const ratio = img.naturalHeight / img.naturalWidth;
|
||||||
|
width = Math.sqrt(maxPx / ratio);
|
||||||
|
height = Math.floor(width * ratio);
|
||||||
|
width = Math.floor(width);
|
||||||
|
} else {
|
||||||
|
width = img.naturalWidth;
|
||||||
|
height = img.naturalHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(data);
|
// Set correct canvas width / height
|
||||||
})
|
canvas.width = width;
|
||||||
})
|
canvas.height = height;
|
||||||
|
|
||||||
|
// draw image into canvas
|
||||||
|
canvasContext.clearRect(0, 0, width, height);
|
||||||
|
canvasContext.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
// Obtain image data
|
||||||
|
resolve(canvasContext.getImageData(0, 0, width, height));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = (e) => {
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// start loading image from file
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData> {
|
async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData> {
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve"
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|
Loading…
Reference in New Issue