import type { PlaceContribField, PlaceContribState } from '@trek/shared'; import { ChevronRight, HeartHandshake, Undo2 } from 'lucide-react'; import { useState } from 'react'; import { useTranslation } from '../../i18n'; import type { TranslationFn } from '../../types'; import { getApiErrorMessage } from '../../utils/apiError'; import { SettingsCard, StatusPill } from '../Settings/settingsKit'; import ToggleSwitch from '../Settings/ToggleSwitch'; import ConfirmDialog from '../shared/ConfirmDialog'; import { fs } from '../shared/DialogShell'; import { useToast } from '../shared/Toast'; import { FigureBadge } from '../Tours/paddle/FigureBadge'; import PlaceContribLog from './PlaceContribLog'; import { contribNote, contribStatus, fieldEntries, fieldLabel, refusalReason, type ContribStatus, } from './placeContribModel'; import PlaceContribPrompt from './PlaceContribPrompt'; import { usePlaceContrib } from './usePlaceContrib'; const PILL_TONE: Record = { active: 'success', paused: 'warning', off: 'neutral', unavailable: 'neutral', }; function Figures({ state, t }: { state: PlaceContribState; t: TranslationFn }) { const { locale } = useTranslation(); const number = new Intl.NumberFormat(locale); const last = state.lastSuccessDay ? new Intl.DateTimeFormat(locale, { day: 'numeric', month: 'short' }).format( new Date(`${state.lastSuccessDay}T12:00:00`) ) : t('admin.contribute.never'); return (
); } /** * Sharing place details with the TREK Places API, inside the TREK API card: * one switch for the installation, one per field, the transmission log and the * way to take everything back. Turning it on for the first time opens the same * prompt an admin gets on the first visit, so an agreement always comes from it. */ /** * Standing alone (managed settings), a card that can only say "not available" * because the operator switched sharing off is a dead end. */ function hiddenAlone(state: PlaceContribState, alone: boolean): boolean { return alone && state.ineligibleReason === 'env_off'; } export default function PlaceContribPanel({ hideWhenSwitchedOff = false }: { hideWhenSwitchedOff?: boolean } = {}) { const { t } = useTranslation(); const toast = useToast(); const contrib = usePlaceContrib(); const { state } = contrib; const [prompting, setPrompting] = useState(null); const [confirm, setConfirm] = useState<'off' | 'withdraw' | null>(null); if (!state) return null; if (hiddenAlone(state, hideWhenSwitchedOff)) return null; const status = contribStatus(state); const agreed = state.consent.decision === 'agreed' && state.consent.termsVersion === state.termsVersion; // The prompt opens by itself once per visit while the server asks for it. const showPrompt = prompting ?? state.showPrompt; const fail = (err: unknown) => { const reason = refusalReason(err); toast.error( reason === 'terms_outdated' || reason === 'no_field' ? t(`admin.contribute.refused.${reason}`) : getApiErrorMessage(err, t('common.error')) ); }; const toggleField = (field: PlaceContribField) => { const fields = { ...state.consent.fields, [field]: !state.consent.fields[field] }; if (!Object.values(fields).some(Boolean)) { setConfirm('off'); return; } contrib.decide({ action: 'fields', fields }).catch(fail); }; const withdraw = () => { setConfirm(null); contrib.withdraw().then((r) => { const total = Object.values(r.withdrawn).reduce((a, b) => a + b, 0); toast.success(t('admin.contribute.withdrawn', { total })); }, fail); }; return ( {t(`admin.contribute.status.${status}`)}} hint={contribNote(t, state)} action={ (agreed && state.consent.enabled ? setConfirm('off') : setPrompting(true))} /> } bodyClassName="flex flex-col gap-3 p-4" > {agreed && state.consent.enabled && ( <>
    {fieldEntries(state.consent.fields).map(([field, on]) => (
  • {fieldLabel(t, field)} toggleField(field)} />
  • ))}
)} {(agreed || state.consent.decision === 'off') && (
{/* The admin page's fold head (pages/admin/FoldSummary), which a component cannot import. */}
)} {showPrompt && state.eligible && ( setPrompting(false)} /> )} setConfirm(null)} onConfirm={() => { setConfirm(null); contrib.decide({ action: 'off' }).catch(fail); }} title={t('admin.contribute.offTitle')} message={t('admin.contribute.offMessage')} confirmLabel={t('admin.contribute.offConfirm')} /> setConfirm(null)} onConfirm={withdraw} title={t('admin.contribute.withdrawTitle')} message={t('admin.contribute.withdrawMessage')} confirmLabel={t('admin.contribute.withdraw')} danger />
); }