news page
This commit is contained in:
216
lib/features/news/data/datasources/news_local_datasource.dart
Normal file
216
lib/features/news/data/datasources/news_local_datasource.dart
Normal 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,
|
||||
),
|
||||
];
|
||||
}
|
||||
180
lib/features/news/data/models/news_article_model.dart
Normal file
180
lib/features/news/data/models/news_article_model.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user