Skip to content

Commit 50a79f6

Browse files
authored
feat: profile (fireship-io#9)
* feat(user_repository): add user profile fields * refactor: reuse quiz button as `ActionButton` * feat: add profile cubit * feat: extend profile view with user info
1 parent dfe995a commit 50a79f6

File tree

15 files changed

+348
-64
lines changed

15 files changed

+348
-64
lines changed

lib/l10n/arb/app_en.arb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,17 @@
112112
"getQuizFailureMessage": "There's been an error getting quiz data 🙄",
113113
"@getQuizFailureMessage": {
114114
"description": "`GetQuizFailure` message"
115+
},
116+
"guestProfileDisplayName": "Guest",
117+
"@guestProfileDisplayName": {
118+
"description": "Display name on guest profiles"
119+
},
120+
"totalCompletedQuizzesLabel": "Quizzes Completed",
121+
"@totalCompletedQuizzesLabel": {
122+
"description": "Total completed quizzes label"
123+
},
124+
"logOutButtonLabel": "Logout",
125+
"@logOutButtonLabel": {
126+
"description": "`LogOutButton`'s label on profile view"
115127
}
116128
}

lib/profile/cubit/profile_cubit.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'dart:async';
2+
3+
import 'package:bloc/bloc.dart';
4+
import 'package:equatable/equatable.dart';
5+
import 'package:user_repository/user_repository.dart';
6+
import 'package:shared/shared.dart';
7+
8+
part 'profile_state.dart';
9+
10+
class ProfileCubit extends Cubit<ProfileState> {
11+
ProfileCubit({required UserRepository userRepository})
12+
: _userRepository = userRepository,
13+
super(const ProfileState.initial()) {
14+
_watchUser();
15+
}
16+
17+
final UserRepository _userRepository;
18+
19+
@override
20+
Future<void> close() async {
21+
await _unwatchUser();
22+
return super.close();
23+
}
24+
25+
void logOut() {
26+
emit(state.copyWith(action: ProfileAction.logOut));
27+
}
28+
29+
void _onUserChanged(User user) {
30+
emit(state.copyWith(user: user));
31+
}
32+
33+
late final StreamSubscription _userSubscription;
34+
void _watchUser() {
35+
_userSubscription = _userRepository.watchUser
36+
// user/auth failures are handled by the AppCubit
37+
.handleFailure(null)
38+
.listen(_onUserChanged);
39+
}
40+
41+
Future<void> _unwatchUser() {
42+
return _userSubscription.cancel();
43+
}
44+
}

lib/profile/cubit/profile_state.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
part of 'profile_cubit.dart';
2+
3+
enum ProfileAction { none, logOut }
4+
5+
extension ProfileActionExtensions on ProfileAction {
6+
bool get isLogOut => this == ProfileAction.logOut;
7+
}
8+
9+
class ProfileState extends Equatable {
10+
const ProfileState._({
11+
this.user = User.none,
12+
this.action = ProfileAction.none,
13+
});
14+
15+
const ProfileState.initial() : this._();
16+
17+
final User user;
18+
final ProfileAction action;
19+
20+
@override
21+
List<Object> get props => [user, action];
22+
23+
ProfileState copyWith({
24+
User? user,
25+
ProfileAction action = ProfileAction.none,
26+
}) {
27+
return ProfileState._(
28+
user: user ?? this.user,
29+
action: action,
30+
);
31+
}
32+
}

lib/profile/view/profile_view.dart

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,165 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
4+
import 'package:quizapp/app/cubit/app_cubit.dart';
5+
import 'package:quizapp/profile/cubit/profile_cubit.dart';
6+
import 'package:quizapp/shared/shared.dart';
7+
import 'package:quizapp/l10n/l10n.dart';
8+
import 'package:transparent_image/transparent_image.dart';
9+
import 'package:user_repository/user_repository.dart';
210

