Skip to content

Commit c09324f

Browse files
committed
feat: product variant select on products list view
1 parent 3ca802f commit c09324f

File tree

8 files changed

+342
-277
lines changed

8 files changed

+342
-277
lines changed

app/Http/Controllers/Frontend/ProductController.php

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ public function index(Request $request)
2121
$selectedCategories = $request->input('categories');
2222
$sortBy = $request->input('sort_by');
2323
$categories = Category::toBase()->select('id','title','is_pc_part')->latest()->get();
24-
$products = Product::with('category')
24+
$products = Product::with([
25+
'category',
26+
'variants.attributeValues.attribute',
27+
])
2528
->when($selectedCategories, function ($query, $selectedCategories) {
2629
return $query->whereIn('category_id', $selectedCategories);
2730
})
@@ -42,24 +45,6 @@ public function show($slug)
4245
'variants.attributeValues'
4346
])->firstOrFail();
4447
$product->image = $this->getImageUrl($product->image);
45-
// $variantIds = ProductVariant::where('product_id', $product->id)->pluck('id');
46-
47-
// $attributeValueIds = DB::table('product_variant_values')
48-
// ->whereIn('product_variant_id', $variantIds)
49-
// ->pluck('attribute_value_id')
50-
// ->unique();
51-
52-
// $attributeIds = AttributeValue::whereIn('id', $attributeValueIds)
53-
// ->pluck('attribute_id')
54-
// ->unique();
55-
56-
// $attributes = Attribute::with(['values' => function ($query) use ($attributeValueIds) {
57-
// $query->whereIn('id', $attributeValueIds);
58-
// }])
59-
// ->whereIn('id', $attributeIds)
60-
// ->get();
61-
62-
// $product->load('variants.attributeValues');
6348

6449
$attributes = Attribute::whereHas('values.variants', function($q) use ($product) {
6550
$q->where('product_id', $product->id);
@@ -69,7 +54,6 @@ public function show($slug)
6954
});
7055
}])->get();
7156

72-
7357
// For the variant images
7458
$product->variants->transform(function ($v) {
7559
return [

app/Http/Requests/ProductRequest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ public function rules(): array
3535
'string',
3636
'regex:/^[a-z0-9-]+$/',
3737
],
38-
'sku' => 'required|string|max:200|unique:products,sku,' . $id,
38+
'sku' => 'nullable|string|max:200|unique:products,sku,' . $id,
3939
'category_id' => 'required',
4040
'type' => 'required',
41-
'price' => 'required|numeric',
41+
'price' => 'nullable|numeric',
4242
'image' => 'nullable|string',
4343
];
4444
}

app/Models/AttributeValue.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function attribute()
1919
// Many variants can have this attribute value
2020
public function variants()
2121
{
22-
return $this->belongsToMany(ProductVariant::class, 'product_variant_values', 'attribute_value_id', 'variant_id');
22+
return $this->belongsToMany(ProductVariant::class, 'product_variant_values', 'attribute_value_id', 'product_variant_id');
2323
}
2424

2525
}

app/Models/Product.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,13 @@ public function getCreatedAtHumanAttribute()
5959
{
6060
return $this->created_at->diffForHumans();
6161
}
62+
63+
public function getAttributesAttribute()
64+
{
65+
return $this->variants->flatMap->attributeValues->map->attribute->pluck('title')->unique()->join(' / ') ?: '';
66+
}
67+
68+
public function getFullImageAttribute(){
69+
return $this->image ? asset($this->image) : 'https://placehold.co/400';
70+
}
6271
}

app/Models/ProductVariant.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use Illuminate\Database\Eloquent\Factories\HasFactory;
66
use Illuminate\Database\Eloquent\Model;
77
use Illuminate\Database\Eloquent\SoftDeletes;
8+
use Illuminate\Support\Facades\Storage;
9+
use Illuminate\Support\Str;
810

911
class ProductVariant extends Model
1012
{
@@ -23,4 +25,9 @@ public function attributeValues()
2325
{
2426
return $this->belongsToMany(AttributeValue::class, 'product_variant_values', 'product_variant_id', 'attribute_value_id');
2527
}
28+
29+
public function getFullImageAttribute()
30+
{
31+
return $this->image ? ((Str::startsWith($this->image, ['http://', 'https://']) ? $this->image : Storage::disk('public')->url($this->image))) : '';
32+
}
2633
}

resources/views/frontend/products/index.blade.php

Lines changed: 285 additions & 248 deletions
Large diffs are not rendered by default.

resources/views/frontend/products/show.blade.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,16 @@ function updateVariantInfo() {
263263
document.querySelectorAll('input[name^="attributes"]').forEach(input => {
264264
input.addEventListener('change', updateVariantInfo);
265265
});
266+
267+
// --- Default to first variant ---
268+
if (variants.length > 0) {
269+
const firstVariant = variants[0];
270+
firstVariant.attribute_value_ids.forEach(id => {
271+
const radio = document.querySelector(`input[name^="attributes"][value="${id}"]`);
272+
if (radio) radio.checked = true;
273+
});
274+
updateVariantInfo();
275+
}
266276
});
267277
</script>
268278

resources/views/frontend/scripts/alpine.blade.php

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
@push('scripts')
2-
<script>
1+
<script>
32
document.addEventListener('alpine:init', () => {
43
Alpine.store('cart', {
54
items: @json(session()->get('cart')['items'] ?? []),
@@ -138,14 +137,34 @@
138137
Alpine.data('cart', (productId) => ({
139138
quantity: 1,
140139
variantId: null,
140+
selectedVariantId: '', // Local state for this product
141+
init() {
142+
// Sync with existing selection if any
143+
const select = document.querySelector(`#variant-select-${productId}`);
144+
if (select) {
145+
this.selectedVariantId = select.value || '';
146+
this.variantId = this.selectedVariantId;
147+
this.updateImage(this.selectedVariantId); // Initialize image
148+
}
149+
},
141150
setVariantId(id) {
142-
this.variantId = id;
151+
this.selectedVariantId = id;
152+
this.variantId = id; // Sync with addToCart payload
153+
},
154+
updateImage(variantId) {
155+
const select = document.querySelector(`#variant-select-${productId}`);
156+
if (select) {
157+
const imgElement = document.getElementById(`product-image-${productId}`);
158+
const selectedOption = select.querySelector(`option[value="${variantId}"]`);
159+
const imageUrl = variantId ? (selectedOption ? selectedOption.getAttribute('data-image') : '') : imgElement.dataset.src;
160+
if (imgElement) imgElement.src = imageUrl;
161+
}
143162
},
144163
addToCart() {
145164
axios.post('/cart/add', {
146165
product_id: productId,
147166
quantity: this.quantity,
148-
variant_id: this.variantId
167+
variant_id: this.variantId || null
149168
})
150169
.then(response => {
151170
Alpine.store('cart').reset(response.data.data);
@@ -161,5 +180,4 @@
161180
}));
162181
163182
});
164-
</script>
165-
@endpush
183+
</script>

0 commit comments

Comments
 (0)