Files
TREK/client/src/pages/admin/TrekApiCard.tsx
T

159 lines
7.1 KiB
TypeScript

import React from 'react'
import { Check, ChevronRight, Globe2, KeyRound, Library, ShieldOff, WifiOff, X } from 'lucide-react'
import TrekMark from '../../components/shared/TrekMark'
import { fs } from '../../components/shared/DialogShell'
import { StatusPill } from '../../components/Settings/settingsKit'
import type { TranslationFn } from '../../types'
interface TrekApiCardProps {
t: TranslationFn
}
/**
* The TREK Places API, above the Google key because it is the alternative to it.
*
* Given real weight on the page on purpose: it is what makes a key optional
* instead of expected. The weight comes from the mark, one line of type and an
* accent ring — not from a tinted panel, which is what the weather block used
* to do and what made it shout over the settings it sat next to.
*
* Everything a reader can check is a measured number, not a claim: 73.6 million
* is the row count of the current index, and "no queries logged" is enforced in
* three places on the server rather than promised here.
*/
/**
* Proper nouns, so they are not translated. Naming them is also a licence
* obligation, not decoration: ODbL and CC BY-SA both require attribution
* wherever their content is shown.
*/
const SOURCES = ['Overture Maps Foundation', 'OpenStreetMap', 'Wikivoyage', 'Wikimedia']
const CHIP = 'rounded-full border px-2.5 py-[3px] font-medium'
/** One of the three lists under "more": an icon and a name, chips, a note. */
function FactGroup({ icon, title, note, children }: { icon: React.ReactNode; title: string; note: string; children: React.ReactNode }) {
return (
<div className="flex flex-col gap-2 px-3.5 py-3">
<p className="m-0 flex items-center gap-1.5 font-semibold text-content" style={fs(12.5, 'body')}>
{icon}
{title}
</p>
<ul className="m-0 flex list-none flex-wrap gap-1.5 p-0" style={fs(11.5)}>{children}</ul>
<p className="m-0 leading-normal text-content-faint" style={fs(11.5)}>{note}</p>
</div>
)
}
export default function TrekApiCard({ t }: TrekApiCardProps): React.ReactElement {
// Reuses the words TREK already has for these fields wherever it has them,
// so the chip row costs two new strings instead of ten.
const fields = [
t('places.formName'),
t('collections.coordinates'),
t('places.formCategory'),
t('places.formAddress'),
t('admin.trekApi.fieldPhone'),
t('common.email'),
t('places.formWebsite'),
t('places.formDescription'),
t('inspector.openingHours'),
t('admin.trekApi.fieldStableId'),
]
const facts = [
{ Icon: Globe2, text: t('admin.trekApi.factPlaces') },
{ Icon: KeyRound, text: t('admin.trekApi.factNoKey') },
{ Icon: WifiOff, text: t('admin.trekApi.factOffline') },
{ Icon: ShieldOff, text: t('admin.trekApi.factPrivacy') },
]
return (
<div className="overflow-hidden rounded-[14px] border bg-surface-card shadow-sm"
style={{ borderColor: 'color-mix(in srgb, var(--accent) 40%, transparent)' }}>
<div className="flex flex-col gap-3 px-3.5 pb-3.5 pt-3.5">
{/* The recommendation, moved here from the Google field. It is the whole
point of the block: a key should be the exception, not the default.
A pill beside the mark rather than a banner, so it does not shout
over the settings around it. */}
<div className="flex flex-wrap items-center justify-between gap-2">
<TrekMark className="h-7 w-auto text-content" aria-label="TREK Places API" />
<StatusPill tone="accent">{t('admin.trekApi.badgeDefault')}</StatusPill>
</div>
<p className="m-0 leading-relaxed text-content-secondary" style={fs(13, 'body')}>
{t('admin.trekApi.tagline')}
</p>
<ul className="m-0 grid list-none grid-cols-1 gap-x-4 gap-y-1.5 p-0 sm:grid-cols-2">
{facts.map(({ Icon, text }) => (
<li key={text} className="flex min-w-0 items-center gap-2 text-content-secondary" style={fs(12, 'body')}>
<span className="grid h-6 w-6 flex-none place-items-center rounded-[8px] bg-surface-tertiary text-content-muted">
<Icon size={13} strokeWidth={2} aria-hidden="true" />
</span>
<span className="min-w-0">{text}</span>
</li>
))}
</ul>
</div>
<details className="group border-t border-edge-faint">
<summary
className="flex cursor-pointer list-none items-center gap-2.5 px-3.5 py-2.5
hover:bg-surface-hover focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[color:var(--text-primary)]
[&::-webkit-details-marker]:hidden"
>
<ChevronRight
size={15}
strokeWidth={2}
className="flex-none text-content-faint transition-transform group-open:rotate-90"
aria-hidden="true"
/>
<span className="font-medium text-content" style={fs(13, 'body')}>{t('admin.trekApi.more')}</span>
</summary>
<div className="divide-y divide-edge-faint border-t border-edge-faint bg-surface-secondary">
{/* The fields as chips rather than a paragraph. A list of what you
get is something you scan, not something you read, and a sentence
forces the reader to parse commas to answer "is the phone number
in there". */}
<FactGroup
icon={<Check size={14} strokeWidth={2.4} className="text-success" aria-hidden="true" />}
title={t('admin.trekApi.included')}
note={t('admin.trekApi.includedNote')}
>
{fields.map(field => (
<li key={field} className={`${CHIP} border-edge-faint bg-surface-card text-content-secondary`}>{field}</li>
))}
</FactGroup>
{/* Named as plainly as what IS included. An admin who switches the
source expecting ratings and a photograph of every restaurant
should find that out here and not three weeks later. */}
<FactGroup
icon={<X size={14} strokeWidth={2.4} className="text-content-faint" aria-hidden="true" />}
title={t('admin.trekApi.notIncluded')}
note={t('admin.trekApi.notIncludedNote')}
>
{[t('admin.trekApi.notRatings'), t('admin.trekApi.notPhotos')].map(item => (
<li key={item} className={`${CHIP} border-dashed border-edge text-content-faint`}>{item}</li>
))}
</FactGroup>
{/* Attribution, and not in the small print: the licences require the
sources to be named, and naming them is also the answer to "where
does this actually come from". */}
<FactGroup
icon={<Library size={14} strokeWidth={2} className="text-content-faint" aria-hidden="true" />}
title={t('admin.trekApi.sourcesLabel')}
note={t('admin.trekApi.sourcesNote')}
>
{SOURCES.map(source => (
<li key={source} className={`${CHIP} border-edge-faint bg-surface-card text-content-secondary`}>{source}</li>
))}
</FactGroup>
</div>
</details>
</div>
)
}