feat: Update navigation paths and enhance personal info page with design system compliance
This commit is contained in:
@@ -8,7 +8,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
extension ProfileNavigator on IModularNavigator {
|
extension ProfileNavigator on IModularNavigator {
|
||||||
/// Navigates to the personal info page.
|
/// Navigates to the personal info page.
|
||||||
void pushPersonalInfo() {
|
void pushPersonalInfo() {
|
||||||
pushNamed('/profile/onboarding/personal-info');
|
pushNamed('./onboarding/personal-info');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the emergency contact page.
|
/// Navigates to the emergency contact page.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ extension ProfileInfoNavigator on IModularNavigator {
|
|||||||
/// This page allows staff members to edit their personal information
|
/// This page allows staff members to edit their personal information
|
||||||
/// including phone, bio, languages, and preferred locations.
|
/// including phone, bio, languages, and preferred locations.
|
||||||
Future<void> pushPersonalInfo() {
|
Future<void> pushPersonalInfo() {
|
||||||
return pushNamed('/profile/onboarding/personal-info');
|
return pushNamed('./personal-info');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the Emergency Contact page.
|
/// Navigates to the Emergency Contact page.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ extension ProfileInfoNavigator on IModularNavigator {
|
|||||||
/// This page allows staff members to edit their personal information
|
/// This page allows staff members to edit their personal information
|
||||||
/// including phone, bio, languages, and preferred locations.
|
/// including phone, bio, languages, and preferred locations.
|
||||||
Future<void> pushPersonalInfo() {
|
Future<void> pushPersonalInfo() {
|
||||||
return pushNamed('/profile/onboarding/personal-info');
|
return pushNamed('./personal-info');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the Emergency Contact page.
|
/// Navigates to the Emergency Contact page.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
@@ -9,99 +10,91 @@ import '../blocs/personal_info_event.dart';
|
|||||||
import '../blocs/personal_info_state.dart';
|
import '../blocs/personal_info_state.dart';
|
||||||
import '../widgets/personal_info_content.dart';
|
import '../widgets/personal_info_content.dart';
|
||||||
|
|
||||||
|
|
||||||
/// The Personal Info page for staff onboarding.
|
/// The Personal Info page for staff onboarding.
|
||||||
///
|
///
|
||||||
/// This page allows staff members to view and edit their personal information
|
/// This page allows staff members to view and edit their personal information
|
||||||
/// including phone number, bio, languages, and preferred locations.
|
/// including phone number and address. Full name and email are read-only as they come from authentication.
|
||||||
/// Full name and email are read-only as they come from authentication.
|
|
||||||
///
|
///
|
||||||
/// This page is a StatelessWidget that uses BLoC for state management,
|
/// This page is a StatelessWidget that uses BLoC for state management,
|
||||||
/// following Clean Architecture principles.
|
/// following Clean Architecture and the design system guidelines.
|
||||||
class PersonalInfoPage extends StatelessWidget {
|
class PersonalInfoPage extends StatelessWidget {
|
||||||
/// Creates a [PersonalInfoPage].
|
/// Creates a [PersonalInfoPage].
|
||||||
const PersonalInfoPage({super.key});
|
const PersonalInfoPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||||
return BlocProvider<PersonalInfoBloc>(
|
return BlocProvider<PersonalInfoBloc>(
|
||||||
create: (context) => Modular.get<PersonalInfoBloc>()
|
create: (BuildContext context) => Modular.get<PersonalInfoBloc>()
|
||||||
..add(const PersonalInfoLoadRequested()),
|
..add(const PersonalInfoLoadRequested()),
|
||||||
child: const _PersonalInfoPageContent(),
|
child: BlocListener<PersonalInfoBloc, PersonalInfoState>(
|
||||||
);
|
listener: (BuildContext context, PersonalInfoState state) {
|
||||||
}
|
if (state.status == PersonalInfoStatus.saved) {
|
||||||
}
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
/// Internal content widget that reacts to BLoC state changes.
|
content: Text(i18n.save_success),
|
||||||
class _PersonalInfoPageContent extends StatelessWidget {
|
duration: const Duration(seconds: 2),
|
||||||
const _PersonalInfoPageContent();
|
),
|
||||||
|
);
|
||||||
@override
|
Modular.to.pop();
|
||||||
Widget build(BuildContext context) {
|
} else if (state.status == PersonalInfoStatus.error) {
|
||||||
final i18n = t.staff.onboarding.personal_info;
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
return BlocListener<PersonalInfoBloc, PersonalInfoState>(
|
content: Text(state.errorMessage ?? 'An error occurred'),
|
||||||
listener: (context, state) {
|
backgroundColor: UiColors.destructive,
|
||||||
if (state.status == PersonalInfoStatus.saved) {
|
duration: const Duration(seconds: 3),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
),
|
||||||
SnackBar(
|
);
|
||||||
content: Text(i18n.save_success),
|
}
|
||||||
duration: const Duration(seconds: 2),
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: UiColors.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: UiColors.bgPopup,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
||||||
|
onPressed: () => Modular.to.pop(),
|
||||||
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||||
),
|
),
|
||||||
);
|
title: Text(
|
||||||
Modular.to.pop();
|
i18n.title,
|
||||||
} else if (state.status == PersonalInfoStatus.error) {
|
style: UiTypography.title1m.copyWith(color: UiColors.textPrimary),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(state.errorMessage ?? 'An error occurred'),
|
|
||||||
backgroundColor: UiColors.destructive,
|
|
||||||
duration: const Duration(seconds: 3),
|
|
||||||
),
|
),
|
||||||
);
|
bottom: PreferredSize(
|
||||||
}
|
preferredSize: const Size.fromHeight(1.0),
|
||||||
},
|
child: Container(
|
||||||
child: Scaffold(
|
color: UiColors.border,
|
||||||
backgroundColor: UiColors.background,
|
height: 1.0,
|
||||||
appBar: AppBar(
|
),
|
||||||
backgroundColor: UiColors.bgPopup,
|
|
||||||
elevation: 0,
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
|
||||||
onPressed: () => Modular.to.pop(),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
i18n.title,
|
|
||||||
style: UiTypography.title1m.copyWith(color: UiColors.textPrimary),
|
|
||||||
),
|
|
||||||
bottom: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(1.0),
|
|
||||||
child: Container(
|
|
||||||
color: UiColors.border,
|
|
||||||
height: 1.0,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
body: SafeArea(
|
||||||
body: BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
child: BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
||||||
builder: (context, state) {
|
builder: (BuildContext context, PersonalInfoState state) {
|
||||||
if (state.status == PersonalInfoStatus.loading ||
|
if (state.status == PersonalInfoStatus.loading ||
|
||||||
state.status == PersonalInfoStatus.initial) {
|
state.status == PersonalInfoStatus.initial) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.staff == null) {
|
if (state.staff == null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Failed to load personal information',
|
'Failed to load personal information',
|
||||||
style: UiTypography.body1r.copyWith(
|
style: UiTypography.body1r.copyWith(
|
||||||
color: UiColors.textSecondary,
|
color: UiColors.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PersonalInfoContent(staff: state.staff!);
|
return PersonalInfoContent(staff: state.staff!);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import 'profile_photo_widget.dart';
|
|||||||
import 'personal_info_form.dart';
|
import 'personal_info_form.dart';
|
||||||
import 'save_button.dart';
|
import 'save_button.dart';
|
||||||
|
|
||||||
|
|
||||||
/// Content widget that displays and manages the staff profile form.
|
/// Content widget that displays and manages the staff profile form.
|
||||||
///
|
///
|
||||||
/// This widget is extracted from the page to handle form state separately,
|
/// This widget is extracted from the page to handle form state separately,
|
||||||
/// following Clean Architecture's separation of concerns principle.
|
/// following Clean Architecture's separation of concerns principle and the design system guidelines.
|
||||||
/// Works with the shared [Staff] entity from the domain layer.
|
/// Works with the shared [Staff] entity from the domain layer.
|
||||||
class PersonalInfoContent extends StatefulWidget {
|
class PersonalInfoContent extends StatefulWidget {
|
||||||
/// The staff profile to display and edit.
|
/// The staff profile to display and edit.
|
||||||
@@ -52,22 +53,23 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _onPhoneChanged() {
|
void _onPhoneChanged() {
|
||||||
context.read<PersonalInfoBloc>().add(
|
context.read<PersonalInfoBloc>().add(
|
||||||
PersonalInfoFieldUpdated(
|
PersonalInfoFieldUpdated(
|
||||||
field: 'phone',
|
field: 'phone',
|
||||||
value: _phoneController.text,
|
value: _phoneController.text,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAddressChanged() {
|
void _onAddressChanged() {
|
||||||
context.read<PersonalInfoBloc>().add(
|
context.read<PersonalInfoBloc>().add(
|
||||||
PersonalInfoFieldUpdated(
|
PersonalInfoFieldUpdated(
|
||||||
field: 'address',
|
field: 'address',
|
||||||
value: _addressController.text,
|
value: _addressController.text,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSave() {
|
void _handleSave() {
|
||||||
@@ -83,23 +85,24 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||||
return BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
return BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
||||||
builder: (context, state) {
|
builder: (BuildContext context, PersonalInfoState state) {
|
||||||
final isSaving = state.status == PersonalInfoStatus.saving;
|
final bool isSaving = state.status == PersonalInfoStatus.saving;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space6),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
ProfilePhotoWidget(
|
ProfilePhotoWidget(
|
||||||
photoUrl: widget.staff.avatar,
|
photoUrl: widget.staff.avatar,
|
||||||
fullName: widget.staff.name,
|
fullName: widget.staff.name,
|
||||||
onTap: isSaving ? null : _handlePhotoTap,
|
onTap: isSaving ? null : _handlePhotoTap,
|
||||||
),
|
),
|
||||||
SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
PersonalInfoForm(
|
PersonalInfoForm(
|
||||||
fullName: widget.staff.name,
|
fullName: widget.staff.name,
|
||||||
email: widget.staff.email,
|
email: widget.staff.email,
|
||||||
@@ -107,16 +110,14 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
|
|||||||
addressController: _addressController,
|
addressController: _addressController,
|
||||||
enabled: !isSaving,
|
enabled: !isSaving,
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(height: UiConstants.space16), // Space for bottom button
|
||||||
height: UiConstants.space16,
|
|
||||||
), // Space for bottom button
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SaveButton(
|
SaveButton(
|
||||||
onPressed: isSaving ? null : _handleSave,
|
onPressed: isSaving ? null : _handleSave,
|
||||||
label: t.staff.onboarding.personal_info.save_button,
|
label: i18n.save_button,
|
||||||
isLoading: isSaving,
|
isLoading: isSaving,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
|
||||||
/// A form widget containing all personal information fields.
|
/// A form widget containing all personal information fields.
|
||||||
///
|
///
|
||||||
/// Includes read-only fields for full name and email,
|
/// Includes read-only fields for full name and email,
|
||||||
/// and editable fields for phone and address.
|
/// and editable fields for phone and address.
|
||||||
|
/// Uses only design system tokens for colors, typography, and spacing.
|
||||||
class PersonalInfoForm extends StatelessWidget {
|
class PersonalInfoForm extends StatelessWidget {
|
||||||
/// The staff member's full name (read-only).
|
/// The staff member's full name (read-only).
|
||||||
final String fullName;
|
final String fullName;
|
||||||
@@ -34,28 +36,32 @@ class PersonalInfoForm extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final i18n = t.staff.onboarding.personal_info;
|
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: <Widget>[
|
||||||
_FieldLabel(text: i18n.full_name_label),
|
_FieldLabel(text: i18n.full_name_label),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
_ReadOnlyField(value: fullName),
|
_ReadOnlyField(value: fullName),
|
||||||
SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
_FieldLabel(text: i18n.email_label),
|
_FieldLabel(text: i18n.email_label),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
_ReadOnlyField(value: email),
|
_ReadOnlyField(value: email),
|
||||||
SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
_FieldLabel(text: i18n.phone_label),
|
_FieldLabel(text: i18n.phone_label),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
_EditableField(
|
_EditableField(
|
||||||
controller: phoneController,
|
controller: phoneController,
|
||||||
hint: i18n.phone_hint,
|
hint: i18n.phone_hint,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
),
|
),
|
||||||
SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
_FieldLabel(text: i18n.locations_label),
|
_FieldLabel(text: i18n.locations_label),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
_EditableField(
|
_EditableField(
|
||||||
controller: addressController,
|
controller: addressController,
|
||||||
hint: i18n.locations_hint,
|
hint: i18n.locations_hint,
|
||||||
@@ -66,6 +72,7 @@ class PersonalInfoForm extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A label widget for form fields.
|
||||||
/// A label widget for form fields.
|
/// A label widget for form fields.
|
||||||
class _FieldLabel extends StatelessWidget {
|
class _FieldLabel extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
@@ -74,16 +81,14 @@ class _FieldLabel extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Text(
|
||||||
padding: EdgeInsets.only(bottom: UiConstants.space2),
|
text,
|
||||||
child: Text(
|
style: UiTypography.body2m.copyWith(color: UiColors.textPrimary),
|
||||||
text,
|
|
||||||
style: UiTypography.body2m.copyWith(color: UiColors.textPrimary),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A read-only field widget for displaying non-editable information.
|
||||||
/// A read-only field widget for displaying non-editable information.
|
/// A read-only field widget for displaying non-editable information.
|
||||||
class _ReadOnlyField extends StatelessWidget {
|
class _ReadOnlyField extends StatelessWidget {
|
||||||
final String value;
|
final String value;
|
||||||
@@ -94,7 +99,7 @@ class _ReadOnlyField extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: UiConstants.space3,
|
horizontal: UiConstants.space3,
|
||||||
vertical: UiConstants.space3,
|
vertical: UiConstants.space3,
|
||||||
),
|
),
|
||||||
@@ -111,17 +116,16 @@ class _ReadOnlyField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An editable text field widget.
|
||||||
/// An editable text field widget.
|
/// An editable text field widget.
|
||||||
class _EditableField extends StatelessWidget {
|
class _EditableField extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String hint;
|
final String hint;
|
||||||
final int maxLines;
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
|
||||||
const _EditableField({
|
const _EditableField({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.hint,
|
required this.hint,
|
||||||
this.maxLines = 1,
|
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,27 +133,26 @@ class _EditableField extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: maxLines,
|
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
hintStyle: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: UiConstants.space3,
|
horizontal: UiConstants.space3,
|
||||||
vertical: UiConstants.space3,
|
vertical: UiConstants.space3,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||||
borderSide: BorderSide(color: UiColors.border),
|
borderSide: const BorderSide(color: UiColors.border),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||||
borderSide: BorderSide(color: UiColors.border),
|
borderSide: const BorderSide(color: UiColors.border),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||||
borderSide: BorderSide(color: UiColors.primary),
|
borderSide: const BorderSide(color: UiColors.primary),
|
||||||
),
|
),
|
||||||
fillColor: UiColors.bgPopup,
|
fillColor: UiColors.bgPopup,
|
||||||
filled: true,
|
filled: true,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
|
||||||
/// A widget displaying the staff member's profile photo with an edit option.
|
/// A widget displaying the staff member's profile photo with an edit option.
|
||||||
///
|
///
|
||||||
/// Shows either the photo URL or an initial avatar if no photo is available.
|
/// Shows either the photo URL or an initial avatar if no photo is available.
|
||||||
/// Includes a camera icon button for changing the photo.
|
/// Includes a camera icon button for changing the photo.
|
||||||
|
/// Uses only design system tokens for colors, typography, and spacing.
|
||||||
class ProfilePhotoWidget extends StatelessWidget {
|
class ProfilePhotoWidget extends StatelessWidget {
|
||||||
/// The URL of the staff member's photo.
|
/// The URL of the staff member's photo.
|
||||||
final String? photoUrl;
|
final String? photoUrl;
|
||||||
@@ -26,14 +28,14 @@ class ProfilePhotoWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final i18n = t.staff.onboarding.personal_info;
|
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
width: 96,
|
width: 96,
|
||||||
height: 96,
|
height: 96,
|
||||||
@@ -67,7 +69,7 @@ class ProfilePhotoWidget extends StatelessWidget {
|
|||||||
color: UiColors.bgPopup,
|
color: UiColors.bgPopup,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
boxShadow: [
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.textPrimary.withOpacity(0.1),
|
color: UiColors.textPrimary.withOpacity(0.1),
|
||||||
blurRadius: UiConstants.space1,
|
blurRadius: UiConstants.space1,
|
||||||
@@ -75,7 +77,7 @@ class ProfilePhotoWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Center(
|
child: const Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
UiIcons.camera,
|
UiIcons.camera,
|
||||||
size: 16,
|
size: 16,
|
||||||
@@ -87,7 +89,7 @@ class ProfilePhotoWidget extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
Text(
|
Text(
|
||||||
i18n.change_photo_hint,
|
i18n.change_photo_hint,
|
||||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
|
||||||
/// A save button widget for the bottom of the personal info page.
|
/// A save button widget for the bottom of the personal info page.
|
||||||
///
|
///
|
||||||
/// Displays a full-width button with a save icon and customizable label.
|
/// Displays a full-width button with a save icon and customizable label.
|
||||||
|
/// Uses only design system tokens for colors, typography, and spacing.
|
||||||
class SaveButton extends StatelessWidget {
|
class SaveButton extends StatelessWidget {
|
||||||
/// Callback when the button is pressed.
|
/// Callback when the button is pressed.
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
@@ -25,8 +27,8 @@ class SaveButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: UiColors.bgPopup,
|
color: UiColors.bgPopup,
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(color: UiColors.border),
|
top: BorderSide(color: UiColors.border),
|
||||||
@@ -46,7 +48,7 @@ class SaveButton extends StatelessWidget {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? SizedBox(
|
? const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
@@ -58,9 +60,9 @@ class SaveButton extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: Row(
|
: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Icon(UiIcons.check, color: UiColors.bgPopup, size: 20),
|
const Icon(UiIcons.check, color: UiColors.bgPopup, size: 20),
|
||||||
SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: UiTypography.body1m.copyWith(
|
style: UiTypography.body1m.copyWith(
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ class StaffProfileInfoModule extends Module {
|
|||||||
'/personal-info',
|
'/personal-info',
|
||||||
child: (BuildContext context) => const PersonalInfoPage(),
|
child: (BuildContext context) => const PersonalInfoPage(),
|
||||||
);
|
);
|
||||||
|
// Alias with trailing slash to be tolerant of external deep links
|
||||||
|
r.child(
|
||||||
|
'/personal-info/',
|
||||||
|
child: (BuildContext context) => const PersonalInfoPage(),
|
||||||
|
);
|
||||||
// Additional routes will be added as more onboarding pages are implemented
|
// Additional routes will be added as more onboarding pages are implemented
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user