product description + variant

This commit is contained in:
Bayu Lukman Yusuf 2026-01-02 11:07:44 +07:00
parent 7b38c511ce
commit c8c96556c0
11 changed files with 1518 additions and 212 deletions

View File

@ -27,7 +27,7 @@ DB_CONNECTION=sqlite
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/

1
.php-version Normal file
View File

@ -0,0 +1 @@
8.4

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers;
use App\Models\Items;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function detail($slug, Request $request)
{
$product = Items::where('slug', $slug)->first();
if ($product == null) {
abort(404);
}
return view('shop.product-fashion',[
'product' => $product,
]);
}
}

View File

@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Awobaz\Compoships\Compoships;
use Illuminate\Support\Facades\Storage;
class ItemVariant extends Model
{
@ -50,4 +51,17 @@ class ItemVariant extends Model
{
return $this->hasMany(\App\Models\VariantValue::class, 'item_variant_id');
}
public function getImageUrlAttribute()
{
$image = $this->images->first()->filename ?? null;
if (str_contains($image,"http")) {
return $image;
}
return $image ? Storage::disk('wms')->url($image) : null;
}
}

View File

@ -95,11 +95,13 @@ class Items extends Model
public function variants()
{
return $this->hasMany(ItemVariant::class,"item_id")->leftJoin("stocks","item_variant_id","=","item_variants.id")
return $this->hasMany(ItemVariant::class,"item_id")
->leftJoin("stocks","item_variant_id","=","item_variants.id")
->select("item_variants.id","item_variants.code","item_variants.display_name","item_variants.is_publish", DB::raw("SUM(quantity) as stock"),"description","item_variants.item_id")
->groupBy("item_variants.id","item_variants.code","description","item_variants.item_id","item_variants.display_name","item_variants.is_publish");
}
public function images()
{
return $this->hasMany(ItemImage::class,'item_id')->whereNull('item_variant_id');
@ -295,6 +297,28 @@ class Items extends Model
}
public function getImageUrlsAttribute()
{
$images = $this->images ?? [];
$imgs = [];
foreach ($images as $img) {
$image = $img->filename;
if (str_contains($image,"http")) {
$imgs[] = $image;
} else {
$imgs[] = $image ? Storage::disk('wms')->url($image) : null;
}
}
return $imgs;
}
public function conversion_value()
{
$convertion = 1;
@ -310,20 +334,28 @@ class Items extends Model
public function getDisplayPriceAttribute()
{
$convertion = $this->conversion_value();
try {
$convertion = $this->conversion_value();
$price = @$this->variants->first()->reference->price->price ?? null;
$price = $price ? $price : $this->net_price;
$price = @$this->variants->first()->reference->price->price ?? null;
$price = $price ? $price : $this->net_price;
return (float) $price * $convertion;
return (float) $price * $convertion;
} catch (\Exception $e) {
return 0;
}
}
public function getDisplayDiscountPriceAttribute()
{
$convertion = $this->conversion_value();
try {
$convertion = $this->conversion_value();
$discountPrice = @$this->discount->price ?? 0;
return (float) $discountPrice * $convertion;
$discountPrice = @$this->discount->price ?? 0;
return (float) $discountPrice * $convertion;
} catch (\Exception $e) {
return 0;
}
}
}

BIN
composer.phar Normal file

Binary file not shown.

