Skip to content

Commit

Permalink
show articles
Browse files Browse the repository at this point in the history
  • Loading branch information
codekeyz committed Jan 5, 2024
1 parent 6065bba commit 56b7820
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 37 deletions.
13 changes: 12 additions & 1 deletion frontend/lib/blog/blog.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:frontend/data/providers/article_provider.dart';
import 'package:frontend/data/providers/auth_provider.dart';
import 'package:provider/provider.dart';

Expand All @@ -13,13 +14,23 @@ class BlogPage extends StatefulWidget {

class _BlogPageState extends State<BlogPage> {
late AuthProvider _authProvider;
late ArticleProvider _articleProvider;

@override
void initState() {
super.initState();

_authProvider = context.read();
Future.microtask(_authProvider.getUser);
_articleProvider = context.read();

_fetchData();
}

void _fetchData() async {
await Future.wait([
_authProvider.getUser(),
_articleProvider.fetchArticles(),
]);
}

@override
Expand Down
17 changes: 9 additions & 8 deletions frontend/lib/blog/widgets/article_card.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:frontend/data/models/article.dart';
import 'package:frontend/utils/misc.dart';

class ArticleCard extends StatelessWidget {
const ArticleCard({super.key});
final Article article;

const ArticleCard(this.article, {super.key});

@override
Widget build(BuildContext context) {
Expand All @@ -18,22 +21,20 @@ class ArticleCard extends StatelessWidget {
children: [
SizedBox(
height: 100,
child: Image.network(
'https://images.unsplash.com/photo-1704303928271-775bb222ab46?q=80&w=3270&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
width: double.maxFinite,
fit: BoxFit.cover,
),
child: article.imageUrl == null
? Container(color: blogColor.withOpacity(0.5))
: Image.network(article.imageUrl!, width: double.maxFinite, fit: BoxFit.cover),
),
const SizedBox(height: 16),
Text(
'Some blog post',
article.title,
style: typography.bodyStrong!.copyWith(color: blogColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
'Some blog post asfjaklsdjfl ajdlfjlsjdflkajkldf asdfjaklsdjflka asdfjakdjf aljdfkaj dfl',
article.description,
style: typography.bodyLarge!.copyWith(color: blogColor, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
Expand Down
46 changes: 33 additions & 13 deletions frontend/lib/blog/widgets/article_items.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:frontend/data/models/article.dart';
import 'package:frontend/data/providers/article_provider.dart';
import 'package:frontend/utils/provider.dart';
import 'package:provider/provider.dart';

import 'article_card.dart';

Expand All @@ -8,19 +12,35 @@ class BlogArticlesWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
const spacing = 16.0;
return Container(
alignment: Alignment.center,
child: const Wrap(
runSpacing: spacing,
spacing: spacing,
children: [
ArticleCard(),
ArticleCard(),
ArticleCard(),
ArticleCard(),
ArticleCard(),
],
),
final articleProv = context.read<ArticleProvider>();

return StreamBuilder<ProviderEvent<List<Article>>>(
stream: articleProv.stream,
initialData: articleProv.lastEvent,
builder: (context, snapshot) {
final articles = snapshot.data?.data ?? [];
final state = snapshot.data?.state;
final loading = state == ProviderState.loading;

if (articles.isEmpty) {
if (loading) {
return Container(height: 400, alignment: Alignment.center, child: const SizedBox(child: ProgressRing()));
}
if (state == ProviderState.error) {
return Container(
height: 400,
alignment: Alignment.center,
child: const Center(child: Text('An error occurred. Refresh the page')));
}
}

return Wrap(
runSpacing: spacing,
spacing: spacing,
alignment: WrapAlignment.start,
children: [...articles.map((e) => ArticleCard(e))],
);
},
);
}
}
41 changes: 41 additions & 0 deletions frontend/lib/data/api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;

import 'package:frontend/data/models/article.dart';
import 'package:http/http.dart' show Response;
import 'package:http/browser_client.dart';

Expand Down Expand Up @@ -53,6 +54,46 @@ class ApiService {
return true;
}

Future<List<Article>> getArticles() async {
final result = await _runCatching(() => client.get(getUri('/articles')));

final items = jsonDecode(result.body)['articles'] as Iterable;
return items.map((e) => Article.fromJson(e)).toList();
}

Future<Article> getArticle(int articleId) async {
final result = await _runCatching(() => client.get(getUri('/articles/$articleId')));

final data = jsonDecode(result.body)['article'];
return Article.fromJson(data);
}

Future<Article> createArticle(String title, String description, String? imageUrl) async {
final result = await _runCatching(() => client.post(getUri('/articles'), body: {
'title': title,
'description': description,
'imageUrl': imageUrl,
}));

final data = jsonDecode(result.body)['article'];
return Article.fromJson(data);
}

Future<Article> updateArticle(int articleId, String title, String description, String? imageUrl) async {
final result = await _runCatching(() => client.put(getUri('/articles/$articleId'), body: {
'title': title,
'description': description,
'imageUrl': imageUrl,
}));

final data = jsonDecode(result.body)['article'];
return Article.fromJson(data);
}

Future<void> deleteArticle(int articleId) async {
await _runCatching(() => client.delete(getUri('/articles/$articleId')));
}

Future<Response> _runCatching(HttpResponseCb apiCall) async {
try {
final response = await apiCall.call();
Expand Down
3 changes: 2 additions & 1 deletion frontend/lib/data/models/article.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ part "article.g.dart";
class Article {
final String title;
final String description;
final String? imageUrl;
final DateTime createdAt;
final DateTime updatedAt;

const Article(this.title, this.description, {required this.createdAt, required this.updatedAt});
const Article(this.title, this.description, {this.imageUrl, required this.createdAt, required this.updatedAt});

Map<String, dynamic> toJson() => _$ArticleToJson(this);

Expand Down
9 changes: 9 additions & 0 deletions frontend/lib/data/providers/article_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@ import 'package:meta/meta.dart';
class ArticleProvider extends BaseProvider<List<Article>> {
@visibleForTesting
ApiService get apiSvc => getIt.get<ApiService>();

Future<void> fetchArticles() async {
if (!apiSvc.hasAuthCookie) return;

final articles = await safeRun(() => apiSvc.getArticles());
if (articles == null) return;

addEvent(ProviderEvent.success(data: articles));
}
}
17 changes: 3 additions & 14 deletions frontend/lib/data/providers/auth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,26 @@ class AuthProvider extends BaseProvider<User> {
Future<void> getUser() async {
if (!apiSvc.hasAuthCookie) return;

final user = await _safeRun(() => apiSvc.getUser());
final user = await safeRun(() => apiSvc.getUser());
if (user == null) return;

addEvent(ProviderEvent.success(data: user));
}

Future<void> login(String email, String password) async {
final user = await _safeRun(() => apiSvc.loginUser(email, password));
final user = await safeRun(() => apiSvc.loginUser(email, password));
if (user == null) return;

addEvent(ProviderEvent.success(data: user));
}

Future<void> register(String displayName, String email, String password) async {
final success = await _safeRun(() => apiSvc.registerUser(displayName, email, password));
final success = await safeRun(() => apiSvc.registerUser(displayName, email, password));
if (success != true) return;

addEvent(const ProviderEvent.idle());
}

Future<T?> _safeRun<T>(FutureOr<T> Function() apiCall) async {
addEvent(const ProviderEvent.loading());

try {
return await apiCall.call();
} on ApiException catch (e) {
addEvent(ProviderEvent.error(errorMessage: e.errors.join('\n')));
return null;
}
}

void logout() {
apiSvc.clearAuthCookie();
addEvent(const ProviderEvent.idle());
Expand Down
13 changes: 13 additions & 0 deletions frontend/lib/utils/provider.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'dart:async';

import '../data/services.dart';

enum ProviderState { idle, loading, success, error }

class ProviderEvent<T> {
Expand Down Expand Up @@ -68,4 +70,15 @@ abstract class BaseProvider<T> extends ChangeNotifier with DataStreamMixin<Provi
super.clear();
addEvent(const ProviderEvent.idle());
}

Future<Result?> safeRun<Result>(FutureOr<Result> Function() apiCall) async {
addEvent(const ProviderEvent.loading());

try {
return await apiCall.call();
} on ApiException catch (e) {
addEvent(ProviderEvent.error(errorMessage: e.errors.join('\n')));
return null;
}
}
}

0 comments on commit 56b7820

Please sign in to comment.