Compare commits

..

2 Commits

Author SHA1 Message Date
Phuoc Nguyen
4738553d2e fix 2025-11-14 11:50:40 +07:00
Phuoc Nguyen
0093b62c29 update 2025-11-11 16:02:09 +07:00
10 changed files with 1387 additions and 571 deletions

View File

@@ -29,8 +29,8 @@
<div class="product-detail-content">
<!-- Image Gallery Section -->
<div class="product-gallery-section">
<div class="main-image-container" >
<img id="mainImage" onclick="openLightbox(0)" src="https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-1.jpg" alt="Gạch Eurotile MỘC LAM E03" class="main-product-image">
<div class="main-image-container" onclick="openLightbox(0)">
<img id="mainImage" src="https://placehold.co/400x400/F5F5F5/005B9A/png?text=Gạch+Eurotile+MỘC+LAM+E03" alt="Gạch Eurotile MỘC LAM E03" class="main-product-image">
<!-- 360° Button overlay -->
<button class="view-360-btn-overlay" onclick="view360Product()" title="Xem sản phẩm 360°">
@@ -50,32 +50,32 @@
<!-- Thumbnail row -->
<div class="thumbnail-gallery">
<div class="thumbnail active" onclick="changeImage(0, this)">
<img src="https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-1.jpg" alt="Thumbnail 1">
<img src="https://placehold.co/80x80/F5F5F5/005B9A/png?text=1" alt="Thumbnail 1">
</div>
<div class="thumbnail" onclick="changeImage(1, this)">
<img src="https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-2.jpg" alt="Thumbnail 2">
<img src="https://placehold.co/80x80/E8E8E8/005B9A/png?text=2" alt="Thumbnail 2">
</div>
<div class="thumbnail" onclick="changeImage(2, this)">
<img src="https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-3.jpg" alt="Thumbnail 3">
<img src="https://placehold.co/80x80/DDDDDD/005B9A/png?text=3" alt="Thumbnail 3">
</div>
<div class="thumbnail" onclick="changeImage(3, this)">
<img src="https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-4.jpg" alt="Thumbnail 4">
<img src="https://placehold.co/80x80/D2D2D2/005B9A/png?text=4" alt="Thumbnail 4">
</div>
</div>
</div>
<!-- Product Information Section -->
<div class="product-info-section">
<div class="product-sku">SKU: CAT S01G</div>
<h1 class="product-title">Gạch Cát Tường 1200x1200</h1>
<div class="product-sku">SKU: ET-ML-E03-60x60</div>
<h1 class="product-title">Gạch Eurotile MỘC LAM E03</h1>
<div class="product-pricing">
<span class="current-price">285.000 VND/m²</span>
<!--<span class="original-price">320.000 VND/m²</span>
<span class="discount-badge">-11%</span>-->
<span class="original-price">320.000 VND/m²</span>
<span class="discount-badge">-11%</span>
</div>
<!-- Rating & Reviews -->
<!--<div class="rating-section">
<!-- Rating & Reviews -->
<div class="rating-section">
<div class="rating-stars">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
@@ -84,57 +84,17 @@
<i class="fas fa-star-half-alt"></i>
</div>
<span class="rating-text">4.8 (125 đánh giá)</span>
</div>-->
<div class="quick-info">
<div class="info-item">
<!--<i class="fas fa-cube info-icon"></i>-->
<i class="fas fa-expand info-icon"></i>
<div class="info-label">Kích thước</div>
<div class="info-value">1200x1200</div>
</div>
<div class="info-item">
<i class="fas fa-cube info-icon"></i>
<div class="info-label">Đóng gói</div>
<div class="info-value">2 viên/thùng</div>
</div>
<div class="info-item">
<i class="fas fa-truck info-icon"></i>
<!--<i class="fas fa-box-open info-icon"></i>
<!--<i class="fas fa-pallet info-icon"></i>-->
<div class="info-label">Giao hàng</div>
<div class="info-value">2-3 Ngày</div>
</div>
</div>
</div>
<!-- Product Tabs Section -->
<div class="product-tabs-section">
<div class="tab-navigation">
<!--<button class="tab-button" onclick="switchTab('description', this)">Mô tả</button>-->
<button class="tab-button active" onclick="switchTab('specifications', this)">Thông số</button>
<button class="tab-button" onclick="switchTab('reviews', this)">Đánh giá</button>
</div>
<!-- Tab Contents -->
<!--<div class="tab-content" id="description">
<div class="tab-content-wrapper">
<h3>Bộ sưu tập Mộc Lam</h3>
<p>Gạch granite Eurotile MỘC LAM E03 lấy cảm hứng từ vẻ đẹp tự nhiên của gỗ tự nhiên, mang đến không gian ấm cúng và gần gũi. Với bề mặt có texture tinh tế, sản phẩm tạo nên những đường vân gỗ tự nhiên chân thực.</p>
<h4>Đặc điểm nổi bật:</h4>
<ul class="feature-list">
<li><i class="fas fa-check"></i>Bề mặt chống trầy xước cao</li>
<li><i class="fas fa-check"></i>Khả năng chống thấm nước tốt</li>
<li><i class="fas fa-check"></i>Màu sắc bền đẹp theo thời gian</li>
<li><i class="fas fa-check"></i>Dễ dàng vệ sinh và bảo trì</li>
<li><i class="fas fa-check"></i>Thân thiện với môi trường</li>
</ul>
<h4>Ứng dụng:</h4>
<p>Phù hợp cho phòng khách, phòng ngủ, hành lang, văn phòng và các không gian thương mại. Đặc biệt phù hợp với phong cách nội thất hiện đại, tối giản và Scandinavian.</p>
</div>
</div>-->
<div class="tab-content active" id="specifications">
<div class="specifications-table">
<div class="spec-row">
@@ -149,6 +109,10 @@
<div class="spec-label">Bề mặt</div>
<div class="spec-value">Matt (Nhám)</div>
</div>
<div class="spec-row">
<div class="spec-label">Loại men</div>
<div class="spec-value">Granite kỹ thuật số</div>
</div>
<div class="spec-row">
<div class="spec-label">Độ hấp thụ nước</div>
<div class="spec-value">< 0.5%</div>
@@ -161,6 +125,14 @@
<div class="spec-label">Chức năng</div>
<div class="spec-value">Lát nền, Ốp tường</div>
</div>
<div class="spec-row">
<div class="spec-label">Xuất xứ</div>
<div class="spec-value">Việt Nam</div>
</div>
<div class="spec-row">
<div class="spec-label">Bảo hành</div>
<div class="spec-value">15 năm</div>
</div>
<div class="spec-row">
<div class="spec-label">Tiêu chuẩn</div>
<div class="spec-value">TCVN 9081:2012, ISO 13006</div>
@@ -169,60 +141,167 @@
</div>
<div class="tab-content" id="reviews">
<!-- Phần 1: Tổng quan Xếp hạng -->
<div class="reviews-summary">
<div class="rating-overview">
<div class="rating-score">4.8</div>
<div class="rating-score-large">4.5</div>
<div class="rating-details">
<div class="rating-stars-large">
<div class="rating-stars-display">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star-half-alt"></i>
</div>
<div class="rating-count">125 đánh giá</div>
<div class="rating-count-text">từ 23 đánh giá</div>
</div>
</div>
<!-- Star Distribution Bars -->
<!-- <div class="star-distribution">
<div class="distribution-row">
<div class="stars-label">5 <i class="fas fa-star"></i></div>
<div class="distribution-bar-container">
<div class="distribution-bar" style="width: 80%;"></div>
</div>
<div class="distribution-percent">80%</div>
</div>
<div class="distribution-row">
<div class="stars-label">4 <i class="fas fa-star"></i></div>
<div class="distribution-bar-container">
<div class="distribution-bar" style="width: 15%;"></div>
</div>
<div class="distribution-percent">15%</div>
</div>
<div class="distribution-row">
<div class="stars-label">3 <i class="fas fa-star"></i></div>
<div class="distribution-bar-container">
<div class="distribution-bar" style="width: 5%;"></div>
</div>
<div class="distribution-percent">5%</div>
</div>
<div class="distribution-row">
<div class="stars-label">2 <i class="fas fa-star"></i></div>
<div class="distribution-bar-container">
<div class="distribution-bar" style="width: 0%;"></div>
</div>
<div class="distribution-percent">0%</div>
</div>
<div class="distribution-row">
<div class="stars-label">1 <i class="fas fa-star"></i></div>
<div class="distribution-bar-container">
<div class="distribution-bar" style="width: 0%;"></div>
</div>
<div class="distribution-percent">0%</div>
</div>
</div>-->
</div>
<!-- Phần 2: Nút "Viết đánh giá" (Hiển thị nếu chưa có đánh giá pending) -->
<div class="write-review-section" id="writeReviewSection">
<button class="btn-write-review" onclick="goToWriteReview()">
<i class="fas fa-edit"></i>
Viết đánh giá của bạn
</button>
</div>
<!-- Phần 3: Card đánh giá đang chờ duyệt (Ẩn mặc định) -->
<div class="pending-review-notice" id="pendingReviewNotice" style="display: none;">
<div class="review-item pending-review-item">
<div class="pending-badge">
<i class="fas fa-clock"></i>
Đang chờ duyệt
</div>
<div class="reviewer-info">
<div class="reviewer-avatar">
<i class="fas fa-user"></i>
</div>
<div class="reviewer-details">
<div class="reviewer-name">Nguyễn Văn A (Bạn)</div>
<div class="review-date">Hôm nay</div>
</div>
</div>
<div class="review-rating">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<div class="review-title">Sản phẩm rất tốt, giao hàng nhanh</div>
<p class="review-text">Chất lượng gạch tuyệt vời, màu sắc đẹp và đúng như mô tả. Đội ngũ giao hàng chuyên nghiệp. Sẽ ủng hộ lâu dài!</p>
<div class="pending-review-note">
<i class="fas fa-info-circle"></i>
Đánh giá của bạn sẽ được hiển thị sau khi Admin xem xét và phê duyệt.
</div>
</div>
</div>
<div class="review-item">
<div class="reviewer-info">
<div class="reviewer-avatar">
<i class="fas fa-user"></i>
<!-- Phần 4: Danh sách đánh giá đã duyệt -->
<div class="reviews-list">
<h4 class="reviews-list-title">Đánh giá từ khách hàng</h4>
<div class="review-item">
<div class="reviewer-info">
<div class="reviewer-avatar">
<i class="fas fa-user"></i>
</div>
<div class="reviewer-details">
<div class="reviewer-name">Trần Văn B</div>
<div class="review-date">2 tuần trước</div>
</div>
</div>
<div class="reviewer-details">
<div class="reviewer-name">Nguyễn Văn A</div>
<div class="review-date">2 tuần trước</div>
<div class="review-rating">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<div class="review-title">Chất lượng xuất sắc!</div>
<p class="review-text">Sản phẩm chất lượng tốt, màu sắc đẹp và dễ lắp đặt. Rất hài lòng với lựa chọn này cho ngôi nhà của gia đình. Giao hàng nhanh và đóng gói cẩn thận.</p>
</div>
<div class="review-rating">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="review-text">Sản phẩm chất lượng tốt, màu sắc đẹp và dễ lắp đặt. Rất hài lòng với lựa chọn này cho ngôi nhà của gia đình.</p>
</div>
<div class="review-item">
<div class="reviewer-info">
<div class="reviewer-avatar">
<i class="fas fa-user"></i>
<div class="review-item">
<div class="reviewer-info">
<div class="reviewer-avatar">
<i class="fas fa-user"></i>
</div>
<div class="reviewer-details">
<div class="reviewer-name">Lê Thị C</div>
<div class="review-date">1 tháng trước</div>
</div>
</div>
<div class="reviewer-details">
<div class="reviewer-name">Trần Thị B</div>
<div class="review-date">1 tháng trước</div>
<div class="review-rating">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="far fa-star"></i>
</div>
<div class="review-title">Đẹp và chất lượng tốt</div>
<p class="review-text">Gạch đẹp, vân gỗ rất chân thực. Giao hàng nhanh chóng và đóng gói cẩn thận. Giá cả hợp lý so với chất lượng.</p>
</div>
<div class="review-rating">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="far fa-star"></i>
<div class="review-item">
<div class="reviewer-info">
<div class="reviewer-avatar">
<i class="fas fa-user"></i>
</div>
<div class="reviewer-details">
<div class="reviewer-name">Phạm Minh D</div>
<div class="review-date">2 tháng trước</div>
</div>
</div>
<div class="review-rating">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
</div>
<p class="review-text">Sản phẩm tốt, đúng mô tả. Tư vấn nhiệt tình. Sẽ giới thiệu cho bạn bè.</p>
</div>
<p class="review-text">Gạch đẹp, vân gỗ rất chân thực. Giao hàng nhanh chóng và đóng gói cẩn thận.</p>
</div>
</div>
</div>
@@ -230,24 +309,9 @@
<!-- Sticky Action Bar -->
<div class="sticky-action-bar">
<!--<div class="quantity-controls">
<button class="qty-btn" onclick="decreaseQuantity()" id="decreaseBtn">
<i class="fas fa-minus"></i>
</button>
<input type="number" class="qty-input" value="1" min="1" id="quantityInput" onchange="updateQuantity()">
<label class="quantity-label">(m²)</label>
<button class="qty-btn" onclick="increaseQuantity()" id="increaseBtn">
<i class="fas fa-plus"></i>
</button>
</div>
<div class="conversion-text" id="conversionText">
Tương đương: 3 viên / 1.08 m²
</div>-->
<div class="quantity-section">
<label class="quantity-label">Số lượng (m²)</label>
<div class="quantity-controls" style="width: 142px;">
<div class="quantity-controls">
<button class="qty-btn" onclick="decreaseQuantity()" id="decreaseBtn">
<i class="fas fa-minus"></i>
</button>
@@ -257,9 +321,9 @@
</button>
</div>
<div class="conversion-text" id="conversionText">
Tương đương: 3 viên / 1.08 m²
</div>
Tương đương: 3 hộp / 12 viên
</div>
</div>
<button class="add-to-cart-btn" onclick="addToCart()">
<i class="fas fa-shopping-cart"></i>
<span>Thêm vào giỏ hàng</span>
@@ -679,6 +743,184 @@
color: var(--text-dark);
}
/* Rating Overview - Enhanced */
.rating-score-large {
font-size: 48px;
font-weight: 700;
color: var(--primary-blue);
line-height: 1;
}
.rating-stars-display {
display: flex;
gap: 4px;
margin-bottom: 6px;
}
.rating-stars-display i {
color: #ffc107;
font-size: 20px;
}
.rating-count-text {
font-size: 14px;
color: var(--text-light);
}
/* Star Distribution */
.star-distribution {
margin-top: 20px;
}
.distribution-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.stars-label {
font-size: 14px;
font-weight: 500;
color: var(--text-dark);
min-width: 40px;
display: flex;
align-items: center;
gap: 4px;
}
.stars-label i {
color: #ffc107;
font-size: 12px;
}
.distribution-bar-container {
flex: 1;
height: 8px;
background: var(--border-color);
border-radius: 4px;
overflow: hidden;
}
.distribution-bar {
height: 100%;
background: linear-gradient(90deg, #ffc107 0%, #ff9800 100%);
border-radius: 4px;
transition: width 0.3s ease;
}
.distribution-percent {
font-size: 13px;
font-weight: 500;
color: var(--text-light);
min-width: 40px;
text-align: right;
}
/* Write Review Section */
.write-review-section {
margin-bottom: 24px;
/* padding: 20px;*/
/* background: var(--background-gray); */
border-radius: 12px;
text-align: center;
}
.btn-write-review {
background: var(--primary-blue);
color: white;
border: none;
padding: 14px 28px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: background 0.3s ease;
}
.btn-write-review:hover {
background: #004578;
}
.btn-write-review i {
font-size: 16px;
}
/* Pending Review Notice */
.pending-review-notice {
margin-bottom: 24px;
}
.pending-review-item {
background: #fffbf0;
border: 2px solid #ffc107;
border-radius: 12px;
padding: 16px;
position: relative;
}
.pending-badge {
position: absolute;
top: 12px;
right: 12px;
background: #ffc107;
color: #856404;
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
}
.pending-badge i {
font-size: 11px;
}
.review-title {
font-size: 15px;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 8px;
}
.pending-review-note {
margin-top: 12px;
padding: 12px;
background: rgba(255, 193, 7, 0.1);
border-left: 3px solid #ffc107;
border-radius: 6px;
font-size: 13px;
color: #856404;
display: flex;
align-items: flex-start;
gap: 8px;
line-height: 1.5;
}
.pending-review-note i {
margin-top: 2px;
flex-shrink: 0;
}
/* Reviews List */
.reviews-list {
margin-top: 24px;
}
.reviews-list-title {
font-size: 16px;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border-color);
}
/* Sticky Action Bar */
.sticky-action-bar {
position: fixed;
@@ -695,14 +937,6 @@
z-index: 100;
}
/*.quantity-controls {
display: flex;
align-items: center;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}*/
.quantity-section {
display: flex;
flex-direction: column;
@@ -715,6 +949,13 @@
font-weight: 500;
}
.conversion-text {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
text-align: center;
}
.quantity-controls {
display: flex;
align-items: center;
@@ -722,6 +963,7 @@
border-radius: 8px;
overflow: hidden;
}
.qty-btn {
width: 40px;
height: 40px;
@@ -935,20 +1177,6 @@
margin: 0 10px;
}
}
.quantity-label {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.conversion-text {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
text-align: center;
}
</style>
<script>
@@ -960,17 +1188,17 @@
let touchEndX = 0;
const images = [
"https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-1.jpg",
"https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-2.jpg",
"https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-3.jpg",
"https://www.eurotile.vn/pictures/catalog/product/0-gachkholon/cat-tuong/CAT-S01G-4.jpg"
"https://placehold.co/400x400/F5F5F5/005B9A/png?text=Gạch+Eurotile+MỘC+LAM+E03",
"https://placehold.co/400x400/E8E8E8/005B9A/png?text=Chi+tiết+texture",
"https://placehold.co/400x400/DDDDDD/005B9A/png?text=Ứng+dụng+thực+tế",
"https://placehold.co/400x400/D2D2D2/005B9A/png?text=Góc+độ+khác"
];
const imageCaptions = [
"Face A",
"Face B",
"Face C",
"Face D"
"Ảnh phối cảnh dòng Mộc Lam với texture gỗ tự nhiên chân thực",
"Chi tiết texture bề mặt với độ nhám tinh tế, chống trượt an toàn",
"Ứng dụng thực tế trong không gian phòng khách hiện đại",
"Góc độ khác cho thấy độ bền màu và chất lượng sản phẩm"
];
function changeImage(index, thumbnail) {
@@ -1018,11 +1246,21 @@
if (newQuantity >= 1) {
quantity = newQuantity;
updateQuantityButtons();
updateConversion();
} else {
input.value = quantity;
}
}
function updateConversion() {
// Example conversion: each m² = 0.36 boxes, each box = 4 pieces
const boxes = Math.ceil(quantity / 2.78); // Round up for boxes needed
const pieces = boxes * 4;
document.getElementById('conversionText').textContent =
`Tương đương: ${boxes} hộp / ${pieces} viên`;
}
function updateQuantityButtons() {
const decreaseBtn = document.getElementById('decreaseBtn');
decreaseBtn.disabled = quantity <= 1;
@@ -1113,7 +1351,7 @@
}
function view360Product() {
window.location.href = 'https://design.eurotile.vn/pub/tool/panorama/show?obsPlanId=3FO3H1VE59R5&locale=en_US&_gl=1*1udzqeo*_gcl_au*MTI3NjIxMzY1NS4xNzU5NzE2Mjg5';
window.location.href = 'product-view-360.html';
}
function addToCart() {
@@ -1209,9 +1447,55 @@
}
// Initialize
// Review Functions
function goToWriteReview() {
// Get product info from page
const productName = document.querySelector('.product-name').textContent;
const productImage = document.getElementById('mainImage').src;
// Store in localStorage for write-review page
localStorage.setItem('reviewProduct', JSON.stringify({
name: productName,
image: productImage,
id: 'ET-ML-E03' // In production, this would be dynamic
}));
// Navigate to write review page
window.location.href = 'write-review.html';
}
// Demo function to toggle pending review display
function togglePendingReview(hasPendingReview) {
const writeReviewSection = document.getElementById('writeReviewSection');
const pendingReviewNotice = document.getElementById('pendingReviewNotice');
if (hasPendingReview) {
writeReviewSection.style.display = 'none';
pendingReviewNotice.style.display = 'block';
} else {
writeReviewSection.style.display = 'block';
pendingReviewNotice.style.display = 'none';
}
}
// Check if user has pending review on page load
// In production, this would come from API
const userHasPendingReview = false; // Change to true to test pending review UI
document.addEventListener('DOMContentLoaded', function() {
updateQuantityButtons();
// Initialize review section
togglePendingReview(userHasPendingReview);
// Check if returning from review submission
const reviewSubmitted = localStorage.getItem('reviewJustSubmitted');
if (reviewSubmitted === 'true') {
// Show pending review
togglePendingReview(true);
localStorage.removeItem('reviewJustSubmitted');
}
// Close lightbox with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && document.getElementById('lightbox').classList.contains('active')) {
@@ -1237,15 +1521,6 @@
}
});
});
function updateConversion() {
// Example conversion: each m² = 0.36 boxes, each box = 4 pieces
const pieces = Math.ceil(quantity / 0.36); // Round up for boxes needed
const dientich = parseFloat((pieces * 0.36).toFixed(2));
document.getElementById('conversionText').textContent =
`Tương đương: ${pieces} viên / ${dientich}`;
}
</script>
</body>
</html>

