Initial commit

This commit is contained in:
2026-06-23 11:29:07 +05:30
commit 401cf600e8
224 changed files with 12165 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
class ApiConstants {
static const String baseUrl = 'https://api.doormile.com/api/v1';
// ---- Auth ----
static const String register = '$baseUrl/customer/register';
static const String login = '$baseUrl/customer/login';
static const String verifyPin = '$baseUrl/customer/verify-pin';
static const String resetPin = '$baseUrl/customer/reset-pin';
static const String logout = '$baseUrl/customer/logout';
// ---- Profile ----
static const String profile = '$baseUrl/customer/profile';
// ---- Pickup Locations ----
static const String locations = '$baseUrl/customer/locations';
static String updateLocation(String id) => '$baseUrl/customer/locations/$id';
static String deleteLocation(String id) => '$baseUrl/customer/locations/$id';
// ---- Pickups (Bookings) ----
static const String createPickup = '$baseUrl/customer/bookings';
static const String getPickups = '$baseUrl/customer/bookings';
static String getPickupDetails(String bookingId) => '$baseUrl/customer/bookings/$bookingId';
static String cancelPickup(String bookingId) => '$baseUrl/customer/bookings/$bookingId/cancel';
// ---- Pricing ----
static const String checkPrice = '$baseUrl/pricing/check';
// ---- Tracking ----
static String trackPickup(String trackingNo) => '$baseUrl/customer/track/$trackingNo';
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
class AppColors {
AppColors._();
// Backgrounds & Surfaces
static const surface = Color(0xFFF7F4F2);
static const surfaceDim = Color(0xFFE8DED8);
static const surfaceContainerLowest = Color(0xFFFFFFFF);
static const surfaceContainerLow = Color(0xFFFCFAF8);
static const surfaceContainer = Color(0xFFF0E8E3);
static const surfaceContainerHigh = Color(0xFFE6DAD3);
static const surfaceContainerHighest = Color(0xFFD8C8BF);
static const surfaceVariant = Color(0xFFEDE3DD);
// Text Colors
static const onSurface = Color(0xFF271D1B);
static const onSurfaceVariant = Color(0xFF756762);
static const outline = Color(0xFFA9958D);
static const outlineVariant = Color(0xFFD8C8BF);
static const inverseSurface = Color(0xFF271D1B);
// Primary (brand red)
static const primary = Color(0xFFC31723);
static const primaryContainer = Color(0xFF7D1017);
static const onPrimary = Color(0xFFFFFFFF);
static const onPrimaryContainer = Color(0xFFFFEDEE);
static const primaryFixed = Color(0xFFFFE9E7);
static const primaryFixedDim = Color(0xFFFFCFCB);
// Secondary (blue)
static const secondary = Color(0xFF126E82);
static const secondaryContainer = Color(0xFFDDF4F4);
static const secondaryFixed = Color(0xFFC6ECEC);
static const secondaryFixedDim = Color(0xFF9FDADB);
static const onSecondaryFixedVariant = Color(0xFF064A5A);
// Tertiary (eco green)
static const tertiary = Color(0xFF16824E);
static const tertiaryContainer = Color(0xFFE7F7ED);
static const tertiaryFixed = Color(0xFFDCFCE7);
static const tertiaryFixedDim = Color(0xFFBBF7D0);
static const onTertiaryFixedVariant = Color(0xFF14532D);
// Status / Warning
static const error = Color(0xFFDC2626);
static const errorContainer = Color(0xFFFEE2E2);
static const onErrorContainer = Color(0xFF7F1D1D);
static const warning = Color(0xFFFF7A00); // Orange
static const success = Color(0xFF16A34A); // Green
// Journey accents (brand narrative)
static const firstMile = Color(0xFFEA580C); // orange — pickup
static const midMile = Color(0xFF0E7C7B); // teal — transit
static const lastMile = Color(0xFF1D4EDB); // blue — delivery
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:doormile/core/theme/app_colors.dart';
/// Central theme + reusable text styles based on the Doormile design system.
class AppTheme {
AppTheme._();
static const fontFamily = 'Public Sans';
static ThemeData get light {
final base = ThemeData.light(useMaterial3: true);
return base.copyWith(
scaffoldBackgroundColor: AppColors.surface,
colorScheme: const ColorScheme.light(
primary: AppColors.primary,
onPrimary: AppColors.onPrimary,
primaryContainer: AppColors.primaryContainer,
onPrimaryContainer: AppColors.onPrimaryContainer,
secondary: AppColors.secondary,
onSecondary: Colors.white,
tertiary: AppColors.tertiary,
onTertiary: Colors.white,
error: AppColors.error,
surface: AppColors.surface,
onSurface: AppColors.onSurface,
surfaceContainerHighest: AppColors.surfaceContainerHighest,
outline: AppColors.outline,
outlineVariant: AppColors.outlineVariant,
),
textTheme: base.textTheme.apply(
fontFamily: fontFamily,
bodyColor: AppColors.onSurface,
displayColor: AppColors.onSurface,
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
foregroundColor: AppColors.primary,
centerTitle: false,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
hintStyle: bodyMd.copyWith(color: AppColors.outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: AppColors.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: AppColors.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: AppColors.primary, width: 1.4),
),
),
splashFactory: InkRipple.splashFactory,
);
}
static const _f = fontFamily;
/// Builds a style backed by the variable font's true `wght` axis so weights
/// render at their real glyph widths (no synthetic-bold widening).
static TextStyle _ts(double size, int wght, double lineHeight, [Color? color, double? letterSpacing]) {
return TextStyle(
fontFamily: _f,
fontSize: size,
height: lineHeight / size,
letterSpacing: letterSpacing,
color: color ?? AppColors.onSurface,
fontWeight: FontWeight.values[(wght ~/ 100) - 1],
fontVariations: [FontVariation('wght', wght.toDouble())],
);
}
// ---- Typography helpers (mirror DESIGN.md scale) ----
static final TextStyle headlineLg = _ts(22, 800, 28, null, -0.5);
static final TextStyle headlineMd = _ts(17, 700, 24, null, -0.3);
static final TextStyle headlineSm = _ts(15, 700, 22, null, -0.2);
static final TextStyle bodyLg = _ts(15, 400, 22);
static final TextStyle bodyMd = _ts(13, 400, 20);
static final TextStyle labelMd = _ts(13, 700, 20);
static final TextStyle caption = _ts(11, 500, 16, AppColors.onSurfaceVariant);
static final TextStyle button = _ts(14, 700, 20, null, 0.2);
/// Ambient card shadow (Level 1 elevation from the design brief).
static List<BoxShadow> get cardShadow => const [
BoxShadow(color: Color(0x12000000), blurRadius: 28, offset: Offset(0, 12)),
BoxShadow(color: Color(0x08C31723), blurRadius: 8, offset: Offset(0, 2)),
];
}

View File

@@ -0,0 +1,434 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:doormile/core/theme/app_colors.dart';
import 'package:doormile/core/theme/app_theme.dart';
/// White rounded card with the design-system ambient shadow.
class AppCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
final VoidCallback? onTap;
final Color color;
final BorderRadius? radius;
final Border? border;
const AppCard({
super.key,
required this.child,
this.padding = const EdgeInsets.all(16),
this.onTap,
this.color = AppColors.surfaceContainerLowest,
this.radius,
this.border,
});
@override
Widget build(BuildContext context) {
final br = radius ?? BorderRadius.circular(18);
return Material(
color: color,
borderRadius: br,
child: InkWell(
onTap: onTap,
borderRadius: br,
child: Ink(
decoration: BoxDecoration(
color: color,
borderRadius: br,
border: border ?? Border.all(color: AppColors.surfaceContainer.withValues(alpha: 0.7)),
boxShadow: AppTheme.cardShadow,
),
child: Padding(padding: padding, child: child),
),
),
);
}
}
/// Primary filled action button (min height 52, 12px radius).
class PrimaryButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final IconData? trailingIcon;
final bool loading;
final Color color;
final double height;
const PrimaryButton({
super.key,
required this.label,
required this.onPressed,
this.trailingIcon,
this.loading = false,
this.color = AppColors.primary,
this.height = 52,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: height,
child: ElevatedButton(
onPressed: loading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
disabledBackgroundColor: color.withValues(alpha: 0.6),
disabledForegroundColor: Colors.white,
elevation: 0,
shadowColor: AppColors.primary.withValues(alpha: 0.2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: loading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2.4, color: Colors.white),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(label,
style: AppTheme.button,
overflow: TextOverflow.ellipsis,
maxLines: 1),
),
if (trailingIcon != null) ...[
const SizedBox(width: 8),
Icon(trailingIcon, size: 20),
],
],
),
),
);
}
}
/// Secondary tinted button (pink bg, red text).
class SecondaryButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
const SecondaryButton({super.key, required this.label, this.onPressed});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 44,
child: OutlinedButton(
onPressed: onPressed,
style: OutlinedButton.styleFrom(
backgroundColor: AppColors.surfaceContainerLowest,
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.outlineVariant),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 18),
),
child: Text(label, style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
),
);
}
}
/// Pill-shaped status / category chip.
class StatusChip extends StatelessWidget {
final String label;
final Color color;
final Color bg;
final IconData? icon;
const StatusChip({
super.key,
required this.label,
required this.color,
required this.bg,
this.icon,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: color.withValues(alpha: 0.12)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[Icon(icon, size: 14, color: color), const SizedBox(width: 4)],
Text(label,
style: AppTheme.caption.copyWith(color: color, fontWeight: FontWeight.w600)),
],
),
);
}
}
/// A horizontal stepper progress bar with a fraction filled.
class JourneyProgressBar extends StatelessWidget {
final double value; // 0..1
final bool gradient;
const JourneyProgressBar({super.key, required this.value, this.gradient = false});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(999),
child: Stack(
children: [
Container(height: 6, color: AppColors.surfaceContainer),
FractionallySizedBox(
widthFactor: value.clamp(0, 1),
child: Container(
height: 6,
decoration: BoxDecoration(
color: gradient ? null : AppColors.primary,
gradient: gradient
? const LinearGradient(colors: [
AppColors.firstMile,
AppColors.midMile,
AppColors.lastMile,
])
: null,
),
),
),
],
),
);
}
}
/// Eco / EV reassurance banner used across screens.
class EcoBanner extends StatelessWidget {
final String text;
const EcoBanner({super.key, required this.text});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.tertiaryFixed.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.tertiary.withValues(alpha: 0.1)),
),
child: Row(
children: [
const Icon(Icons.eco, color: AppColors.tertiary, size: 22),
const SizedBox(width: 12),
Expanded(
child: Text(text,
style: AppTheme.bodyMd.copyWith(color: AppColors.tertiary, height: 1.35)),
),
],
),
);
}
}
/// Warm page backdrop with restrained route/map texture.
class AppBackground extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
const AppBackground({
super.key,
required this.child,
this.padding = EdgeInsets.zero,
});
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFFBF8), AppColors.surface],
),
),
child: Stack(
children: [
const Positioned(
top: 240,
left: -70,
child: _SoftCircle(size: 180, color: Color(0x1A126E82)),
),
Padding(padding: padding, child: child),
],
),
);
}
}
class _SoftCircle extends StatelessWidget {
final double size;
final Color color;
const _SoftCircle({required this.size, required this.color});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
);
}
}
/// Reusable immersive red header used by primary tabs.
class ImmersiveHeader extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Widget? trailing;
final bool showBack;
const ImmersiveHeader({
super.key,
required this.title,
required this.subtitle,
required this.icon,
this.trailing,
this.showBack = false,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.primaryContainer, AppColors.primary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)),
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
child: Stack(
children: [
Positioned(
right: -20,
bottom: -30,
child: Icon(icon, size: 140, color: Colors.white.withValues(alpha: 0.04)),
),
Positioned(
right: 100,
top: -10,
child: Transform.rotate(
angle: 0.2,
child: Icon(Icons.auto_awesome_rounded, size: 60, color: Colors.white.withValues(alpha: 0.03)),
),
),
SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
child: Row(
children: [
if (showBack) ...[
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back, color: Colors.white),
padding: EdgeInsets.zero,
alignment: Alignment.centerLeft,
),
const SizedBox(width: 4),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTheme.headlineLg.copyWith(color: Colors.white)),
const SizedBox(height: 4),
Text(subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTheme.bodyMd.copyWith(color: Colors.white.withValues(alpha: 0.8))),
],
),
),
if (trailing != null) trailing!,
],
),
),
),
],
),
),
),
);
}
}
/// A small circular icon avatar with a tinted background.
class IconBadge extends StatelessWidget {
final IconData icon;
final Color bg;
final Color color;
final double size;
final double iconSize;
final bool circle;
const IconBadge({
super.key,
required this.icon,
required this.bg,
required this.color,
this.size = 48,
this.iconSize = 22,
this.circle = false,
});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(circle ? size : 12),
),
child: Icon(icon, color: color, size: iconSize),
);
}
}
/// Title row with an optional trailing action ("View All").
class SectionHeader extends StatelessWidget {
final String title;
final String? action;
final VoidCallback? onAction;
const SectionHeader({super.key, required this.title, this.action, this.onAction});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(title,
style: AppTheme.headlineSm, overflow: TextOverflow.ellipsis, maxLines: 1),
),
if (action != null)
GestureDetector(
onTap: onAction,
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(action!,
style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
),
),
],
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:doormile/core/theme/app_colors.dart';
import 'package:doormile/core/theme/app_theme.dart';
/// Stylised Doormile badge mark — a rounded pin holding a package glyph.
class DoormileMark extends StatelessWidget {
final double size;
final Color color;
final Color glyphColor;
const DoormileMark({
super.key,
this.size = 48,
this.color = AppColors.primary,
this.glyphColor = Colors.white,
});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(size * 0.28),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.25),
blurRadius: size * 0.3,
offset: Offset(0, size * 0.12),
),
],
),
child: Icon(Icons.local_shipping_rounded, color: glyphColor, size: size * 0.55),
);
}
}
/// Full wordmark: badge + "Doormile" text.
class DoormileWordmark extends StatelessWidget {
final double markSize;
final double fontSize;
final Color textColor;
final Color markColor;
final Color glyphColor;
const DoormileWordmark({
super.key,
this.markSize = 40,
this.fontSize = 24,
this.textColor = AppColors.primary,
this.markColor = AppColors.primary,
this.glyphColor = Colors.white,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
DoormileMark(size: markSize, color: markColor, glyphColor: glyphColor),
const SizedBox(width: 10),
Text(
'Doormile',
style: AppTheme.headlineLg.copyWith(color: textColor, fontSize: fontSize),
),
],
);
}
}