Book a pickup, track it to delivery — the rebuilt customer app. - Design language from doormile-screens.html: brand #8F0F06, Manrope + Geist Mono (variable fonts), bordered cards instead of shadows, crimson brand headers, sliding tab indicator, mono for anything read digit by digit. - lib/data (one live API implementation, plus a debug-only offline fake), lib/state, lib/ui (tokens, widgets, screens). - 84 tests, plus a design snapshot harness that renders every screen with the real fonts: flutter test test/design_snapshot_test.dart --run-skipped --update-goldens This replaces the previous app (pubspec 'doormile', app id com.doormile.customer). That tree remains in history at 6c7d656; note its android/app/google-services.json is not carried over, and the application id here is in.doormile.customer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
2.0 KiB
Dart
81 lines
2.0 KiB
Dart
/// Small formatting helpers shared across screens.
|
|
library;
|
|
|
|
String greetingForNow([DateTime? now]) {
|
|
final h = (now ?? DateTime.now()).hour;
|
|
if (h < 12) return 'Good morning';
|
|
if (h < 17) return 'Good afternoon';
|
|
return 'Good evening';
|
|
}
|
|
|
|
/// "Just now" / "18 min ago" / "3 hr ago" / "5 d ago"
|
|
String relativeTime(DateTime? at, [DateTime? now]) {
|
|
if (at == null) return '';
|
|
final mins = (now ?? DateTime.now()).difference(at).inMinutes;
|
|
if (mins < 1) return 'Just now';
|
|
if (mins < 60) return '$mins min ago';
|
|
final hrs = (mins / 60).round();
|
|
if (hrs < 24) return '$hrs hr ago';
|
|
return '${(hrs / 24).round()} d ago';
|
|
}
|
|
|
|
const _months = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec',
|
|
];
|
|
|
|
/// "Today" / "Yesterday" / "29 Aug"
|
|
String dateLabel(DateTime date, [DateTime? now]) {
|
|
final today = now ?? DateTime.now();
|
|
bool sameDay(DateTime a, DateTime b) =>
|
|
a.year == b.year && a.month == b.month && a.day == b.day;
|
|
|
|
if (sameDay(date, today)) return 'Today';
|
|
final yesterday = today.subtract(const Duration(days: 1));
|
|
if (sameDay(date, yesterday)) return 'Yesterday';
|
|
return '${date.day} ${_months[date.month - 1]}';
|
|
}
|
|
|
|
/// "SEPTEMBER 2026" — the eyebrow over a tab root's title. Spelled in full
|
|
/// rather than abbreviated: it is read once, at the top of the screen, and
|
|
/// "SEP 2026" saves four characters nobody needed saved.
|
|
String monthLabel(DateTime date) => '${_monthsFull[date.month - 1]} ${date.year}';
|
|
|
|
const _monthsFull = [
|
|
'January',
|
|
'February',
|
|
'March',
|
|
'April',
|
|
'May',
|
|
'June',
|
|
'July',
|
|
'August',
|
|
'September',
|
|
'October',
|
|
'November',
|
|
'December',
|
|
];
|
|
|
|
/// Initials for an avatar, e.g. "Joe Oommen" -> "JO".
|
|
String initialsOf(String? name) {
|
|
final n = (name ?? '').trim();
|
|
if (n.isEmpty) return 'DM';
|
|
final letters = n
|
|
.split(RegExp(r'\s+'))
|
|
.map((p) => p.isEmpty ? '' : p[0])
|
|
.join();
|
|
return letters
|
|
.substring(0, letters.length < 2 ? letters.length : 2)
|
|
.toUpperCase();
|
|
}
|