Skip to content

Commit

Permalink
Merge branch 'main' into add-keycloak-config
Browse files Browse the repository at this point in the history
  • Loading branch information
daniellrgn committed Dec 24, 2024
2 parents a9dd2b3 + fe7cca9 commit 0477e6d
Show file tree
Hide file tree
Showing 30 changed files with 801 additions and 119 deletions.
7 changes: 0 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"merge-images": "^2.0.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.55.1",
Expand Down
7 changes: 7 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
<meta charset="utf-8" />
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preload" as="font" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/fonts/bootstrap-icons.woff2?8d200481aa7f02a2d63a331fc782cfaf" type="font/woff2" crossorigin="anonymous">
<link rel="preload" as="image" href="/img/doh_logo_doh-black.png" />
<link rel="preload" as="image" href="/img/waverifypluslogo.png" />
<link rel="preload" as="image" href="/img/qr-banner-top.png" />
<link rel="preload" as="image" href="/img/qr-banner-bottom.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />

%sveltekit.head%
Expand Down
247 changes: 139 additions & 108 deletions src/lib/components/app/HealthLink.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
ModalFooter,
Row
} from 'sveltestrap';
import mergeImages from 'merge-images';
import { goto } from '$app/navigation';
import type { Writable } from 'svelte/store';
import type { SHLAdminParams, SHLClient } from '$lib/utils/managementClient';
Expand All @@ -45,18 +44,13 @@
let copyNotice = '';
let href: Promise<string>;
let qrCode: Promise<string>;
let qrCodeImage: Promise<string>;
let showPassword = false;
$: type = showPassword ? 'text' : 'password';
$: icon = showPassword ? 'eye-fill' : 'eye-slash-fill';
$: {
href = getUrl(shl);
}
$: {
qrCode = href
.then((r) => QRCode.toDataURL(r, { errorCorrectionLevel: 'M' }))
// .then(qrCode => mergeImages([qrCode, {src: '/img/qrcode-logo.png', x:0, y:4}]));
qrCodeImage = createQrCodeImage(href);
}
let canShare = navigator?.canShare?.({ url: 'https://example.com', title: 'Title' });
Expand All @@ -74,6 +68,48 @@
}
});
// Combine header, qr code, and footer images into one image
async function createQrCodeImage(href: Promise<string>) {
// create the qr code image
const qrCodeURI = await href.then(href => QRCode.toDataURL(href, { errorCorrectionLevel: 'M' }));
const qrCode = await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(new Error('Failed to load image from data URI.'));
img.src = qrCodeURI;
}) as HTMLImageElement;
// get the header and footer images
const header = document.getElementById('qrcode-header') as HTMLImageElement;
const footer = document.getElementById('qrcode-footer') as HTMLImageElement;
// scale the images down to match the smallest size
const targetWidth: number = Math.min(qrCode.width, header.width, footer.width);
const headerHeight: number = (header.height / header.width) * targetWidth;
const qrCodeImageHeight: number = (qrCode.height / qrCode.width) * targetWidth;
const footerHeight: number = (footer.height / footer.width) * targetWidth;
// get the canvas and combine the images
const canvas = document.getElementById('qrcode') as HTMLCanvasElement;
if (!canvas) {
throw Error('Could not get qrcode canvas element');
}
const ctx = canvas.getContext('2d');
if (!ctx) {
throw Error('Could not get canvas context');
}
const marginX = 5;
const marginY = 10;
canvas.width = targetWidth + marginX * 2;
canvas.height = headerHeight + qrCodeImageHeight + footerHeight + marginY * 2;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(header, marginX, marginY, targetWidth, headerHeight);
ctx.drawImage(qrCode, marginX, headerHeight + marginY, targetWidth, qrCodeImageHeight);
ctx.drawImage(footer, marginX, headerHeight + qrCodeImageHeight + marginY, targetWidth, footerHeight);
const fullImageDataUrl = canvas.toDataURL('image/png');
return fullImageDataUrl;
}
async function getUrl(shl: SHLAdminParams) {
let shlMin = {
id: shl.id,
Expand Down Expand Up @@ -125,73 +161,76 @@
</Col>
</Alert>
{/if}

<!-- Placeholder elements for QR Code image construction -->
<img id="qrcode-header" class="img-fluid" alt="QR Code Header for SHL" src="/img/qr-banner-top.png" style="display: none;"/>
<img id="qrcode-footer" class="img-fluid" alt="QR Code Footer for SHL" src="/img/qr-banner-bottom.png" style="display: none;"/>
<canvas id="qrcode" class="img-fluid" style="display: none;"/>

<Row cols={{ md: 2, xs: 1 }}>
<Col>
<Col class="d-flex justify-content-center">
<Card class="mb-3" color="light">
<CardHeader>
<CardTitle>
<Icon name={shl.passcode ? 'lock' : 'unlock'} />
{shl.label}
</CardTitle>
</CardHeader>
<CardBody>
{#if shl.exp}
<CardSubtitle color="success">
Expires: {new Date(shl.exp * 1000).toISOString().slice(0, 10)}
</CardSubtitle>
{/if}
<CardText>
{#await qrCode then dataUrl}
<CardImg class="img-fluid" alt="QR Code for SHL" src="/img/qr-banner-top.png" />
<CardImg class="img-fluid" alt="QR Code for SHL" src={dataUrl} />
<CardImg class="img-fluid" alt="QR Code for SHL" src="/img/qr-banner-bottom.png" />
{/await}
</CardText>
</CardHeader>
<CardBody>
{#await qrCodeImage then dataUrl}
<CardImg class="img-fluid" alt="QR Code for SHL" src={dataUrl} style="background-color: white"/>
{/await}
</CardBody>
<CardFooter>
<Row class="justify-content-center">
{#if canShare}
{#await qrCode then dataUrl}
<Button
size="sm"
color="primary"
class="mx-1" style="width: fit-content"
on:click={async () => {
const blob = await (await fetch(dataUrl)).blob();
const file = new File([blob], 'wa-verify-plus-qrcode.png', { type: blob.type });
if (navigator.canShare({ files: [file]})) {
navigator.share({
files: [file],
url: await href,
text: `${(shl.label ? `${shl.label}\n\n` : "")}Here's my WA Verify+ Health Summary:\n\n`
});
} else {
navigator.share({ url: await href, title: shl.label });
}
}}>
<Icon name="share" /> Share
</Button>
{/await}
{#await qrCodeImage then dataUrl}
<Button
size="sm"
color="primary"
class="mx-1" style="width: fit-content"
on:click={async () => {
const blob = await (await fetch(dataUrl)).blob();
const file = new File([blob], 'wa-verify-plus-qrcode.png', { type: blob.type });
if (navigator.canShare({ files: [file]})) {
navigator.share({
files: [file],
url: await href,
text: `${(shl.label ? `${shl.label}\n\n` : "")}Here's my WA Verify+ Health Summary:\n\n`
});
} else {
navigator.share({ url: await href, title: shl.label });
}
}}>
<Icon name="share" /> Share
</Button>
{/await}
{/if}
<Button size="sm" color="primary" class="mx-1" style="width: fit-content" on:click={copyShl} disabled={!!copyNotice}>
<Icon name="clipboard" />
{#if copyNotice}
{copyNotice}
{:else}
Copy Link
{/if}
</Button>
<Button size="sm" color="primary" class="mx-1" style="width: fit-content" on:click={copyShl} disabled={!!copyNotice}>
<Icon name="clipboard" />
{#if copyNotice}
{copyNotice}
{:else}
Copy Link
{/if}
</Button>
{#await href then href}
<Button size="sm" color="primary" class="mx-1" style="width: fit-content" {href} target="_blank">
<Icon name="box-arrow-up-right" /> Open
</Button>
<Button size="sm" color="primary" class="mx-1" style="width: fit-content" {href} target="_blank">
<Icon name="box-arrow-up-right" /> Open
</Button>
{/await}
</Row>
</CardFooter>
</Card>
</Col>
<Col>
<FormGroup class="label shlbutton">
<Col class="d-flex justify-content-center">
<FormGroup class="label shlbutton" style="width: 100%">
<div style="border-bottom: 1px solid rgb(204, 204, 204); margin-bottom: 1em"><h3>Manage Link</h3></div>
{#await linkIsActive then active}
<Alert isOpen={active === false} color="danger" fade={false}>
<Col class="d-flex justify-content-between">
Expand All @@ -218,9 +257,9 @@
</Alert>
{/await}
{#if showActive}
<Alert color="success">
<Icon name="check-circle-fill" />&nbsp;Active
</Alert>
<Alert color="success">
<Icon name="check-circle-fill" />&nbsp;Active
</Alert>
{/if}
<Label for="label">Label for SMART Health Link</Label>
<Input
Expand All @@ -246,8 +285,6 @@
}}>
<Icon name="sticky" /> Update Label
</Button>
</FormGroup>
<FormGroup class="passcode shlbutton">
<Label for="passcode">Add or Update Passcode (optional)</Label>
<div style="position:relative">
<Input
Expand Down Expand Up @@ -277,8 +314,6 @@
e.id === shl.id ? { ...shl, passcode: shlControlled.passcode } : e
);
}}><Icon name="lock" /> Update Passcode</Button>
</FormGroup>
<FormGroup class="shlbutton">
<Button size="sm" on:click={toggle} color="danger"><Icon name="trash3" /> Delete SMART Health Link</Button>
<Modal isOpen={open} backdrop="static" {toggle}>
<ModalHeader {toggle}>Delete SMART Health Link</ModalHeader>
Expand All @@ -292,56 +327,48 @@
</Modal>
</FormGroup>
</Col>
</Row>
{#if $mode === 'advanced'}
<Row>
<h3>Contents</h3>
<Label>Add or remove summaries shared by this link.</Label>
</Row>
{#if shl.files.length == 0}
<Row>
<p><em>No Summaries found</em></p>
</Row>
{/if}
{#each shl.files as file (file.contentEncrypted)}
<Row>
<Col>
<Card class="mb-3" color="light">
<CardHeader>
<Row class="align-items-center">
<Col xs=6 class="align-items-center">
{#if file.date}
<strong><Icon name="calendar"></Icon> {file.date}</strong>
{/if}
</Col>
<Col xs=6>
<Row class="justify-content-end">
<Button size="sm" color="danger" style="width: fit-content" on:click={(e) => {
deleteFile(file.contentEncrypted);
}}>
<Icon name="trash3" />
</Button>
</Row>
</Col>
</Row>
</CardHeader>
<CardBody>
{#if file.contentType}
<CardText color="light" style="overflow: hidden; text-overflow: ellipsis">
<Icon name="file-earmark-text" /> {file.label ?? file.contentType}
</CardText>
{/if}
</CardBody>
</Card>
</Col>
</Row>
{/each}
<Row>
<Col>
{#if $mode === 'advanced'}
<Col class="d-flex justify-content-center">
<FormGroup class="label shlbutton" style="width: 100%">
<div style="border-bottom: 1px solid rgb(204, 204, 204); margin-bottom: 1em"><h3>Content</h3></div>
<Label>Add, update, or remove summaries shared by this link.</Label>
{#if shl.files.length == 0}
<p><em>No Summaries found</em></p>
{/if}
{#each shl.files as file (file.contentEncrypted)}
<Card class="mb-2" color="light">
<CardHeader>
<Row class="align-items-center">
<Col xs=6 class="align-items-center">
{#if file.date}
<strong><Icon name="calendar"></Icon> {file.date}</strong>
{/if}
</Col>
<Col xs=6>
<Row class="justify-content-end">
<Button size="sm" color="danger" class="my-0 mx-1" style="width: fit-content" on:click={(e) => {
deleteFile(file.contentEncrypted);
}}>
<Icon name="trash3" />
</Button>
</Row>
</Col>
</Row>
</CardHeader>
<CardBody>
{#if file.contentType}
<CardText color="light" style="overflow: hidden; text-overflow: ellipsis">
<Icon name="file-earmark-text" /> {file.label ?? file.contentType}
</CardText>
{/if}
</CardBody>
</Card>
{/each}
<Button class="mb-3" color="primary" on:click={addFile}><Icon name="file-earmark-plus" /> Add {shl.files.length == 0 ? "a" : "another"} Summary</Button>
</Col>
</Row>
{/if}
</FormGroup>
</Col>
{/if}
</Row>

<style>
img.qr {
Expand All @@ -368,6 +395,10 @@
width: 100%;
display: inline-block;
}
:global(.shlbutton button) {
margin-top: 1em;
margin-bottom: 1em;
}
:global(div.card) {
max-width: 300px;
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/layout/Banner.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<Image
alt="WA Verify Logo"
width="200"
src="/img/waverifypluslogo.png"
src="/img/waverifypluslogo.svg"
style="align-self: center"
/>
</Col>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/layout/LanguageMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
let locales: Readable<Record<string, Language>> = getContext('locales');
</script>

<Dropdown nav inNavbar size="sm" direction="down">
<Dropdown nav inNavbar class="navbar-dropdown" size="sm" direction="down">
<DropdownToggle color="primary" nav caret>
<Icon name="globe2" />
{$locales[$locale].lang}
Expand Down
Loading

0 comments on commit 0477e6d

Please sign in to comment.