first app design
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/dashboard_providers.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../../../widgets/status_badge.dart';
|
||||
import '../../../widgets/error_view.dart';
|
||||
|
||||
class VehicleDetailScreen extends ConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const VehicleDetailScreen({super.key, required this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vehicleAsync = ref.watch(vehicleDetailProvider(id));
|
||||
|
||||
return vehicleAsync.when(
|
||||
loading: () => const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (e, _) => Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: ErrorView(
|
||||
message: 'Could not load vehicle',
|
||||
onRetry: () => ref.invalidate(vehicleDetailProvider(id)),
|
||||
),
|
||||
),
|
||||
data: (vehicle) {
|
||||
final baseUrl =
|
||||
AppConstants.baseUrl.replaceAll('/api/v1', '');
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 240,
|
||||
pinned: true,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: vehicle.primaryPhoto != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: vehicle.primaryPhoto!.startsWith('http')
|
||||
? vehicle.primaryPhoto!
|
||||
: '$baseUrl${vehicle.primaryPhoto!}',
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, _, _) => Container(
|
||||
color: const Color(0xFFF3F4F6),
|
||||
child: const Icon(Icons.directions_car,
|
||||
size: 72, color: Color(0xFFD1D5DB)),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: const Color(0xFFF3F4F6),
|
||||
child: const Icon(Icons.directions_car,
|
||||
size: 72, color: Color(0xFFD1D5DB)),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
vehicle.displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF111928),
|
||||
),
|
||||
),
|
||||
),
|
||||
StatusBadge(status: vehicle.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
vehicle.licensePlate,
|
||||
style: const TextStyle(
|
||||
fontSize: 15, color: Color(0xFF6B7280)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_InfoGrid(vehicle: vehicle),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Pricing',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF111928),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.attach_money,
|
||||
color: Color(0xFF1A56DB)),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'\$${vehicle.dailyRate.toStringAsFixed(2)}/day',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1A56DB),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Daily rate',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: vehicle.isPublished,
|
||||
onChanged: (v) async {
|
||||
try {
|
||||
await ref
|
||||
.read(vehicleServiceProvider)
|
||||
.togglePublish(id, v);
|
||||
ref.invalidate(vehicleDetailProvider(id));
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
const Text(
|
||||
'Published',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF6B7280)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoGrid extends StatelessWidget {
|
||||
final dynamic vehicle;
|
||||
|
||||
const _InfoGrid({required this.vehicle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
('Category', vehicle.category),
|
||||
if (vehicle.seats != null) ('Seats', '${vehicle.seats}'),
|
||||
if (vehicle.transmission != null) ('Transmission', vehicle.transmission!),
|
||||
if (vehicle.fuelType != null) ('Fuel', vehicle.fuelType!),
|
||||
if (vehicle.mileage != null)
|
||||
('Mileage', '${vehicle.mileage} km'),
|
||||
];
|
||||
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: items
|
||||
.map((item) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3F4F6),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.$1,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Color(0xFF9CA3AF)),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item.$2,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF111928),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user