news page

This commit is contained in:
Phuoc Nguyen
2025-11-03 11:48:41 +07:00
parent 21c1c3372c
commit ea485d8c3a
14 changed files with 2017 additions and 13 deletions

View File

@@ -0,0 +1,216 @@
/// News Local DataSource
///
/// Handles all local data operations for news articles.
/// Currently provides mock data for development and testing.
/// Will be extended to use Hive cache when backend API is available.
library;
import 'package:worker/features/news/data/models/news_article_model.dart';
/// News Local Data Source
///
/// Provides mock data for news articles.
/// In production, this will cache data from the remote API.
class NewsLocalDataSource {
/// Get all news articles
///
/// Returns a list of all articles from mock data.
/// In production, this will fetch from Hive cache.
Future<List<NewsArticleModel>> getAllArticles() async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 300));
return _mockArticles;
}
/// Get featured article
///
/// Returns the main featured article for the top section.
Future<NewsArticleModel?> getFeaturedArticle() async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 200));
try {
return _mockArticles.firstWhere((article) => article.isFeatured);
} catch (e) {
return null;
}
}
/// Get articles by category
///
/// Returns filtered list of articles matching the [category].
Future<List<NewsArticleModel>> getArticlesByCategory(String category) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 200));
if (category == 'all') {
return _mockArticles;
}
return _mockArticles
.where(
(article) => article.category.toLowerCase() == category.toLowerCase(),
)
.toList();
}
/// Get a specific article by ID
///
/// Returns the article if found, null otherwise.
Future<NewsArticleModel?> getArticleById(String articleId) async {
// Simulate network delay
await Future<void>.delayed(const Duration(milliseconds: 100));
try {
return _mockArticles.firstWhere((article) => article.id == articleId);
} catch (e) {
return null;
}
}
/// Check if cache is valid
///
/// Returns true if cached data is still valid.
/// Currently always returns false since we're using mock data.
Future<bool> isCacheValid() async {
// TODO: Implement cache validation when using Hive
return false;
}
/// Cache articles locally
///
/// Saves articles to Hive for offline access.
/// Currently not implemented (using mock data).
Future<void> cacheArticles(List<NewsArticleModel> articles) async {
// TODO: Implement Hive caching when backend API is ready
}
/// Clear cached articles
///
/// Removes all cached articles from Hive.
/// Currently not implemented (using mock data).
Future<void> clearCache() async {
// TODO: Implement cache clearing when using Hive
}
/// Mock articles matching HTML design
///
/// This data will be replaced with real API data in production.
static final List<NewsArticleModel> _mockArticles = [
// Featured article
const NewsArticleModel(
id: 'featured-1',
title: '5 xu hướng gạch men phòng tắm được ưa chuộng năm 2024',
excerpt:
'Khám phá những mẫu gạch men hiện đại, sang trọng cho không gian phòng tắm. Từ những tone màu trung tính đến các họa tiết độc đáo, cùng tìm hiểu các xu hướng đang được yêu thích nhất.',
imageUrl:
'https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=400&h=200&fit=crop',
category: 'news',
publishedDate: '2024-11-15T00:00:00.000Z',
viewCount: 2300,
readingTimeMinutes: 5,
isFeatured: true,
),
// Latest articles
const NewsArticleModel(
id: 'news-1',
title: 'Hướng dẫn thi công gạch granite 60x60 chuyên nghiệp',
excerpt:
'Quy trình thi công chi tiết từ A-Z cho thầy thợ xây dựng. Các bước chuẩn bị, kỹ thuật thi công và kinh nghiệm thực tế.',
imageUrl:
'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=80&h=80&fit=crop',
category: 'professional',
publishedDate: '2024-11-12T00:00:00.000Z',
viewCount: 1800,
readingTimeMinutes: 8,
isFeatured: false,
),
const NewsArticleModel(
id: 'news-2',
title: 'Bảng giá gạch men cao cấp mới nhất tháng 11/2024',
excerpt:
'Cập nhật bảng giá chi tiết các dòng sản phẩm gạch men nhập khẩu. So sánh giá các thương hiệu hàng đầu.',
imageUrl:
'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=80&h=80&fit=crop',
category: 'news',
publishedDate: '2024-11-10T00:00:00.000Z',
viewCount: 3100,
readingTimeMinutes: 4,
isFeatured: false,
),
const NewsArticleModel(
id: 'news-3',
title: 'Mẹo chọn gạch ốp tường phòng bếp đẹp và bền',
excerpt:
'Những lưu ý quan trọng khi chọn gạch ốp tường cho khu vực bếp. Tư vấn về chất liệu, màu sắc và kích thước phù hợp.',
imageUrl:
'https://images.unsplash.com/photo-1545558014-8692077e9b5c?w=80&h=80&fit=crop',
category: 'professional',
publishedDate: '2024-11-08T00:00:00.000Z',
viewCount: 1500,
readingTimeMinutes: 6,
isFeatured: false,
),
const NewsArticleModel(
id: 'news-4',
title: 'Dự án biệt thự Quận 2: Ứng dụng gạch men cao cấp',
excerpt:
'Case study về việc sử dụng gạch men trong dự án biệt thự 300m². Chia sẻ kinh nghiệm và bài học từ thầu thợ.',
imageUrl:
'https://images.unsplash.com/photo-1484101403633-562f891dc89a?w=80&h=80&fit=crop',
category: 'projects',
publishedDate: '2024-11-05T00:00:00.000Z',
viewCount: 2700,
readingTimeMinutes: 10,
isFeatured: false,
),
const NewsArticleModel(
id: 'news-5',
title: 'Công cụ hỗ trợ tính toán diện tích gạch chính xác',
excerpt:
'Hướng dẫn sử dụng các công cụ và ứng dụng giúp tính toán diện tích gạch cần thiết cho công trình một cách chính xác nhất.',
imageUrl:
'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=80&h=80&fit=crop',
category: 'professional',
publishedDate: '2024-11-03T00:00:00.000Z',
viewCount: 1200,
readingTimeMinutes: 7,
isFeatured: false,
),
// Additional articles for different categories
const NewsArticleModel(
id: 'event-1',
title: 'Hội nghị thầu thợ miền Nam 2024',
excerpt:
'Tham gia sự kiện kết nối, chia sẻ kinh nghiệm và cập nhật xu hướng mới nhất trong ngành xây dựng.',
imageUrl:
'https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=80&h=80&fit=crop',
category: 'events',
publishedDate: '2024-11-01T00:00:00.000Z',
viewCount: 950,
readingTimeMinutes: 3,
isFeatured: false,
),
const NewsArticleModel(
id: 'promo-1',
title: 'Khuyến mãi mua 10 tặng 1 - Gạch Granite 60x60',
excerpt:
'Chương trình ưu đãi đặc biệt dành cho thầu thợ và đại lý. Áp dụng cho đơn hàng từ 500m² trở lên.',
imageUrl:
'https://images.unsplash.com/photo-1607400201889-565b1ee75f8e?w=80&h=80&fit=crop',
category: 'promotions',
publishedDate: '2024-10-28T00:00:00.000Z',
viewCount: 4200,
readingTimeMinutes: 2,
isFeatured: false,
),
];
}

