Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add card detail page #11

Merged
merged 15 commits into from
Nov 5, 2024
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