522
html/write-review.html Normal file
View File

@@ -0,0 +1,522 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Viết đánh giá sản phẩm - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="product-detail-1.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Viết đánh giá sản phẩm</h1>
<div style="width: 40px;"></div>
</div>
<div class="container" style="padding-bottom: 120px;">
<!-- Product Card (Read-only) -->
<div class="product-review-card">
<img id="productImage"
src="https://images.unsplash.com/photo-1615971677499-5467cbab01c0?w=100&h=100&fit=crop"
alt="Product"
class="product-review-image">
<div class="product-review-info">
<h3 id="productName" class="product-review-name">Gạch Eurotile MỘC LAM E03</h3>
<p class="product-review-code">Mã: ET-ML-E03</p>
</div>
</div>
<!-- Review Form -->
<form id="reviewForm" onsubmit="submitReview(event)">
<!-- Rating Section (Required) -->
<div class="form-section">
<h3 class="section-title required-field">Xếp hạng của bạn</h3>
<p class="section-subtitle">Bấm vào ngôi sao để chọn đánh giá</p>
<div class="star-rating-selector" id="starRatingSelector">
<button type="button" class="star-btn" data-rating="1" onclick="selectRating(1)">
<i class="far fa-star"></i>
</button>
<button type="button" class="star-btn" data-rating="2" onclick="selectRating(2)">
<i class="far fa-star"></i>
</button>
<button type="button" class="star-btn" data-rating="3" onclick="selectRating(3)">
<i class="far fa-star"></i>
</button>
<button type="button" class="star-btn" data-rating="4" onclick="selectRating(4)">
<i class="far fa-star"></i>
</button>
<button type="button" class="star-btn" data-rating="5" onclick="selectRating(5)">
<i class="far fa-star"></i>
</button>
</div>
<div class="rating-label-container">
<span id="ratingLabel" class="rating-label">Chưa chọn đánh giá</span>
</div>
<input type="hidden" id="ratingValue" name="rating" required>
<div id="ratingError" class="error-message" style="display: none;">
Vui lòng chọn số sao đánh giá
</div>
</div>
<!-- Review Title (Optional) -->
<!--<div class="form-section">
<label for="reviewTitle" class="section-title">Tiêu đề đánh giá</label>
<input type="text"
id="reviewTitle"
name="title"
class="form-input"
placeholder="VD: Sản phẩm chất lượng tốt"
maxlength="100">
<div class="input-hint">Không bắt buộc • Tối đa 100 ký tự</div>
</div>-->
<!-- Review Content (Required) -->
<div class="form-section">
<label for="reviewContent" class="section-title required-field">Nội dung đánh giá</label>
<textarea id="reviewContent"
name="content"
class="form-textarea"
placeholder="Chia sẻ trải nghiệm của bạn về sản phẩm này..."
rows="6"
required
minlength="20"
maxlength="1000"></textarea>
<div class="textarea-counter">
<span id="charCount">0</span> / 1000 ký tự
</div>
<div id="contentError" class="error-message" style="display: none;">
Nội dung đánh giá phải có ít nhất 20 ký tự
</div>
</div>
<!-- Guidelines -->
<div class="review-guidelines">
<div class="guideline-header">
<i class="fas fa-lightbulb"></i>
<span>Gợi ý viết đánh giá tốt</span>
</div>
<ul class="guideline-list">
<li>Chia sẻ trải nghiệm thực tế của bạn về sản phẩm</li>
<li>Đề cập đến chất lượng, màu sắc, độ bền của sản phẩm</li>
<li>Nêu rõ điểm tốt và điểm chưa tốt (nếu có)</li>
<li>Tránh spam, nội dung không phù hợp hoặc vi phạm</li>
</ul>
</div>
<!-- Submit Button -->
<div class="fixed-bottom-action">
<button type="submit" class="btn-submit-review" id="submitBtn">
<i class="fas fa-paper-plane"></i>
Gửi đánh giá
</button>
</div>
</form>
</div>
</div>
<style>
:root {
--primary-blue: #005B9A;
--star-gold: #ffc107;
--star-selected: #ff9800;
--text-dark: #333;
--text-light: #666;
--text-muted: #999;
--border-color: #e0e0e0;
--background-gray: #f8f9fa;
--success-color: #28a745;
--danger-color: #dc3545;
--white: #ffffff;
}
/* Product Review Card */
.product-review-card {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: var(--white);
border: 2px solid var(--border-color);
border-radius: 12px;
margin-bottom: 24px;
}
.product-review-image {
width: 80px;
height: 80px;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
}
.product-review-info {
flex: 1;
}
.product-review-name {
font-size: 16px;
font-weight: 600;
color: var(--text-dark);
margin: 0 0 6px 0;
line-height: 1.4;
}
.product-review-code {
font-size: 13px;
color: var(--text-muted);
margin: 0;
}
/* Form Sections */
.form-section {
margin-bottom: 28px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 8px;
display: block;
}
.section-title.required-field::after {
content: ' *';
color: var(--danger-color);
}
.section-subtitle {
font-size: 14px;
color: var(--text-light);
margin-bottom: 16px;
}
/* Star Rating Selector */
.star-rating-selector {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 12px;
}
.star-btn {
background: none;
border: none;
padding: 8px;
cursor: pointer;
transition: transform 0.2s ease;
}
.star-btn:hover {
transform: scale(1.1);
}
.star-btn:active {
transform: scale(0.95);
}
.star-btn i {
font-size: 36px;
color: var(--border-color);
transition: color 0.2s ease;
}
.star-btn:hover i {
color: var(--star-gold);
}
.star-btn.selected i {
color: var(--star-selected);
}
.star-btn.selected:hover i {
color: var(--star-gold);
}
/* Rating Label */
.rating-label-container {
text-align: center;
margin-top: 12px;
}
.rating-label {
font-size: 15px;
font-weight: 600;
color: var(--text-light);
padding: 8px 20px;
background: var(--background-gray);
border-radius: 20px;
display: inline-block;
}
.rating-label.selected {
color: var(--star-selected);
background: #fff3e0;
}
/* Form Inputs */
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 15px;
color: var(--text-dark);
transition: border-color 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary-blue);
}
.form-textarea {
width: 100%;
padding: 14px 16px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 15px;
color: var(--text-dark);
resize: vertical;
font-family: inherit;
line-height: 1.6;
transition: border-color 0.3s ease;
}
.form-textarea:focus {
outline: none;
border-color: var(--primary-blue);
}
/* Input Hints */
.input-hint {
font-size: 13px;
color: var(--text-muted);
margin-top: 6px;
}
.textarea-counter {
font-size: 13px;
color: var(--text-muted);
text-align: right;
margin-top: 6px;
}
/* Error Messages */
.error-message {
font-size: 13px;
color: var(--danger-color);
margin-top: 6px;
display: flex;
align-items: center;
gap: 4px;
}
.error-message::before {
content: '⚠';
font-size: 14px;
}
/* Guidelines */
.review-guidelines {
padding: 16px;
background: #f0f7ff;
border-left: 4px solid var(--primary-blue);
border-radius: 8px;
margin-bottom: 24px;
}
.guideline-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: var(--primary-blue);
margin-bottom: 12px;
}
.guideline-header i {
font-size: 16px;
}
.guideline-list {
margin: 0;
padding-left: 24px;
}
.guideline-list li {
font-size: 14px;
color: var(--text-dark);
line-height: 1.6;
margin-bottom: 6px;
}
/* Fixed Bottom Action */
.fixed-bottom-action {
position: fixed;
bottom: 70px;
left: 0;
right: 0;
padding: 16px;
background: var(--white);
border-top: 1px solid var(--border-color);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
z-index: 100;
}
.btn-submit-review {
width: 100%;
background: var(--primary-blue);
color: white;
border: none;
padding: 16px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-submit-review:hover {
background: #004578;
}
.btn-submit-review:active {
transform: scale(0.98);
}
.btn-submit-review:disabled {
background: var(--border-color);
cursor: not-allowed;
}
.btn-submit-review i {
font-size: 16px;
}
</style>
<script>
let selectedRating = 0;
const ratingLabels = {
0: 'Chưa chọn đánh giá',
1: 'Rất không hài lòng',
2: 'Không hài lòng',
3: 'Bình thường',
4: 'Hài lòng',
5: 'Rất hài lòng'
};
// Load product info from localStorage
document.addEventListener('DOMContentLoaded', function() {
const productData = localStorage.getItem('reviewProduct');
if (productData) {
const product = JSON.parse(productData);
document.getElementById('productName').textContent = product.name;
document.getElementById('productImage').src = product.image;
}
// Character counter
const textarea = document.getElementById('reviewContent');
const charCount = document.getElementById('charCount');
textarea.addEventListener('input', function() {
const count = this.value.length;
charCount.textContent = count;
if (count < 20) {
charCount.style.color = 'var(--danger-color)';
} else {
charCount.style.color = 'var(--text-muted)';
}
});
});
// Star rating selection
function selectRating(rating) {
selectedRating = rating;
document.getElementById('ratingValue').value = rating;
document.getElementById('ratingError').style.display = 'none';
// Update star buttons
const starButtons = document.querySelectorAll('.star-btn');
starButtons.forEach((btn, index) => {
const icon = btn.querySelector('i');
if (index < rating) {
btn.classList.add('selected');
icon.classList.remove('far');
icon.classList.add('fas');
} else {
btn.classList.remove('selected');
icon.classList.remove('fas');
icon.classList.add('far');
}
});
// Update rating label
const ratingLabel = document.getElementById('ratingLabel');
ratingLabel.textContent = ratingLabels[rating];
ratingLabel.classList.add('selected');
}
// Form submission
function submitReview(event) {
event.preventDefault();
// Validation
let isValid = true;
// Check rating
if (selectedRating === 0) {
document.getElementById('ratingError').style.display = 'block';
isValid = false;
}
// Check content length
const content = document.getElementById('reviewContent').value;
if (content.length < 20) {
document.getElementById('contentError').style.display = 'block';
isValid = false;
} else {
document.getElementById('contentError').style.display = 'none';
}
if (!isValid) {
return false;
}
// Show loading state
const submitBtn = document.getElementById('submitBtn');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Đang gửi...';
submitBtn.disabled = true;
// Simulate API call
setTimeout(() => {
// Set flag for product-detail page
localStorage.setItem('reviewJustSubmitted', 'true');
// Navigate to success page
window.location.href = 'review-submitted.html';
}, 1500);
return false;
}
</script>
</body>
</html>

View File

@@ -176,7 +176,7 @@ SPEC CHECKSUMS:
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
@@ -196,7 +196,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Worker</string>
<string>DBIZ Partner</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -49,5 +49,7 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

View File

@@ -193,6 +193,7 @@ class AuthRemoteDataSource {
'customer': 1,
},
'limit_page_length': 0,
'order_by': 'custom_line_no asc'
},
options: Options(
headers: {

View File

@@ -71,14 +71,50 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
name: 'LPKD',
description: 'Đơn vị kinh doanh LPKD',
),
const BusinessUnit(
id: '2',
code: 'HSKD',
name: 'HSKD',
description: 'Đơn vị kinh doanh HSKD',
),
const BusinessUnit(
id: '3',
code: 'LPKD',
name: 'LPKD',
description: 'Đơn vị kinh doanh LPKD',
),
const BusinessUnit(
id: '2',
code: 'HSKD',
name: 'HSKD',
description: 'Đơn vị kinh doanh HSKD',
),
const BusinessUnit(
id: '3',
code: 'LPKD',
name: 'LPKD',
description: 'Đơn vị kinh doanh LPKD',
),
const BusinessUnit(
id: '2',
code: 'HSKD',
name: 'HSKD',
description: 'Đơn vị kinh doanh HSKD',
),
const BusinessUnit(
id: '3',
code: 'LPKD',
name: 'LPKD',
description: 'Đơn vị kinh doanh LPKD',
),
];
}
void _handleContinue() {
if (_selectedUnit == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Vui lòng chọn đơn vị kinh doanh'),
const SnackBar(
content: Text('Vui lòng chọn đơn vị kinh doanh'),
backgroundColor: AppColors.danger,
),
);
@@ -146,249 +182,252 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
const SizedBox(width: AppSpacing.sm),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo Section
Center(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primaryBlue, AppColors.lightBlue],
body: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo Section
Center(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primaryBlue, AppColors.lightBlue],
),
borderRadius: BorderRadius.circular(20),
),
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'DBIZ',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
borderRadius: BorderRadius.circular(20),
),
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'DBIZ',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
Text(
'Worker App',
style: TextStyle(color: Colors.white, fontSize: 12),
),
],
),
Text(
'Worker App',
style: TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
),
const SizedBox(height: AppSpacing.xl),
const SizedBox(height: AppSpacing.xl),
// Welcome Message
const Text(
'Chọn đơn vị kinh doanh để tiếp tục',
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.grey500, fontSize: 14),
// Welcome Message
const Text(
'Chọn đơn vị kinh doanh để tiếp tục',
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.grey500, fontSize: 14),
),
const SizedBox(height: 40),
const Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm),
child: Text(
'Đơn vị kinh doanh',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
),
),
const SizedBox(height: AppSpacing.md),
const SizedBox(height: 40),
// Business Unit Selection List
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm),
child: Text(
'Đơn vị kinh doanh',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
),
),
const SizedBox(height: AppSpacing.md),
// Business Unit List Tiles
...(_availableUnits.asMap().entries.map((entry) {
final index = entry.key;
final unit = entry.value;
final isSelected = _selectedUnit?.id == unit.id;
final isFirst = index == 0;
final isLast = index == _availableUnits.length - 1;
return Container(
margin: EdgeInsets.only(
bottom: isLast ? 0 : AppSpacing.xs,
),
decoration: BoxDecoration(
color: AppColors.white,
border: Border.all(
color: isSelected
? AppColors.primaryBlue
: AppColors.grey100,
width: isSelected ? 2 : 1,
// Business Unit Selection List
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Business Unit List Tiles
...(_availableUnits.asMap().entries.map((entry) {
final index = entry.key;
final unit = entry.value;
final isSelected = _selectedUnit?.id == unit.id;
final isFirst = index == 0;
final isLast = index == _availableUnits.length - 1;
return Container(
margin: EdgeInsets.only(
bottom: isLast ? 0 : AppSpacing.xs,
),
borderRadius: BorderRadius.vertical(
top: isFirst
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
bottom: isLast
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColors.primaryBlue.withValues(
alpha: 0.1,
),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: InkWell(
onTap: () {
setState(() {
_selectedUnit = unit;
});
},
borderRadius: BorderRadius.vertical(
top: isFirst
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
bottom: isLast
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.md,
decoration: BoxDecoration(
color: AppColors.white,
border: Border.all(
color: isSelected
? AppColors.primaryBlue
: AppColors.grey100,
width: isSelected ? 2 : 1,
),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? AppColors.primaryBlue.withValues(
alpha: 0.1,
)
: AppColors.grey50,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.business,
color: isSelected
? AppColors.primaryBlue
: AppColors.grey500,
size: 20,
),
),
const SizedBox(width: AppSpacing.md),
// Unit Name
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
unit.name,
style: TextStyle(
fontSize: 16,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w500,
color: isSelected
? AppColors.primaryBlue
: AppColors.grey900,
),
borderRadius: BorderRadius.vertical(
top: isFirst
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
bottom: isLast
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColors.primaryBlue.withValues(
alpha: 0.1,
),
if (unit.description != null) ...[
const SizedBox(height: 2),
Text(
unit.description!,
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
],
),
),
// Radio indicator
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: InkWell(
onTap: () {
setState(() {
_selectedUnit = unit;
});
},
borderRadius: BorderRadius.vertical(
top: isFirst
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
bottom: isLast
? const Radius.circular(
InputFieldSpecs.borderRadius,
)
: Radius.zero,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.md,
),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected
? AppColors.primaryBlue.withValues(
alpha: 0.1,
)
: AppColors.grey50,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.business,
color: isSelected
? AppColors.primaryBlue
: AppColors.grey500,
width: 2,
size: 20,
),
color: isSelected
? AppColors.primaryBlue
: Colors.transparent,
),
child: isSelected
? const Icon(
Icons.circle,
size: 10,
color: AppColors.white,
)
: null,
),
],
const SizedBox(width: AppSpacing.md),
// Unit Name
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
unit.name,
style: TextStyle(
fontSize: 16,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w500,
color: isSelected
? AppColors.primaryBlue
: AppColors.grey900,
),
),
if (unit.description != null) ...[
const SizedBox(height: 2),
Text(
unit.description!,
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
],
),
),
// Radio indicator
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? AppColors.primaryBlue
: AppColors.grey500,
width: 2,
),
color: isSelected
? AppColors.primaryBlue
: Colors.transparent,
),
child: isSelected
? const Icon(
Icons.circle,
size: 10,
color: AppColors.white,
)
: null,
),
],
),
),
),
),
);
}).toList()),
],
),
const SizedBox(height: AppSpacing.xl),
// Continue Button
SizedBox(
height: ButtonSpecs.height,
child: ElevatedButton(
onPressed: _handleContinue,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
ButtonSpecs.borderRadius,
),
),
),
child: const Text(
'Tiếp tục',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
);
}).toList()),
],
),
),
],
),
),
const SizedBox(height: AppSpacing.xl),
// Continue Button
SizedBox(
height: ButtonSpecs.height,
child: ElevatedButton(
onPressed: _handleContinue,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
ButtonSpecs.borderRadius,
),
),
),
child: const Text(
'Tiếp tục',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
const SizedBox(height: AppSpacing.xl),
],
),
),
);