View File

@@ -0,0 +1,180 @@
/// Data Model: News Article Model
///
/// Data layer model for news articles.
/// Handles JSON serialization and conversion to/from domain entity.
library;
import 'package:worker/features/news/domain/entities/news_article.dart';
/// News Article Model
///
/// Used in the data layer for:
/// - JSON serialization/deserialization from API
/// - Conversion to domain entity
/// - Local storage (if needed)
class NewsArticleModel {
/// Unique article ID
final String id;
/// Article title
final String title;
/// Article excerpt/summary
final String excerpt;
/// Full article content (optional)
final String? content;
/// Featured image URL
final String imageUrl;
/// Article category
final String category;
/// Publication date (ISO 8601 string)
final String publishedDate;
/// View count
final int viewCount;
/// Estimated reading time in minutes
final int readingTimeMinutes;
/// Whether this is a featured article
final bool isFeatured;
/// Author name (optional)
final String? authorName;
/// Author avatar URL (optional)
final String? authorAvatar;
/// Constructor
const NewsArticleModel({
required this.id,
required this.title,
required this.excerpt,
this.content,
required this.imageUrl,
required this.category,
required this.publishedDate,
required this.viewCount,
required this.readingTimeMinutes,
this.isFeatured = false,
this.authorName,
this.authorAvatar,
});
/// Create model from JSON
factory NewsArticleModel.fromJson(Map<String, dynamic> json) {
return NewsArticleModel(
id: json['id'] as String,
title: json['title'] as String,
excerpt: json['excerpt'] as String,
content: json['content'] as String?,
imageUrl: json['image_url'] as String,
category: json['category'] as String,
publishedDate: json['published_date'] as String,
viewCount: json['view_count'] as int,
readingTimeMinutes: json['reading_time_minutes'] as int,
isFeatured: json['is_featured'] as bool? ?? false,
authorName: json['author_name'] as String?,
authorAvatar: json['author_avatar'] as String?,
);
}
/// Convert model to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'excerpt': excerpt,
'content': content,
'image_url': imageUrl,
'category': category,
'published_date': publishedDate,
'view_count': viewCount,
'reading_time_minutes': readingTimeMinutes,
'is_featured': isFeatured,
'author_name': authorName,
'author_avatar': authorAvatar,
};
}
/// Convert model to domain entity
NewsArticle toEntity() {
return NewsArticle(
id: id,
title: title,
excerpt: excerpt,
content: content,
imageUrl: imageUrl,
category: _parseCategory(category),
publishedDate: DateTime.parse(publishedDate),
viewCount: viewCount,
readingTimeMinutes: readingTimeMinutes,
isFeatured: isFeatured,
authorName: authorName,
authorAvatar: authorAvatar,
);
}
/// Create model from domain entity
factory NewsArticleModel.fromEntity(NewsArticle entity) {
return NewsArticleModel(
id: entity.id,
title: entity.title,
excerpt: entity.excerpt,
content: entity.content,
imageUrl: entity.imageUrl,
category: _categoryToString(entity.category),
publishedDate: entity.publishedDate.toIso8601String(),
viewCount: entity.viewCount,
readingTimeMinutes: entity.readingTimeMinutes,
isFeatured: entity.isFeatured,
authorName: entity.authorName,
authorAvatar: entity.authorAvatar,
);
}
/// Parse category from string
static NewsCategory _parseCategory(String category) {
switch (category.toLowerCase()) {
case 'news':
return NewsCategory.news;
case 'professional':
case 'technique':
return NewsCategory.professional;
case 'projects':
return NewsCategory.projects;
case 'events':
return NewsCategory.events;
case 'promotions':
return NewsCategory.promotions;
default:
return NewsCategory.news;
}
}
/// Convert category to string
static String _categoryToString(NewsCategory category) {
switch (category) {
case NewsCategory.news:
return 'news';
case NewsCategory.professional:
return 'professional';
case NewsCategory.projects:
return 'projects';
case NewsCategory.events:
return 'events';
case NewsCategory.promotions:
return 'promotions';
}
}
@override
String toString() {
return 'NewsArticleModel(id: $id, title: $title, category: $category, '
'publishedDate: $publishedDate)';
}
}

