feat: implement core architecture, design system, localization, and trip features for both driver and rider apps.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/l10n/app_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/design/tokens.dart';
|
||||
import '../../../core/ui/tripz_scaffold.dart';
|
||||
import '../../../core/ui/tripz_text_field.dart';
|
||||
import '../cubit/destination_cubit.dart';
|
||||
import '../data/models/place_model.dart';
|
||||
import 'widgets/place_tile.dart';
|
||||
|
||||
/// شاشة البحث عن وجهة — full-screen مع autocomplete.
|
||||
class DestinationScreen extends StatefulWidget {
|
||||
const DestinationScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DestinationScreen> createState() => _DestinationScreenState();
|
||||
}
|
||||
|
||||
class _DestinationScreenState extends State<DestinationScreen> {
|
||||
final _controller = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return TripzScaffold(
|
||||
padding: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_SearchHeader(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
l10n: l10n,
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<DestinationCubit, DestinationState>(
|
||||
builder: (context, state) {
|
||||
if (state.searching) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state.results.isEmpty && state.query.length >= 2) {
|
||||
return Center(
|
||||
child: Text(l10n.noResults, style: Theme.of(context).textTheme.bodyLarge),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: state.results.length,
|
||||
itemBuilder: (context, index) {
|
||||
final place = state.results[index];
|
||||
return PlaceTile(
|
||||
place: place,
|
||||
onTap: () => context.pop<PlaceModel>(place),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(T.pagePadding),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => context.pop<PlaceModel>(null),
|
||||
icon: const Icon(Icons.map_outlined),
|
||||
label: Text(l10n.chooseOnMap),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchHeader extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final FocusNode focusNode;
|
||||
final AppLocalizations l10n;
|
||||
|
||||
const _SearchHeader({
|
||||
required this.controller,
|
||||
required this.focusNode,
|
||||
required this.l10n,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
color: cs.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
left: T.pagePadding,
|
||||
right: T.pagePadding,
|
||||
top: T.s8,
|
||||
bottom: T.s12,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
),
|
||||
Expanded(
|
||||
child: TripzTextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
hint: l10n.searchPlaces,
|
||||
prefix: const Icon(Icons.search_rounded),
|
||||
onChanged: (v) => context.read<DestinationCubit>().onQueryChanged(v),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: T.s8),
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Container(width: 1.5, height: 24, color: cs.outlineVariant),
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.error,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: T.s12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.pickupLocation,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: T.s4),
|
||||
Text(
|
||||
l10n.pickupSet,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: T.s16),
|
||||
Text(
|
||||
l10n.dropoffLocation,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user