capgo and adk autoupdates. card tags
This commit is contained in:
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user