Skip to content

Commit 6555ec5

Browse files
committed
feat(UserProfile): build screen UserProfile
# Conflicts: # src/lib/features/main/view/screens/main_screen.dart
1 parent bb72c59 commit 6555ec5

13 files changed

Lines changed: 726 additions & 2 deletions

File tree

src/lib/core/theme/app_theme.dart

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'package:flutter/material.dart';
2+
3+
class AppTheme {
4+
// =========================================================
5+
// ☀️ LIGHT THEME DICTIONARY
6+
// Inherited from the legacy AppColors class
7+
// =========================================================
8+
static final ThemeData lightTheme = ThemeData(
9+
brightness: Brightness.light,
10+
scaffoldBackgroundColor: const Color(0xFFF4F6F9), // Legacy: AppColors.background
11+
12+
colorScheme: const ColorScheme.light(
13+
// Brand Colors
14+
primary: Color(0xFF5A8DF3), // Legacy: AppColors.primary
15+
secondary: Color(0xFF4A90E2), // Legacy: AppColors.primaryBlue
16+
17+
// Background & Surface Colors
18+
// Note: 'background' is deprecated, scaffoldBackgroundColor handles the main background.
19+
surface: Colors.white, // Legacy: AppColors.taskCardBg / white
20+
surfaceContainerHighest: Color(0xFFE0F7FA), // Legacy: AppColors.backgroundBlue (Tinted background)
21+
22+
// Text Colors
23+
onSurface: Color(0xFF2D3440), // Legacy: AppColors.textDark
24+
onSurfaceVariant: Color(0xFF757575), // Legacy: AppColors.grayText / textSecondary
25+
26+
// Border & Status Colors
27+
outline: Color(0xFFE2E8F0), // Legacy: AppColors.border
28+
error: Colors.redAccent, // Legacy: AppColors.error
29+
tertiary: Colors.green, // Using Tertiary for Success (Green)
30+
),
31+
32+
appBarTheme: const AppBarTheme(
33+
backgroundColor: Colors.transparent,
34+
elevation: 0,
35+
iconTheme: IconThemeData(color: Color(0xFF5A8DF3)),
36+
titleTextStyle: TextStyle(
37+
color: Color(0xFF5A8DF3),
38+
fontWeight: FontWeight.w800,
39+
fontSize: 20,
40+
),
41+
),
42+
43+
dividerTheme: const DividerThemeData(color: Color(0xFFE2E8F0)),
44+
);
45+
46+
// =========================================================
47+
// 🌙 DARK THEME DICTIONARY
48+
// Extracted from the provided Stitch Design System
49+
// =========================================================
50+
static final ThemeData darkTheme = ThemeData(
51+
brightness: Brightness.dark,
52+
scaffoldBackgroundColor: const Color(0xFF0F172A), // Neutral Slate Dark
53+
54+
colorScheme: const ColorScheme.dark(
55+
// Brand Colors (Slightly brighter to stand out on dark background)
56+
primary: Color(0xFF60A5FA), // Bright Blue
57+
secondary: Color(0xFF61789A), // Slate Blue
58+
59+
// Background & Surface Colors
60+
// Note: 'background' is deprecated, scaffoldBackgroundColor handles the main background.
61+
surface: Color(0xFF1E293B), // Slightly lighter than background, used for Cards
62+
surfaceContainerHighest: Color(0xFF162032), // Dark mode counterpart for backgroundBlue
63+
64+
// Text Colors
65+
onSurface: Colors.white, // Primary text (White)
66+
onSurfaceVariant: Color(0xFF94A3B8), // Secondary text (Light Slate)
67+
68+
// Border, Status & Highlight Colors
69+
outline: Color(0xFF334155), // Faint Card Border
70+
error: Color(0xFFF87171), // Pinkish Red (Similar to Trash Icon)
71+
tertiary: Color(0xFFD19900), // Mustard Yellow (Similar to Edit Pencil Icon)
72+
),
73+
74+
appBarTheme: const AppBarTheme(
75+
backgroundColor: Colors.transparent,
76+
elevation: 0,
77+
iconTheme: IconThemeData(color: Color(0xFF60A5FA)),
78+
titleTextStyle: TextStyle(
79+
color: Color(0xFF60A5FA),
80+
fontWeight: FontWeight.w800,
81+
fontSize: 20,
82+
),
83+
),
84+
85+
dividerTheme: const DividerThemeData(color: Color(0xFF334155)),
86+
);
87+
}

