Skip to content

Commit 6535deb

Browse files
committed
✨ Add second qr code for vCard
Resolves #74
1 parent 5777cc1 commit 6535deb

5 files changed

Lines changed: 118 additions & 43 deletions

File tree

src/Cards/src/components/app.tsx

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { BrowserData, BrowserDataAdt, getBrowserData, shareUrl } from "../model/
1313
import { ElmishResult, Init, Subscribe, Update, cmd } from "@fun-ts/elmish";
1414
import { ErrorIcon, LoaderIcon } from "./icons";
1515
import { UrlDataOrigin, UrlDataOriginAdt, compressToUrlParam, getParametersFromUrl, getStableStringFromParameters, makeCurrentUrl } from "../model/url-data";
16-
import { VCardDataAdt, getVCardUrl, vCardFieldsFromAppData, vCardFieldsFromAppDataLoaded } from "../model/v-card-url";
17-
import { constant, identity, pipe } from "fp-ts/function";
16+
import { VCardDataAdt, getVCardString, getVCardUrl, vCardFieldsFromAppData, vCardFieldsFromAppDataLoaded, vCardStringAsUrl } from "../model/v-card-url";
17+
import { constant, flow, identity, pipe } from "fp-ts/function";
1818
import { getWindowTitleFromAppData, setWindowTitle } from "../model/window-title";
1919

