Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions mobile-app/lib/models/news/tutorial_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Tutorial {
final String authorSlug;
final String? createdAt;
final List<Widget> tagNames;
final List<dynamic> rawTags;
final String? url;
final String? text;

Expand All @@ -26,20 +27,23 @@ class Tutorial {
required this.authorSlug,
this.createdAt,
this.tagNames = const [],
this.rawTags = const [],
this.url,
this.text,
});

static List<Widget> returnTags(
list,
) {
list, {
bool compact = false,
}) {
List<Widget> tags = [];

for (int i = 0; i < list.length; i++) {
tags.add(TagButton(
tagName: list[i]['name'],
tagSlug: list[i]['slug'] ?? list[i]['id'],
key: UniqueKey(),
compact: compact,
));
}
return tags;
Expand All @@ -57,6 +61,7 @@ class Tutorial {
authorName: data['author']['name'],
authorSlug: data['author']['username'],
tagNames: returnTags(data['tags']),
rawTags: data['tags'] ?? [],
id: data['id'],
slug: data['slug'],
);
Expand All @@ -72,6 +77,7 @@ class Tutorial {
authorName: data['author']['name'],
authorSlug: returnSlug(data['author']['url']),
tagNames: returnTags(data['tags']),
rawTags: data['tags'] ?? [],
id: data['objectID'],
slug: data['slug'],
);
Expand All @@ -92,6 +98,7 @@ class Tutorial {
authorSlug: json['author']['username'],
profileImage: json['author']['profilePicture'],
tagNames: returnTags(json['tags']),
rawTags: json['tags'] ?? [],
id: json['id'],
title: json['title'],
url: json['url'],
Expand Down
217 changes: 106 additions & 111 deletions mobile-app/lib/ui/views/news/news-feed/news_feed_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:freecodecamp/extensions/i18n_extension.dart';
import 'package:freecodecamp/models/news/tutorial_model.dart';
import 'package:freecodecamp/ui/views/news/news-feed/news_feed_viewmodel.dart';
import 'package:freecodecamp/ui/views/news/widgets/tag_widget.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:stacked/stacked.dart';

Expand Down Expand Up @@ -82,8 +83,8 @@ class NewsFeedView extends StatelessWidget {
fetchNextPage: fetchNextPage,
separatorBuilder: (context, int i) => const Divider(
color: Color.fromRGBO(0x2A, 0x2A, 0x40, 1),
thickness: 3,
height: 3,
thickness: 1,
height: 1,
),
builderDelegate: PagedChildBuilderDelegate<Tutorial>(
itemBuilder: (context, tutorial, index) => Container(
Expand Down Expand Up @@ -126,143 +127,137 @@ class NewsFeedView extends StatelessWidget {
// );
// }

InkWell tutorialThumbnailBuilder(Tutorial tutorial, NewsFeedViewModel model) {
Widget tutorialThumbnailBuilder(Tutorial tutorial, NewsFeedViewModel model) {
return InkWell(
key: Key(tutorial.id),
splashColor: Colors.transparent,
onTap: () {
model.navigateTo(tutorial.id, tutorial.slug);
},
child: Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: thumbnailView(tutorial, model),
),
);
}

Column thumbnailView(Tutorial tutorial, NewsFeedViewModel model) {
return Column(
children: [
Container(
color: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1),
child: AspectRatio(
aspectRatio: 16 / 9,
child: tutorial.featureImage == null
? Image.asset(
'assets/images/freecodecamp-banner.png',
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: tutorial.featureImage!,
errorWidget: (context, url, error) {
log('Error loading image: $url - $tutorial.featureImage $error');
return const Icon(Icons.error);
},
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Wrap(
children: [
for (int j = 0; j < tutorial.tagNames.length && j < 3; j++)
tutorial.tagNames[j]
],
),
),
),
Container(
padding: const EdgeInsets.only(left: 16, right: 16, top: 8),
child: tutorialHeader(tutorial, model),
)
],
);
}

Widget tutorialHeader(Tutorial tutorial, NewsFeedViewModel model) {
return Column(
children: [
Row(
children: [
Expanded(
child: Text(
tutorial.title,
maxLines: 2,
style: const TextStyle(
fontSize: 20,
overflow: TextOverflow.ellipsis,
height: 1.5,
),
),
),
],
),
Row(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 16, top: 16),
child: InkWell(
onTap: () {
model.navigateToAuthor(tutorial.authorSlug);
},
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
width: 45,
height: 45,
color: const Color.fromRGBO(0x2A, 0x2A, 0x40, 1),
child: tutorial.profileImage == null
color: const Color(0xFF2A2A40),
child: tutorial.featureImage == null
? Image.asset(
'assets/images/placeholder-profile-img.png',
width: 45,
height: 45,
'assets/images/freecodecamp-banner.png',
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: tutorial.profileImage as String,
errorWidget: (context, url, error) => Image.asset(
'assets/images/placeholder-profile-img.png',
width: 45,
height: 45,
fit: BoxFit.cover,
),
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
imageUrl: tutorial.featureImage!,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: const Color(0xFF2A2A40),
),
errorWidget: (context, url, error) {
log('Error loading image: $url - ${tutorial.featureImage} $error');
return Image.asset(
'assets/images/freecodecamp-banner.png',
fit: BoxFit.cover,
);
},
),
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 8),
if (tutorial.rawTags.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Wrap(
spacing: 0,
runSpacing: 4,
children: [
for (int j = 0; j < tutorial.rawTags.length && j < 3; j++)
TagButton(
tagName: tutorial.rawTags[j]['name'],
tagSlug: tutorial.rawTags[j]['slug'] ?? tutorial.rawTags[j]['id'],
compact: true,
key: UniqueKey(),
),
],
),
),
Text(
tutorial.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
const SizedBox(height: 8),
Row(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10, top: 16),
child: Text(
tutorial.authorName.toUpperCase(),
GestureDetector(
onTap: () => model.navigateToAuthor(tutorial.authorSlug),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
width: 24,
height: 24,
child: tutorial.profileImage == null
? Image.asset(
'assets/images/placeholder-profile-img.png',
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: tutorial.profileImage!,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: const Color(0xFF2A2A40),
),
errorWidget: (context, url, error) => Image.asset(
'assets/images/placeholder-profile-img.png',
fit: BoxFit.cover,
),
),
),
),
),
const SizedBox(width: 8),
Flexible(
child: GestureDetector(
onTap: () => model.navigateToAuthor(tutorial.authorSlug),
child: Text(
tutorial.authorName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: 0.8),
),
),
),
),
Text(
' • ',
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: 0.5),
),
),
Text(
NewsFeedViewModel.parseDate(tutorial.createdAt),
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: 0.5),
),
),
],
),
],
),
],
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class NewsFeedViewModel extends BaseViewModel {

void initState(String tagSlug, String authorId) {
_pagingController = PagingController(
getNextPageKey: (state) => nextPageKey,
getNextPageKey: (state) => (state.pages?.isEmpty ?? true) ? '' : nextPageKey,
fetchPage: (pageKey) =>
fetchTutorials(pageKey, tagSlug: tagSlug, authorId: authorId),
);
Expand Down
18 changes: 11 additions & 7 deletions mobile-app/lib/ui/views/news/widgets/tag_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ class TagButton extends StatefulWidget {
super.key,
required this.tagName,
required this.tagSlug,
this.compact = false,
});

final String tagName;
final String tagSlug;
final bool compact;

static Color randomColor() {
var randomNum = Random();
Expand All @@ -39,8 +41,10 @@ class _TagButtonState extends State<TagButton>
@override
// ignore: must_call_super
Widget build(BuildContext context) {
final isCompact = widget.compact;

return Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 8, 0),
padding: EdgeInsets.fromLTRB(0, isCompact ? 0 : 8, isCompact ? 6 : 8, 0),
child: InkWell(
onTap: () {
_navigationService.navigateTo(
Expand All @@ -54,24 +58,24 @@ class _TagButtonState extends State<TagButton>
},
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.45),
maxWidth: MediaQuery.of(context).size.width * (isCompact ? 0.35 : 0.45)),
decoration: ShapeDecoration(
color: _tagColor,
shape: const StadiumBorder(),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
padding: EdgeInsets.symmetric(
vertical: isCompact ? 2 : 4,
horizontal: isCompact ? 6 : 8,
),
child: Tooltip(
message: '#${widget.tagName}',
child: Text(
'#${widget.tagName}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
style: TextStyle(
fontSize: isCompact ? 11 : 16,
color: Colors.black,
),
),
Expand Down
Loading