first app design

This commit is contained in:
root
2026-05-24 02:35:37 -04:00
parent c842eed601
commit d04c8ce437
138 changed files with 9672 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class ErrorView extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const ErrorView({super.key, required this.message, this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Color(0xFFE02424)),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF6B7280)),
),
if (onRetry != null) ...[
const SizedBox(height: 16),
ElevatedButton(onPressed: onRetry, child: const Text('Retry')),
],
],
),
),
);
}
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class LoadingList extends StatelessWidget {
final int count;
final double itemHeight;
const LoadingList({super.key, this.count = 5, this.itemHeight = 100});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: const Color(0xFFE5E7EB),
highlightColor: const Color(0xFFF9FAFB),
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: count,
separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (_, _) => Container(
height: itemHeight,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
),
),
);
}
}
+89
View File
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/providers/locale_provider.dart';
import '../core/providers/theme_provider.dart';
const _orange = Color(0xFFFF6B00);
/// Compact row of language pills + theme toggle.
/// [onDark] = true when rendered on a dark background (landing screen).
class PreferencesBar extends ConsumerWidget {
final bool onDark;
const PreferencesBar({super.key, this.onDark = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = ref.watch(localeProvider);
final themeMode = ref.watch(themeProvider);
final isDark = themeMode == ThemeMode.dark;
final inactiveText = onDark
? Colors.white.withValues(alpha: 0.55)
: Theme.of(context).colorScheme.onSurfaceVariant;
final activeText = Colors.white;
final inactiveBg = onDark
? Colors.white.withValues(alpha: 0.12)
: Theme.of(context).colorScheme.surfaceContainerHighest;
const langs = [
('EN', Locale('en')),
('FR', Locale('fr')),
('AR', Locale('ar')),
];
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// Language pills
...langs.map((item) {
final (code, locale) = item;
final selected =
currentLocale.languageCode == locale.languageCode;
return GestureDetector(
onTap: () => ref.read(localeProvider.notifier).setLocale(locale),
child: Container(
margin: const EdgeInsets.only(right: 6),
padding:
const EdgeInsets.symmetric(horizontal: 11, vertical: 6),
decoration: BoxDecoration(
color: selected ? _orange : inactiveBg,
borderRadius: BorderRadius.circular(20),
),
child: Text(
code,
style: TextStyle(
color: selected ? activeText : inactiveText,
fontSize: 12,
fontWeight:
selected ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}),
const SizedBox(width: 2),
// Theme toggle icon
GestureDetector(
onTap: () => ref.read(themeProvider.notifier).toggle(),
child: Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: inactiveBg,
borderRadius: BorderRadius.circular(20),
),
child: Icon(
isDark ? Icons.light_mode_outlined : Icons.dark_mode_outlined,
size: 15,
color: onDark
? Colors.white.withValues(alpha: 0.8)
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
);
}
}
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../core/models/reservation.dart';
import 'status_badge.dart';
class ReservationCard extends StatelessWidget {
final Reservation reservation;
final VoidCallback? onTap;
const ReservationCard({super.key, required this.reservation, this.onTap});
@override
Widget build(BuildContext context) {
final fmt = DateFormat('MMM d, yyyy');
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
reservation.vehicle?.displayName ?? 'Reservation',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
),
StatusBadge(status: reservation.status),
],
),
if (reservation.customer != null) ...[
const SizedBox(height: 4),
Text(
reservation.customer!.fullName,
style: const TextStyle(
fontSize: 13, color: Color(0xFF6B7280)),
),
],
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.calendar_today,
size: 14, color: Color(0xFF9CA3AF)),
const SizedBox(width: 4),
Text(
'${fmt.format(reservation.startDate)}${fmt.format(reservation.endDate)}',
style: const TextStyle(
fontSize: 13, color: Color(0xFF6B7280)),
),
],
),
const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.attach_money,
size: 14, color: Color(0xFF9CA3AF)),
const SizedBox(width: 4),
Text(
'\$${reservation.totalAmount.toStringAsFixed(2)}',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xFF111928),
),
),
const SizedBox(width: 8),
StatusBadge(status: reservation.paymentStatus),
],
),
],
),
),
),
);
}
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
class StatCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color color;
final String? subtitle;
const StatCard({
super.key,
required this.title,
required this.value,
required this.icon,
required this.color,
this.subtitle,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const Spacer(),
],
),
const SizedBox(height: 12),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF111928),
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF6B7280),
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF9CA3AF),
),
),
],
],
),
),
);
}
}
+68
View File
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
class StatusBadge extends StatelessWidget {
final String status;
const StatusBadge({super.key, required this.status});
@override
Widget build(BuildContext context) {
final (color, bg, label) = _style(status);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(20),
),
child: Text(
label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
static (Color, Color, String) _style(String status) {
switch (status.toUpperCase()) {
case 'AVAILABLE':
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Available');
case 'RENTED':
return (const Color(0xFF1A56DB), const Color(0xFFE1EFFE), 'Rented');
case 'MAINTENANCE':
return (const Color(0xFFB45309), const Color(0xFFFDF6B2), 'Maintenance');
case 'OUT_OF_SERVICE':
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Out of Service');
case 'CONFIRMED':
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Confirmed');
case 'ACTIVE':
return (const Color(0xFF1A56DB), const Color(0xFFE1EFFE), 'Active');
case 'COMPLETED':
return (const Color(0xFF5521B5), const Color(0xFFEDEBFE), 'Completed');
case 'CANCELLED':
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Cancelled');
case 'DRAFT':
return (const Color(0xFF374151), const Color(0xFFF3F4F6), 'Draft');
case 'NO_SHOW':
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'No Show');
case 'PENDING':
return (const Color(0xFFB45309), const Color(0xFFFDF6B2), 'Pending');
case 'SUCCEEDED':
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Paid');
case 'FAILED':
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Failed');
case 'APPROVED':
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Approved');
case 'DENIED':
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Denied');
case 'VALID':
return (const Color(0xFF057A55), const Color(0xFFDEF7EC), 'Valid');
case 'EXPIRED':
return (const Color(0xFF9B1C1C), const Color(0xFFFDE8E8), 'Expired');
default:
return (const Color(0xFF374151), const Color(0xFFF3F4F6), status);
}
}
}
+122
View File
@@ -0,0 +1,122 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../core/models/vehicle.dart';
import '../core/constants/app_constants.dart';
import 'status_badge.dart';
class VehicleCard extends StatelessWidget {
final Vehicle vehicle;
final VoidCallback? onTap;
const VehicleCard({super.key, required this.vehicle, this.onTap});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImage(),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
vehicle.displayName,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
StatusBadge(status: vehicle.status),
],
),
const SizedBox(height: 4),
Text(
vehicle.licensePlate,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF6B7280),
),
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.attach_money,
size: 16, color: Color(0xFF1A56DB)),
Text(
'${vehicle.dailyRate.toStringAsFixed(0)}/day',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xFF1A56DB),
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(6),
),
child: Text(
vehicle.category,
style: const TextStyle(
fontSize: 11, color: Color(0xFF6B7280)),
),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildImage() {
final photo = vehicle.primaryPhoto;
if (photo == null) {
return Container(
height: 150,
color: const Color(0xFFF3F4F6),
child: const Center(
child: Icon(Icons.directions_car, size: 48, color: Color(0xFFD1D5DB)),
),
);
}
final url = photo.startsWith('http')
? photo
: '${AppConstants.baseUrl.replaceAll('/api/v1', '')}$photo';
return CachedNetworkImage(
imageUrl: url,
height: 150,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (_, _) => Container(
height: 150,
color: const Color(0xFFF3F4F6),
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
),
errorWidget: (_, _, _) => Container(
height: 150,
color: const Color(0xFFF3F4F6),
child: const Center(
child: Icon(Icons.directions_car, size: 48, color: Color(0xFFD1D5DB)),
),
),
);
}
}