mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(client): add the Startup section to General settings
Desktop and mobile both get it, in the style of the file they live in - the On/Off button pairs on desktop, segments and a picker sheet on mobile. The tab picker only appears once "Active trip" is chosen, since there is nothing for it to mean otherwise. Labels come from the same keys the planner's tab bar uses, so the picker can never name a tab differently from the tab itself.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-COMP-DISPLAY-001 to FE-COMP-DISPLAY-048
|
||||
// FE-COMP-DISPLAY-001 to FE-COMP-DISPLAY-052
|
||||
import { render, screen, within, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
@@ -395,3 +395,52 @@ describe('DisplaySettingsTab – Map and privacy toggles', () => {
|
||||
expect(screen.getAllByText('Nope').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DisplaySettingsTab – startup destination', () => {
|
||||
it('FE-COMP-DISPLAY-049: defaults to the dashboard and hides the tab picker', () => {
|
||||
seedStore(useSettingsStore, { settings: buildSettings() });
|
||||
render(<DisplaySettingsTab />);
|
||||
const block = optionBlock(/^Start page$/);
|
||||
|
||||
expect(within(block).getByText('Dashboard').closest('button')!.style.border).toContain('var(--text-primary)');
|
||||
// Nothing to pick a tab for while TREK opens on the dashboard.
|
||||
expect(screen.queryByText('Start tab')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-050: choosing the active trip saves it and reveals the tab picker', async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useSettingsStore, { settings: buildSettings(), updateSetting });
|
||||
render(<DisplaySettingsTab />);
|
||||
|
||||
await user.click(within(optionBlock(/^Start page$/)).getByText('Active trip'));
|
||||
|
||||
expect(updateSetting).toHaveBeenCalledWith('start_page', 'active_trip');
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-051: with the active trip chosen, the tab picker stores the planner tab id', async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateSetting = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useSettingsStore, {
|
||||
settings: buildSettings({ start_page: 'active_trip', start_trip_tab: 'plan' }),
|
||||
updateSetting,
|
||||
});
|
||||
render(<DisplaySettingsTab />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Plan' }));
|
||||
await user.click(await screen.findByText('Costs'));
|
||||
|
||||
// The German legacy id, not the English label the user sees.
|
||||
expect(updateSetting).toHaveBeenCalledWith('start_trip_tab', 'finanzplan');
|
||||
});
|
||||
|
||||
it('FE-COMP-DISPLAY-052: a rejected start-page change surfaces the error', async () => {
|
||||
const user = userEvent.setup();
|
||||
seedFailing('Start locked');
|
||||
render(<><ToastContainer /><DisplaySettingsTab /></>);
|
||||
|
||||
await user.click(within(optionBlock(/^Start page$/)).getByText('Active trip'));
|
||||
|
||||
await screen.findByText('Start locked');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { Languages, Map, ChevronDown, Check } from 'lucide-react'
|
||||
import { Languages, Map, ChevronDown, Check, Rocket } from 'lucide-react'
|
||||
import { SUPPORTED_LANGUAGES, useTranslation } from '../../i18n'
|
||||
import { useSettingsStore, DEFAULT_SETTINGS } from '../../store/settingsStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import { SYMBOLS, currenciesWith } from '../Budget/BudgetPanel.constants'
|
||||
import Section from './Section'
|
||||
import { TRIP_TAB_IDS, TRIP_TAB_LABEL_KEYS } from '../../constants/tripTabs'
|
||||
import { DEFAULT_START_PAGE, DEFAULT_START_TRIP_TAB } from '../../utils/startDestination'
|
||||
import type { DistanceUnit } from '../../types'
|
||||
|
||||
export default function DisplaySettingsTab(): React.ReactElement {
|
||||
@@ -34,8 +36,60 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
||||
setDistanceUnit(settings.distance_unit || DEFAULT_SETTINGS.distance_unit)
|
||||
}, [settings.distance_unit])
|
||||
|
||||
const startPage = settings.start_page === 'active_trip' ? 'active_trip' : DEFAULT_START_PAGE
|
||||
const startTripTab = settings.start_trip_tab || DEFAULT_START_TRIP_TAB
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title={t('settings.general.startup')} icon={Rocket}>
|
||||
{/* Where opening TREK lands */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.startPage')}</label>
|
||||
<div className="flex gap-3">
|
||||
{([
|
||||
{ value: 'dashboard', label: t('settings.startPageDashboard') },
|
||||
{ value: 'active_trip', label: t('settings.startPageActiveTrip') },
|
||||
] as const).map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={async () => {
|
||||
try { await updateSetting('start_page', opt.value) }
|
||||
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
|
||||
}}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 20px', borderRadius: 10, cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 500,
|
||||
border: startPage === opt.value ? '2px solid var(--text-primary)' : '2px solid var(--border-primary)',
|
||||
background: startPage === opt.value ? 'var(--bg-hover)' : 'var(--bg-card)',
|
||||
color: 'var(--text-primary)',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs mt-1 text-content-faint">{t('settings.startPageHint')}</p>
|
||||
</div>
|
||||
|
||||
{/* Which planner tab the trip opens on — only meaningful for 'active_trip' */}
|
||||
{startPage === 'active_trip' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.startTripTab')}</label>
|
||||
<CustomSelect
|
||||
value={startTripTab}
|
||||
onChange={async v => {
|
||||
try { await updateSetting('start_trip_tab', String(v)) }
|
||||
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
|
||||
}}
|
||||
options={TRIP_TAB_IDS.map(id => ({ value: id, label: t(TRIP_TAB_LABEL_KEYS[id]) }))}
|
||||
/>
|
||||
<p className="text-xs text-content-faint mt-2">{t('settings.startTripTabHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title={t('settings.general.languageRegion')} icon={Languages}>
|
||||
{/* Display currency */}
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user