3-
class ProfileView extends StatelessWidget {
11+
class ProfileView extends StatefulWidget {
412
const ProfileView({Key? key}) : super(key: key);
513

14+
@override
15+
_ProfileViewState createState() => _ProfileViewState();
16+
}
17+
18+
class _ProfileViewState extends State<ProfileView>
19+
with AutomaticKeepAliveClientMixin {
20+
@override
21+
Widget build(BuildContext context) {
22+
super.build(context);
23+
return BlocProvider(
24+
create: (_) => ProfileCubit(
25+
userRepository: context.read<UserRepository>(),
26+
),
27+
child: const Profile(),
28+
);
29+
}
30+
31+
@override
32+
bool get wantKeepAlive => true;
33+
}
34+
35+
class Profile extends StatelessWidget {
36+
const Profile({Key? key}) : super(key: key);
37+
38+
@override
39+
Widget build(BuildContext context) {
40+
return BlocListener<ProfileCubit, ProfileState>(
41+
listenWhen: (_, current) => current.action.isLogOut,
42+
listener: (_, __) => context.read<AppCubit>().logOut(),
43+
child: Scaffold(
44+
appBar: AppBar(
45+
backgroundColor: kDeepOrange,
46+
title: const DisplayName(),
47+
),
48+
body: Center(
49+
child: Column(
50+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
51+
crossAxisAlignment: CrossAxisAlignment.center,
52+
children: const [
53+
SizedBox(height: 50),
54+
ProfilePhoto(),
55+
EmailAddress(),
56+
Spacer(),
57+
TotalCompletedQuizzes(),
58+
Spacer(),
59+
LogOutButton(),
60+
Spacer(),
61+
],
62+
),
63+
),
64+
),
65+
);
66+
}
67+
}
68+
69+
class DisplayName extends StatelessWidget {
70+
const DisplayName({Key? key}) : super(key: key);
71+
72+
@override
73+
Widget build(BuildContext context) {
74+
final displayName =
75+
context.select((ProfileCubit cubit) => cubit.state.user.displayName);
76+
return Text(displayName.isNotEmpty
77+
? displayName
78+
: context.l10n.guestProfileDisplayName);
79+
}
80+
}
81+
82+
const _kProfilePhotoSize = 100.0;
83+
84+
class ProfilePhoto extends StatelessWidget {
85+
const ProfilePhoto({Key? key}) : super(key: key);
86+
87+
@override
88+
Widget build(BuildContext context) {
89+
final photoURL =
90+
context.select((ProfileCubit cubit) => cubit.state.user.photoURL);
91+
return photoURL.isNotEmpty
92+
? Stack(
93+
alignment: AlignmentDirectional.center,
94+
children: [
95+
const Loader(),
96+
ClipOval(
97+
child: FadeInImage.memoryNetwork(
98+
image: photoURL,
99+
placeholder: kTransparentImage,
100+
width: _kProfilePhotoSize,
101+
height: _kProfilePhotoSize,
102+
fit: BoxFit.cover,
103+
imageErrorBuilder: (_, __, ___) {
104+
return Container(
105+
color: context.theme.canvasColor,
106+
child: const Icon(
107+
FontAwesomeIcons.userCircle,
108+
size: _kProfilePhotoSize,
109+
),
110+
);
111+
},
112+
),
113+
),
114+
],
115+
)
116+
: const Empty();
117+
}
118+
}
119+
120+
class EmailAddress extends StatelessWidget {
121+
const EmailAddress({Key? key}) : super(key: key);
122+
123+
@override
124+
Widget build(BuildContext context) {
125+
final email =
126+
context.select((ProfileCubit cubit) => cubit.state.user.email);
127+
return Text(email, style: context.textTheme.headline5);
128+
}
129+
}
130+
131+
class TotalCompletedQuizzes extends StatelessWidget {
132+
const TotalCompletedQuizzes({Key? key}) : super(key: key);
133+
134+
@override
135+
Widget build(BuildContext context) {
136+
final totalCompletedQuizzes = context
137+
.select((ProfileCubit cubit) => cubit.state.user.totalCompletedQuizzes);
138+
return Column(
139+
mainAxisSize: MainAxisSize.min,
140+
children: [
141+
Text(
142+
'$totalCompletedQuizzes',
143+
style: context.textTheme.headline2,
144+
),
145+
Text(
146+
context.l10n.totalCompletedQuizzesLabel,
147+
style: context.textTheme.subtitle1,
148+
),
149+
],
150+
);
151+
}
152+
}
153+
154+
class LogOutButton extends StatelessWidget {
155+
const LogOutButton({Key? key}) : super(key: key);
156+
6157
@override
7158
Widget build(BuildContext context) {
8-
return const Center(
9-
child: Text('Profile view...'),
159+
return ActionButton(
160+
onPressed: context.read<ProfileCubit>().logOut,
161+
backgroundColor: kRed,
162+
label: Text(context.l10n.logOutButtonLabel),
10163
);
11164
}
12165
}

lib/quiz/view/complete_view.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
33
import 'package:quizapp/quiz/cubit/quiz_cubit.dart';
4-
import 'package:quizapp/quiz/view/widgets.dart';
54
import 'package:provider/provider.dart';
65
import 'package:quizapp/shared/shared.dart';
76
import 'package:quizapp/l10n/l10n.dart';
@@ -56,7 +55,7 @@ class CompleteQuizButton extends StatelessWidget {
5655

5756
@override
5857
Widget build(BuildContext context) {
59-
return QuizButton(
58+
return ActionButton.icon(
6059
label: Text(context.l10n.markQuizCompletedButtonLabel),
6160
icon: const Icon(FontAwesomeIcons.check),
6261
backgroundColor: Colors.green,

lib/quiz/view/question_view.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
44
import 'package:provider/provider.dart';
55
import 'package:quizapp/quiz/cubit/quiz_cubit.dart';
6-
import 'package:quizapp/quiz/view/widgets.dart';
76
import 'package:quizapp/shared/shared.dart';
87
import 'package:quizzes_repository/quizzes_repository.dart';
98
import 'package:quizapp/l10n/l10n.dart';
@@ -150,7 +149,7 @@ class QuizAnswerDetails extends StatelessWidget {
150149
color: Colors.white54,
151150
),
152151
),
153-
QuizButton(
152+
ActionButton(
154153
onPressed: () {
155154
context.read<QuizCubit>().validateAnswer();
156155
},

lib/quiz/view/start_view.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:quizapp/quiz/cubit/quiz_cubit.dart';
4-
import 'package:quizapp/quiz/view/widgets.dart';
54
import 'package:quizapp/shared/shared.dart';
65
import 'package:quizapp/l10n/l10n.dart';
76

@@ -66,7 +65,7 @@ class QuizStartButton extends StatelessWidget {
6665

6766
@override
6867
Widget build(BuildContext context) {
69-
return QuizButton(
68+
return ActionButton.icon(
7069
onPressed: context.read<QuizCubit>().incrementStep,
7170
icon: const Icon(Icons.poll),
7271
backgroundColor: kGreen,

lib/quiz/view/widgets.dart

Lines changed: 0 additions & 43 deletions
This file was deleted.

lib/shared/styles.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const kPink = Color(0xFFF48FB1);
99
const kRed = Colors.red;
1010
const kGreen = Colors.green;
1111
const kBlue = Colors.blue;
12+
const kDeepOrange = Colors.deepOrange;
1213

1314
const kMargin = 30.0;
1415
const kInsets = EdgeInsets.all(kMargin);

0 commit comments

Comments
 (0)