2020
import { Card } from "./card";
@@ -193,8 +193,12 @@ export const update: Update<Model, Msg> = (model, msg) => pipe(
193193
pipe(
194194
data,
195195
vCardFieldsFromAppData,
196-
d => getVCardUrl({ ...d, avatarBase64: O.none }),
197-
url => VCardDataAdt.as.Loaded({ url }),
196+
d => getVCardString({ ...d, avatarBase64: O.none }),
197+
vCardString => VCardDataAdt.as.Loaded({
198+
url: vCardStringAsUrl(vCardString),
199+
vCard: vCardString,
200+
vCardNoImage: vCardString,
201+
}),
198202
),
199203
},
200204
pipe(
@@ -317,12 +321,20 @@ export const update: Update<Model, Msg> = (model, msg) => pipe(
317321
vCardFieldsFromAppDataLoaded,
318322
O.fold(
319323
() => VCardDataAdt.of.NotLoaded({}),
320-
data => VCardDataAdt.of.Loaded({
321-
url: getVCardUrl({
324+
flow(
325+
data => ({
322326
...data,
323327
avatarBase64: O.some(imageData.base64)
328+
}),
329+
dataWithImg => VCardDataAdt.of.Loaded({
330+
url: getVCardUrl(dataWithImg),
331+
vCard: getVCardString(dataWithImg),
332+
vCardNoImage: getVCardString({
333+
...dataWithImg,
334+
avatarBase64: O.none
335+
})
324336
})
325-
})
337+
)
326338
),
327339
),
328340
},
@@ -339,13 +351,17 @@ export const update: Update<Model, Msg> = (model, msg) => pipe(
339351
vCardFieldsFromAppDataLoaded,
340352
O.fold(
341353
() => VCardDataAdt.of.NotLoaded({}),
342-
data => VCardDataAdt.of.Loaded({
343-
url: getVCardUrl({
354+
flow(
355+
data => ({
344356
...data,
345-
avatarBase64: O.none
357+
avatarBase64: O.none,
358+
}),
359+
dataWithoutImg => VCardDataAdt.of.Loaded({
360+
url: getVCardUrl(dataWithoutImg),
361+
vCard: getVCardString(dataWithoutImg),
362+
vCardNoImage: getVCardString(dataWithoutImg),
346363
})
347-
})
348-
),
364+
)),
349365
),
350366
},
351367
cmd.none
@@ -418,6 +434,7 @@ export const view: PreactView<Model, Msg> = (dispatch, model) => (
418434
<QrCodeView
419435
appData={model.appData}
420436
browserData={model.browserData}
437+
vCardData={model.vCardData}
421438
/>
422439
</Page>
423440
<Page route={Routes.of.Share}>
@@ -506,11 +523,14 @@ const CardView: FunctionComponent<CardViewProps> = ({
506523
type QrCodeViewProps = {
507524
appData: ADTType<typeof AppDataAdt>;
508525
browserData: ADTType<typeof BrowserDataAdt>;
526+
vCardData: ADTType<typeof VCardDataAdt>;
509527
};
510528

529+
// TODO: replace nested matchers with sequencing
511530
const QrCodeView: FunctionComponent<QrCodeViewProps> = ({
512531
appData,
513532
browserData,
533+
vCardData
514534
}) => pipe(
515535
browserData,
516536
BrowserDataAdt.matchStrict({
@@ -530,14 +550,23 @@ const QrCodeView: FunctionComponent<QrCodeViewProps> = ({
530550
Failure: () => <>
531551
<ErrorIcon /> An error occurred while loading application data.
532552
</>,
533-
Loaded: (appDataLoaded) => (
534-
<QrCodeCard
535-
href={pipe(
536-
appDataLoaded,
537-
appDataToUrlParams,
538-
makeCurrentUrl(location)
539-
)}
540-
/>
553+
Loaded: (appDataLoaded) => pipe(
554+
vCardData,
555+
VCardDataAdt.matchStrict({
556+
NotLoaded: () => <></>,
557+
Loading: () => <LoaderIcon />,
558+
Failure: () => <>
559+
<ErrorIcon /> An error occurred while creating vcard data.
560+
</>,
561+
Loaded: (vCardDataLoaded) => <QrCodeCard
562+
href={pipe(
563+
appDataLoaded,
564+
appDataToUrlParams,
565+
makeCurrentUrl(location)
566+
)}
567+
vcard={vCardDataLoaded.vCardNoImage}
568+
/>
569+
})
541570
)
542571
})
543572
),
Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,51 @@
1-
import { keyframes, style } from "@vanilla-extract/css";
2-
1+
import { style } from "@vanilla-extract/css";
32
import { varsApp } from "../theme/variables.css";
43

5-
const fadeIn = keyframes({
6-
"0%": { opacity: 0 },
7-
"100%": { opacity: 1 },
8-
});
9-
104
export const qrCode = style({
115
borderRadius: varsApp.space.small,
126

13-
animation: `${fadeIn} 1s`,
147
backgroundColor: `rgb(${varsApp.color.light.rgb},.75)`,
158
color: varsApp.color.dark.hex,
16-
transition: "background 1s, color 1s",
179

18-
selectors: {
19-
"&:hover": {
20-
backgroundColor: varsApp.color.light.hex,
10+
scrollSnapAlign: "center",
11+
scrollSnapStop: "always",
12+
13+
display: "block"
14+
});
15+
16+
export const firstQr = style({
17+
// marginBottom: varsApp.space.large
18+
});
19+
20+
export const outer = style({
21+
borderRadius: varsApp.space.small,
22+
23+
display: "flex",
24+
flexDirection: "column",
25+
flexWrap: "wrap",
26+
gap: varsApp.space.large,
27+
28+
scrollbarWidth: "none",
29+
30+
overflowX: "auto",
31+
overflowY: "hidden",
32+
33+
aspectRatio: "1",
34+
35+
scrollSnapType: "both mandatory",
36+
scrollBehavior: "smooth",
37+
38+
overscrollBehaviorX: "none",
39+
overscrollBehaviorY: "unset",
40+
41+
"@media": {
42+
"(hover: none)": {
43+
// ...and vertical on desktop
44+
flexDirection: "row",
45+
overflowX: "hidden",
46+
overflowY: "auto",
47+
overscrollBehaviorX: "unset",
48+
overscrollBehaviorY: "none",
2149
}
2250
}
2351
});

src/Cards/src/components/qr-code-card.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,25 @@ import { QrCode } from "./qr-code";
77

88
type QrCodeCardProps = {
99
href: string;
10+
vcard: string;
1011
className?: string;
1112
};
1213

1314
export const QrCodeCard: FunctionComponent<QrCodeCardProps> = ({
1415
href,
16+
vcard,
1517
className = "",
1618
}) => (
17-
<PageContent className={className}>
18-
<QrCode text={href}
19-
border={5}
20-
className={`${styles.qrCode} ${theme.lightTheme}`}
21-
/>
19+
<PageContent className={`${className}`}>
20+
<div className={styles.outer}>
21+
<QrCode text={href}
22+
border={5}
23+
className={`${styles.qrCode} ${styles.firstQr} ${theme.lightTheme}`}
24+
/>
25+
<QrCode text={vcard}
26+
border={5}
27+
className={`${styles.qrCode} ${theme.lightTheme}`}
28+
/>
29+
</div>
2230
</PageContent>
2331
);

src/Cards/src/components/qr-code.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const QrCode: FunctionComponent<QrCodeProps> = ({
3030
border = 0,
3131
className = ""
3232
}) => pipe(
33+
// Throws RangeError if qrcode too long
34+
// TODO: convert to Either
3335
qrCodeGen.QrCode.encodeText(text, ecc),
3436
code => <svg xmlns="http://www.w3.org/2000/svg"
3537
version="1.1"

src/Cards/src/model/v-card-url.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const vCardParamEncoder = (prefix: string) => O.map<string, string>(
2323
);
2424

2525
const vCardImageEncoder = O.map<Base64Data, string>(
26-
d => `PHOTO;ENCODING=BASE64;TYPE=${d.type.toUpperCase()}:${d.content}\n\n`
26+
d => `PHOTO;TYPE=${d.type.toUpperCase()};ENCODING=b:${d.content}\n`
2727
);
2828

2929
type VCardInput =
@@ -45,18 +45,26 @@ const renderVCard = (params: VCardFields) => pipe(
4545
content => `BEGIN:VCARD\nVERSION:3.0\n${content}END:VCARD`
4646
);
4747

48-
const vCardUrl = (vcardData: string) => pipe(
49-
new Blob([vcardData], { type: "text/vcard" }),
48+
export const vCardStringAsUrl = (vCardData: string) => pipe(
49+
new Blob([vCardData], { type: "text/vcard" }),
5050
URL.createObjectURL
5151
);
5252

53-
export const getVCardUrl = flow(
53+
export const getVCardString = flow(
5454
encodeVCardFields,
5555
renderVCard,
56-
vCardUrl
5756
);
5857

59-
export const VCardDataAdt = makeRemoteResultADT<{ url: string; }>();
58+
export const getVCardUrl = flow(
59+
getVCardString,
60+
vCardStringAsUrl
61+
);
62+
63+
export const VCardDataAdt = makeRemoteResultADT<{
64+
url: string;
65+
vCard: string;
66+
vCardNoImage: string;
67+
}>();
6068

6169
export const vCardFieldsFromAppData = (a: AppData) => pipe(
6270
a,

0 commit comments

Comments
 (0)