View File

@@ -0,0 +1,87 @@
/// Repository Implementation: News Repository
///
/// Concrete implementation of the NewsRepository interface.
/// Coordinates between local and remote data sources.
library;
import 'package:worker/features/news/data/datasources/news_local_datasource.dart';
import 'package:worker/features/news/domain/entities/news_article.dart';
import 'package:worker/features/news/domain/repositories/news_repository.dart';
/// News Repository Implementation
class NewsRepositoryImpl implements NewsRepository {
/// Local data source
final NewsLocalDataSource localDataSource;
/// Constructor
NewsRepositoryImpl({required this.localDataSource});
@override
Future<List<NewsArticle>> getAllArticles() async {
try {
final models = await localDataSource.getAllArticles();
final entities = models.map((model) => model.toEntity()).toList();
// Sort by published date (newest first)
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
return entities;
} catch (e) {
print('[NewsRepository] Error getting articles: $e');
return [];
}
}
@override
Future<NewsArticle?> getFeaturedArticle() async {
try {
final model = await localDataSource.getFeaturedArticle();
return model?.toEntity();
} catch (e) {
print('[NewsRepository] Error getting featured article: $e');
return null;
}
}
@override
Future<List<NewsArticle>> getArticlesByCategory(NewsCategory category) async {
try {
final categoryString = category.filterName;
final models = await localDataSource.getArticlesByCategory(
categoryString,
);
final entities = models.map((model) => model.toEntity()).toList();
// Sort by published date (newest first)
entities.sort((a, b) => b.publishedDate.compareTo(a.publishedDate));
return entities;
} catch (e) {
print('[NewsRepository] Error getting articles by category: $e');
return [];
}
}
@override
Future<NewsArticle?> getArticleById(String articleId) async {
try {
final model = await localDataSource.getArticleById(articleId);
return model?.toEntity();
} catch (e) {
print('[NewsRepository] Error getting article by id: $e');
return null;
}
}
@override
Future<List<NewsArticle>> refreshArticles() async {
try {
await localDataSource.clearCache();
return getAllArticles();
} catch (e) {
print('[NewsRepository] Error refreshing articles: $e');
return [];
}
}
}