src/lib/features/main/view/screens/main_screen.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import 'package:flutter/material.dart';
22
import 'package:task_management_app/features/statistics/viewmodel/statistics_viewmodel.dart';
33
import 'package:task_management_app/features/tasks/view/screens/home_screen.dart';
44
import 'settings_screen.dart';
5+
import 'package:task_management_app/features/user/viewmodel/user_profile_viewmodel.dart';
56
import '../../../../core/theme/app_colors.dart';
67
import '../../../note/view/focus_screen.dart';
78
import '../../../note/viewmodel/focus_viewmodel.dart';
89
import '../../../statistics/view/screens/statistics_screen.dart';
910
// import '../../../tasks/view/screens/home_screen.dart';
1011
import 'package:provider/provider.dart';
1112

13+
import '../../../user/view/user_profile_view.dart';
14+
1215
class MainScreen extends StatefulWidget {
1316
const MainScreen({super.key});
1417

@@ -30,6 +33,10 @@ class _MainScreenState extends State<MainScreen> {
3033
create: (_) => StatisticsViewmodel(),
3134
child: const StatisticsScreen(),
3235
),
36+
ChangeNotifierProvider(
37+
create: (_) => UserProfileViewModel()..loadProfile(),
38+
child: const UserProfileView(),
39+
),
3340
const SettingsScreen(),
3441
];
3542

