feat: implement splash and onboarding flows, update app assets, and add project design documentation
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/l10n/app_localizations.dart';
|
||||
import '../../../core/design/tokens.dart';
|
||||
import '../cubit/auth_cubit.dart';
|
||||
import '../cubit/auth_state.dart';
|
||||
import '../../../core/ui/tripz_scaffold.dart';
|
||||
import '../../../core/ui/tripz_button.dart';
|
||||
import '../../../core/ui/tripz_text_field.dart';
|
||||
|
||||
/// شاشة إدخال الاسم (أول دخول فقط) — تطابق stitch/name_entry.html:
|
||||
/// شعار صغير بالمنتصف أعلى الشاشة + عنوان وسطر فرعي + بطاقة بيضاء
|
||||
/// مرفوعة فيها حقل الاسم بأيقونة، وزر متابعة أسفل الشاشة.
|
||||
///
|
||||
/// ملاحظة: تصميم Stitch يعرض 3 حقول (الاسم الأول/الأخير/البريد)، لكن
|
||||
/// AuthCubit.updateName ينادي PATCH /users/me بحقل name واحد فقط
|
||||
/// (docs/29 §3.5 ينص صراحة على «حقل واحد»). الشاشة هنا تستعمل حقلاً
|
||||
/// واحداً للاسم الكامل بنفس لغة Stitch البصرية (بطاقة مرفوعة + أيقونة
|
||||
/// + label عائم) دون تغيير عقد الـCubit.
|
||||
class NameScreen extends StatefulWidget {
|
||||
const NameScreen({super.key});
|
||||
@override
|
||||
State<NameScreen> createState() => _NameScreenState();
|
||||
}
|
||||
|
||||
class _NameScreenState extends State<NameScreen> {
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
bool _valid = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_firstNameController.addListener(_validate);
|
||||
_lastNameController.addListener(_validate);
|
||||
}
|
||||
|
||||
void _validate() {
|
||||
final isValid = _firstNameController.text.trim().isNotEmpty &&
|
||||
_lastNameController.text.trim().isNotEmpty;
|
||||
if (_valid != isValid) {
|
||||
setState(() => _valid = isValid);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
if (!_valid) return;
|
||||
final fullName =
|
||||
'${_firstNameController.text.trim()} ${_lastNameController.text.trim()}';
|
||||
context.read<AuthCubit>().updateName(fullName);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final cs = theme.colorScheme;
|
||||
|
||||
return BlocConsumer<AuthCubit, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == AuthStatus.error && state.error != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.error!)),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isLoading = state.status == AuthStatus.authenticating;
|
||||
return TripzScaffold(
|
||||
backgroundColor: cs.surfaceContainerLow,
|
||||
padding: false,
|
||||
bottomAction: TripzButton(
|
||||
onPressed: (_valid && !isLoading) ? _submit : null,
|
||||
label: l10n.nameButton,
|
||||
loading: isLoading,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// ── رأس الشاشة: زر عودة (يبدأ الجلسة من جديد) + شعار مصغّر ──
|
||||
SizedBox(
|
||||
height: T.s48,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
color: cs.onSurface,
|
||||
tooltip: l10n.back,
|
||||
onPressed: () => context.read<AuthCubit>().logout(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/name_hero.png',
|
||||
height: T.iconLg,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => Icon(
|
||||
Icons.directions_car_filled_rounded,
|
||||
size: T.iconLg,
|
||||
color: cs.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: T.s48),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: T.s24),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: T.pagePadding),
|
||||
child: Column(
|
||||
children: [
|
||||
// العنوان والسطر الفرعي
|
||||
Text(
|
||||
l10n.nameTitle,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: T.s8),
|
||||
Text(
|
||||
l10n.nameSubtitle,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: T.s24),
|
||||
|
||||
// بطاقة بيضاء مرفوعة تحوي حقول الاسم الأول واسم العائلة
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(T.s24),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(T.r16),
|
||||
boxShadow: const [T.shadowSm],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TripzTextField(
|
||||
controller: _firstNameController,
|
||||
label: l10n.firstNameHint,
|
||||
autofocus: true,
|
||||
prefix: Icon(Icons.person_outline_rounded, color: cs.outline),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: T.s16),
|
||||
TripzTextField(
|
||||
controller: _lastNameController,
|
||||
label: l10n.lastNameHint,
|
||||
prefix: Icon(Icons.badge_outlined, color: cs.outline),
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user