update news

This commit is contained in:
Phuoc Nguyen
2025-11-10 15:37:55 +07:00
parent 36bdf6613b
commit 67fd5ed142
17 changed files with 1016 additions and 211 deletions

View File

@@ -0,0 +1,179 @@
/// Blog Post Model for Frappe API
///
/// Maps to Frappe Blog Post doctype from blog.sh API.
library;
import 'package:json_annotation/json_annotation.dart';
import 'package:worker/features/news/domain/entities/news_article.dart';
part 'blog_post_model.g.dart';
/// Blog Post Model
///
/// Represents a blog post from Frappe ERPNext system.
/// Fields match the API response from frappe.client.get_list for Blog Post doctype.
@JsonSerializable()
class BlogPostModel {
/// Unique post identifier (docname)
final String name;
/// Post title
final String title;
/// Publication date (ISO 8601 string)
@JsonKey(name: 'published_on')
final String? publishedOn;
/// Author/blogger name
final String? blogger;
/// Short introduction/excerpt
@JsonKey(name: 'blog_intro')
final String? blogIntro;
/// Full HTML content (legacy field)
final String? content;
/// Full HTML content (new field from Frappe)
@JsonKey(name: 'content_html')
final String? contentHtml;
/// Featured/meta image URL
@JsonKey(name: 'meta_image')
final String? metaImage;
/// Meta description for SEO
@JsonKey(name: 'meta_description')
final String? metaDescription;
/// Blog category name
@JsonKey(name: 'blog_category')
final String? blogCategory;
/// Constructor
const BlogPostModel({
required this.name,
required this.title,
this.publishedOn,
this.blogger,
this.blogIntro,
this.content,
this.contentHtml,
this.metaImage,
this.metaDescription,
this.blogCategory,
});
/// Create model from JSON
factory BlogPostModel.fromJson(Map<String, dynamic> json) =>
_$BlogPostModelFromJson(json);
/// Convert model to JSON
Map<String, dynamic> toJson() => _$BlogPostModelToJson(this);
/// Convert to domain entity (NewsArticle)
///
/// Stores the original blog_category name in tags[0] for filtering purposes.
NewsArticle toEntity() {
// Parse published date
DateTime publishedDate = DateTime.now();
if (publishedOn != null) {
try {
publishedDate = DateTime.parse(publishedOn!);
} catch (e) {
// Use current date if parsing fails
publishedDate = DateTime.now();
}
}
// Extract excerpt from blogIntro or metaDescription
final excerpt = blogIntro ?? metaDescription ?? '';
// Use content_html preferentially, fall back to content
final htmlContent = contentHtml ?? content;
// Use meta image with full URL path
String imageUrl;
if (metaImage != null && metaImage!.isNotEmpty) {
// If meta_image starts with /, prepend the base URL
if (metaImage!.startsWith('/')) {
imageUrl = 'https://land.dbiz.com$metaImage';
} else if (metaImage!.startsWith('http')) {
imageUrl = metaImage!;
} else {
imageUrl = 'https://land.dbiz.com/$metaImage';
}
} else {
imageUrl = 'https://via.placeholder.com/400x300?text=${Uri.encodeComponent(title)}';
}
// Parse category
final category = _parseCategory(blogCategory ?? '');
// Calculate reading time (rough estimate: 200 words per minute)
final wordCount = (htmlContent ?? excerpt).split(' ').length;
final readingTime = (wordCount / 200).ceil().clamp(1, 60);
return NewsArticle(
id: name,
title: title,
excerpt: excerpt.length > 200 ? '${excerpt.substring(0, 200)}...' : excerpt,
content: htmlContent,
imageUrl: imageUrl,
category: category,
publishedDate: publishedDate,
viewCount: 0, // Not provided by API
readingTimeMinutes: readingTime,
isFeatured: false, // Not provided by API
authorName: blogger,
authorAvatar: null, // Not provided by API
tags: blogCategory != null ? [blogCategory!] : [], // Store original category name for filtering
likeCount: 0, // Not provided by API
commentCount: 0, // Not provided by API
shareCount: 0, // Not provided by API
);
}
/// Parse category from blog category name
static NewsCategory _parseCategory(String categoryName) {
final lower = categoryName.toLowerCase();
if (lower.contains('tin') || lower.contains('news')) {
return NewsCategory.news;
} else if (lower.contains('chuyên') ||
lower.contains('kỹ thuật') ||
lower.contains('professional') ||
lower.contains('technique')) {
return NewsCategory.professional;
} else if (lower.contains('dự án') ||
lower.contains('project') ||
lower.contains('công trình')) {
return NewsCategory.projects;
} else if (lower.contains('sự kiện') || lower.contains('event')) {
return NewsCategory.events;
} else if (lower.contains('khuyến mãi') ||
lower.contains('promotion') ||
lower.contains('ưu đãi')) {
return NewsCategory.promotions;
}
return NewsCategory.news;
}
@override
String toString() {
return 'BlogPostModel(name: $name, title: $title, category: $blogCategory, '
'publishedOn: $publishedOn, hasContentHtml: ${contentHtml != null})';
}
}
/// Response wrapper for Blog Post list API
@JsonSerializable()
class BlogPostsResponse {
final List<BlogPostModel> message;
const BlogPostsResponse({required this.message});
factory BlogPostsResponse.fromJson(Map<String, dynamic> json) =>
_$BlogPostsResponseFromJson(json);
Map<String, dynamic> toJson() => _$BlogPostsResponseToJson(this);
}