1310
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
<div class="animate-underline hover-effect-opacity">
<div class="position-relative mb-3">
<span
class="badge text-bg-danger position-absolute top-0 start-0 z-2 mt-2 mt-sm-3 ms-2 ms-sm-3">Sale</span>
<button type="button"
class="btn btn-icon btn-secondary animate-pulse fs-base bg-transparent border-0 position-absolute top-0 end-0 z-2 mt-1 mt-sm-2 me-1 me-sm-2"
aria-label="Add to Wishlist">
<i class="ci-heart animate-target"></i>
</button>
<a class="d-flex bg-body-tertiary rounded p-3" href="{{ route('product.detail', $product->slug) }}">
<div class="ratio" style="--cz-aspect-ratio: calc(308 / 274 * 100%)">
<img src="{{ $product->image_url }}" loading="lazy" class="w-100 h-100 object-cover">
</div>
</a>
{{-- <div
class="hover-effect-target position-absolute start-0 bottom-0 w-100 z-2 opacity-0 pb-2 pb-sm-3 px-2 px-sm-3">
<div
class="d-flex align-items-center justify-content-center gap-2 gap-xl-3 bg-body rounded-2 p-2">
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">XS</span>
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">S</span>
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">M</span>
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">L</span>
<div class="nav">
<a class="nav-link fs-xs text-body-tertiary py-1 px-2"
href="{{ route('second', ['shop', 'product-fashion']) }}">+3</a>
</div>
</div>
</div> --}}
</div>
<div class="nav mb-2">
<a class="nav-link animate-target min-w-0 text-dark-emphasis p-0"
href="{{ route('product.detail', $product->slug) }}">
<span class="text-truncate">{{ $product->name ?? '' }}</span>
</a>
</div>
<div class="h6 mb-2">Rp {{ number_format($product->display_price,0,",",".") }} @if ($product->display_discount_price > 0)<del class="fs-sm fw-normal text-body-tertiary">Rp {{ number_format($product->display_discount_price,0,",",".") }}</del>@endif</div>
<div class="position-relative">
@if ($product->variants->count() > 1)
<div class=" fs-xs text-body-secondary opacity-100">+{{ $product->variants->count() - 1 }} Varian</div>
@endif
{{-- <div class="hover-effect-target fs-xs text-body-secondary opacity-100">+1 color</div> --}}
{{-- <div class="hover-effect-target d-flex gap-2 position-absolute top-0 start-0 opacity-0">
<input type="radio" class="btn-check" name="colors-1" id="color-1-1" checked>
<label for="color-1-1" class="btn btn-color fs-base" style="color: #284971">
<span class="visually-hidden">Dark denim</span>
</label>
<input type="radio" class="btn-check" name="colors-1" id="color-1-2">
<label for="color-1-2" class="btn btn-color fs-base" style="color: #8b9bc4">
<span class="visually-hidden">Light denim</span>
</label>
</div> --}}
</div>
</div>

View File

@ -26,62 +26,9 @@
<!-- Item -->
@foreach ($products as $key => $product)
<div class="col mb-2 mb-sm-3 mb-md-0">
<div class="animate-underline hover-effect-opacity">
<div class="position-relative mb-3">
<span
class="badge text-bg-danger position-absolute top-0 start-0 z-2 mt-2 mt-sm-3 ms-2 ms-sm-3">Sale</span>
<button type="button"
class="btn btn-icon btn-secondary animate-pulse fs-base bg-transparent border-0 position-absolute top-0 end-0 z-2 mt-1 mt-sm-2 me-1 me-sm-2"
aria-label="Add to Wishlist">
<i class="ci-heart animate-target"></i>
</button>
<a class="d-flex bg-body-tertiary rounded p-3" href="{{ route('second', ['shop', 'product-fashion']) }}">
<div class="ratio" style="--cz-aspect-ratio: calc(308 / 274 * 100%)">
<img src="{{ $product->image_url }}" loading="lazy" class="w-100 h-100 object-cover">
</div>
</a>
{{-- <div
class="hover-effect-target position-absolute start-0 bottom-0 w-100 z-2 opacity-0 pb-2 pb-sm-3 px-2 px-sm-3">
<div
class="d-flex align-items-center justify-content-center gap-2 gap-xl-3 bg-body rounded-2 p-2">
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">XS</span>
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">S</span>
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">M</span>
<span class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">L</span>
<div class="nav">
<a class="nav-link fs-xs text-body-tertiary py-1 px-2"
href="{{ route('second', ['shop', 'product-fashion']) }}">+3</a>
</div>
</div>
</div> --}}
</div>
<div class="nav mb-2">
<a class="nav-link animate-target min-w-0 text-dark-emphasis p-0"
href="{{ route('second', ['shop', 'product-fashion']) }}">
<span class="text-truncate">{{ $product->name ?? '' }}</span>
</a>
</div>
<div class="h6 mb-2">Rp {{ number_format($product->display_price,0,",",".") }} @if ($product->display_discount_price > 0)<del class="fs-sm fw-normal text-body-tertiary">Rp {{ number_format($product->display_discount_price,0,",",".") }}</del>@endif</div>
<div class="position-relative">
@if ($product->variants->count() > 1)
<div class=" fs-xs text-body-secondary opacity-100">+{{ $product->variants->count() - 1 }} Varian</div>
@endif
{{-- <div class="hover-effect-target fs-xs text-body-secondary opacity-100">+1 color</div> --}}
{{-- <div class="hover-effect-target d-flex gap-2 position-absolute top-0 start-0 opacity-0">
<input type="radio" class="btn-check" name="colors-1" id="color-1-1" checked>
<label for="color-1-1" class="btn btn-color fs-base" style="color: #284971">
<span class="visually-hidden">Dark denim</span>
</label>
<input type="radio" class="btn-check" name="colors-1" id="color-1-2">
<label for="color-1-2" class="btn btn-color fs-base" style="color: #8b9bc4">
<span class="visually-hidden">Light denim</span>
</label>
</div> --}}
</div>
</div>
</div>
<div class="col mb-2 mb-sm-3 mb-md-0">
<x-home.product-card :product="$product" />
</div>
@endforeach
</div>

