182 lines
5.4 KiB
Dart
182 lines
5.4 KiB
Dart
/// 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();
|
|
}
|
|
}
|
|
|
|
// Use content_html preferentially, fall back to content
|
|
final htmlContent = contentHtml ?? content;
|
|
|
|
// Excerpt is ONLY from blog_intro (plain text)
|
|
final excerpt = blogIntro ?? '';
|
|
|
|
// 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.isNotEmpty
|
|
? (excerpt.length > 300 ? '${excerpt.substring(0, 300)}...' : excerpt)
|
|
: 'Không có mô tả',
|
|
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);
|
|
}
|