sample project
This commit is contained in:
@@ -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<String, dynamic> 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<Map<String, String>>)
|
||||
.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<String, dynamic> 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<String, dynamic> modelData,
|
||||
) {
|
||||
final images = modelData['images'] as List<Map<String, String>>;
|
||||
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<Map<String, String>> images,
|
||||
List<SampleProjectFile> images,
|
||||
int initialIndex,
|
||||
) {
|
||||
showDialog<void>(
|
||||
@@ -455,62 +455,17 @@ class ModelHouseDetailPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _shareModel(BuildContext context, Map<String, dynamic> modelData) {
|
||||
Share.share(
|
||||
'Xem mô hình 360° ${modelData['title']}\n${modelData['url360']}',
|
||||
subject: modelData['title'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _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<Map<String, String>> images;
|
||||
final List<SampleProjectFile> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<ModelHousesPage>
|
||||
}
|
||||
|
||||
/// 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user