139 lines
5.8 KiB
TypeScript
139 lines
5.8 KiB
TypeScript
|
|
import type { ReactNode } from 'react';
|
||
|
|
import { useI18n } from '../lib/i18n';
|
||
|
|
import type { Confidence, Fact, SourceKind, Status, Verdict } from '../lib/types';
|
||
|
|
|
||
|
|
const SPRITE = `${import.meta.env.BASE_URL}uswds/sprite.svg`;
|
||
|
|
|
||
|
|
export function Icon({ name, size = 3, className = '' }: { name: string; size?: 2 | 3 | 4 | 5; className?: string }) {
|
||
|
|
return (
|
||
|
|
<svg className={`usa-icon usa-icon--size-${size} ${className}`} aria-hidden="true" focusable="false" role="img">
|
||
|
|
<use href={`${SPRITE}#${name}`} />
|
||
|
|
</svg>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export const VERDICT_STYLE: Record<Verdict, { icon: string; tag: string; alert: string; hex: string }> = {
|
||
|
|
match: { icon: 'check_circle', tag: 'bg-success-dark text-white', alert: 'usa-alert--success', hex: '#00a91c' },
|
||
|
|
partial: { icon: 'warning', tag: 'bg-warning text-ink', alert: 'usa-alert--warning', hex: '#ffbe2e' },
|
||
|
|
conflict: { icon: 'report', tag: 'bg-accent-warm-darker text-white', alert: 'usa-alert--warning', hex: '#a2461c' },
|
||
|
|
incomplete: { icon: 'help', tag: 'bg-base-lighter text-ink', alert: 'usa-alert--info', hex: '#ffffff' },
|
||
|
|
fail: { icon: 'error', tag: 'bg-error-dark text-white', alert: 'usa-alert--error', hex: '#b50909' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export function VerdictTag({ verdict }: { verdict: Verdict }) {
|
||
|
|
const { t } = useI18n();
|
||
|
|
const s = VERDICT_STYLE[verdict];
|
||
|
|
return (
|
||
|
|
<span className={`usa-tag display-inline-flex flex-align-center ${s.tag}`}>
|
||
|
|
<Icon name={s.icon} size={2} className="margin-right-05" />
|
||
|
|
{t(`verdict.${verdict}`)}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const STATUS_STYLE: Record<Status, { icon: string; text: string }> = {
|
||
|
|
ok: { icon: 'check_circle', text: 'text-success-dark' },
|
||
|
|
warn: { icon: 'warning', text: 'text-warning-darker' },
|
||
|
|
fail: { icon: 'error', text: 'text-error-dark' },
|
||
|
|
unknown: { icon: 'help', text: 'text-base-dark' },
|
||
|
|
conflict: { icon: 'report', text: 'text-accent-warm-darker' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export function StatusLabel({ status }: { status: Status }) {
|
||
|
|
const { t } = useI18n();
|
||
|
|
const s = STATUS_STYLE[status];
|
||
|
|
return (
|
||
|
|
<span className={`display-inline-flex flex-align-center text-bold ${s.text}`}>
|
||
|
|
<Icon name={s.icon} size={3} className="margin-right-05" />
|
||
|
|
{t(`status.${status}`)}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const CONF_STYLE: Record<Confidence, string> = {
|
||
|
|
verified: 'bg-success-dark text-white',
|
||
|
|
mapped: 'bg-primary text-white',
|
||
|
|
stale: 'bg-warning text-ink',
|
||
|
|
inferred: 'bg-base-dark text-white',
|
||
|
|
user: 'bg-accent-warm-darker text-white',
|
||
|
|
none: 'bg-base-lighter text-ink',
|
||
|
|
};
|
||
|
|
const CONF_ICON: Record<Confidence, string> = {
|
||
|
|
verified: 'verified', mapped: 'check', stale: 'update', inferred: 'info_outline', user: 'edit', none: 'help_outline',
|
||
|
|
};
|
||
|
|
|
||
|
|
export function ConfidenceTag({ confidence }: { confidence: Confidence }) {
|
||
|
|
const { t } = useI18n();
|
||
|
|
return (
|
||
|
|
<span className={`usa-tag display-inline-flex flex-align-center ${CONF_STYLE[confidence]}`} title={t(`conf.${confidence}.help`)}>
|
||
|
|
<Icon name={CONF_ICON[confidence]} size={2} className="margin-right-05" />
|
||
|
|
{t(`conf.${confidence}`)}
|
||
|
|
</span>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SourceLine({ source, confidence, date }: { source: SourceKind; confidence: Confidence; date: string | null }) {
|
||
|
|
const { t, date: fmt } = useI18n();
|
||
|
|
return (
|
||
|
|
<dl className="kbb-source font-sans-3xs margin-top-1 margin-bottom-0">
|
||
|
|
<div>
|
||
|
|
<dt className="text-base-dark">{t('src.label')}</dt>
|
||
|
|
<dd>{t(`src.${source}`)}</dd>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<dt className="text-base-dark">{t('fact.updated')}</dt>
|
||
|
|
<dd>{date ? fmt(date) : t('fact.dateUnknown')}</dd>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<dt className="text-base-dark">{t('fact.confidence')}</dt>
|
||
|
|
<dd><ConfidenceTag confidence={confidence} /></dd>
|
||
|
|
</div>
|
||
|
|
</dl>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function FactCard({ fact, onReport }: { fact: Fact; onReport?: (() => void) | undefined }) {
|
||
|
|
const { t, tm } = useI18n();
|
||
|
|
const canReport = onReport && ['entrance', 'steps', 'width', 'toilet', 'ramp'].includes(fact.key);
|
||
|
|
return (
|
||
|
|
<li className="border-1px border-base-lighter radius-md padding-2 margin-bottom-2 bg-white">
|
||
|
|
<div className="display-flex flex-justify flex-wrap flex-align-center">
|
||
|
|
<h4 className="margin-0 font-sans-md">{t(`fact.title.${fact.key}`)}</h4>
|
||
|
|
<StatusLabel status={fact.status} />
|
||
|
|
</div>
|
||
|
|
<p className="margin-top-1 margin-bottom-0">{tm(fact.headline)}</p>
|
||
|
|
{fact.detail && <p className="margin-top-05 margin-bottom-0 text-base-dark font-sans-xs">{tm(fact.detail)}</p>}
|
||
|
|
<SourceLine source={fact.source} confidence={fact.confidence} date={fact.date} />
|
||
|
|
{fact.alt && (
|
||
|
|
<div className="usa-alert usa-alert--warning usa-alert--slim margin-top-2">
|
||
|
|
<div className="usa-alert__body">
|
||
|
|
<p className="usa-alert__text">
|
||
|
|
<strong>{t('fact.otherSource')}:</strong> {tm(fact.alt.headline)}
|
||
|
|
</p>
|
||
|
|
<SourceLine source={fact.alt.source} confidence={fact.alt.confidence} date={fact.alt.date} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
<div className="display-flex flex-wrap flex-align-center margin-top-1">
|
||
|
|
{fact.required && <span className="font-sans-3xs text-base-dark margin-right-2">{t('fact.required')}</span>}
|
||
|
|
{canReport && (
|
||
|
|
<button type="button" className="usa-button usa-button--unstyled font-sans-xs" onClick={onReport}>
|
||
|
|
{t('fact.report')}
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</li>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function Alert({ kind, heading, children, slim = false, role }: { kind: 'info' | 'warning' | 'error' | 'success'; heading?: string; children: ReactNode; slim?: boolean; role?: 'alert' | 'status' }) {
|
||
|
|
return (
|
||
|
|
<div className={`usa-alert usa-alert--${kind} ${slim ? 'usa-alert--slim' : ''}`} role={role}>
|
||
|
|
<div className="usa-alert__body">
|
||
|
|
{heading && <h4 className="usa-alert__heading">{heading}</h4>}
|
||
|
|
<div className="usa-alert__text">{children}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|