Replace the customer app with Doormile CX
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>
This commit is contained in:
80
lib/ui/format.dart
Normal file
80
lib/ui/format.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
/// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user