mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
feat(native): home and lock screen widgets in the TREK look
Four widgets instead of one plain text block, drawn in the phone tokens (warm paper by day, near black by night) with contour lines and a route arc as the travel motif: - Trip (small, medium, large, lock screen): the cover photo, a countdown ring and a plane on its arc before the trip; the day of the trip as segments, the weather and the next stops on frosted glass while it runs - Today (medium, large): the day as a timeline in its category colours, with a now line, the day's progress and the weather - Bookings (small, medium, large, lock screen): flights, trains, hotels and tables ahead as coloured tiles, the next one featured, status dots - To-dos (small, medium, large, lock screen): to-dos and packing as nested rings, the most pressing open to-dos with priority and due dates The app now sends the destination, a small cover JPEG, the weather where the trip is, the bookings ahead, the open to-dos and the packing count; every new field is optional on both sides, so older binaries and servers keep working. Timelines step every quarter hour so the now line moves without the app. client/ios/WidgetPreviews renders every widget in every size, light and dark, on the CI's Mac, uploaded as the widget-previews artifact.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { PackingItem, Reservation, TodoItem } from '../types';
|
||||
import { bookingsAhead, packingSummary, stopLook, temperature, todosSummary, tripPlace } from './widgetExtras';
|
||||
|
||||
const reservation = (over: Partial<Reservation>): Reservation =>
|
||||
({ id: 1, trip_id: 1, title: 'Booking', status: 'confirmed', type: 'other', ...over }) as Reservation;
|
||||
|
||||
const todo = (over: Partial<TodoItem>): TodoItem =>
|
||||
({
|
||||
id: 1,
|
||||
trip_id: 1,
|
||||
name: 'Task',
|
||||
category: null,
|
||||
checked: 0,
|
||||
sort_order: 0,
|
||||
due_date: null,
|
||||
description: null,
|
||||
assigned_user_id: null,
|
||||
priority: 0,
|
||||
...over,
|
||||
}) as TodoItem;
|
||||
|
||||
describe('bookingsAhead', () => {
|
||||
const days = new Map([[7, '2026-11-09']]);
|
||||
const time = (t: string) => `T${t}`;
|
||||
|
||||
it('lists the bookings from today on, soonest first, with their kind, legs and status', () => {
|
||||
const list = bookingsAhead(
|
||||
[
|
||||
reservation({ id: 1, title: 'Old', reservation_time: '2026-11-01T10:00' }),
|
||||
reservation({
|
||||
id: 2,
|
||||
title: 'Dinner',
|
||||
type: 'restaurant',
|
||||
reservation_time: '2026-11-08T19:30',
|
||||
status: 'pending',
|
||||
location: 'Pontocho',
|
||||
}),
|
||||
reservation({
|
||||
id: 3,
|
||||
title: 'LH 716',
|
||||
type: 'flight',
|
||||
reservation_time: '2026-11-08T13:05',
|
||||
endpoints: [
|
||||
{
|
||||
role: 'to',
|
||||
sequence: 1,
|
||||
name: 'Tokyo Haneda',
|
||||
code: 'HND',
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
timezone: null,
|
||||
local_time: null,
|
||||
local_date: null,
|
||||
},
|
||||
{
|
||||
role: 'from',
|
||||
sequence: 0,
|
||||
name: 'Frankfurt',
|
||||
code: 'FRA',
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
timezone: null,
|
||||
local_time: null,
|
||||
local_date: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
reservation({ id: 4, title: 'Museum', type: 'activity', day_id: 7 }),
|
||||
reservation({ id: 5, title: 'Undated' }),
|
||||
reservation({ id: 6, title: 'Cancelled', reservation_time: '2026-11-08T08:00', status: 'cancelled' }),
|
||||
reservation({ id: 7, title: 'Staged', reservation_time: '2026-11-08T08:00', ingest_state: 'staged' }),
|
||||
],
|
||||
{ dayDates: days, today: '2026-11-06', tripPath: '/trips/1', formatTime: time }
|
||||
);
|
||||
expect(list.map((b) => b.title)).toEqual(['LH 716', 'Dinner', 'Museum', 'Undated']);
|
||||
expect(list[0]).toEqual({
|
||||
title: 'LH 716',
|
||||
kind: 'flight',
|
||||
date: '2026-11-08',
|
||||
time: 'T13:05',
|
||||
detail: 'FRA → HND',
|
||||
confirmed: true,
|
||||
path: '/trips/1?tab=transports',
|
||||
});
|
||||
expect(list[1]).toMatchObject({
|
||||
kind: 'restaurant',
|
||||
detail: 'Pontocho',
|
||||
confirmed: false,
|
||||
path: '/trips/1?tab=buchungen',
|
||||
});
|
||||
expect(list[2]).toMatchObject({ kind: 'tour', date: '2026-11-09', time: null });
|
||||
expect(list[3]).toMatchObject({ kind: 'other', date: null });
|
||||
});
|
||||
|
||||
it('stops at the limit', () => {
|
||||
const many = Array.from({ length: 8 }, (_, i) => reservation({ id: i, reservation_time: `2026-11-1${i}T10:00` }));
|
||||
expect(
|
||||
bookingsAhead(many, { dayDates: new Map(), today: '2026-11-06', tripPath: '/trips/1', formatTime: (t) => t })
|
||||
).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('todosSummary', () => {
|
||||
it('counts the open ones and orders them by due date, then priority', () => {
|
||||
const summary = todosSummary(
|
||||
[
|
||||
todo({ id: 1, name: 'Done', checked: 1 }),
|
||||
todo({ id: 2, name: 'Someday', priority: 3 }),
|
||||
todo({ id: 3, name: 'Soon', due_date: '2026-11-08' }),
|
||||
todo({ id: 4, name: 'First', due_date: '2026-11-07T00:00:00' }),
|
||||
todo({ id: 5, name: 'Low', priority: 1 }),
|
||||
],
|
||||
'/trips/1?tab=listen'
|
||||
);
|
||||
expect(summary.open).toBe(4);
|
||||
expect(summary.total).toBe(5);
|
||||
expect(summary.items.map((i) => i.name)).toEqual(['First', 'Soon', 'Someday', 'Low']);
|
||||
expect(summary.items[0]).toEqual({ name: 'First', due: '2026-11-07', priority: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('packingSummary', () => {
|
||||
it('counts packed against all, and is null without a list', () => {
|
||||
const items = [{ checked: 1 }, { checked: 0 }, { checked: 1 }] as PackingItem[];
|
||||
expect(packingSummary(items, '/p')).toEqual({ packed: 2, total: 3, path: '/p' });
|
||||
expect(packingSummary([], '/p')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tripPlace', () => {
|
||||
it('names the town and country most places share, without postcodes', () => {
|
||||
expect(
|
||||
tripPlace([
|
||||
{ address: '68 Fukakusa Yabunouchicho, Fushimi Ward, 612-0882 Kyoto, Japan' },
|
||||
{ address: 'Nakagyo Ward, 604-8054 Kyoto, Japan' },
|
||||
{ address: '1-1 Chiyoda, 100-8111 Tokyo, Japan' },
|
||||
{ address: null },
|
||||
{ address: 'Nowhere' },
|
||||
])
|
||||
).toBe('Kyoto, Japan');
|
||||
});
|
||||
|
||||
it('is null without usable addresses', () => {
|
||||
expect(tripPlace([{ address: 'Main Street' }, { address: '' }])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopLook and temperature', () => {
|
||||
it('maps a category to a colour and an SF Symbol, ignoring what it cannot use', () => {
|
||||
expect(stopLook({ color: '#E8A13A', icon: 'Coffee' })).toEqual({ color: '#E8A13A', symbol: 'cup.and.saucer.fill' });
|
||||
expect(stopLook({ color: 'red', icon: 'Unknown' })).toEqual({ color: null, symbol: null });
|
||||
expect(stopLook(null)).toEqual({ color: null, symbol: null });
|
||||
});
|
||||
|
||||
it('converts to Fahrenheit only when asked, rounded', () => {
|
||||
expect(temperature(18.4, 'celsius')).toBe(18);
|
||||
expect(temperature(20, 'fahrenheit')).toBe(68);
|
||||
expect(temperature(-0.6, undefined)).toBe(-1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user