Skip to content

Commit

Permalink
Add card detail page (#11)
Browse files Browse the repository at this point in the history
* Update plan

* Add card-detail-page.

* Use MainMessage component.

* Style card detail a bit.

* Add jsbarcode lib

* Render Code 39

* Add a toggle.

* Implement mapHtml5QrcodeFormatToJsbarcodeFormat

* Display images for barcodes.

* Fix detecting of QrCode

* Install qrcode

* Display QR codes.

* Add new item to plan

* Suppress warning.

* Clean up.
  • Loading branch information
lukasbicus authored Nov 5, 2024
1 parent 3ded0fe commit d772e2c
Show file tree
Hide file tree
Showing 18 changed files with 517 additions and 50 deletions.
2 changes: 1 addition & 1 deletion app/(homescreens)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<div className={styles.childrenHeight}>{children}</div>
<footer
className={clsx(
`btm-nav btm-nav-lg text-base-content bg-base-200`,
`btm-nav btm-nav-lg text-base-content bg-base-300`,
styles.bottomBarNavHeight
)}
>
Expand Down
109 changes: 109 additions & 0 deletions app/card/card-detail-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use client';

import useAppState from '@/app/lib/app-state/app-state';
import { Routes } from '@/app/lib/shared';
import { Barcode } from '@/app/ui/barcode';
import CompanyIcon from '@/app/ui/company-icon';
import { MainMessage } from '@/app/ui/main-message';
import PageTemplate from '@/app/ui/page-template';
import { Qrcode } from '@/app/ui/qrcode';
import { SecondaryHeader } from '@/app/ui/secondary-header';
import { IconCards, IconEdit, IconTrash } from '@tabler/icons-react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';

export function CardDetailPage() {
const searchParams = useSearchParams();
const id = searchParams.get('id');
const [state] = useAppState();
const card = state.cards.find(c => c.id === id);
if (!card) {
return (
<PageTemplate
header={<SecondaryHeader title="Card detail" href={Routes.MyCards} />}
>
<div className="flex h-2/3 w-full items-center justify-center">
<MainMessage
title="Card not found"
description={`Something went wrong. Found with id ${id} not found.`}
>
<Link href={Routes.MyCards} replace>
<button className="btn btn-primary">
<IconCards className="w-6 h-6" />
My cards
</button>
</Link>
</MainMessage>
</div>
</PageTemplate>
);
}
return (
<PageTemplate
header={
<SecondaryHeader
title={card!.name}
href={Routes.MyCards}
rightAction={
<div className="flex gap-2">
<Link href={Routes.MyCards} replace>
<button className="btn btn-square btn-ghost">
<IconTrash />
</button>
</Link>
<Link href={Routes.MyCards} replace>
<button className="btn btn-square btn-ghost">
<IconEdit />
</button>
</Link>
</div>
}
/>
}
>
<div className="h-full w-full grid grid-col grid-rows-[1fr_auto]">
{card.codeFormat === 'QR' ? (
<Qrcode code={card.code} />
) : (
<Barcode code={card.code} codeFormat={card.codeFormat} />
)}
<div
className="bg-base-300 p-6"
style={card.bgColor ? { backgroundColor: card.bgColor } : {}}
>
<div className="flex gap-6 items-center">
<CompanyIcon {...card} className="w-16 h-16" />
<label className="form-control flex-1">
<div className="label">
<span className="label-text">Card name</span>
</div>
<input
type="text"
disabled
value={card.name}
className="input input-bordered w-full"
/>
</label>
</div>

<label className="form-control">
<div className="label">
<span className="label-text">Toggle note visibility</span>
</div>
<input type="checkbox" className="toggle" defaultChecked />
</label>
<label className="form-control">
<div className="label">
<span className="label-text">Card note</span>
</div>
<textarea
className="textarea textarea-bordered h-24"
value={card.note}
disabled
/>
</label>
</div>
</div>
</PageTemplate>
);
}
10 changes: 9 additions & 1 deletion app/card/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { CardDetailPage } from '@/app/card/card-detail-page';
import Loading from '@/app/ui/loading';
import { Suspense } from 'react';

export default function CardPage() {
return <div>Card</div>;
return (
<Suspense fallback={<Loading />}>
<CardDetailPage />
</Suspense>
);
}
21 changes: 10 additions & 11 deletions app/create-card/create-card-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CreateCardFormState,
} from '@/app/create-card/createCardFormReducer';
import useAppState from '@/app/lib/app-state/app-state';
import { mapHtml5QrcodeFormatToJsbarcodeFormat } from '@/app/lib/app-state/codeFormat';
import { predefinedCompanies } from '@/app/lib/predefined-companies';
import {
CardIcon,
Expand All @@ -26,7 +27,7 @@ import {
IconRefresh,
IconX,
} from '@tabler/icons-react';
import { Html5QrcodeResult, Html5QrcodeSupportedFormats } from 'html5-qrcode';
import { Html5QrcodeResult } from 'html5-qrcode';
import Image from 'next/image';
import { useRouter, useSearchParams } from 'next/navigation';
import { Reducer, useCallback, useEffect, useReducer, useRef } from 'react';
Expand All @@ -44,7 +45,7 @@ enum FormNames {
type CreateCardForm = {
[FormNames.Name]: string;
[FormNames.Code]: string;
[FormNames.CodeFormat]: number;
[FormNames.CodeFormat]: string;
[FormNames.Note]: string;
[FormNames.Color]: string | null;
[FormNames.Icon]: string | SvgProps;
Expand All @@ -63,11 +64,8 @@ export default function CreateCardForm() {
[FormNames.Name]: predefinedCompany.name,
[FormNames.Color]: null,
[FormNames.Icon]: predefinedCompany.svg,
[FormNames.CodeFormat]: predefinedCompany.codeFormat,
}
: {
[FormNames.CodeFormat]: Html5QrcodeSupportedFormats.EAN_13,
},
: {},
});
const [, appDispatch] = useAppState();
const [{ devices, activeDevice, isModalVisible }, dispatch] = useReducer<
Expand All @@ -78,8 +76,11 @@ export default function CreateCardForm() {
const handleCodeDetected = useCallback(
(text: string, { result }: Html5QrcodeResult) => {
setValue(FormNames.Code, text);
if (result.format?.format) {
setValue(FormNames.CodeFormat, result.format.format);
if (typeof result.format?.format === 'number') {
setValue(
FormNames.CodeFormat,
mapHtml5QrcodeFormatToJsbarcodeFormat(result.format.format)
);
}
cameraModalRef.current?.close();
},
Expand Down Expand Up @@ -133,9 +134,7 @@ export default function CreateCardForm() {
note: data[FormNames.Note] || undefined,
bgColor: data[FormNames.Color] || null,
icon: (data[FormNames.Icon] as CardIcon) || null,
codeFormat: data[
FormNames.CodeFormat
] as Html5QrcodeSupportedFormats,
codeFormat: data[FormNames.CodeFormat],
},
});
router.replace(Routes.MyCards);
Expand Down
13 changes: 6 additions & 7 deletions app/create-card/scanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CreateCardFormActions,
CreateCardFormActionTypes,
} from '@/app/create-card/createCardFormReducer';
import { MainMessage } from '@/app/ui/main-message';
import {
Html5Qrcode,
Html5QrcodeResult,
Expand Down Expand Up @@ -112,16 +113,14 @@ export default function Scanner({
>
<div id="reader" className="max-h-full"></div>
{!activeDevice && (
<div className="text-center">
<p className="text-xl py-4">Please grant camera permissions.</p>
<p className="text-sm text-gray-500 pb-4">
Without those permissions, it will be impossible to scan and save
your cards.
</p>
<MainMessage
title="Please grant camera permissions."
description="Without those permissions, it will be impossible to scan and save your cards."
>
<button className="btn btn-primary" onClick={getCameraDevices}>
Request devices
</button>
</div>
</MainMessage>
)}
</div>
);
Expand Down
44 changes: 44 additions & 0 deletions app/lib/app-state/codeFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Html5QrcodeSupportedFormats } from 'html5-qrcode';

/**
* @param codeFormat
* jsbarcode format docs:
* @link https://github.com/lindell/JsBarcode/wiki/Options#format
* @link https://github.com/lindell/JsBarcode/wiki#barcodes
* html5-code format docs:
* https://scanapp.org/html5-qrcode-docs/docs/supported_code_formats
*/
export function mapHtml5QrcodeFormatToJsbarcodeFormat(
codeFormat: Html5QrcodeSupportedFormats
): string {
switch (codeFormat) {
case Html5QrcodeSupportedFormats.QR_CODE:
return 'QR';
case Html5QrcodeSupportedFormats.CODE_128:
return 'CODE128';
case Html5QrcodeSupportedFormats.EAN_13:
return 'EAN13';
case Html5QrcodeSupportedFormats.EAN_8:
return 'EAN8';
case Html5QrcodeSupportedFormats.UPC_A:
case Html5QrcodeSupportedFormats.UPC_E:
case Html5QrcodeSupportedFormats.UPC_EAN_EXTENSION:
return 'UPC';
case Html5QrcodeSupportedFormats.ITF:
return 'ITF14'; // ITF format in jsbarcode is often represented as ITF14
case Html5QrcodeSupportedFormats.CODE_39:
return 'CODE39';
case Html5QrcodeSupportedFormats.CODE_93:
return 'CODE93';
case Html5QrcodeSupportedFormats.CODABAR:
return 'codebar';
case Html5QrcodeSupportedFormats.AZTEC:
case Html5QrcodeSupportedFormats.DATA_MATRIX:
case Html5QrcodeSupportedFormats.MAXICODE:
case Html5QrcodeSupportedFormats.PDF_417:
case Html5QrcodeSupportedFormats.RSS_14:
case Html5QrcodeSupportedFormats.RSS_EXPANDED:
default:
throw new Error(`Unsupported code format: ${codeFormat}`);
}
}
9 changes: 7 additions & 2 deletions app/lib/app-state/reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mapHtml5QrcodeFormatToJsbarcodeFormat } from '@/app/lib/app-state/codeFormat';
import {
AddCardAction,
appReducer,
Expand All @@ -16,7 +17,9 @@ const dummyCard: Card = {
code: '123adfa657SFV',
bgColor: '#4523C9',
icon: CardIcon.Retail,
codeFormat: Html5QrcodeSupportedFormats.QR_CODE,
codeFormat: mapHtml5QrcodeFormatToJsbarcodeFormat(
Html5QrcodeSupportedFormats.QR_CODE
),
};

describe('appReducer', () => {
Expand All @@ -25,7 +28,9 @@ describe('appReducer', () => {
const newCard: Omit<Card, 'id'> = {
bgColor: '#4523C9',
icon: CardIcon.Retail,
codeFormat: Html5QrcodeSupportedFormats.QR_CODE,
codeFormat: mapHtml5QrcodeFormatToJsbarcodeFormat(
Html5QrcodeSupportedFormats.QR_CODE
),
name: 'Test Card',
code: 'ABC123',
};
Expand Down
3 changes: 1 addition & 2 deletions app/lib/app-state/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CardIcon, SvgProps } from '@/app/lib/shared';
import { Html5QrcodeSupportedFormats } from 'html5-qrcode/src/core';
import { v4 as uuid } from 'uuid';

export type Card = {
Expand All @@ -10,7 +9,7 @@ export type Card = {
bgColor: string | null;
icon: CardIcon | null | SvgProps;
favorite?: boolean;
codeFormat: Html5QrcodeSupportedFormats;
codeFormat: string;
};

export type AppState = {
Expand Down
Loading

0 comments on commit d772e2c

Please sign in to comment.