Files
open-design/src/components/DesignsTab.tsx
T
pftom a98096a042 Add initial project structure with essential files
- Created .gitignore to exclude build artifacts and dependencies.
- Added index.html as the main entry point for the application.
- Included LICENSE file with Apache 2.0 terms.
- Initialized package.json and package-lock.json for project dependencies.
- Added pnpm-lock.yaml for package management.
- Created QUICKSTART.md for setup instructions.
- Added README.md and README.zh-CN.md for project documentation in English and Chinese.
2026-04-28 12:25:59 +08:00

140 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react';
import { useT } from '../i18n';
import type { DesignSystemSummary, Project, SkillSummary } from '../types';
import { Icon } from './Icon';
type SubTab = 'recent' | 'yours';
interface Props {
projects: Project[];
skills: SkillSummary[];
designSystems: DesignSystemSummary[];
onOpen: (id: string) => void;
onDelete: (id: string) => void;
}
export function DesignsTab({ projects, skills, designSystems, onOpen, onDelete }: Props) {
const t = useT();
const [filter, setFilter] = useState('');
const [sub, setSub] = useState<SubTab>('recent');
const filtered = useMemo(() => {
const q = filter.trim().toLowerCase();
let list = projects;
if (sub === 'recent') {
list = [...list].sort((a, b) => b.updatedAt - a.updatedAt);
}
if (!q) return list;
return list.filter((p) => p.name.toLowerCase().includes(q));
}, [projects, filter, sub]);
const skillName = (id: string | null) => skills.find((s) => s.id === id)?.name ?? '';
const dsName = (id: string | null) => designSystems.find((d) => d.id === id)?.title ?? '';
return (
<div className="tab-panel">
<div className="tab-panel-toolbar">
<div className="toolbar-left">
<div
className="subtab-pill"
role="tablist"
aria-label={t('designs.filterAria')}
>
<button
role="tab"
aria-selected={sub === 'recent'}
className={sub === 'recent' ? 'active' : ''}
onClick={() => setSub('recent')}
>
{t('designs.subRecent')}
</button>
<button
role="tab"
aria-selected={sub === 'yours'}
className={sub === 'yours' ? 'active' : ''}
onClick={() => setSub('yours')}
>
{t('designs.subYours')}
</button>
</div>
</div>
<div className="toolbar-search">
<span className="search-icon" aria-hidden>
<Icon name="search" size={13} />
</span>
<input
placeholder={t('designs.searchPlaceholder')}
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</div>
</div>
{filtered.length === 0 ? (
<div className="tab-empty">
{projects.length === 0
? t('designs.emptyNoProjects')
: t('designs.emptyNoMatch')}
</div>
) : (
<div className="design-grid">
{filtered.map((p) => {
const skill = skillName(p.skillId);
const ds = dsName(p.designSystemId);
return (
<div
key={p.id}
className="design-card"
role="button"
tabIndex={0}
onClick={() => onOpen(p.id)}
onKeyDown={(e) => {
if (e.key === 'Enter') onOpen(p.id);
}}
>
<button
className="design-card-close"
title={t('designs.deleteTitle')}
onClick={(e) => {
e.stopPropagation();
if (confirm(t('designs.deleteConfirm', { name: p.name }))) {
onDelete(p.id);
}
}}
>
×
</button>
<div className="design-card-thumb" aria-hidden />
<div className="design-card-meta-block">
<div className="design-card-name" title={p.name}>{p.name}</div>
<div className="design-card-meta">
{ds ? (
<span className="ds">{ds}</span>
) : (
<span>{t('designs.cardFreeform')}</span>
)}
{skill ? ` · ${skill}` : ''}
{' · '}
{relativeTime(p.updatedAt, t)}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
}
function relativeTime(ts: number, t: ReturnType<typeof useT>): string {
const diff = Date.now() - ts;
const min = 60_000;
const hr = 60 * min;
const day = 24 * hr;
if (diff < min) return t('common.justNow');
if (diff < hr) return t('common.minutesAgo', { n: Math.floor(diff / min) });
if (diff < day) return t('common.hoursAgo', { n: Math.floor(diff / hr) });
if (diff < 7 * day) return t('common.daysAgo', { n: Math.floor(diff / day) });
return new Date(ts).toLocaleDateString();
}