View File

@@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'blog_post_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BlogPostModel _$BlogPostModelFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'BlogPostModel',
json,
($checkedConvert) {
final val = BlogPostModel(
name: $checkedConvert('name', (v) => v as String),
title: $checkedConvert('title', (v) => v as String),
publishedOn: $checkedConvert('published_on', (v) => v as String?),
blogger: $checkedConvert('blogger', (v) => v as String?),
blogIntro: $checkedConvert('blog_intro', (v) => v as String?),
content: $checkedConvert('content', (v) => v as String?),
contentHtml: $checkedConvert('content_html', (v) => v as String?),
metaImage: $checkedConvert('meta_image', (v) => v as String?),
metaDescription: $checkedConvert(
'meta_description',
(v) => v as String?,
),
blogCategory: $checkedConvert('blog_category', (v) => v as String?),
);
return val;
},
fieldKeyMap: const {
'publishedOn': 'published_on',
'blogIntro': 'blog_intro',
'contentHtml': 'content_html',
'metaImage': 'meta_image',
'metaDescription': 'meta_description',
'blogCategory': 'blog_category',
},
);
Map<String, dynamic> _$BlogPostModelToJson(BlogPostModel instance) =>
<String, dynamic>{
'name': instance.name,
'title': instance.title,
'published_on': ?instance.publishedOn,
'blogger': ?instance.blogger,
'blog_intro': ?instance.blogIntro,
'content': ?instance.content,
'content_html': ?instance.contentHtml,
'meta_image': ?instance.metaImage,
'meta_description': ?instance.metaDescription,
'blog_category': ?instance.blogCategory,
};
BlogPostsResponse _$BlogPostsResponseFromJson(Map<String, dynamic> json) =>
$checkedCreate('BlogPostsResponse', json, ($checkedConvert) {
final val = BlogPostsResponse(
message: $checkedConvert(
'message',
(v) => (v as List<dynamic>)
.map((e) => BlogPostModel.fromJson(e as Map<String, dynamic>))
.toList(),
),
);
return val;
});
Map<String, dynamic> _$BlogPostsResponseToJson(BlogPostsResponse instance) =>
<String, dynamic>{
'message': instance.message.map((e) => e.toJson()).toList(),
};