Skip to content

Commit 91b2bd1

Browse files
SembaukeNirajn2311
andauthored
feat: rehash news feed layout (#1713)
Co-authored-by: Niraj Nandish <nirajnandish@icloud.com>
1 parent c7e9f8e commit 91b2bd1

4 files changed

Lines changed: 127 additions & 121 deletions

File tree

mobile-app/lib/models/news/tutorial_model.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Tutorial {
1212
final String authorSlug;
1313
final String? createdAt;
1414
final List<Widget> tagNames;
15+
final List<dynamic> rawTags;
1516
final String? url;
1617
final String? text;
1718

@@ -26,20 +27,23 @@ class Tutorial {
2627
required this.authorSlug,
2728
this.createdAt,
2829
this.tagNames = const [],
30+
this.rawTags = const [],
2931
this.url,
3032
this.text,
3133
});
3234

3335
static List<Widget> returnTags(
34-
list,
35-
) {
36+
list, {
37+
bool compact = false,
38+
}) {
3639
List<Widget> tags = [];
3740

3841
for (int i = 0; i < list.length; i++) {
3942
tags.add(TagButton(
4043
tagName: list[i]['name'],
4144
tagSlug: list[i]['slug'] ?? list[i]['id'],
4245
key: UniqueKey(),
46+
compact: compact,
4347
));
4448
}
4549
return tags;
@@ -57,6 +61,7 @@ class Tutorial {
5761
authorName: data['author']['name'],
5862
authorSlug: data['author']['username'],
5963
tagNames: returnTags(data['tags']),
64+
rawTags: data['tags'] ?? [],
6065
id: data['id'],
6166
slug: data['slug'],
6267
);
@@ -72,6 +77,7 @@ class Tutorial {
7277
authorName: data['author']['name'],
7378
authorSlug: returnSlug(data['author']['url']),
7479
tagNames: returnTags(data['tags']),
80+
rawTags: data['tags'] ?? [],
7581
id: data['objectID'],
7682
slug: data['slug'],
7783
);
@@ -92,6 +98,7 @@ class Tutorial {
9298
authorSlug: json['author']['username'],
9399
profileImage: json['author']['profilePicture'],
94100
tagNames: returnTags(json['tags']),
101+
rawTags: json['tags'] ?? [],
95102
id: json['id'],
96103
title: json['title'],
97104
url: json['url'],

mobile-app/lib/ui/views/news/news-feed/news_feed_view.dart

Lines changed: 106 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
55
import 'package:freecodecamp/extensions/i18n_extension.dart';
66
import 'package:freecodecamp/models/news/tutorial_model.dart';
77
import 'package:freecodecamp/ui/views/news/news-feed/news_feed_viewmodel.dart';
8+
import 'package:freecodecamp/ui/views/news/widgets/tag_widget.dart';
89
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
910
import 'package:stacked/stacked.dart';
1011

@@ -82,8 +83,8 @@ class NewsFeedView extends StatelessWidget {
8283
fetchNextPage: fetchNextPage,
8384
separatorBuilder: (context, int i) => const Divider(
8485
color: Color.fromRGBO(0x2A, 0x2A, 0x40, 1),
85-
thickness: 3,
86-
height: 3,
86+
thickness: 1,
87+
height: 1,
8788
),
8889
builderDelegate: PagedChildBuilderDelegate<Tutorial>(
8990
itemBuilder: (context, tutorial, index) => Container(
@@ -126,143 +127,137 @@ class NewsFeedView extends StatelessWidget {
126127
// );
127128
// }
128129

129-
InkWell tutorialThumbnailBuilder(Tutorial tutorial, NewsFeedViewModel model) {
130+
Widget tutorialThumbnailBuilder(Tutorial tutorial, NewsFeedViewModel model) {
130131
return InkWell(
131132
key: Key(tutorial.id),
132133
splashColor: Colors.transparent,
133134
onTap: () {
134135
model.navigateTo(tutorial.id, tutorial.slug);
135136
},
136137
child: Padding(
137-
padding: const EdgeInsets.only(bottom: 32.0),
138-
child: thumbnailView(tutorial, model),
139-
),
140-
);
141-
}
142-
143-
Column thumbnailView(Tutorial tutorial, NewsFeedViewModel model) {
144-
return Column(
145-
children: [
146-
Container(
147-
color: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1),
148-
child: AspectRatio(
149-
aspectRatio: 16 / 9,
150-
child: tutorial.featureImage == null
151-
? Image.asset(
152-
'assets/images/freecodecamp-banner.png',
153-
fit: BoxFit.cover,
154-
)
155-
: CachedNetworkImage(
156-
imageUrl: tutorial.featureImage!,
157-
errorWidget: (context, url, error) {
158-
log('Error loading image: $url - $tutorial.featureImage $error');
159-
return const Icon(Icons.error);
160-
},
161-
imageBuilder: (context, imageProvider) => Container(
162-
decoration: BoxDecoration(
163-
image: DecorationImage(
164-
image: imageProvider,
165-
fit: BoxFit.cover,
166-
),
167-
),
168-
),
169-
),
170-
),
171-
),
172-
Align(
173-
alignment: Alignment.centerLeft,
174-
child: Padding(
175-
padding: const EdgeInsets.only(left: 16, right: 16),
176-
child: Wrap(
177-
children: [
178-
for (int j = 0; j < tutorial.tagNames.length && j < 3; j++)
179-
tutorial.tagNames[j]
180-
],
181-
),
182-
),
183-
),
184-
Container(
185-
padding: const EdgeInsets.only(left: 16, right: 16, top: 8),
186-
child: tutorialHeader(tutorial, model),
187-
)
188-
],
189-
);
190-
}
191-
192-
Widget tutorialHeader(Tutorial tutorial, NewsFeedViewModel model) {
193-
return Column(
194-
children: [
195-
Row(
196-
children: [
197-
Expanded(
198-
child: Text(
199-
tutorial.title,
200-
maxLines: 2,
201-
style: const TextStyle(
202-
fontSize: 20,
203-
overflow: TextOverflow.ellipsis,
204-
height: 1.5,
205-
),
206-
),
207-
),
208-
],
209-
),
210-
Row(
138+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
139+
child: Column(
140+
crossAxisAlignment: CrossAxisAlignment.start,
211141
children: [
212-
Padding(
213-
padding: const EdgeInsets.only(right: 16, top: 16),
214-
child: InkWell(
215-
onTap: () {
216-
model.navigateToAuthor(tutorial.authorSlug);
217-
},
142+
ClipRRect(
143+
borderRadius: BorderRadius.circular(12),
144+
child: AspectRatio(
145+
aspectRatio: 16 / 9,
218146
child: Container(
219-
width: 45,
220-
height: 45,
221-
color: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1),
222-
child: tutorial.profileImage == null
147+
color: const Color(0xFF2A2A40),
148+
child: tutorial.featureImage == null
223149
? Image.asset(
224-
'assets/images/placeholder-profile-img.png',
225-
width: 45,
226-
height: 45,
150+
'assets/images/freecodecamp-banner.png',
227151
fit: BoxFit.cover,
228152
)
229153
: CachedNetworkImage(
230-
imageUrl: tutorial.profileImage as String,
231-
errorWidget: (context, url, error) => Image.asset(
232-
'assets/images/placeholder-profile-img.png',
233-
width: 45,
234-
height: 45,
235-
fit: BoxFit.cover,
236-
),
237-
imageBuilder: (context, imageProvider) => Container(
238-
decoration: BoxDecoration(
239-
image: DecorationImage(
240-
image: imageProvider,
241-
fit: BoxFit.cover,
242-
),
243-
),
154+
imageUrl: tutorial.featureImage!,
155+
fit: BoxFit.cover,
156+
placeholder: (context, url) => Container(
157+
color: const Color(0xFF2A2A40),
244158
),
159+
errorWidget: (context, url, error) {
160+
log('Error loading image: $url - ${tutorial.featureImage} $error');
161+
return Image.asset(
162+
'assets/images/freecodecamp-banner.png',
163+
fit: BoxFit.cover,
164+
);
165+
},
245166
),
246167
),
247168
),
248169
),
249-
Column(
250-
crossAxisAlignment: CrossAxisAlignment.start,
170+
const SizedBox(height: 8),
171+
if (tutorial.rawTags.isNotEmpty)
172+
Padding(
173+
padding: const EdgeInsets.only(bottom: 6),
174+
child: Wrap(
175+
spacing: 0,
176+
runSpacing: 4,
177+
children: [
178+
for (int j = 0; j < tutorial.rawTags.length && j < 3; j++)
179+
TagButton(
180+
tagName: tutorial.rawTags[j]['name'],
181+
tagSlug: tutorial.rawTags[j]['slug'] ?? tutorial.rawTags[j]['id'],
182+
compact: true,
183+
key: UniqueKey(),
184+
),
185+
],
186+
),
187+
),
188+
Text(
189+
tutorial.title,
190+
maxLines: 2,
191+
overflow: TextOverflow.ellipsis,
192+
style: const TextStyle(
193+
fontSize: 16,
194+
fontWeight: FontWeight.w600,
195+
height: 1.25,
196+
),
197+
),
198+
const SizedBox(height: 8),
199+
Row(
251200
children: [
252-
Padding(
253-
padding: const EdgeInsets.only(bottom: 10, top: 16),
254-
child: Text(
255-
tutorial.authorName.toUpperCase(),
201+
GestureDetector(
202+
onTap: () => model.navigateToAuthor(tutorial.authorSlug),
203+
child: ClipRRect(
204+
borderRadius: BorderRadius.circular(12),
205+
child: SizedBox(
206+
width: 24,
207+
height: 24,
208+
child: tutorial.profileImage == null
209+
? Image.asset(
210+
'assets/images/placeholder-profile-img.png',
211+
fit: BoxFit.cover,
212+
)
213+
: CachedNetworkImage(
214+
imageUrl: tutorial.profileImage!,
215+
fit: BoxFit.cover,
216+
placeholder: (context, url) => Container(
217+
color: const Color(0xFF2A2A40),
218+
),
219+
errorWidget: (context, url, error) => Image.asset(
220+
'assets/images/placeholder-profile-img.png',
221+
fit: BoxFit.cover,
222+
),
223+
),
224+
),
225+
),
226+
),
227+
const SizedBox(width: 8),
228+
Flexible(
229+
child: GestureDetector(
230+
onTap: () => model.navigateToAuthor(tutorial.authorSlug),
231+
child: Text(
232+
tutorial.authorName,
233+
maxLines: 1,
234+
overflow: TextOverflow.ellipsis,
235+
style: TextStyle(
236+
fontSize: 13,
237+
color: Colors.white.withValues(alpha: 0.8),
238+
),
239+
),
240+
),
241+
),
242+
Text(
243+
' • ',
244+
style: TextStyle(
245+
fontSize: 13,
246+
color: Colors.white.withValues(alpha: 0.5),
256247
),
257248
),
258249
Text(
259250
NewsFeedViewModel.parseDate(tutorial.createdAt),
251+
style: TextStyle(
252+
fontSize: 13,
253+
color: Colors.white.withValues(alpha: 0.5),
254+
),
260255
),
261256
],
262257
),
263258
],
264259
),
265-
],
260+
),
266261
);
267262
}
268263
}

mobile-app/lib/ui/views/news/news-feed/news_feed_viewmodel.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class NewsFeedViewModel extends BaseViewModel {
2222

2323
void initState(String tagSlug, String authorId) {
2424
_pagingController = PagingController(
25-
getNextPageKey: (state) => nextPageKey,
25+
getNextPageKey: (state) => (state.pages?.isEmpty ?? true) ? '' : nextPageKey,
2626
fetchPage: (pageKey) =>
2727
fetchTutorials(pageKey, tagSlug: tagSlug, authorId: authorId),
2828
);

mobile-app/lib/ui/views/news/widgets/tag_widget.dart

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ class TagButton extends StatefulWidget {
1010
super.key,
1111
required this.tagName,
1212
required this.tagSlug,
13+
this.compact = false,
1314
});
1415

1516
final String tagName;
1617
final String tagSlug;
18+
final bool compact;
1719

1820
static Color randomColor() {
1921
var randomNum = Random();
@@ -39,8 +41,10 @@ class _TagButtonState extends State<TagButton>
3941
@override
4042
// ignore: must_call_super
4143
Widget build(BuildContext context) {
44+
final isCompact = widget.compact;
45+
4246
return Padding(
43-
padding: const EdgeInsets.fromLTRB(0, 8, 8, 0),
47+
padding: EdgeInsets.fromLTRB(0, isCompact ? 0 : 8, isCompact ? 6 : 8, 0),
4448
child: InkWell(
4549
onTap: () {
4650
_navigationService.navigateTo(
@@ -54,24 +58,24 @@ class _TagButtonState extends State<TagButton>
5458
},
5559
child: Container(
5660
constraints: BoxConstraints(
57-
maxWidth: MediaQuery.of(context).size.width * 0.45),
61+
maxWidth: MediaQuery.of(context).size.width * (isCompact ? 0.35 : 0.45)),
5862
decoration: ShapeDecoration(
5963
color: _tagColor,
6064
shape: const StadiumBorder(),
6165
),
6266
child: Padding(
63-
padding: const EdgeInsets.symmetric(
64-
vertical: 4,
65-
horizontal: 8,
67+
padding: EdgeInsets.symmetric(
68+
vertical: isCompact ? 2 : 4,
69+
horizontal: isCompact ? 6 : 8,
6670
),
6771
child: Tooltip(
6872
message: '#${widget.tagName}',
6973
child: Text(
7074
'#${widget.tagName}',
7175
maxLines: 1,
7276
overflow: TextOverflow.ellipsis,
73-
style: const TextStyle(
74-
fontSize: 16,
77+
style: TextStyle(
78+
fontSize: isCompact ? 11 : 16,
7579
color: Colors.black,
7680
),
7781
),

0 commit comments

Comments
 (0)