pwa and mobile client

This commit is contained in:
azykov@mail.ru
2026-09-29 15:55:25 +03:00
parent 657009bd6c
commit 1935acf2ff
96 changed files with 8370 additions and 58 deletions
+112
View File
@@ -0,0 +1,112 @@
import { useEffect, useState } from 'react';
import { CODE_TYPE_LABELS, textColorOn, type Card } from '@cardy/shared';
import { setMaxBrightness } from '../platform.js';
import { Banner, CardBadge, CodeImage, ConfirmDialog, Icon, IconButton, Screen, useStoredImage } from '../ui.js';
export function CardScreen({
card,
imageEpoch,
onBack,
onEdit,
onDelete,
}: {
card: Card;
imageEpoch: number;
onBack: () => void;
onEdit: () => void;
onDelete: () => Promise<void>;
}) {
const [confirmDelete, setConfirmDelete] = useState(false);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState<string>();
const [viewing, setViewing] = useState<string>();
// Scanners at the till read the code better at full brightness.
useEffect(() => {
void setMaxBrightness(true);
return () => void setMaxBrightness(false);
}, []);
const remove = async () => {
setConfirmDelete(false);
setDeleting(true);
setError(undefined);
try {
await onDelete();
} catch (e) {
setError((e as Error).message);
setDeleting(false);
}
};
return (
<Screen
className="card-screen"
title={card.name}
left={<IconButton icon="back" label="Back" onClick={onBack} />}
right={
<>
<IconButton icon="edit" label="Edit" onClick={onEdit} disabled={deleting} />
<IconButton icon="trash" label="Delete" onClick={() => setConfirmDelete(true)} disabled={deleting} />
</>
}
>
<div className="card-hero" style={{ background: card.color, color: textColorOn(card.color) }}>
<CardBadge card={card} size={56} version={imageEpoch} />
<div className="card-hero-name">{card.name}</div>
</div>
{error && <Banner onClose={() => setError(undefined)}>{error}</Banner>}
<CodeImage code={card.code} codeType={card.codeType} large />
<div className="code-caption">
<span className="mono">{card.code}</span>
<span className="muted">{CODE_TYPE_LABELS[card.codeType]}</span>
</div>
{card.comment && <p className="card-comment">{card.comment}</p>}
{card.images.length > 0 && (
<div className="gallery">
{card.images.map((img) => (
<Thumb key={img.id} cardId={card.id} imageId={img.id} version={imageEpoch} onOpen={() => setViewing(img.id)} />
))}
</div>
)}
{viewing && <ImageViewer cardId={card.id} imageId={viewing} version={imageEpoch} onClose={() => setViewing(undefined)} />}
{confirmDelete && (
<ConfirmDialog
title="Delete card?"
message={`“${card.name}” will be removed for everyone in your group.`}
confirmLabel="Delete"
danger
onCancel={() => setConfirmDelete(false)}
onConfirm={remove}
/>
)}
</Screen>
);
}
function Thumb({ cardId, imageId, version, onOpen }: { cardId: string; imageId: string; version: number; onOpen: () => void }) {
const url = useStoredImage(cardId, imageId, version);
return (
<button className="thumb" onClick={onOpen} disabled={!url} aria-label="Open image">
{url ? <img src={url} alt="" /> : <span className="muted small">Not downloaded</span>}
</button>
);
}
function ImageViewer({ cardId, imageId, version, onClose }: { cardId: string; imageId: string; version: number; onClose: () => void }) {
const url = useStoredImage(cardId, imageId, version);
return (
<div className="viewer" onClick={onClose}>
{url && <img src={url} alt="" />}
<button className="icon-button viewer-close" aria-label="Close">
<Icon name="close" />
</button>
</div>
);
}