capgo and adk autoupdates. card tags

This commit is contained in:
azykov@mail.ru
2026-09-29 19:36:08 +03:00
parent 1981d976eb
commit b6fb2fe40f
43 changed files with 1329 additions and 54 deletions
+30 -4
View File
@@ -1,5 +1,14 @@
import { useEffect, useState } from 'react';
import { CODE_TYPES, CODE_TYPE_LABELS, cardFieldsSchema, randomCardColor, type Card, type CardFields } from '@cardy/shared';
import {
CODE_TYPES,
CODE_TYPE_LABELS,
cardFieldsSchema,
formatTags,
parseTags,
randomCardColor,
type Card,
type CardFields,
} from '@cardy/shared';
import { api } from '../api.js';
import { CardBadge, CodePreview, ErrorText, FilePicker, formatDate, useAction, useLoad } from '../components.js';
import { linkClick, navigate } from '../router.js';
@@ -30,10 +39,12 @@ function Editor({ initial }: { initial?: Card }) {
? { name: initial.name, color: initial.color, code: initial.code, codeType: initial.codeType, comment: initial.comment ?? '' }
: { name: '', color: randomCardColor(), code: '', codeType: 'EAN13', comment: '' },
);
// Edited as typed ("a, b"); parsed into the tag list on save.
const [tagsText, setTagsText] = useState(formatTags(initial?.tags));
const action = useAction();
const [saved, setSaved] = useState(false);
useEffect(() => setSaved(false), [fields]);
useEffect(() => setSaved(false), [fields, tagsText]);
const dirty =
!card ||
@@ -41,12 +52,13 @@ function Editor({ initial }: { initial?: Card }) {
card.color !== fields.color ||
card.code !== fields.code ||
card.codeType !== fields.codeType ||
(card.comment ?? '') !== (fields.comment ?? '');
(card.comment ?? '') !== (fields.comment ?? '') ||
(card.tags ?? []).join('\n') !== parseTags(tagsText).join('\n');
const set = <K extends keyof CardFields>(key: K, value: CardFields[K]) => setFields((f) => ({ ...f, [key]: value }));
const save = async (e: React.FormEvent) => {
e.preventDefault();
const parsed = cardFieldsSchema.safeParse(fields);
const parsed = cardFieldsSchema.safeParse({ ...fields, tags: tagsText });
if (!parsed.success) {
action.setError(parsed.error.issues.map((i) => i.message).join('; '));
return;
@@ -54,6 +66,7 @@ function Editor({ initial }: { initial?: Card }) {
const result = await action.run(() => (card ? api.updateCard(card.id, parsed.data) : api.createCard(parsed.data)));
if (!result) return;
setCard(result);
setTagsText(formatTags(result.tags));
setSaved(true);
if (!card) navigate(`/cards/${result.id}`, { replace: true });
};
@@ -133,6 +146,19 @@ function Editor({ initial }: { initial?: Card }) {
onChange={(e) => set('comment', e.target.value)}
/>
</div>
<div className="field">
<label htmlFor="tags">Tags</label>
<input id="tags" value={tagsText} onChange={(e) => setTagsText(e.target.value)} placeholder="Comma-separated, e.g. food, pharmacy" />
{parseTags(tagsText).length > 0 && (
<div className="row">
{parseTags(tagsText).map((t) => (
<span key={t} className="pill tag-pill">
{t}
</span>
))}
</div>
)}
</div>
<div className="row">
<button type="submit" disabled={action.busy || (!dirty && !!card)}>
{card ? 'Save' : 'Create card'}
+17 -9
View File
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react';
import { CODE_TYPE_LABELS } from '@cardy/shared';
import { CODE_TYPE_LABELS, cardMatchesQuery } from '@cardy/shared';
import { api } from '../api.js';
import { CardBadge, ErrorText, useLoad } from '../components.js';
import { linkClick } from '../router.js';
@@ -9,13 +9,14 @@ export function CardsPage() {
const [filter, setFilter] = useState('');
const [showDeleted, setShowDeleted] = useState(false);
const visible = useMemo(() => {
const q = filter.trim().toLowerCase();
return (cards.data ?? [])
.filter((c) => showDeleted || !c.deleted)
.filter((c) => !q || c.name.toLowerCase().includes(q))
.sort((a, b) => a.name.localeCompare(b.name));
}, [cards.data, filter, showDeleted]);
// Search by name, tags and comment.
const visible = useMemo(
() =>
(cards.data ?? [])
.filter((c) => (showDeleted || !c.deleted) && cardMatchesQuery(c, filter))
.sort((a, b) => a.name.localeCompare(b.name)),
[cards.data, filter, showDeleted],
);
const deletedCount = cards.data?.filter((c) => c.deleted).length ?? 0;
return (
@@ -27,7 +28,13 @@ export function CardsPage() {
</a>
</div>
<div className="row">
<input className="grow" type="search" placeholder="Filter by name" value={filter} onChange={(e) => setFilter(e.target.value)} />
<input
className="grow"
type="search"
placeholder="Search by name, tag or comment"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<label className="checkbox">
<input type="checkbox" checked={showDeleted} onChange={(e) => setShowDeleted(e.target.checked)} />
Show deleted ({deletedCount})
@@ -45,6 +52,7 @@ export function CardsPage() {
{CODE_TYPE_LABELS[c.codeType]}
{c.images.length > 0 && ` · ${c.images.length} image${c.images.length === 1 ? '' : 's'}`}
</div>
{c.tags?.length > 0 && <div className="card-tags small">{c.tags.join(' · ')}</div>}
</div>
{c.deleted && <span className="pill danger">deleted</span>}
</a>
+12
View File
@@ -420,6 +420,18 @@ section > .row {
opacity: 0.55;
}
.pill.tag-pill {
margin-left: 0;
color: var(--text);
}
.card-tags {
color: var(--accent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-name {
font-weight: 600;
overflow: hidden;