Files
TREK/client/src/components/Settings/AirTrailConnectionSection.tsx
T
Maurice 68e2e0639b refactor(airtrail): share the AirTrail connection form between desktop and phone
Both settings shells now read their state, the save and test handlers and the canSave rule from one useAirTrailSettings hook and keep only their own markup.
2026-10-09 15:19:47 +02:00

112 lines
3.9 KiB
TypeScript

import React from 'react'
import { Loader2, Plane, Save } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { useAirTrailSettings } from './useAirTrailSettings'
import AirTrailIcon from '../shared/AirTrailIcon'
import Section from './Section'
import ToggleSwitch from './ToggleSwitch'
import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint, StatusPill } from './settingsKit'
import { EditorField, GRID_2, INPUT } from '../shared/dialogParts'
import { fs } from '../shared/DialogShell'
/**
* Settings → Integrations → AirTrail. Per-user connection to a self-hosted
* AirTrail instance (URL + Bearer API key). Mirrors the photo-provider (Immich)
* connection layout: the two fields side by side, the switches as rows, then
* Save / Test-connection, with the connection state as a pill in the card's band. The key is stored encrypted and never prefilled.
*/
export default function AirTrailConnectionSection(): React.ReactElement {
const { t } = useTranslation()
const {
url,
setUrl,
apiKey,
setApiKey,
allowInsecureTls,
toggleInsecureTls,
writeEnabled,
toggleWriteEnabled,
connected,
loading,
saving,
testing,
canSave,
handleSave,
handleTest,
} = useAirTrailSettings()
const fieldId = (name: string) => `airtrail-${name}`
return (
<Section
title={t('settings.airtrail.title')}
icon={AirTrailIcon}
badge={
<StatusPill tone={connected ? 'success' : 'neutral'} icon={<span className="h-1.5 w-1.5 rounded-full bg-current" />}>
{connected ? t('settings.airtrail.connected') : t('settings.airtrail.notConnected')}
</StatusPill>
}
>
<div className={GRID_2}>
<EditorField label={t('settings.airtrail.url')} htmlFor={fieldId('url')}>
<input
id={fieldId('url')}
type="url"
value={url}
onChange={e => setUrl(e.target.value)}
placeholder="https://airtrail.example.com"
className={INPUT}
/>
</EditorField>
<EditorField label={t('settings.airtrail.apiKey')} htmlFor={fieldId('key')} hint={t('settings.airtrail.apiKeyHint')}>
<input
id={fieldId('key')}
type="password"
value={apiKey}
onChange={e => setApiKey(e.target.value)}
autoComplete="off"
placeholder={connected && !apiKey ? '••••••••' : t('settings.airtrail.apiKeyPlaceholder')}
className={INPUT}
/>
</EditorField>
</div>
<SettingRows>
<SettingRow
label={t('settings.airtrail.allowInsecureTls')}
control={<ToggleSwitch on={allowInsecureTls} onToggle={toggleInsecureTls} label={t('settings.airtrail.allowInsecureTls')} />}
/>
<SettingRow
label={t('settings.airtrail.writeBack')}
hint={t('settings.airtrail.writeBackHint')}
control={<ToggleSwitch on={writeEnabled} onToggle={toggleWriteEnabled} label={t('settings.airtrail.writeBack')} />}
/>
</SettingRows>
<div className="flex flex-wrap items-center gap-2" style={fs(13, 'body')}>
<button type="button"
onClick={handleSave}
disabled={saving || loading || !canSave}
className={SETTINGS_BUTTON_PRIMARY}
>
<Save size={14} strokeWidth={2.2} /> {t('common.save')}
</button>
<button type="button"
onClick={handleTest}
disabled={testing || loading || !url.trim()}
className={SETTINGS_BUTTON}
>
{testing
? <Loader2 size={14} strokeWidth={2.2} className="animate-spin" />
: <Plane size={14} strokeWidth={2.2} />}
{t('settings.airtrail.test.button')}
</button>
</div>
<SettingsHint>{t('settings.airtrail.hint')}</SettingsHint>
</Section>
)
}