@@ -56,7 +63,7 @@ class _MainScreenState extends State<MainScreen> {
5663
_buildNavItem(Icons.calendar_today_rounded, 'Lịch', 1),
5764
_buildNavItem(Icons.timer_rounded, 'Tập trung', 2),
5865
_buildNavItem(Icons.bar_chart_rounded, 'Thống kê', 3),
59-
_buildNavItem(Icons.settings_rounded, 'Cài đặt', 4),
66+
_buildNavItem(Icons.person_2_rounded, 'Cá nhân', 4),
6067
],
6168
),
6269
),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
class UserProfileModel {
2+
final String id;
3+
final String name;
4+
final int tasksDone;
5+
final int streaks;
6+
final String avatarUrl;
7+
bool isNotificationEnabled;
8+
String appearance;
9+
10+
UserProfileModel({
11+
required this.id,
12+
required this.name,
13+
required this.tasksDone,
14+
required this.streaks,
15+
required this.avatarUrl,
16+
this.isNotificationEnabled = true,
17+
this.appearance = 'Light',
18+
});
19+
20+
factory UserProfileModel.fromJson(Map<String, dynamic> json) {
21+
return UserProfileModel(
22+
// Dùng as String? để ép kiểu an toàn, kèm ?? để gán giá trị mặc định nếu bị null
23+
id: json['id'] as String? ?? '',
24+
name: json['name'] as String? ?? 'Unknown User',
25+
tasksDone: json['tasksDone'] as int? ?? 0,
26+
streaks: json['streaks'] as int? ?? 0,
27+
avatarUrl: json['avatarUrl'] as String? ?? '',
28+
isNotificationEnabled: json['isNotificationEnabled'] as bool? ?? true,
29+
appearance: json['appearance'] as String? ?? 'Light',
30+
);
31+
}
32+
33+
34+
Map<String, dynamic> toJson() {
35+
return {
36+
'id': id,
37+
'name': name,
38+
'tasksDone': tasksDone,
39+
'streaks': streaks,
40+
'avatarUrl': avatarUrl,
41+
'isNotificationEnabled': isNotificationEnabled,
42+
'appearance': appearance,
43+
};
44+
}
45+
46+
UserProfileModel copyWith({
47+
String? id,
48+
String? name,
49+
int? tasksDone,
50+
int? streaks,
51+
String? avatarUrl,
52+
bool? isNotificationEnabled,
53+
String? appearance,
54+
}) {
55+
return UserProfileModel(
56+
id: id ?? this.id,
57+
name: name ?? this.name,
58+
tasksDone: tasksDone ?? this.tasksDone,
59+
streaks: streaks ?? this.streaks,
60+
avatarUrl: avatarUrl ?? this.avatarUrl,
61+
isNotificationEnabled: isNotificationEnabled ?? this.isNotificationEnabled,
62+
appearance: appearance ?? this.appearance,
63+
);
64+
}
65+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:supabase_flutter/supabase_flutter.dart';
2+
3+
import '../model/user_profile_model.dart';
4+
5+
class UserService {
6+
final SupabaseClient _supabase = Supabase.instance.client;
7+
/// Simulates fetching user profile data with a fake network delay
8+
Future<UserProfileModel> fetchUserProfile() async {
9+
// Mimic API call delay for smooth state switching
10+
try{
11+
final response = await _supabase.rpc('get_user_profile_stats');
12+
response['id'] = _supabase.auth.currentUser!.id;
13+
return UserProfileModel.fromJson(response);
14+
}
15+
catch(e){
16+
throw Exception("Failed to fetch user profile: $e");
17+
}
18+
}
19+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:provider/provider.dart';
3+
import '../../../core/theme/app_colors.dart';
4+
5+
import '../viewmodel/user_profile_viewmodel.dart';
6+
import 'widgets/profile_header.dart';
7+
import 'widgets/stat_card.dart';
8+
import 'widgets/settings_section.dart';
9+
import 'widgets/settings_list_tile.dart';
10+
import 'widgets/logout_button.dart';
11+
12+
class UserProfileView extends StatelessWidget {
13+
const UserProfileView({super.key});
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return Scaffold(
18+
backgroundColor: AppColors.background,
19+
appBar: AppBar(
20+
backgroundColor: AppColors.background,
21+
elevation: 0,
22+
centerTitle: true,
23+
scrolledUnderElevation: 0,
24+
title: const Text(
25+
'Profile',
26+
style: TextStyle(
27+
color: AppColors.primaryBlue,
28+
fontWeight: FontWeight.w800,
29+
fontSize: 20,
30+
),
31+
),
32+
actions: [
33+
IconButton(
34+
icon: const Icon(Icons.settings, color: AppColors.primaryBlue),
35+
onPressed: () {},
36+
splashRadius: 24,
37+
),
38+
],
39+
),
40+
body: Consumer<UserProfileViewModel>(
41+
builder: (context, vm, child) {
42+
return AnimatedSwitcher(
43+
duration: const Duration(milliseconds: 300),
44+
child: vm.isLoading
45+
? const Center(
46+
key: ValueKey('loading'),
47+
child: CircularProgressIndicator(color: AppColors.primaryBlue),
48+
)
49+
: vm.user == null
50+
? const Center(
51+
key: ValueKey('error'),
52+
child: Text("Error loading profile"),
53+
)
54+
: _buildProfileContent(context, vm),
55+
);
56+
},
57+
),
58+
);
59+
}
60+
61+
Widget _buildProfileContent(BuildContext context, UserProfileViewModel vm) {
62+
final user = vm.user!;
63+
return SingleChildScrollView(
64+
key: const ValueKey('content'),
65+
physics: const BouncingScrollPhysics(),
66+
padding: const EdgeInsets.fromLTRB(24, 8, 24, 40),
67+
child: Column(
68+
children: [
69+
// 1. Header (Avatar & Name)
70+
ProfileHeader(user: user),
71+
const SizedBox(height: 32),
72+
73+
// 2. Stats Row
74+
Row(
75+
children: [
76+
Expanded(
77+
child: StatCard(
78+
value: user.tasksDone.toString(),
79+
label: 'Tasks Done',
80+
onTap: () {},
81+
),
82+
),
83+
const SizedBox(width: 16),
84+
Expanded(
85+
child: StatCard(
86+
value: user.streaks.toString(),
87+
label: 'Streaks',
88+
onTap: () {},
89+
),
90+
),
91+
],
92+
),
93+
const SizedBox(height: 32),
94+
95+
SettingsSection(
96+
title: 'Preferences',
97+
children: [
98+
SettingsListTile(
99+
icon: Icons.notifications,
100+
title: 'Notifications',
101+
iconBgColor: AppColors.border,
102+
iconColor: AppColors.grayText,
103+
onTap: () => vm.toggleNotification(!user.isNotificationEnabled),
104+
trailing: Switch(
105+
value: user.isNotificationEnabled,
106+
activeColor: AppColors.white,
107+
activeTrackColor: AppColors.primaryBlue,
108+
inactiveThumbColor: AppColors.grayText,
109+
inactiveTrackColor: AppColors.border,
110+
onChanged: (val) => vm.toggleNotification(val),
111+
),
112+
),
113+
const Divider(height: 1, indent: 64, endIndent: 24, color: AppColors.border),
114+
SettingsListTile(
115+
icon: Icons.dark_mode,
116+
title: 'Appearance',
117+
iconBgColor: AppColors.border,
118+
iconColor: AppColors.grayText,
119+
trailing: Text(
120+
user.appearance,
121+
style: const TextStyle(
122+
fontSize: 14,
123+
fontWeight: FontWeight.w600,
124+
color: AppColors.grayText,
125+
),
126+
),
127+
onTap: () {},
128+
),
129+
],
130+
),
131+
const SizedBox(height: 40),
132+
133+
// 4. Logout Button
134+
LogoutButton(onPressed: () => vm.logout(context)),
135+
],
136+
),
137+
);
138+
}
139+
}

0 commit comments

Comments
 (0)