View File

@@ -10,6 +10,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/news/domain/entities/news_article.dart';
@@ -159,76 +161,73 @@ class _NewsDetailPageState extends ConsumerState<NewsDetailPage> {
// Article Body - Render HTML content
if (article.content != null && article.content!.isNotEmpty)
Container(
// Wrap Html in Container to prevent rendering issues
child: Html(
data: article.content,
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(16),
lineHeight: const LineHeight(1.7),
color: const Color(0xFF1E293B),
Html(
data: article.content,
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(16),
lineHeight: const LineHeight(1.7),
color: const Color(0xFF1E293B),
),
"h2": Style(
fontSize: FontSize(20),
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
margin: Margins.only(top: 32, bottom: 16),
),
"h3": Style(
fontSize: FontSize(18),
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
margin: Margins.only(top: 24, bottom: 12),
),
"p": Style(
fontSize: FontSize(16),
color: const Color(0xFF1E293B),
lineHeight: const LineHeight(1.7),
margin: Margins.only(bottom: 16),
),
"strong": Style(
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
),
"img": Style(
margin: Margins.symmetric(vertical: 16),
),
"ul": Style(
margin: Margins.only(left: 16, bottom: 16),
),
"ol": Style(
margin: Margins.only(left: 16, bottom: 16),
),
"li": Style(
fontSize: FontSize(16),
color: const Color(0xFF1E293B),
lineHeight: const LineHeight(1.5),
margin: Margins.only(bottom: 8),
),
"blockquote": Style(
backgroundColor: const Color(0xFFF0F9FF),
border: const Border(
left: BorderSide(color: AppColors.primaryBlue, width: 4),
),
"h2": Style(
fontSize: FontSize(20),
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
margin: Margins.only(top: 32, bottom: 16),
),
"h3": Style(
fontSize: FontSize(18),
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
margin: Margins.only(top: 24, bottom: 12),
),
"p": Style(
fontSize: FontSize(16),
color: const Color(0xFF1E293B),
lineHeight: const LineHeight(1.7),
margin: Margins.only(bottom: 16),
),
"strong": Style(
fontWeight: FontWeight.w600,
color: const Color(0xFF1E293B),
),
"img": Style(
margin: Margins.symmetric(vertical: 16),
),
"ul": Style(
margin: Margins.only(left: 16, bottom: 16),
),
"ol": Style(
margin: Margins.only(left: 16, bottom: 16),
),
"li": Style(
fontSize: FontSize(16),
color: const Color(0xFF1E293B),
lineHeight: const LineHeight(1.5),
margin: Margins.only(bottom: 8),
),
"blockquote": Style(
backgroundColor: const Color(0xFFF0F9FF),
border: const Border(
left: BorderSide(color: AppColors.primaryBlue, width: 4),
),
padding: HtmlPaddings.all(16),
margin: Margins.symmetric(vertical: 24),
fontStyle: FontStyle.italic,
),
"div": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
),
},
onLinkTap: (url, attributes, element) {
// Handle link taps if needed
if (url != null) {
debugPrint('Link tapped: $url');
}
},
),
padding: HtmlPaddings.all(16),
margin: Margins.symmetric(vertical: 24),
fontStyle: FontStyle.italic,
),
"div": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
),
},
onLinkTap: (url, attributes, element) {
// Handle link taps if needed
if (url != null) {
debugPrint('Link tapped: $url');
}
},
),
const SizedBox(height: 32),
@@ -256,9 +255,8 @@ class _NewsDetailPageState extends ConsumerState<NewsDetailPage> {
/// Build metadata
Widget _buildMetadata(NewsArticle article) {
return Wrap(
return Row(
spacing: 16,
runSpacing: 8,
children: [
// Category badge
Container(
@@ -281,13 +279,13 @@ class _NewsDetailPageState extends ConsumerState<NewsDetailPage> {
_buildMetaItem(Icons.calendar_today, article.formattedDate),
// Reading time
_buildMetaItem(Icons.schedule, article.readingTimeText),
// _buildMetaItem(Icons.schedule, article.readingTimeText),
// Views
_buildMetaItem(
Icons.visibility,
'${article.formattedViewCount} lượt xem',
),
// _buildMetaItem(
// Icons.visibility,
// '${article.formattedViewCount} lượt xem',
// ),
],
);
}
@@ -362,48 +360,27 @@ class _NewsDetailPageState extends ConsumerState<NewsDetailPage> {
Widget _buildSocialActions(NewsArticle article) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
decoration: const BoxDecoration(
border: Border.symmetric(
horizontal: BorderSide(color: const Color(0xFFE2E8F0)),
horizontal: BorderSide(color: Color(0xFFE2E8F0)),
),
),
child: Column(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Engagement stats
Wrap(
spacing: 16,
runSpacing: 8,
children: [
_buildStatItem(Icons.favorite, '${article.likeCount} lượt thích'),
_buildStatItem(
Icons.comment,
'${article.commentCount} bình luận',
),
_buildStatItem(Icons.share, '${article.shareCount} lượt chia sẻ'),
],
_buildActionButton(
icon: _isLiked ? Icons.favorite : Icons.favorite_border,
onPressed: _onLikeTap,
color: _isLiked ? Colors.red : null,
),
const SizedBox(height: 16),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildActionButton(
icon: _isLiked ? Icons.favorite : Icons.favorite_border,
onPressed: _onLikeTap,
color: _isLiked ? Colors.red : null,
),
const SizedBox(width: 8),
_buildActionButton(
icon: _isBookmarked ? Icons.bookmark : Icons.bookmark_border,
onPressed: _onBookmarkTap,
color: _isBookmarked ? AppColors.warning : null,
),
const SizedBox(width: 8),
_buildActionButton(icon: Icons.share, onPressed: _onShareTap),
],
const SizedBox(width: 8),
_buildActionButton(
icon: _isBookmarked ? Icons.bookmark : Icons.bookmark_border,
onPressed: _onBookmarkTap,
color: _isBookmarked ? AppColors.warning : null,
),
const SizedBox(width: 8),
_buildActionButton(icon: Icons.share, onPressed: _onShareTap),
],
),
);
@@ -577,21 +554,21 @@ class _NewsDetailPageState extends ConsumerState<NewsDetailPage> {
/// Handle share tap
void _onShareTap() {
// Copy link to clipboard
Clipboard.setData(
ClipboardData(text: 'https://worker.app/news/${widget.articleId}'),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã sao chép link bài viết!'),
duration: Duration(seconds: 2),
),
);
// Clipboard.setData(
// ClipboardData(text: 'https://worker.app/news/${widget.articleId}'),
// );
//
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Đã sao chép link bài viết!'),
// duration: Duration(seconds: 2),
// ),
// );
// TODO: Implement native share when share_plus package is added
// Share.share(
// 'Xem bài viết: ${article.title}\nhttps://worker.app/news/${article.id}',
// subject: article.title,
// );
SharePlus.instance.share(
ShareParams(text: 'Xem bài viết: ${ApiConstants.baseUrl}')
);
}
}

View File

@@ -10,7 +10,6 @@ import 'package:intl/intl.dart';
import 'package:shimmer/shimmer.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
import 'package:worker/features/products/domain/entities/product.dart';
import 'package:worker/generated/l10n/app_localizations.dart';
@@ -38,7 +37,7 @@ class ProductCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final isFavorited = ref.watch(isFavoriteProvider(product.productId));
// final isFavorited = ref.watch(isFavoriteProvider(product.productId));
return Card(
elevation: ProductCardSpecs.elevation,
@@ -135,64 +134,64 @@ class ProductCard extends ConsumerWidget {
),
// Favorite Button (bottom-left corner)
Positioned(
bottom: AppSpacing.sm,
left: AppSpacing.sm,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
// Capture current state before toggle
final wasfavorited = isFavorited;
// Toggle favorite
await ref
.read(favoritesProvider.notifier)
.toggleFavorite(product.productId);
// Show feedback with correct message
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
wasfavorited
? 'Đã xóa khỏi yêu thích'
: 'Đã thêm vào yêu thích',
),
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
),
);
}
},
borderRadius: BorderRadius.circular(20),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Icon(
isFavorited
? Icons.favorite
: Icons.favorite_border,
color: isFavorited
? AppColors.danger
: AppColors.grey500,
size: 20,
),
),
),
),
),
// Positioned(
// bottom: AppSpacing.sm,
// left: AppSpacing.sm,
// child: Material(
// color: Colors.transparent,
// child: InkWell(
// onTap: () async {
// // Capture current state before toggle
// final wasfavorited = isFavorited;
//
// // Toggle favorite
// await ref
// .read(favoritesProvider.notifier)
// .toggleFavorite(product.productId);
//
// // Show feedback with correct message
// if (context.mounted) {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text(
// wasfavorited
// ? 'Đã xóa khỏi yêu thích'
// : 'Đã thêm vào yêu thích',
// ),
// duration: const Duration(seconds: 1),
// behavior: SnackBarBehavior.floating,
// ),
// );
// }
// },
// borderRadius: BorderRadius.circular(20),
// child: Container(
// width: 36,
// height: 36,
// decoration: BoxDecoration(
// color: AppColors.white,
// shape: BoxShape.circle,
// boxShadow: [
// BoxShadow(
// color: Colors.black.withValues(alpha: 0.15),
// blurRadius: 6,
// offset: const Offset(0, 2),
// ),
// ],
// ),
// child: Icon(
// isFavorited
// ? Icons.favorite
// : Icons.favorite_border,
// color: isFavorited
// ? AppColors.danger
// : AppColors.grey500,
// size: 20,
// ),
// ),
// ),
// ),
// ),
],
),
),

View File

@@ -413,10 +413,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev"
source: hosted
version: "8.0.7"
version: "8.3.7"
file_selector_linux:
dependency: transitive
description:
@@ -1160,18 +1160,18 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544
sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "12.0.1"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4"
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "6.1.0"
shared_preferences:
dependency: "direct main"
description:
@@ -1565,10 +1565,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "1.1.1"
web_socket:
dependency: transitive
description:

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
version: 1.0.0+3
environment:
sdk: ^3.9.2
@@ -63,9 +63,10 @@ dependencies:
qr_flutter: ^4.1.0
mobile_scanner: ^5.2.3
# Utilities
intl: ^0.20.0
share_plus: ^9.0.0
share_plus: ^12.0.1
image_picker: ^1.1.2
file_picker: ^8.0.0
url_launcher: ^6.3.0