diff --git a/docs/sample_project.sh b/docs/sample_project.sh
new file mode 100644
index 0000000..4a4a0f2
--- /dev/null
+++ b/docs/sample_project.sh
@@ -0,0 +1,61 @@
+#get list
+curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sample_project.get_list' \
+--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
+--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
+--header 'Content-Type: application/json' \
+--data '{
+ "limit_page_length" : 0,
+ "limit_start" : 0
+
+}'
+
+#response
+{
+ "message": [
+ {
+ "name": "PROJ-0001",
+ "project_name": "Căn hộ Studio",
+ "notes": "
Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.
",
+ "link": "https://vr.house3d.com/web/panorama-player/H00179549",
+ "thumbnail": "https://land.dbiz.com//private/files/photo-1600596542815-ffad4c1539a9.jpg"
+ }
+ ]
+}
+
+#GET DETAIL OF A SAMPLE PROJECT
+curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sample_project.get_detail' \
+--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
+--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
+--header 'Content-Type: application/json' \
+--data '{
+ "name" : "PROJ-0001"
+}'
+
+#RESPONSE
+{
+ "message": {
+ "name": "PROJ-0001",
+ "project_name": "Căn hộ Studio",
+ "notes": "Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.
",
+ "link": "https://vr.house3d.com/web/panorama-player/H00179549",
+ "thumbnail": "https://land.dbiz.com/private/files/photo-1600596542815-ffad4c1539a9.jpg",
+ "files_list": [
+ {
+ "name": "1fe604db77",
+ "file_url": "https://land.dbiz.com/private/files/photo-1600596542815-ffad4c1539a9.jpg"
+ },
+ {
+ "name": "0e3d2714ee",
+ "file_url": "https://land.dbiz.com/files/main_img.jpg"
+ },
+ {
+ "name": "fd7970daa3",
+ "file_url": "https://land.dbiz.com/files/project_img_0.jpg"
+ },
+ {
+ "name": "a42fbef956",
+ "file_url": "https://land.dbiz.com/files/project_img_1.jpg"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart
index 33a6fd4..5d90e09 100644
--- a/lib/core/constants/api_constants.dart
+++ b/lib/core/constants/api_constants.dart
@@ -320,6 +320,24 @@ class ApiConstants {
/// Form-data: { "fid": "file_id", "dt": "Architectural Project", "dn": "project_name" }
static const String removeProjectFile = '/frappe.desk.form.utils.remove_attach';
+ // ============================================================================
+ // Sample Project / Model House Endpoints (Frappe ERPNext)
+ // ============================================================================
+
+ /// Get list of sample/model house projects (requires sid and csrf_token)
+ /// POST /api/method/building_material.building_material.api.sample_project.get_list
+ /// Body: { "limit_start": 0, "limit_page_length": 0 }
+ /// Returns: { "message": [{ "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "..." }] }
+ static const String getSampleProjectList =
+ '/building_material.building_material.api.sample_project.get_list';
+
+ /// Get detail of a sample/model house project (requires sid and csrf_token)
+ /// POST /api/method/building_material.building_material.api.sample_project.get_detail
+ /// Body: { "name": "PROJ-0001" }
+ /// Returns: { "message": { "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "...", "files_list": [...] } }
+ static const String getSampleProjectDetail =
+ '/building_material.building_material.api.sample_project.get_detail';
+
/// Create new project (legacy endpoint - may be deprecated)
/// POST /projects
static const String createProject = '/projects';
diff --git a/lib/features/showrooms/data/datasources/sample_project_remote_datasource.dart b/lib/features/showrooms/data/datasources/sample_project_remote_datasource.dart
new file mode 100644
index 0000000..f025bd6
--- /dev/null
+++ b/lib/features/showrooms/data/datasources/sample_project_remote_datasource.dart
@@ -0,0 +1,96 @@
+/// Sample Project Remote Data Source
+///
+/// Handles remote API calls for sample/model house projects.
+library;
+
+import 'package:worker/core/constants/api_constants.dart';
+import 'package:worker/core/network/dio_client.dart';
+import 'package:worker/features/showrooms/data/models/sample_project_model.dart';
+
+/// Sample Project Remote Data Source Interface
+abstract class SampleProjectRemoteDataSource {
+ /// Fetch list of sample/model house projects from API
+ Future> getSampleProjects({
+ int limitStart = 0,
+ int limitPageLength = 0,
+ });
+
+ /// Fetch detail of a sample/model house project by name
+ Future getSampleProjectDetail(String name);
+}
+
+/// Sample Project Remote Data Source Implementation
+class SampleProjectRemoteDataSourceImpl implements SampleProjectRemoteDataSource {
+ const SampleProjectRemoteDataSourceImpl(this._dioClient);
+
+ final DioClient _dioClient;
+
+ /// Get list of sample projects
+ ///
+ /// Calls: POST /api/method/building_material.building_material.api.sample_project.get_list
+ /// Body: { "limit_start": 0, "limit_page_length": 0 }
+ /// Returns: List of sample projects with 360° view links
+ @override
+ Future> getSampleProjects({
+ int limitStart = 0,
+ int limitPageLength = 0,
+ }) async {
+ try {
+ final response = await _dioClient.post>(
+ '${ApiConstants.frappeApiMethod}${ApiConstants.getSampleProjectList}',
+ data: {
+ 'limit_start': limitStart,
+ 'limit_page_length': limitPageLength,
+ },
+ );
+
+ final data = response.data;
+ if (data == null) {
+ throw Exception('No data received from getSampleProjectList API');
+ }
+
+ // API returns: { "message": [...] }
+ final message = data['message'];
+ if (message == null) {
+ throw Exception('No message field in getSampleProjectList response');
+ }
+
+ final List projectsList = message as List;
+ return projectsList
+ .map((json) => SampleProjectModel.fromJson(json as Map))
+ .toList();
+ } catch (e) {
+ throw Exception('Failed to get sample projects: $e');
+ }
+ }
+
+ /// Get detail of a sample project by name
+ ///
+ /// Calls: POST /api/method/building_material.building_material.api.sample_project.get_detail
+ /// Body: { "name": "PROJ-0001" }
+ /// Returns: Full project detail with files_list
+ @override
+ Future getSampleProjectDetail(String name) async {
+ try {
+ final response = await _dioClient.post>(
+ '${ApiConstants.frappeApiMethod}${ApiConstants.getSampleProjectDetail}',
+ data: {'name': name},
+ );
+
+ final data = response.data;
+ if (data == null) {
+ throw Exception('No data received from getSampleProjectDetail API');
+ }
+
+ // API returns: { "message": {...} }
+ final message = data['message'];
+ if (message == null) {
+ throw Exception('No message field in getSampleProjectDetail response');
+ }
+
+ return SampleProjectModel.fromJson(message as Map);
+ } catch (e) {
+ throw Exception('Failed to get sample project detail: $e');
+ }
+ }
+}
diff --git a/lib/features/showrooms/data/models/sample_project_model.dart b/lib/features/showrooms/data/models/sample_project_model.dart
new file mode 100644
index 0000000..6c6c3b0
--- /dev/null
+++ b/lib/features/showrooms/data/models/sample_project_model.dart
@@ -0,0 +1,134 @@
+/// Data Model: Sample Project Model
+///
+/// JSON serialization model for sample project API responses.
+library;
+
+import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
+
+/// Sample Project File Model
+///
+/// Handles JSON serialization for file attachments.
+class SampleProjectFileModel {
+ /// Unique file identifier (API: name)
+ final String name;
+
+ /// Full URL to the file (API: file_url)
+ final String fileUrl;
+
+ const SampleProjectFileModel({
+ required this.name,
+ required this.fileUrl,
+ });
+
+ /// Create model from JSON map
+ factory SampleProjectFileModel.fromJson(Map json) {
+ return SampleProjectFileModel(
+ name: json['name'] as String? ?? '',
+ fileUrl: json['file_url'] as String? ?? '',
+ );
+ }
+
+ /// Convert model to JSON map
+ Map toJson() {
+ return {
+ 'name': name,
+ 'file_url': fileUrl,
+ };
+ }
+
+ /// Convert to domain entity
+ SampleProjectFile toEntity() {
+ return SampleProjectFile(
+ id: name,
+ fileUrl: fileUrl,
+ );
+ }
+}
+
+/// Sample Project Model
+///
+/// Handles JSON serialization/deserialization for API communication.
+class SampleProjectModel {
+ /// Unique project identifier (API: name)
+ final String name;
+
+ /// Project display name (API: project_name)
+ final String projectName;
+
+ /// Project description/notes - may contain HTML (API: notes)
+ final String? notes;
+
+ /// URL to 360° view (API: link)
+ final String? link;
+
+ /// Thumbnail image URL (API: thumbnail)
+ final String? thumbnail;
+
+ /// List of attached files/images (API: files_list) - available in detail
+ final List filesList;
+
+ const SampleProjectModel({
+ required this.name,
+ required this.projectName,
+ this.notes,
+ this.link,
+ this.thumbnail,
+ this.filesList = const [],
+ });
+
+ /// Create model from JSON map
+ factory SampleProjectModel.fromJson(Map json) {
+ final filesListJson = json['files_list'] as List?;
+
+ return SampleProjectModel(
+ name: json['name'] as String? ?? '',
+ projectName: json['project_name'] as String? ?? '',
+ notes: json['notes'] as String?,
+ link: json['link'] as String?,
+ thumbnail: json['thumbnail'] as String?,
+ filesList: filesListJson != null
+ ? filesListJson
+ .map((f) => SampleProjectFileModel.fromJson(f as Map))
+ .toList()
+ : [],
+ );
+ }
+
+ /// Convert model to JSON map
+ Map toJson() {
+ return {
+ 'name': name,
+ 'project_name': projectName,
+ 'notes': notes,
+ 'link': link,
+ 'thumbnail': thumbnail,
+ 'files_list': filesList.map((f) => f.toJson()).toList(),
+ };
+ }
+
+ /// Convert to domain entity
+ SampleProject toEntity() {
+ return SampleProject(
+ id: name,
+ projectName: projectName,
+ description: notes,
+ viewUrl: link,
+ thumbnailUrl: thumbnail,
+ filesList: filesList.map((f) => f.toEntity()).toList(),
+ );
+ }
+
+ /// Create model from domain entity
+ factory SampleProjectModel.fromEntity(SampleProject entity) {
+ return SampleProjectModel(
+ name: entity.id,
+ projectName: entity.projectName,
+ notes: entity.description,
+ link: entity.viewUrl,
+ thumbnail: entity.thumbnailUrl,
+ filesList: entity.filesList
+ .map((f) => SampleProjectFileModel(name: f.id, fileUrl: f.fileUrl))
+ .toList(),
+ );
+ }
+}
diff --git a/lib/features/showrooms/data/repositories/sample_project_repository_impl.dart b/lib/features/showrooms/data/repositories/sample_project_repository_impl.dart
new file mode 100644
index 0000000..a6de9a4
--- /dev/null
+++ b/lib/features/showrooms/data/repositories/sample_project_repository_impl.dart
@@ -0,0 +1,41 @@
+/// Sample Project Repository Implementation
+///
+/// Implements the sample project repository interface.
+library;
+
+import 'package:worker/features/showrooms/data/datasources/sample_project_remote_datasource.dart';
+import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
+import 'package:worker/features/showrooms/domain/repositories/sample_project_repository.dart';
+
+/// Sample Project Repository Implementation
+class SampleProjectRepositoryImpl implements SampleProjectRepository {
+ const SampleProjectRepositoryImpl(this._remoteDataSource);
+
+ final SampleProjectRemoteDataSource _remoteDataSource;
+
+ @override
+ Future> getSampleProjects({
+ int limitStart = 0,
+ int limitPageLength = 0,
+ }) async {
+ try {
+ final models = await _remoteDataSource.getSampleProjects(
+ limitStart: limitStart,
+ limitPageLength: limitPageLength,
+ );
+ return models.map((model) => model.toEntity()).toList();
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ @override
+ Future getSampleProjectDetail(String name) async {
+ try {
+ final model = await _remoteDataSource.getSampleProjectDetail(name);
+ return model.toEntity();
+ } catch (e) {
+ rethrow;
+ }
+ }
+}
diff --git a/lib/features/showrooms/domain/entities/sample_project.dart b/lib/features/showrooms/domain/entities/sample_project.dart
new file mode 100644
index 0000000..ef7622f
--- /dev/null
+++ b/lib/features/showrooms/domain/entities/sample_project.dart
@@ -0,0 +1,89 @@
+/// Domain Entity: Sample Project
+///
+/// Represents a sample/model house project with 360° view.
+/// Based on API response from building_material.building_material.api.sample_project
+library;
+
+import 'package:equatable/equatable.dart';
+
+/// Project File Entity
+///
+/// Represents an uploaded file attached to a sample project.
+class SampleProjectFile extends Equatable {
+ /// Unique file identifier (API: name)
+ final String id;
+
+ /// Full URL to the file (API: file_url)
+ final String fileUrl;
+
+ const SampleProjectFile({
+ required this.id,
+ required this.fileUrl,
+ });
+
+ @override
+ List get props => [id, fileUrl];
+}
+
+/// Sample Project Entity
+///
+/// Contains information about a model house/sample project.
+/// API field mapping:
+/// - name -> id
+/// - project_name -> projectName
+/// - notes -> description (HTML content)
+/// - link -> viewUrl (360° viewer URL)
+/// - thumbnail -> thumbnailUrl
+/// - files_list -> filesList (detail only)
+class SampleProject extends Equatable {
+ /// Unique project identifier (API: name)
+ final String id;
+
+ /// Project display name (API: project_name)
+ final String projectName;
+
+ /// Project description/notes - may contain HTML (API: notes)
+ final String? description;
+
+ /// URL to 360° view (API: link)
+ final String? viewUrl;
+
+ /// Thumbnail image URL (API: thumbnail)
+ final String? thumbnailUrl;
+
+ /// List of attached files/images (API: files_list) - available in detail
+ final List filesList;
+
+ const SampleProject({
+ required this.id,
+ required this.projectName,
+ this.description,
+ this.viewUrl,
+ this.thumbnailUrl,
+ this.filesList = const [],
+ });
+
+ /// Check if project has 360° view available
+ bool get has360View => viewUrl != null && viewUrl!.isNotEmpty;
+
+ /// Get plain text description (strips HTML tags)
+ String get plainDescription {
+ if (description == null) return '';
+ // Simple HTML tag removal
+ return description!
+ .replaceAll(RegExp(r'<[^>]*>'), '')
+ .replaceAll(' ', ' ')
+ .trim();
+ }
+
+ /// Get all image URLs for gallery (from filesList)
+ List get imageUrls => filesList.map((f) => f.fileUrl).toList();
+
+ @override
+ List get props => [id, projectName, description, viewUrl, thumbnailUrl, filesList];
+
+ @override
+ String toString() {
+ return 'SampleProject(id: $id, projectName: $projectName, has360View: $has360View, filesCount: ${filesList.length})';
+ }
+}
diff --git a/lib/features/showrooms/domain/repositories/sample_project_repository.dart b/lib/features/showrooms/domain/repositories/sample_project_repository.dart
new file mode 100644
index 0000000..75ca89f
--- /dev/null
+++ b/lib/features/showrooms/domain/repositories/sample_project_repository.dart
@@ -0,0 +1,26 @@
+/// Sample Project Repository Interface
+///
+/// Defines contract for sample project data operations.
+library;
+
+import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
+
+/// Sample Project Repository
+///
+/// Repository interface for sample/model house project operations.
+abstract class SampleProjectRepository {
+ /// Get list of sample/model house projects
+ ///
+ /// Returns list of sample projects with 360° view links.
+ /// [limitStart] - Pagination offset
+ /// [limitPageLength] - Number of items per page (0 = all)
+ Future> getSampleProjects({
+ int limitStart = 0,
+ int limitPageLength = 0,
+ });
+
+ /// Get detail of a sample/model house project by name
+ ///
+ /// Returns full project detail with files_list for gallery.
+ Future getSampleProjectDetail(String name);
+}
diff --git a/lib/features/showrooms/presentation/pages/model_house_detail_page.dart b/lib/features/showrooms/presentation/pages/model_house_detail_page.dart
index 85741fe..05c2edb 100644
--- a/lib/features/showrooms/presentation/pages/model_house_detail_page.dart
+++ b/lib/features/showrooms/presentation/pages/model_house_detail_page.dart
@@ -12,6 +12,8 @@ import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
+import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
+import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
/// Model House Detail Page
class ModelHouseDetailPage extends ConsumerWidget {
@@ -24,8 +26,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
- // Mock data - in real app, fetch from provider
- final modelData = _getMockData(modelId);
+ final detailAsync = ref.watch(sampleProjectDetailProvider(modelId));
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
@@ -43,13 +44,16 @@ class ModelHouseDetailPage extends ConsumerWidget {
style: TextStyle(color: Colors.black),
),
actions: [
- IconButton(
- icon: const FaIcon(
- FontAwesomeIcons.shareNodes,
- color: Colors.black,
- size: 20,
+ detailAsync.maybeWhen(
+ data: (project) => IconButton(
+ icon: const FaIcon(
+ FontAwesomeIcons.shareNodes,
+ color: Colors.black,
+ size: 20,
+ ),
+ onPressed: () => _shareModel(context, project),
),
- onPressed: () => _shareModel(context, modelData),
+ orElse: () => const SizedBox.shrink(),
),
const SizedBox(width: AppSpacing.sm),
],
@@ -57,34 +61,69 @@ class ModelHouseDetailPage extends ConsumerWidget {
backgroundColor: AppColors.white,
centerTitle: false,
),
- body: SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // 360° View Launcher
- _build360ViewLauncher(context, modelData),
+ body: detailAsync.when(
+ data: (project) => SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 360° View Launcher
+ _build360ViewLauncher(context, project),
- const SizedBox(height: 16),
+ const SizedBox(height: 16),
- // Project Information
- _buildProjectInfo(modelData),
+ // Project Information
+ _buildProjectInfo(project),
- const SizedBox(height: 16),
+ const SizedBox(height: 16),
- // Image Gallery
- _buildImageGallery(context, modelData),
+ // Image Gallery
+ if (project.filesList.isNotEmpty)
+ _buildImageGallery(context, project),
- const SizedBox(height: 40),
- ],
+ const SizedBox(height: 40),
+ ],
+ ),
+ ),
+ loading: () => const Center(
+ child: CircularProgressIndicator(),
+ ),
+ error: (error, stack) => Center(
+ child: Padding(
+ padding: const EdgeInsets.all(40),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.error_outline,
+ size: 64,
+ color: AppColors.danger,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ fontSize: 14,
+ color: AppColors.grey500,
+ ),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: () => ref.invalidate(sampleProjectDetailProvider(modelId)),
+ child: const Text('Thử lại'),
+ ),
+ ],
+ ),
+ ),
),
),
);
}
- Widget _build360ViewLauncher(
- BuildContext context,
- Map modelData,
- ) {
+ Widget _build360ViewLauncher(BuildContext context, SampleProject project) {
+ final hasImages = project.filesList.isNotEmpty;
+ final firstImageUrl = hasImages ? project.filesList.first.fileUrl : project.thumbnailUrl;
+
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
@@ -100,7 +139,9 @@ class ModelHouseDetailPage extends ConsumerWidget {
child: Material(
color: Colors.transparent,
child: InkWell(
- onTap: () => _launch360View(context, modelData['url360'] as String),
+ onTap: project.has360View
+ ? () => _launch360View(context, project.viewUrl!)
+ : null,
borderRadius: BorderRadius.circular(12),
child: Container(
height: 400,
@@ -115,19 +156,20 @@ class ModelHouseDetailPage extends ConsumerWidget {
child: Stack(
children: [
// Background image with overlay
- Positioned.fill(
- child: ClipRRect(
- borderRadius: BorderRadius.circular(12),
- child: Opacity(
- opacity: 0.3,
- child: CachedNetworkImage(
- imageUrl: (modelData['images'] as List>)
- .first['url']!,
- fit: BoxFit.cover,
+ if (firstImageUrl != null)
+ Positioned.fill(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: Opacity(
+ opacity: 0.3,
+ child: CachedNetworkImage(
+ imageUrl: firstImageUrl,
+ fit: BoxFit.cover,
+ errorWidget: (context, url, error) => const SizedBox.shrink(),
+ ),
),
),
),
- ),
// Content
Center(
child: Column(
@@ -190,42 +232,62 @@ class ModelHouseDetailPage extends ConsumerWidget {
),
const SizedBox(height: 20),
// Launch Button
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 12,
- ),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(24),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withValues(alpha: 0.15),
- blurRadius: 12,
- offset: const Offset(0, 4),
- ),
- ],
- ),
- child: const Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- FaIcon(
- FontAwesomeIcons.play,
- size: 14,
- color: Color(0xFF667eea),
- ),
- SizedBox(width: 8),
- Text(
- 'Bắt đầu tham quan',
- style: TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w600,
+ if (project.has360View)
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 12,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(24),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.15),
+ blurRadius: 12,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ FaIcon(
+ FontAwesomeIcons.play,
+ size: 14,
color: Color(0xFF667eea),
),
+ SizedBox(width: 8),
+ Text(
+ 'Bắt đầu tham quan',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF667eea),
+ ),
+ ),
+ ],
+ ),
+ )
+ else
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 12,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(24),
+ ),
+ child: const Text(
+ 'Chưa có view 360°',
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ color: Colors.white,
),
- ],
+ ),
),
- ),
],
),
),
@@ -237,7 +299,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
);
}
- Widget _buildProjectInfo(Map modelData) {
+ Widget _buildProjectInfo(SampleProject project) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(20),
@@ -257,94 +319,32 @@ class ModelHouseDetailPage extends ConsumerWidget {
children: [
// Title
Text(
- modelData['title'] as String,
+ project.projectName,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
),
),
- const SizedBox(height: 16),
-
- // Specs Grid
- Row(
- children: [
- Expanded(
- child: _buildSpecItem(
- 'Diện tích',
- modelData['area'] as String,
- ),
+ if (project.plainDescription.isNotEmpty) ...[
+ const SizedBox(height: 16),
+ // Description
+ Text(
+ project.plainDescription,
+ style: const TextStyle(
+ fontSize: 14,
+ color: Color(0xFF4b5563),
+ height: 1.6,
),
- const SizedBox(width: 12),
- Expanded(
- child: _buildSpecItem(
- 'Địa điểm',
- modelData['location'] as String,
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: _buildSpecItem(
- 'Phong cách',
- modelData['style'] as String,
- ),
- ),
- ],
- ),
- const SizedBox(height: 20),
-
- // Description
- Text(
- modelData['description'] as String,
- style: const TextStyle(
- fontSize: 14,
- color: Color(0xFF4b5563),
- height: 1.6,
),
- ),
+ ],
],
),
);
}
- Widget _buildSpecItem(String label, String value) {
- return Container(
- padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
- decoration: BoxDecoration(
- color: const Color(0xFFF8FAFC),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Column(
- children: [
- Text(
- label.toUpperCase(),
- style: const TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w600,
- color: AppColors.grey500,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 4),
- Text(
- value,
- style: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w700,
- color: AppColors.grey900,
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- );
- }
-
- Widget _buildImageGallery(
- BuildContext context,
- Map modelData,
- ) {
- final images = modelData['images'] as List>;
+ Widget _buildImageGallery(BuildContext context, SampleProject project) {
+ final images = project.filesList;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
@@ -364,17 +364,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Gallery Title
- const Row(
+ Row(
children: [
- FaIcon(
+ const FaIcon(
FontAwesomeIcons.images,
size: 18,
color: AppColors.grey900,
),
- SizedBox(width: 8),
+ const SizedBox(width: 8),
Text(
- 'Thư viện Hình ảnh',
- style: TextStyle(
+ 'Thư viện Hình ảnh (${images.length})',
+ style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
@@ -402,7 +402,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
width: 120,
height: 120,
child: CachedNetworkImage(
- imageUrl: image['url']!,
+ imageUrl: image.fileUrl,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: AppColors.grey100,
@@ -442,7 +442,7 @@ class ModelHouseDetailPage extends ConsumerWidget {
void _showImageViewer(
BuildContext context,
- List> images,
+ List images,
int initialIndex,
) {
showDialog(
@@ -455,62 +455,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
);
}
- void _shareModel(BuildContext context, Map modelData) {
- Share.share(
- 'Xem mô hình 360° ${modelData['title']}\n${modelData['url360']}',
- subject: modelData['title'] as String,
- );
- }
-
- Map _getMockData(String modelId) {
- // Mock data - in real app, fetch from repository
- return {
- 'title': 'Căn hộ Studio',
- 'area': '35m²',
- 'location': 'Quận 7',
- 'style': 'Hiện đại',
- 'description':
- 'Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.',
- 'url360': 'https://vr.house3d.com/web/panorama-player/H00179549',
- 'images': [
- {
- 'url':
- 'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=600&fit=crop',
- 'caption': 'Phối cảnh tổng thể căn hộ studio với thiết kế hiện đại',
- },
- {
- 'url':
- 'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/main_img.jpg',
- 'caption': 'Khu vực phòng khách với gạch granite cao cấp',
- },
- {
- 'url':
- 'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_1.jpg?v=1',
- 'caption': 'Phòng ngủ chính với gạch ceramic màu trung tính',
- },
- {
- 'url':
- 'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_0.jpg?v=1',
- 'caption': 'Khu vực bếp với gạch mosaic điểm nhấn',
- },
- {
- 'url':
- 'https://images.unsplash.com/photo-1620626011761-996317b8d101?w=800&h=600&fit=crop',
- 'caption': 'Phòng tắm hiện đại với gạch chống thấm cao cấp',
- },
- {
- 'url':
- 'https://center.eurotile.vn/data/eurotileData/design/202009/23/4/project_img_3.jpg?v=1',
- 'caption': 'Khu vực bàn ăn ấm cúng',
- },
- ],
- };
+ void _shareModel(BuildContext context, SampleProject project) {
+ final shareText = project.has360View
+ ? 'Xem mô hình 360° ${project.projectName}\n${project.viewUrl}'
+ : 'Xem nhà mẫu ${project.projectName}';
+ SharePlus.instance.share(ShareParams(text: shareText));
}
}
/// Image Viewer Dialog with Swipe Navigation
class _ImageViewerDialog extends StatefulWidget {
- final List> images;
+ final List images;
final int initialIndex;
const _ImageViewerDialog({
@@ -561,7 +516,7 @@ class _ImageViewerDialogState extends State<_ImageViewerDialog> {
itemBuilder: (context, index) {
return Center(
child: CachedNetworkImage(
- imageUrl: widget.images[index]['url']!,
+ imageUrl: widget.images[index].fileUrl,
fit: BoxFit.contain,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Colors.white),
@@ -618,34 +573,6 @@ class _ImageViewerDialogState extends State<_ImageViewerDialog> {
),
),
),
-
- // Caption at bottom
- Positioned(
- bottom: 0,
- left: 0,
- right: 0,
- child: Container(
- padding: const EdgeInsets.all(20),
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [
- Colors.transparent,
- Colors.black.withValues(alpha: 0.7),
- ],
- ),
- ),
- child: Text(
- widget.images[_currentIndex]['caption']!,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 14,
- ),
- textAlign: TextAlign.center,
- ),
- ),
- ),
],
),
),
diff --git a/lib/features/showrooms/presentation/pages/model_houses_page.dart b/lib/features/showrooms/presentation/pages/model_houses_page.dart
index 6a6c50c..f01e0ce 100644
--- a/lib/features/showrooms/presentation/pages/model_houses_page.dart
+++ b/lib/features/showrooms/presentation/pages/model_houses_page.dart
@@ -10,6 +10,8 @@ import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
+import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
+import 'package:worker/features/showrooms/presentation/providers/sample_project_provider.dart';
/// Model Houses Page
///
@@ -160,76 +162,94 @@ class _ModelHousesPageState extends ConsumerState
}
/// Library Tab - Model house 360° library
-class _LibraryTab extends StatelessWidget {
+class _LibraryTab extends ConsumerWidget {
const _LibraryTab();
@override
- Widget build(BuildContext context) {
- return ListView(
- padding: const EdgeInsets.all(20),
- children: const [
- _LibraryCard(
- modelId: 'studio-01',
- imageUrl:
- 'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=800&h=200&fit=crop',
- title: 'Căn hộ Studio',
- date: '15/11/2024',
- description:
- 'Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa.',
- has360View: true,
+ Widget build(BuildContext context, WidgetRef ref) {
+ final sampleProjectsAsync = ref.watch(sampleProjectsListProvider);
+
+ return sampleProjectsAsync.when(
+ data: (projects) {
+ if (projects.isEmpty) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.all(40),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.home_work_outlined,
+ size: 64,
+ color: AppColors.grey500,
+ ),
+ SizedBox(height: 16),
+ Text(
+ 'Chưa có mẫu nhà nào',
+ style: TextStyle(
+ fontSize: 16,
+ color: AppColors.grey500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ return RefreshIndicator(
+ onRefresh: () => ref.read(sampleProjectsListProvider.notifier).refresh(),
+ child: ListView.builder(
+ padding: const EdgeInsets.all(20),
+ itemCount: projects.length,
+ itemBuilder: (context, index) {
+ final project = projects[index];
+ return _LibraryCard(project: project);
+ },
+ ),
+ );
+ },
+ loading: () => const Center(
+ child: CircularProgressIndicator(),
+ ),
+ error: (error, stack) => Center(
+ child: Padding(
+ padding: const EdgeInsets.all(40),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.error_outline,
+ size: 64,
+ color: AppColors.danger,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Lỗi tải dữ liệu: ${error.toString().replaceAll('Exception: ', '')}',
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ fontSize: 14,
+ color: AppColors.grey500,
+ ),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: () => ref.invalidate(sampleProjectsListProvider),
+ child: const Text('Thử lại'),
+ ),
+ ],
+ ),
),
- _LibraryCard(
- modelId: 'villa-01',
- imageUrl:
- 'https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=800&h=200&fit=crop',
- title: 'Biệt thự Hiện đại',
- date: '12/11/2024',
- description:
- 'Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.',
- has360View: true,
- ),
- _LibraryCard(
- modelId: 'townhouse-01',
- imageUrl:
- 'https://images.unsplash.com/photo-1562663474-6cbb3eaa4d14?w=800&h=200&fit=crop',
- title: 'Nhà phố Tối giản',
- date: '08/11/2024',
- description:
- 'Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính.',
- has360View: true,
- ),
- _LibraryCard(
- modelId: 'apartment-01',
- imageUrl:
- 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=200&fit=crop',
- title: 'Chung cư Cao cấp',
- date: '05/11/2024',
- description:
- 'Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy.',
- has360View: true,
- ),
- ],
+ ),
);
}
}
/// Library Card Widget
class _LibraryCard extends StatelessWidget {
- const _LibraryCard({
- required this.modelId,
- required this.imageUrl,
- required this.title,
- required this.date,
- required this.description,
- this.has360View = false,
- });
+ const _LibraryCard({required this.project});
- final String modelId;
- final String imageUrl;
- final String title;
- final String date;
- final String description;
- final bool has360View;
+ final SampleProject project;
@override
Widget build(BuildContext context) {
@@ -239,7 +259,7 @@ class _LibraryCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 20),
child: InkWell(
onTap: () {
- context.push('/model-houses/$modelId');
+ context.push('/model-houses/${project.id}');
},
borderRadius: BorderRadius.circular(12),
child: Column(
@@ -252,28 +272,38 @@ class _LibraryCard extends StatelessWidget {
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
- child: CachedNetworkImage(
- imageUrl: imageUrl,
- width: double.infinity,
- height: 200,
- fit: BoxFit.cover,
- placeholder: (context, url) => Container(
- height: 200,
- color: AppColors.grey100,
- child: const Center(child: CircularProgressIndicator()),
- ),
- errorWidget: (context, url, error) => Container(
- height: 200,
- color: AppColors.grey100,
- child: const Icon(
- Icons.image_not_supported,
- size: 48,
- color: AppColors.grey500,
- ),
- ),
- ),
+ child: project.thumbnailUrl != null
+ ? CachedNetworkImage(
+ imageUrl: project.thumbnailUrl!,
+ width: double.infinity,
+ height: 200,
+ fit: BoxFit.cover,
+ placeholder: (context, url) => Container(
+ height: 200,
+ color: AppColors.grey100,
+ child: const Center(child: CircularProgressIndicator()),
+ ),
+ errorWidget: (context, url, error) => Container(
+ height: 200,
+ color: AppColors.grey100,
+ child: const Icon(
+ Icons.image_not_supported,
+ size: 48,
+ color: AppColors.grey500,
+ ),
+ ),
+ )
+ : Container(
+ height: 200,
+ color: AppColors.grey100,
+ child: const Icon(
+ Icons.home_work,
+ size: 48,
+ color: AppColors.grey500,
+ ),
+ ),
),
- if (has360View)
+ if (project.has360View)
Positioned(
top: 12,
right: 12,
@@ -307,7 +337,7 @@ class _LibraryCard extends StatelessWidget {
children: [
// Title
Text(
- title,
+ project.projectName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
@@ -315,38 +345,20 @@ class _LibraryCard extends StatelessWidget {
),
),
- const SizedBox(height: 8),
-
- // Date
- Row(
- children: [
- const Icon(
- Icons.calendar_today,
- size: 14,
+ if (project.plainDescription.isNotEmpty) ...[
+ const SizedBox(height: 12),
+ // Description
+ Text(
+ project.plainDescription,
+ style: const TextStyle(
+ fontSize: 14,
color: AppColors.grey500,
+ height: 1.5,
),
- const SizedBox(width: 6),
- Text(
- 'Ngày đăng: $date',
- style: const TextStyle(
- fontSize: 14,
- color: AppColors.grey500,
- ),
- ),
- ],
- ),
-
- const SizedBox(height: 12),
-
- // Description
- Text(
- description,
- style: const TextStyle(
- fontSize: 14,
- color: AppColors.grey500,
- height: 1.5,
+ maxLines: 3,
+ overflow: TextOverflow.ellipsis,
),
- ),
+ ],
],
),
),
diff --git a/lib/features/showrooms/presentation/providers/sample_project_provider.dart b/lib/features/showrooms/presentation/providers/sample_project_provider.dart
new file mode 100644
index 0000000..ace06d2
--- /dev/null
+++ b/lib/features/showrooms/presentation/providers/sample_project_provider.dart
@@ -0,0 +1,58 @@
+/// Providers: Sample Project
+///
+/// Riverpod providers for managing sample/model house projects state.
+library;
+
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:worker/core/network/dio_client.dart';
+import 'package:worker/features/showrooms/data/datasources/sample_project_remote_datasource.dart';
+import 'package:worker/features/showrooms/data/repositories/sample_project_repository_impl.dart';
+import 'package:worker/features/showrooms/domain/entities/sample_project.dart';
+import 'package:worker/features/showrooms/domain/repositories/sample_project_repository.dart';
+
+part 'sample_project_provider.g.dart';
+
+/// Sample Project Remote Data Source Provider
+@riverpod
+Future sampleProjectRemoteDataSource(Ref ref) async {
+ final dioClient = await ref.watch(dioClientProvider.future);
+ return SampleProjectRemoteDataSourceImpl(dioClient);
+}
+
+/// Sample Project Repository Provider
+@riverpod
+Future sampleProjectRepository(Ref ref) async {
+ final remoteDataSource = await ref.watch(sampleProjectRemoteDataSourceProvider.future);
+ return SampleProjectRepositoryImpl(remoteDataSource);
+}
+
+/// Sample Projects List Provider
+///
+/// Fetches and manages sample/model house projects from API.
+@riverpod
+class SampleProjectsList extends _$SampleProjectsList {
+ @override
+ Future> build() async {
+ final repository = await ref.watch(sampleProjectRepositoryProvider.future);
+ return repository.getSampleProjects();
+ }
+
+ /// Refresh sample projects from remote
+ Future refresh() async {
+ state = const AsyncValue.loading();
+ state = await AsyncValue.guard(() async {
+ final repository = await ref.read(sampleProjectRepositoryProvider.future);
+ return repository.getSampleProjects();
+ });
+ }
+}
+
+/// Sample Project Detail Provider
+///
+/// Fetches detail of a specific sample project by name.
+/// Uses family modifier to cache by project name.
+@riverpod
+Future sampleProjectDetail(Ref ref, String name) async {
+ final repository = await ref.watch(sampleProjectRepositoryProvider.future);
+ return repository.getSampleProjectDetail(name);
+}
diff --git a/lib/features/showrooms/presentation/providers/sample_project_provider.g.dart b/lib/features/showrooms/presentation/providers/sample_project_provider.g.dart
new file mode 100644
index 0000000..f4c29e0
--- /dev/null
+++ b/lib/features/showrooms/presentation/providers/sample_project_provider.g.dart
@@ -0,0 +1,266 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'sample_project_provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+/// Sample Project Remote Data Source Provider
+
+@ProviderFor(sampleProjectRemoteDataSource)
+const sampleProjectRemoteDataSourceProvider =
+ SampleProjectRemoteDataSourceProvider._();
+
+/// Sample Project Remote Data Source Provider
+
+final class SampleProjectRemoteDataSourceProvider
+ extends
+ $FunctionalProvider<
+ AsyncValue,
+ SampleProjectRemoteDataSource,
+ FutureOr
+ >
+ with
+ $FutureModifier,
+ $FutureProvider {
+ /// Sample Project Remote Data Source Provider
+ const SampleProjectRemoteDataSourceProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'sampleProjectRemoteDataSourceProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$sampleProjectRemoteDataSourceHash();
+
+ @$internal
+ @override
+ $FutureProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $FutureProviderElement(pointer);
+
+ @override
+ FutureOr create(Ref ref) {
+ return sampleProjectRemoteDataSource(ref);
+ }
+}
+
+String _$sampleProjectRemoteDataSourceHash() =>
+ r'551677016d2d5d5185537f4871b161456370158e';
+
+/// Sample Project Repository Provider
+
+@ProviderFor(sampleProjectRepository)
+const sampleProjectRepositoryProvider = SampleProjectRepositoryProvider._();
+
+/// Sample Project Repository Provider
+
+final class SampleProjectRepositoryProvider
+ extends
+ $FunctionalProvider<
+ AsyncValue,
+ SampleProjectRepository,
+ FutureOr
+ >
+ with
+ $FutureModifier,
+ $FutureProvider {
+ /// Sample Project Repository Provider
+ const SampleProjectRepositoryProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'sampleProjectRepositoryProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$sampleProjectRepositoryHash();
+
+ @$internal
+ @override
+ $FutureProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $FutureProviderElement(pointer);
+
+ @override
+ FutureOr create(Ref ref) {
+ return sampleProjectRepository(ref);
+ }
+}
+
+String _$sampleProjectRepositoryHash() =>
+ r'a675cd70c32a2d9331992a1db3d5646a38e0c953';
+
+/// Sample Projects List Provider
+///
+/// Fetches and manages sample/model house projects from API.
+
+@ProviderFor(SampleProjectsList)
+const sampleProjectsListProvider = SampleProjectsListProvider._();
+
+/// Sample Projects List Provider
+///
+/// Fetches and manages sample/model house projects from API.
+final class SampleProjectsListProvider
+ extends $AsyncNotifierProvider> {
+ /// Sample Projects List Provider
+ ///
+ /// Fetches and manages sample/model house projects from API.
+ const SampleProjectsListProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'sampleProjectsListProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$sampleProjectsListHash();
+
+ @$internal
+ @override
+ SampleProjectsList create() => SampleProjectsList();
+}
+
+String _$sampleProjectsListHash() =>
+ r'1255814621d429e09c8e0cdec38fbc91bacc1c77';
+
+/// Sample Projects List Provider
+///
+/// Fetches and manages sample/model house projects from API.
+
+abstract class _$SampleProjectsList
+ extends $AsyncNotifier> {
+ FutureOr> build();
+ @$mustCallSuper
+ @override
+ void runBuild() {
+ final created = build();
+ final ref =
+ this.ref as $Ref>, List>;
+ final element =
+ ref.element
+ as $ClassProviderElement<
+ AnyNotifier>, List>,
+ AsyncValue>,
+ Object?,
+ Object?
+ >;
+ element.handleValue(ref, created);
+ }
+}
+
+/// Sample Project Detail Provider
+///
+/// Fetches detail of a specific sample project by name.
+/// Uses family modifier to cache by project name.
+
+@ProviderFor(sampleProjectDetail)
+const sampleProjectDetailProvider = SampleProjectDetailFamily._();
+
+/// Sample Project Detail Provider
+///
+/// Fetches detail of a specific sample project by name.
+/// Uses family modifier to cache by project name.
+
+final class SampleProjectDetailProvider
+ extends
+ $FunctionalProvider<
+ AsyncValue,
+ SampleProject,
+ FutureOr
+ >
+ with $FutureModifier, $FutureProvider {
+ /// Sample Project Detail Provider
+ ///
+ /// Fetches detail of a specific sample project by name.
+ /// Uses family modifier to cache by project name.
+ const SampleProjectDetailProvider._({
+ required SampleProjectDetailFamily super.from,
+ required String super.argument,
+ }) : super(
+ retry: null,
+ name: r'sampleProjectDetailProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$sampleProjectDetailHash();
+
+ @override
+ String toString() {
+ return r'sampleProjectDetailProvider'
+ ''
+ '($argument)';
+ }
+
+ @$internal
+ @override
+ $FutureProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $FutureProviderElement(pointer);
+
+ @override
+ FutureOr create(Ref ref) {
+ final argument = this.argument as String;
+ return sampleProjectDetail(ref, argument);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is SampleProjectDetailProvider && other.argument == argument;
+ }
+
+ @override
+ int get hashCode {
+ return argument.hashCode;
+ }
+}
+
+String _$sampleProjectDetailHash() =>
+ r'6ea1bf329f69e0274df7f072ef1494ed04c3238d';
+
+/// Sample Project Detail Provider
+///
+/// Fetches detail of a specific sample project by name.
+/// Uses family modifier to cache by project name.
+
+final class SampleProjectDetailFamily extends $Family
+ with $FunctionalFamilyOverride, String> {
+ const SampleProjectDetailFamily._()
+ : super(
+ retry: null,
+ name: r'sampleProjectDetailProvider',
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ isAutoDispose: true,
+ );
+
+ /// Sample Project Detail Provider
+ ///
+ /// Fetches detail of a specific sample project by name.
+ /// Uses family modifier to cache by project name.
+
+ SampleProjectDetailProvider call(String name) =>
+ SampleProjectDetailProvider._(argument: name, from: this);
+
+ @override
+ String toString() => r'sampleProjectDetailProvider';
+}