View File

@ -1811,65 +1811,40 @@
<i class="ci-heart animate-target"></i>
</button>
<a class="hover-effect-scale hover-effect-opacity position-relative d-flex rounded overflow-hidden mb-3 mb-sm-4 mb-md-3 mb-lg-4"
href="/img/shop/fashion/product/01.png" data-glightbox data-gallery="product-gallery">
href="{{ $product->image_url ?? '' }}" data-glightbox data-gallery="product-gallery">
<i
class="ci-zoom-in hover-effect-target fs-3 text-white position-absolute top-50 start-50 translate-middle opacity-0 z-2"></i>
<div class="ratio hover-effect-target bg-body-tertiary rounded"
style="--cz-aspect-ratio: calc(706 / 636 * 100%)">
<img src="/img/shop/fashion/product/01.png" alt="Image">
<img src="{{ $product->image_url ?? '' }}" class="product-main-image" alt="Image">
</div>
</a>
</div>
<div class="collapse d-md-block" id="morePictures">
<div class="row row-cols-2 g-3 g-sm-4 g-md-3 g-lg-4 pb-3 pb-sm-4 pb-md-0">
<div class="col">
@foreach ($product->image_urls as $key => $url)
@if ($key == 0)
@continue
@endif
<div class="col">
<a class="hover-effect-scale hover-effect-opacity position-relative d-flex rounded overflow-hidden"
href="/img/shop/fashion/product/02.png" data-glightbox
href="{{ $url }}" data-glightbox
data-gallery="product-gallery">
<i
class="ci-zoom-in hover-effect-target fs-3 text-white position-absolute top-50 start-50 translate-middle opacity-0 z-2"></i>
<div class="ratio hover-effect-target bg-body-tertiary rounded"
style="--cz-aspect-ratio: calc(342 / 306 * 100%)">
<img src="/img/shop/fashion/product/02.png" alt="Image">
</div>
</a>
</div>
<div class="col">
<a class="hover-effect-scale hover-effect-opacity position-relative d-flex rounded overflow-hidden"
href="/img/shop/fashion/product/03.png" data-glightbox
data-gallery="product-gallery">
<i
class="ci-zoom-in hover-effect-target fs-3 text-white position-absolute top-50 start-50 translate-middle opacity-0 z-2"></i>
<div class="ratio hover-effect-target bg-body-tertiary rounded"
style="--cz-aspect-ratio: calc(342 / 306 * 100%)">
<img src="/img/shop/fashion/product/03.png" alt="Image">
</div>
</a>
</div>
<div class="col">
<a class="hover-effect-scale hover-effect-opacity position-relative d-flex rounded overflow-hidden"
href="/img/shop/fashion/product/04.png" data-glightbox
data-gallery="product-gallery">
<i
class="ci-zoom-in hover-effect-target fs-3 text-white position-absolute top-50 start-50 translate-middle opacity-0 z-2"></i>
<div class="ratio hover-effect-target bg-body-tertiary rounded"
style="--cz-aspect-ratio: calc(342 / 306 * 100%)">
<img src="/img/shop/fashion/product/04.png" alt="Image">
</div>
</a>
</div>
<div class="col">
<a class="hover-effect-scale hover-effect-opacity position-relative d-flex rounded overflow-hidden"
href="/img/shop/fashion/product/05.png" data-glightbox
data-gallery="product-gallery">
<i
class="ci-zoom-in hover-effect-target fs-3 text-white position-absolute top-50 start-50 translate-middle opacity-0 z-2"></i>
<div class="ratio hover-effect-target bg-body-tertiary rounded"
style="--cz-aspect-ratio: calc(342 / 306 * 100%)">
<img src="/img/shop/fashion/product/05.png" alt="Image">
<img src="{{ $url }}" alt="Image">
</div>
</a>
</div>
@endforeach
</div>
</div>
<button type="button" class="btn btn-lg btn-outline-secondary w-100 collapsed d-md-none"
@ -1886,7 +1861,7 @@
<div class="ps-md-4 ps-xl-5">
<!-- Reviews -->
<a class="d-none d-md-flex align-items-center gap-2 text-decoration-none mb-3" href="#reviews">
{{-- <a class="d-none d-md-flex align-items-center gap-2 text-decoration-none mb-3" href="#reviews">
<div class="d-flex gap-1 fs-sm">
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
@ -1895,67 +1870,60 @@
<i class="ci-star text-body-tertiary opacity-75"></i>
</div>
<span class="text-body-tertiary fs-sm">23 reviews</span>
</a>
</a> --}}
<!-- Title -->
<h1 class="h3">Denim midi skirt with pockets</h1>
<h1 class="h3">{{ $product->name }}</h1>
<!-- Description -->
<p class="fs-sm mb-0">Made from high-quality denim fabric, this midi skirt offers durability and
comfort for all-day wear. The mid-length design strikes the perfect balance between casual and
chic, making it suitable for various occasions, from casual outings to semi-formal events.</p>
<p class="fs-sm mb-0">{!! nl2br(Str::limit($product->description, 500)) !!}</p>
<div class="collapse" id="moreDescription">
<div class="fs-sm pt-3">
<p>One of the standout features of this skirt is its functional pockets. With two spacious
pockets at the front, you can conveniently carry your essentials such as keys, phone, or
wallet without the need for a bulky bag. The pockets also add a touch of utility and
flair to the overall look.</p>
<p class="mb-0">The skirt's classic denim color and timeless design make it easy to pair
with a variety of tops, blouses, and footwear, allowing you to create endless stylish
ensembles. Whether you prefer a laid-back look with a graphic tee and sneakers or a more
polished ensemble with a blouse and heels, this skirt effortlessly adapts to your style.
</p>
@if(strlen($product->description) > 500)
<p>{!! nl2br(substr($product->description, 500)) !!}</p>
@endif
</div>
</div>
<!-- Price -->
@if(strlen($product->description) > 500)
<a class="d-inline-block fs-sm fw-medium text-dark-emphasis collapsed mt-1"
href="#moreDescription" data-bs-toggle="collapse" aria-expanded="false"
aria-controls="moreDescription" data-label-collapsed="Read more"
data-label-expanded="Show less" aria-label="Show / hide description"></a>
@endif
<div class="h4 d-flex align-items-center my-4">
$126.50
<del class="fs-sm fw-normal text-body-tertiary ms-2">$156.00</del>
<span class="display_price">Rp {{ number_format($product->display_price, 0, ',', '.') }}</span>
@if ($product->discount_display_price)
<del class="display_discount_price fs-sm fw-normal text-body-tertiary ms-2"> Rp {{ number_format($product->discount_display_price, 0, ',', '.') }}</del>
@endif
</div>
<!-- Color options -->
<div class="mb-4">
<label class="form-label fw-semibold pb-1 mb-2">Color: <span class="text-body fw-normal"
id="colorOption">Dark blue</span></label>
<label class="form-label fw-semibold pb-1 mb-2">Varian: <span class="text-body fw-normal"
id="colorOption">{{ $product->variants->first()->description ?? '' }}</span></label>
<div class="d-flex flex-wrap gap-2" data-binded-label="#colorOption">
<input type="radio" class="btn-check" name="colors" id="dark-blue" checked>
<label for="dark-blue" class="btn btn-image p-0" data-label="Dark blue">
<img src="/img/shop/fashion/product/colors/color01.png" width="56"
alt="Dark blue">
<span class="visually-hidden">Dark blue</span>
</label>
<input type="radio" class="btn-check" name="colors" id="pink">
<label for="pink" class="btn btn-image p-0" data-label="Pink">
<img src="/img/shop/fashion/product/colors/color02.png" width="56"
alt="Pink">
<span class="visually-hidden">Pink</span>
</label>
<input type="radio" class="btn-check" name="colors" id="light-blue">
<label for="light-blue" class="btn btn-image p-0" data-label="Light blue">
<img src="/img/shop/fashion/product/colors/color03.png" width="56"
alt="Light blue">
<span class="visually-hidden">Light blue</span>
</label>
@foreach ($product->variants as $key => $variant)
<input type="radio" class="btn-check" name="colors" id="variant-id-{{ $variant->id }}" {{ $key == 0 ? 'checked' : '' }}>
<label for="variant-id-{{ $variant->id }}" class="btn btn-image p-0" data-label="{{ $variant->description }}"
data-price="Rp {{ number_format($variant->display_price, 0, ",",".") }}"
data-image="{{ $variant->image_url }}"
>
<img src="{{ $variant->image_url ?? $product->image_url }}" width="56"
alt="{{ $variant->name }}">
<span class="visually-hidden">{{ $variant->name }}</span>
</label>
@endforeach
</div>
</div>
<!-- Size select -->
<div class="mb-3">
{{-- <div class="mb-3">
<div class="d-flex align-items-center justify-content-between mb-1">
<label class="form-label fw-semibold mb-0">Size</label>
<div class="nav">
@ -1980,7 +1948,7 @@
<option value="l">12-14 (L)</option>
<option value="xl">14-16 (XL)</option>
</select>
</div>
</div> --}}
<!-- Count input + Add to cart button -->
<div class="d-flex gap-3 pb-3 pb-lg-4 mb-3">
@ -2533,73 +2501,9 @@
<div class="row row-cols-2">
<!-- Item -->
<div class="col">
<div class="animate-underline hover-effect-opacity">
<div class="position-relative mb-3">
<button type="button"
class="btn btn-icon btn-secondary animate-pulse fs-base bg-transparent border-0 position-absolute top-0 end-0 z-2 mt-1 mt-sm-2 me-1 me-sm-2"
aria-label="Add to Wishlist">
<i class="ci-heart animate-target"></i>
</button>
<a class="d-flex bg-body-tertiary rounded p-3" href="#!">
<div class="ratio"
style="--cz-aspect-ratio: calc(308 / 274 * 100%)">
<img src="/img/shop/fashion/02.png" alt="Image">
</div>
</a>
<div
class="hover-effect-target position-absolute start-0 bottom-0 w-100 z-2 opacity-0 pb-2 pb-sm-3 px-2 px-sm-3">
<div
class="d-flex align-items-center justify-content-center gap-2 gap-xl-3 bg-body rounded-2 p-2">
<span
class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">S</span>
<span
class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">M</span>
<span
class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">L</span>
<span
class="fs-xs fw-medium text-secondary-emphasis py-1 px-sm-2">XL</span>
<div class="nav">
<a class="nav-link fs-xs text-body-tertiary py-1 px-2"
href="#!">+1</a>
</div>
</div>
</div>
</div>
<div class="nav mb-2">
<a class="nav-link animate-target min-w-0 text-dark-emphasis p-0"
href="#!">
<span class="text-truncate">Cotton lace blouse with necklace</span>
</a>
</div>
<div class="h6 mb-2">$54.00</div>
<div class="position-relative">
<div class="hover-effect-target fs-xs text-body-secondary opacity-100">+2
colors</div>
<div
class="hover-effect-target d-flex gap-2 position-absolute top-0 start-0 opacity-0">
<input type="radio" class="btn-check" name="colors-2"
id="color-2-1" checked>
<label for="color-2-1" class="btn btn-color fs-base"
style="color: #dcb1b1">
<span class="visually-hidden">Pink</span>
</label>
<input type="radio" class="btn-check" name="colors-2"
id="color-2-2">
<label for="color-2-2" class="btn btn-color fs-base"
style="color: #ced6f0">
<span class="visually-hidden">Blue</span>
</label>
<input type="radio" class="btn-check" name="colors-2"
id="color-2-3">
<label for="color-2-3" class="btn btn-color fs-base"
style="color: #e1e0cf">
<span class="visually-hidden">Mustard</span>
</label>
</div>
</div>
</div>
</div>
{{-- <div class="col">
<x-home.product-card :product="$product" />
</div> --}}
<!-- Item -->
<div class="col">
@ -3395,4 +3299,21 @@
@endsection
@section('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const radios = document.querySelectorAll('input[name="colors"]');
radios.forEach(radio => {
radio.addEventListener('change', function() {
const label = document.querySelector(`label[for="${this.id}"]`);
const price = label.getAttribute('data-price');
const image = label.getAttribute('data-image');
const labelText = label.getAttribute('data-label');
document.querySelector('.display_price').textContent = price;
document.querySelector('.product-main-image').src = image;
document.querySelector('#colorOption').textContent = labelText;
});
});
});
</script>
@endsection

View File

@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route;
use App\Http\Controllers\RoutingController;
use App\Http\Controllers\LocationController;
use App\Http\Controllers\LocaleController;
use App\Http\Controllers\ProductController;
Route::group(['prefix' => '/dummy'], function () {
Route::get('', [RoutingController::class, 'index'])->name('root');
@ -21,3 +22,5 @@ Route::post('/location/select', [LocationController::class, 'select'])->name('lo
Route::post('/locale/switch', [LocaleController::class, 'switch'])->name('locale.switch');
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/product/{slug}',[ProductController::class, 'detail'])->name('product.detail');