├── stubs
├── shared
│ ├── resources
│ │ └── css
│ │ │ ├── swiper.css
│ │ │ ├── app.css
│ │ │ └── links.css
│ ├── postcss.config.js
│ ├── app
│ │ ├── Models
│ │ │ ├── Brand.php
│ │ │ ├── Category.php
│ │ │ ├── Channel.php
│ │ │ ├── Collection.php
│ │ │ ├── User.php
│ │ │ ├── ProductVariant.php
│ │ │ └── Product.php
│ │ ├── Contracts
│ │ │ └── ManageOrder.php
│ │ ├── DTO
│ │ │ ├── PriceData.php
│ │ │ ├── OptionData.php
│ │ │ ├── CountryByZoneData.php
│ │ │ └── AddressData.php
│ │ ├── helpers.php
│ │ ├── Actions
│ │ │ ├── CountriesWithZone.php
│ │ │ ├── Payment
│ │ │ │ └── PayWithCash.php
│ │ │ ├── ZoneSessionManager.php
│ │ │ ├── GetCountriesByZone.php
│ │ │ └── CreateOrder.php
│ │ ├── Enums
│ │ │ └── PaymentType.php
│ │ ├── Http
│ │ │ ├── Controllers
│ │ │ │ └── Auth
│ │ │ │ │ └── VerifyEmailController.php
│ │ │ └── Middleware
│ │ │ │ └── ZoneDetector.php
│ │ ├── Traits
│ │ │ └── HasProductPricing.php
│ │ └── Filament
│ │ │ └── Components
│ │ │ └── Form
│ │ │ └── AddressFields.php
│ ├── config
│ │ ├── starterkit.php
│ │ └── shopper
│ │ │ └── models.php
│ └── tailwind.config.js
├── blade-common
│ └── resources
│ │ └── views
│ │ └── components
│ │ ├── container.blade.php
│ │ ├── order
│ │ ├── status.blade.php
│ │ ├── items.blade.php
│ │ ├── item.blade.php
│ │ ├── index.blade.php
│ │ └── summary.blade.php
│ │ ├── forms
│ │ ├── radio.blade.php
│ │ ├── label.blade.php
│ │ ├── errors.blade.php
│ │ ├── input.blade.php
│ │ ├── text-area.blade.php
│ │ └── select.blade.php
│ │ ├── nav
│ │ ├── item.blade.php
│ │ └── account-link.blade.php
│ │ ├── icons
│ │ └── payments
│ │ │ ├── cash.blade.php
│ │ │ ├── stripe.blade.php
│ │ │ └── visa.blade.php
│ │ ├── auth-session-status.blade.php
│ │ ├── footer-link.blade.php
│ │ ├── discount-badge.blade.php
│ │ ├── product
│ │ ├── reviews.blade.php
│ │ ├── thumbnail.blade.php
│ │ ├── gallery.blade.php
│ │ ├── price.blade.php
│ │ ├── card.blade.php
│ │ └── additionnal-infos.blade.php
│ │ ├── banner.blade.php
│ │ ├── page-heading.blade.php
│ │ ├── loading-dots.blade.php
│ │ ├── buttons
│ │ ├── submit.blade.php
│ │ ├── danger.blade.php
│ │ ├── primary.blade.php
│ │ └── default.blade.php
│ │ ├── rate-stars.blade.php
│ │ ├── status-indicator.blade.php
│ │ ├── account-card-link.blade.php
│ │ ├── alert
│ │ ├── success.blade.php
│ │ └── error.blade.php
│ │ ├── cart
│ │ ├── element.blade.php
│ │ └── item.blade.php
│ │ ├── auth-oauth.blade.php
│ │ └── stats.blade.php
└── livewire
│ ├── resources
│ └── views
│ │ ├── components
│ │ ├── layouts
│ │ │ ├── templates
│ │ │ │ ├── light.blade.php
│ │ │ │ ├── app.blade.php
│ │ │ │ └── account.blade.php
│ │ │ ├── base.blade.php
│ │ │ └── header.blade.php
│ │ ├── link.blade.php
│ │ ├── zones-selector.blade.php
│ │ ├── modal.blade.php
│ │ ├── attributes
│ │ │ ├── color.blade.php
│ │ │ ├── index.blade.php
│ │ │ └── size.blade.php
│ │ ├── checkout-steps.blade.php
│ │ └── address
│ │ │ └── edit-address.blade.php
│ │ └── livewire
│ │ ├── components
│ │ ├── tax-price.blade.php
│ │ ├── navigation.blade.php
│ │ ├── shipping-price.blade.php
│ │ ├── README.md
│ │ ├── cart-total.blade.php
│ │ ├── global-search.blade.php
│ │ ├── account-menu.blade.php
│ │ ├── currency-selector.blade.php
│ │ ├── shopping-cart-button.blade.php
│ │ ├── product
│ │ │ └── images.blade.php
│ │ ├── profile
│ │ │ ├── delete-user-form.blade.php
│ │ │ ├── update-password-form.blade.php
│ │ │ └── update-profile-information-form.blade.php
│ │ ├── variants-selector.blade.php
│ │ └── checkout
│ │ │ ├── payment.blade.php
│ │ │ └── delivery.blade.php
│ │ ├── pages
│ │ ├── account
│ │ │ ├── profile.blade.php
│ │ │ ├── addresses.blade.php
│ │ │ ├── orders
│ │ │ │ ├── index.blade.php
│ │ │ │ └── detail.blade.php
│ │ │ └── index.blade.php
│ │ ├── auth
│ │ │ ├── confirm-password.blade.php
│ │ │ ├── verify-email.blade.php
│ │ │ ├── forgot-password.blade.php
│ │ │ └── login.blade.php
│ │ ├── single-product.blade.php
│ │ └── home.blade.php
│ │ └── modals
│ │ ├── zone-selector.blade.php
│ │ └── shopping-cart.blade.php
│ ├── tests
│ ├── Feature
│ │ └── Livewire
│ │ │ ├── Pages
│ │ │ └── HomeTest.php
│ │ │ ├── Auth
│ │ │ ├── RegistrationTest.php
│ │ │ ├── PasswordConfirmationTest.php
│ │ │ ├── PasswordUpdateTest.php
│ │ │ ├── EmailVerificationTest.php
│ │ │ ├── AuthenticationTest.php
│ │ │ └── PasswordResetTest.php
│ │ │ └── Modals
│ │ │ └── AddressTest.php
│ └── Pest.php
│ ├── app
│ └── Livewire
│ │ ├── Actions
│ │ └── Logout.php
│ │ ├── Components
│ │ ├── CheckoutWizard.php
│ │ ├── Checkout
│ │ │ ├── Delivery.php
│ │ │ ├── Payment.php
│ │ │ └── Shipping.php
│ │ └── VariantsSelector.php
│ │ ├── Pages
│ │ ├── Account
│ │ │ ├── Orders.php
│ │ │ └── Addresses.php
│ │ ├── Home.php
│ │ ├── Checkout.php
│ │ └── SingleProduct.php
│ │ ├── Modals
│ │ ├── ShoppingCart.php
│ │ ├── ZoneSelector.php
│ │ └── Account
│ │ │ └── AddressForm.php
│ │ └── Forms
│ │ └── LoginForm.php
│ ├── vite.config.js
│ └── routes
│ ├── web.php
│ └── auth.php
├── src
└── StarterKitServiceProvider.php
├── LICENSE.md
├── composer.json
└── README.md
/stubs/shared/resources/css/swiper.css:
--------------------------------------------------------------------------------
1 | .swiper-scrollbar-drag {
2 | @apply bg-primary-800 rounded-none;
3 | }
4 |
--------------------------------------------------------------------------------
/stubs/shared/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/container.blade.php:
--------------------------------------------------------------------------------
1 |
twMerge(['class' => 'size-8']) }}
3 | stroke-width="1.5"
4 | aria-hidden="true"
5 | />
6 |
--------------------------------------------------------------------------------
/stubs/shared/resources/css/app.css:
--------------------------------------------------------------------------------
1 | @import "links.css";
2 | @import "swiper.css";
3 |
4 | @tailwind base;
5 | @tailwind components;
6 | @tailwind utilities;
7 |
8 | [x-cloak] {
9 | display: none !important;
10 | }
11 |
--------------------------------------------------------------------------------
/stubs/shared/app/Models/Category.php:
--------------------------------------------------------------------------------
1 | merge(['class' => 'font-medium text-sm text-green-600']) }}>
5 | {{ $status }}
6 |
7 | @endif
8 |
--------------------------------------------------------------------------------
/stubs/shared/config/starterkit.php:
--------------------------------------------------------------------------------
1 | env('SHOPPER_DEFAULT_ZONE', 'EU'),
8 |
9 | 'free_shipping_amount' => env('SHOPPER_FREE_SHIPPING_AMOUNT', 500),
10 |
11 | ];
12 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/layouts/templates/app.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $slot }}
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/stubs/shared/app/Contracts/ManageOrder.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/footer-link.blade.php:
--------------------------------------------------------------------------------
1 | twMerge(['class' => 'text-sm text-gray-900 group group-link-underline']) }}
3 | >
4 |
5 | {{ $slot }}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/stubs/shared/app/Models/ProductVariant.php:
--------------------------------------------------------------------------------
1 | 0])
8 |
9 | ?>
10 |
11 |
12 | {{ shopper_money_format(amount: $price, currency: current_currency()) }}
13 |
14 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/discount-badge.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'discount',
3 | ])
4 |
5 | twMerge(['class' => 'inline-flex items-center bg-pink-50 px-2 py-1 text-xs font-medium text-pink-700 ring-1 ring-inset ring-pink-600/10']) }}>
6 | {{ __('-:discount%', ['discount' => $discount]) }}
7 |
8 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/forms/errors.blade.php:
--------------------------------------------------------------------------------
1 | @props(['messages'])
2 |
3 | @if ($messages)
4 | merge(['class' => 'text-sm text-danger-600 space-y-1']) }}>
5 | @foreach ((array) $messages as $message)
6 | {{ $message }}
7 | @endforeach
8 |
9 | @endif
10 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/product/reviews.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'rating' => 0,
3 | 'count' => 0
4 | ])
5 |
6 |
7 |
{{ __('shopper::pages.products.reviews.rating_count', ['rating' => $rating, 'count' => $count]) }}
8 |
9 |
--------------------------------------------------------------------------------
/stubs/shared/app/DTO/PriceData.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ __('Free shipping from :amount', ['amount' => shopper_money_format(config('starterkit.free_shipping_amount'), current_currency())]) }}
4 |
5 |
6 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/forms/input.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'disabled' => false,
3 | ])
4 |
5 | merge(['class' => 'inline-flex w-full py-2 placeholder-gray-500 border-gray-300 focus:ring-primary-500 focus:ring-2 focus:border-transparent border-gray-300 focus:outline-none sm:text-sm']) !!}>
6 |
--------------------------------------------------------------------------------
/stubs/shared/app/DTO/OptionData.php:
--------------------------------------------------------------------------------
1 | false,
3 | ])
4 |
5 |
7 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/page-heading.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'title',
3 | 'description' => null,
4 | ])
5 |
6 |
7 |
8 | {{ $title }}
9 |
10 |
11 | @if ($description)
12 |
{{ $description }}
13 | @endif
14 |
15 |
--------------------------------------------------------------------------------
/stubs/shared/app/helpers.php:
--------------------------------------------------------------------------------
1 | currencyCode
12 | : shopper_currency();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/stubs/shared/app/Actions/CountriesWithZone.php:
--------------------------------------------------------------------------------
1 | handle();
15 | });
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/nav/account-link.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'href',
3 | 'title',
4 | 'active' => false,
5 | ])
6 |
7 | $active
10 | ])>
11 | {{ $title }}
12 |
13 |
--------------------------------------------------------------------------------
/stubs/shared/app/Models/Product.php:
--------------------------------------------------------------------------------
1 | is_visible && $this->published_at && $this->published_at <= now();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/loading-dots.blade.php:
--------------------------------------------------------------------------------
1 | @php
2 | $dots = 'mx-[1px] inline-block size-1 animate-blink';
3 | @endphp
4 |
5 |
6 | twMerge(['class' => $dots]) }}>
7 | twMerge(['class' => $dots . ' animation-delay-[200ms]']) }}>
8 | twMerge(['class' => $dots . ' animation-delay-[400ms]']) }}>
9 |
10 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Pages/HomeTest.php:
--------------------------------------------------------------------------------
1 | assertOk();
13 |
14 | Livewire::test(Home::class)
15 | ->assertSuccessful();
16 | });
17 | })->group('pages');
18 |
--------------------------------------------------------------------------------
/stubs/shared/app/Actions/Payment/PayWithCash.php:
--------------------------------------------------------------------------------
1 | forget('checkout');
15 |
16 | return redirect()->route('order-confirmed', ['number' => $order->number]);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/stubs/shared/app/Enums/PaymentType.php:
--------------------------------------------------------------------------------
1 | logout();
18 |
19 | Session::invalidate();
20 | Session::regenerateToken();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/navigation.blade.php:
--------------------------------------------------------------------------------
1 | once(
10 | fn () => Category::isRoot()->scopes(['enabled'])->get()
11 | ),
12 | ]);
13 |
14 | ?>
15 |
16 |
17 | @foreach ($categories as $category)
18 | {{ $category->name }}
19 | @endforeach
20 |
21 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/product/thumbnail.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'product',
3 | 'containerClass' => null,
4 | ])
5 |
6 |
7 |
twMerge(['class' => 'size-full max-w-none object-cover object-center group-hover:opacity-75']) }}
11 | />
12 |
13 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/product/gallery.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'images',
3 | ])
4 |
5 |
6 | @foreach ($images as $image)
7 |
8 |
14 |
15 | @endforeach
16 |
17 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/shipping-price.blade.php:
--------------------------------------------------------------------------------
1 | 0]);
8 |
9 | on(['cart-price-update' => function () {
10 | $this->price = data_get(session()->get('checkout'), 'shipping_option')
11 | ? data_get(session()->get('checkout'), 'shipping_option')[0]['price']
12 | : 0;
13 | }]);
14 |
15 | ?>
16 |
17 |
18 | {{ shopper_money_format(amount: $price, currency: current_currency()) }}
19 |
20 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/buttons/submit.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'title',
3 | ])
4 |
5 | twMerge(['class' => 'px-4 py-3 text-sm data-[loading]:pointer-events-none']) }}
8 | >
9 |
10 | {{ $title }}
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/README.md:
--------------------------------------------------------------------------------
1 | ### Livewire Volt components
2 |
3 | All Livewire Volt components.
4 |
5 | #### Components
6 |
7 | The list of components available in this folder with explanations and actions for each one
8 |
9 | - `global-search` component for search in the entire website
10 | - `shopping-cart-button` it's the component of the basket button, it gives the quantity of items in the user's cart
11 | - `variants-selector` this is the component that will manage product variants
12 | - `profile/*` all components related to user profile management
13 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/forms/select.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'items' => false,
3 | 'key' => 'id',
4 | 'value' => 'name',
5 | ])
6 |
7 | merge(['class' => 'block w-full px-3 border border-gray-300 bg-white focus:outline-none focus:border-transparent focus:ring-2 focus:ring-primary-500 sm:text-sm']) !!}>
8 | @if($items)
9 | @foreach($items as $item)
10 | {{ $item->{$value} }}
11 | @endforeach
12 | @else
13 | {{ $slot }}
14 | @endif
15 |
16 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/rate-stars.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'rating' => 0,
3 | ])
4 |
5 |
6 | @foreach ([1, 2, 3, 4, 5] as $star)
7 | {{-- format-ignore-start --}}
8 | $rating >= $star,
12 | 'text-gray-200' => $rating <= $star,
13 | 'text-yellow-200' => $rating < 1
14 | ])
15 | aria-hidden="true"
16 | />
17 | {{-- format-ignore-end --}}
18 | @endforeach
19 |
20 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Components/CheckoutWizard.php:
--------------------------------------------------------------------------------
1 | extend(Tests\TestCase::class)
18 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
19 | ->in('Feature');
20 |
--------------------------------------------------------------------------------
/stubs/shared/app/Actions/ZoneSessionManager.php:
--------------------------------------------------------------------------------
1 | exists('zone');
14 | }
15 |
16 | public static function setSession(CountryByZoneData $zone): void
17 | {
18 | if (self::checkSession()) {
19 | session()->forget('zone');
20 | }
21 |
22 | session()->put('zone', $zone);
23 | }
24 |
25 | public static function getSession(): ?CountryByZoneData
26 | {
27 | return session()->get('zone');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/stubs/livewire/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import laravel, { refreshPaths } from 'laravel-vite-plugin';
3 |
4 | export default defineConfig({
5 | plugins: [
6 | laravel({
7 | input: [
8 | 'resources/css/app.css',
9 | 'resources/js/app.js',
10 | ],
11 | refresh: [
12 | ...refreshPaths,
13 | 'app/Livewire/**',
14 | ],
15 | }),
16 | {
17 | name: 'blade',
18 | handleHotUpdate({ file, server }) {
19 | if (file.endsWith('.blade.php')) {
20 | server.ws.send({
21 | type: 'full-reload',
22 | path: '*',
23 | });
24 | }
25 | },
26 | }
27 | ],
28 | });
29 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/account/profile.blade.php:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/cart-total.blade.php:
--------------------------------------------------------------------------------
1 | CartFacade::session(session()->getId())->getTotal()]);
10 |
11 | on(['cart-price-update' => function () {
12 | $this->price = data_get(session()->get('checkout'), 'shipping_option')
13 | ? (int) data_get(session()->get('checkout'), 'shipping_option')[0]['price'] + CartFacade::session(session()->getId())->getTotal()
14 | : 0;
15 | }]);
16 |
17 | ?>
18 |
19 |
20 | {{ shopper_money_format(amount: $price, currency: current_currency()) }}
21 |
22 |
--------------------------------------------------------------------------------
/src/StarterKitServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
20 | return;
21 | }
22 |
23 | $this->commands([
24 | Console\InstallCommand::class,
25 | ]);
26 | }
27 |
28 | public function provides(): array
29 | {
30 | return [Console\InstallCommand::class];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/status-indicator.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'title',
3 | 'variant',
4 | ])
5 |
6 | @php
7 | $dotClass = match ($variant) {
8 | 'info' => 'bg-blue-500',
9 | 'teal' => 'bg-teal-500',
10 | 'green' => 'bg-green-500',
11 | 'danger' => 'bg-rose-500',
12 | 'warning' => 'bg-yellow-500',
13 | 'primary' => 'bg-violet-500',
14 | 'sky' => 'bg-sky-500',
15 | default => 'bg-gray-400',
16 | };
17 | @endphp
18 |
19 | twMerge(['class' => 'inline-flex items-center gap-2 bg-white rounded-full px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-200']) }}>
20 |
24 |
{{ $title }}
25 |
26 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/account-card-link.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'href',
3 | 'icon',
4 | 'title',
5 | 'description',
6 | ])
7 |
8 |
9 |
10 |
11 | @svg($icon, 'size-6', ['aria-hidden' => true, 'stroke-width' => '1.5'])
12 |
13 |
14 |
15 |
16 | {{ $title }}
17 |
18 |
19 |
20 |
{{ $description }}
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/global-search.blade.php:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 | {{ __('Recherche') }}
14 |
22 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/alert/success.blade.php:
--------------------------------------------------------------------------------
1 | @props(['status'])
2 |
3 | @if (session('status'))
4 | merge(['class' => 'bg-green-50 p-4']) }}>
5 |
6 |
11 |
12 |
13 | {{ $status }}
14 |
15 |
16 |
17 |
18 | @endif
19 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/product/price.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'product',
3 | ])
4 |
5 | @php
6 | $price = $product->getPrice();
7 | @endphp
8 |
9 | twMerge(['class' => 'inline-flex flex-col gap-0.5 text-sm']) }}>
10 |
11 | {{ $price?->value->formatted }}
12 |
13 |
14 | @if($price && $price->percentage && $price->percentage > 0)
15 |
16 | {{ __('Original :') }}
17 |
18 | {{ $price->compare->formatted }}
19 |
20 |
24 |
25 | @endif
26 |
27 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/zones-selector.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ __('Shipping to') }} :
4 |
5 |
10 |
15 |
16 | {{ \App\Actions\ZoneSessionManager::getSession()?->countryName }}
17 |
18 | , {{ __('change zone') }}
19 |
20 |
21 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/alert/error.blade.php:
--------------------------------------------------------------------------------
1 | @props(['status'])
2 |
3 | @if (session('error'))
4 | merge(['class' => 'bg-red-50 p-4']) }}>
5 |
6 |
11 |
12 |
13 | {{ $status }}
14 |
15 |
16 |
17 |
18 | @endif
19 |
--------------------------------------------------------------------------------
/stubs/shared/app/Http/Controllers/Auth/VerifyEmailController.php:
--------------------------------------------------------------------------------
1 | user()->hasVerifiedEmail()) {
20 | return redirect()->intended(route('account', absolute: false).'?verified=1');
21 | }
22 |
23 | if ($request->user()->markEmailAsVerified()) {
24 | event(new Verified($request->user()));
25 | }
26 |
27 | return redirect()->intended(route('account', absolute: false).'?verified=1');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Pages/Account/Orders.php:
--------------------------------------------------------------------------------
1 | auth()->user()
21 | ->orders()
22 | ->with([
23 | 'items',
24 | 'items.product',
25 | 'shippingOption',
26 | 'shippingAddress',
27 | 'billingAddress',
28 | ])
29 | ->latest()
30 | ->simplePaginate(3),
31 | ])
32 | ->title(__('My orders'));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Auth/RegistrationTest.php:
--------------------------------------------------------------------------------
1 | get('/register');
12 |
13 | $response
14 | ->assertOk()
15 | ->assertSeeVolt('pages.auth.register');
16 | });
17 |
18 | test('new users can register', function (): void {
19 | $component = Volt::test('pages.auth.register')
20 | ->set('last_name', 'Test')
21 | ->set('first_name', 'User')
22 | ->set('email', 'test@example.com')
23 | ->set('password', 'password')
24 | ->set('password_confirmation', 'password');
25 |
26 | $component->call('register');
27 |
28 | $component->assertRedirect('/account');
29 |
30 | $this->assertAuthenticated();
31 | });
32 | })->group('auth');
33 |
--------------------------------------------------------------------------------
/stubs/shared/app/DTO/CountryByZoneData.php:
--------------------------------------------------------------------------------
1 | loadMissing('prices', 'prices.currency')
17 | ->prices
18 | ->reject(fn ($price) => $price->currency->code !== $currencyCode)
19 | ->first();
20 |
21 | return $price
22 | ? new PriceData(
23 | value: Price::from($price->amount, $currencyCode),
24 | compare: $price->compare_amount ? Price::from($price->compare_amount, $currencyCode) : null,
25 | percentage: $price->compare_amount > 0
26 | ? round((($price->compare_amount - $price->amount) / $price->compare_amount) * 100)
27 | : null
28 | )
29 | : null;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/order/items.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'items',
3 | 'currency_code',
4 | ])
5 |
6 |
7 | @foreach($items as $item)
8 |
9 |
10 |
11 |
12 | {{ $item->name }}
13 |
14 |
15 | {{ __('Unit price') }} : {{ shopper_money_format($item->unit_price_amount, $currency_code) }}
16 |
17 |
18 | {{ __('Quantity') }} : {{ $item->quantity }}
19 |
20 |
21 |
22 | @endforeach
23 |
24 |
--------------------------------------------------------------------------------
/stubs/livewire/routes/web.php:
--------------------------------------------------------------------------------
1 | name('home');
21 | Route::get('/products/{slug}', Pages\SingleProduct::class)->name('single-product');
22 |
23 | Route::middleware('auth')->group(function (): void {
24 | Volt::route('/order/confirmed/{number}', 'pages.order.confirmed')
25 | ->name('order-confirmed');
26 |
27 | Route::get('checkout', Pages\Checkout::class)->name('checkout');
28 | });
29 |
30 | require __DIR__.'/auth.php';
31 |
32 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/account-menu.blade.php:
--------------------------------------------------------------------------------
1 | redirect('/', navigate: true);
18 | }
19 | }; ?>
20 |
21 |
22 |
23 |
24 | {{ __('My account') }}
25 |
26 |
27 |
28 | {{ __('Logout') }}
29 |
30 |
31 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/product/card.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'product',
3 | ])
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ $product->name }}
14 |
15 |
16 |
17 | @if ($product->brand_id)
18 |
19 | {{ $product->brand->name }}
20 |
21 | @endif
22 |
23 |
24 |
25 |
26 | @if($product->variants_count > 0)
27 |
28 | {{ __('+:count variants', ['count' => $product->variants_count]) }}
29 |
30 | @endif
31 |
32 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/account/addresses.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
13 | {{ __('Add address') }}
14 |
15 |
16 | @if($addresses->isNotEmpty())
17 |
18 | @foreach($addresses as $address)
19 |
20 | @endforeach
21 |
22 | @else
23 |
24 | {{ __('You have not yet added any addresses to your space.') }}
25 |
26 | @endif
27 |
28 |
29 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Pages/Account/Addresses.php:
--------------------------------------------------------------------------------
1 | find($id)->delete();
20 |
21 | Notification::make()
22 | ->title(__('The address has been correctly removed from your list!'))
23 | ->success()
24 | ->send();
25 |
26 | $this->dispatch('addresses-updated');
27 | }
28 |
29 | #[On('addresses-updated')]
30 | public function render(): View
31 | {
32 | return view('livewire.pages.account.addresses', [
33 | 'addresses' => auth()->user()->addresses->load('country'),
34 | ])
35 | ->title(__('My addresses'));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Arthur Monney
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Pages/Home.php:
--------------------------------------------------------------------------------
1 | Product::with([
20 | 'brand',
21 | 'media',
22 | 'prices' => function ($query) {
23 | $query->whereRelation('currency', 'code', current_currency());
24 | },
25 | 'prices.currency',
26 | ])
27 | ->withCount('variants')
28 | ->publish()
29 | ->get(),
30 | 'collections' => Collection::with(['media'])
31 | ->select('id', 'slug', 'name')
32 | ->scopes('manual')
33 | ->take(3)
34 | ->get(),
35 | ]);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/order/item.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'item',
3 | 'currency_code',
4 | ])
5 |
6 |
7 |
8 |
9 |
10 |
{{ $item->name }}
11 |
12 |
13 | {{ shopper_money_format($item->total, $currency_code) }}
14 |
15 |
16 | ({{ $item->quantity }}x {{ shopper_money_format($item->unit_price_amount, $currency_code) }})
17 |
18 |
19 |
20 |
21 |
22 |
27 |
28 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shopper/starter-kits",
3 | "description": "Minimum scaffolding starter kit for Shopper e-commerce",
4 | "keywords": ["laravel", "shopper", "e-commerce", "livewire", "inertiajs", "vue", "react"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Arthur Monney",
9 | "email": "arthur@laravelshopper.dev"
10 | }
11 | ],
12 | "require": {
13 | "php": "^8.2",
14 | "illuminate/console": "^11.0",
15 | "illuminate/filesystem": "^11.0",
16 | "illuminate/support": "^11.0",
17 | "illuminate/validation": "^11.0",
18 | "shopper/framework": "^2.0",
19 | "symfony/console": "^7.0"
20 | },
21 | "require-dev": {
22 | "laravel/framework": "^11.0",
23 | "orchestra/testbench-core": "^9.0",
24 | "phpstan/phpstan": "^2.0"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Shopper\\StarterKit\\": "src/"
29 | }
30 | },
31 | "extra": {
32 | "laravel": {
33 | "providers": [
34 | "Shopper\\StarterKit\\StarterKitServiceProvider"
35 | ]
36 | }
37 | },
38 | "config": {
39 | "sort-packages": true
40 | },
41 | "minimum-stability": "dev",
42 | "prefer-stable": true
43 | }
44 |
--------------------------------------------------------------------------------
/stubs/shared/app/DTO/AddressData.php:
--------------------------------------------------------------------------------
1 | sessionKey = session()->getId();
20 |
21 | // @phpstan-ignore-next-line
22 | if (CartFacade::session($this->sessionKey)->isEmpty()) {
23 | if (session()->exists('checkout')) {
24 | session()->forget('checkout');
25 | }
26 |
27 | $this->redirect(route('home'), true);
28 | }
29 | }
30 |
31 | public function render(): View
32 | {
33 | return view('livewire.pages.checkout', [
34 | 'items' => CartFacade::session($this->sessionKey)->getContent(), // @phpstan-ignore-line
35 | 'subtotal' => CartFacade::session($this->sessionKey)->getSubTotal(), // @phpstan-ignore-line
36 | ])
37 | ->title(__('Proceed to checkout'));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/account/orders/index.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
6 | @if($orders->isEmpty())
7 |
8 |
13 |
14 | {{ __("You haven't ordered anything from us yet. Is this the day to change that?") }}
15 |
16 |
17 | {{ __('Continue shopping') }}
18 |
19 |
20 | @else
21 |
22 | @foreach($orders as $order)
23 |
24 | @endforeach
25 |
26 |
27 |
28 | {{ $orders->links() }}
29 |
30 | @endif
31 |
32 |
--------------------------------------------------------------------------------
/stubs/shared/app/Actions/GetCountriesByZone.php:
--------------------------------------------------------------------------------
1 | scopes('enabled')
18 | ->get();
19 |
20 | $countriesByZone = $zones->map(function (Zone $zone) {
21 | return $zone->countries->map(function (Country $country) use ($zone) {
22 | return CountryByZoneData::fromArray([
23 | 'zone_id' => $zone->id,
24 | 'zone_name' => $zone->name,
25 | 'zone_code' => $zone->code,
26 | 'country_id' => $country->id,
27 | 'country_name' => $country->name,
28 | 'country_code' => $country->cca2,
29 | 'country_flag' => $country->svg_flag,
30 | 'currency_code' => $zone->currency->code,
31 | ]);
32 | });
33 | });
34 |
35 | return $countriesByZone->flatten(1);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/stubs/shared/app/Http/Middleware/ZoneDetector.php:
--------------------------------------------------------------------------------
1 | handle();
20 |
21 | $currencyZone = $countries->firstWhere('currencyCode', shopper_currency());
22 |
23 | if ($currencyZone) {
24 | ZoneSessionManager::setSession($currencyZone);
25 | } else {
26 | $this->setDefaultZone($countries);
27 | }
28 | }
29 |
30 | return $next($request);
31 | }
32 |
33 | private function setDefaultZone(Collection $countries): void
34 | {
35 | $defaultZone = $countries->firstWhere('zoneCode', config('starterkit.default_zone'));
36 |
37 | if (! ZoneSessionManager::checkSession() && $defaultZone) {
38 | ZoneSessionManager::setSession($defaultZone);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/layouts/base.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'title' => null,
3 | ])
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ $title ?? 'Starter Kit by Shopper' }} // {{ config('app.name') }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | @filamentStyles
21 | @vite('resources/css/app.css')
22 |
23 |
24 | {{ $slot }}
25 |
26 |
27 |
28 | @livewire('notifications')
29 | @livewire('slide-over-panel')
30 | @livewire('wire-elements-modal')
31 |
32 | @filamentScripts
33 | @vite('resources/js/app.js')
34 |
35 |
36 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Pages/SingleProduct.php:
--------------------------------------------------------------------------------
1 | function ($query): void {
26 | $query->whereRelation('currency', 'code', current_currency());
27 | },
28 | 'prices.currency',
29 | 'inventoryHistories',
30 | 'categories' => function ($query): void {
31 | $query->select('id', 'name');
32 | }
33 | ])
34 | ->withCount('variants')
35 | ->where('slug', $slug)
36 | ->firstOrFail();
37 |
38 | abort_unless($product->isPublished(), 404);
39 |
40 | $this->product = $product;
41 | }
42 |
43 | public function render(): View
44 | {
45 | return view('livewire.pages.single-product')
46 | ->title($this->product->name);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/icons/payments/visa.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/stubs/shared/resources/css/links.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-color: theme('colors.primary.500');
3 | --primary-color-hint: theme('colors.primary.800');
4 | }
5 |
6 | .link {
7 | border-color: transparent;
8 | border-bottom-width: 1px;
9 | transition-property: background-color, border-color, color, fill, stroke;
10 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
11 | transition-duration: 0.5s;
12 | }
13 |
14 | a.group-link-underline .link-underline {
15 | border-bottom-width: 0;
16 | background-size:
17 | 100% 2px,
18 | 0 2px;
19 | background-position:
20 | 100% 100%,
21 | 0 100%;
22 | background-repeat: no-repeat;
23 | transition:
24 | background-size 0.5s ease-in-out,
25 | background-position 0.5s ease-in-out;
26 | }
27 |
28 | a.group-link-underline .link-underline-black {
29 | background-image: linear-gradient(transparent, transparent), linear-gradient(#000, #000);
30 | }
31 |
32 | a.group-link-underline .link-underline-brand {
33 | background-image: linear-gradient(transparent, transparent), linear-gradient(var(--primary-color), var(--primary-color-hint));
34 | }
35 |
36 | a.group-link-underline .link-underline-white {
37 | background-image: linear-gradient(transparent, transparent), linear-gradient(#ffffff, #f5f5f5);
38 | }
39 |
40 | a.group-link-underline:hover .link-underline {
41 | background-size:
42 | 0 2px,
43 | 100% 2px;
44 | background-position:
45 | 100% 100%,
46 | 0 100%;
47 | }
48 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/currency-selector.blade.php:
--------------------------------------------------------------------------------
1 | countryFlag
16 | : (
17 | shopper_setting('country_id')
18 | ? Country::query()->find(shopper_setting('country_id'))->svg_flag
19 | : null
20 | );
21 | }
22 | } ?>
23 |
24 |
25 |
! session()->has('zone')
33 | ])
34 | >
35 | @if($this->countryFlag)
36 |
37 | @endif
38 |
39 | {{ current_currency() }}
40 | , {{ __('change currency') }}
41 |
42 |
43 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/buttons/danger.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'href' => null,
3 | ])
4 |
5 | @if ($href)
6 | twMerge(['class' => 'group relative py-2.5 inline-flex border border-transparent text-sm font-medium text-white shadow-sm focus:outline-none']) }}
9 | >
10 |
13 |
14 |
15 | {{ $slot }}
16 |
17 |
18 | @else
19 | twMerge(['class' => 'group relative py-2.5 inline-flex border border-transparent text-sm font-medium text-white shadow-sm focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed']) }}
21 | >
22 |
25 |
26 |
27 | {{ $slot }}
28 |
29 |
30 | @endif
31 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/buttons/primary.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'href' => null,
3 | ])
4 |
5 | @if ($href)
6 | twMerge(['class' => 'group relative py-2.5 inline-flex border border-transparent text-sm font-medium text-white shadow-sm focus:outline-none']) }}
9 | >
10 |
13 |
14 |
15 | {{ $slot }}
16 |
17 |
18 | @else
19 | twMerge(['class' => 'group relative py-2.5 inline-flex border border-transparent text-sm font-medium text-white shadow-sm focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed']) }}
21 | >
22 |
25 |
26 |
27 | {{ $slot }}
28 |
29 |
30 | @endif
31 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Auth/PasswordConfirmationTest.php:
--------------------------------------------------------------------------------
1 | create();
13 |
14 | $response = $this->actingAs($user)->get('/confirm-password');
15 |
16 | $response
17 | ->assertSeeVolt('pages.auth.confirm-password')
18 | ->assertStatus(200);
19 | });
20 |
21 | test('password can be confirmed', function (): void {
22 | $user = User::factory()->create();
23 |
24 | $this->actingAs($user);
25 |
26 | $component = Volt::test('pages.auth.confirm-password')
27 | ->set('password', 'password');
28 |
29 | $component->call('confirmPassword');
30 |
31 | $component
32 | ->assertRedirect('/account')
33 | ->assertHasNoErrors();
34 | });
35 |
36 | test('password is not confirmed with invalid password', function (): void {
37 | $user = User::factory()->create();
38 |
39 | $this->actingAs($user);
40 |
41 | $component = Volt::test('pages.auth.confirm-password')
42 | ->set('password', 'wrong-password');
43 |
44 | $component->call('confirmPassword');
45 |
46 | $component
47 | ->assertNoRedirect()
48 | ->assertHasErrors('password');
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Auth/PasswordUpdateTest.php:
--------------------------------------------------------------------------------
1 | create();
14 |
15 | $this->actingAs($user);
16 |
17 | $component = Volt::test('components.profile.update-password-form')
18 | ->set('current_password', 'password')
19 | ->set('password', 'new-password')
20 | ->set('password_confirmation', 'new-password')
21 | ->call('updatePassword');
22 |
23 | $component
24 | ->assertHasNoErrors()
25 | ->assertNoRedirect();
26 |
27 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password));
28 | });
29 |
30 | test('correct password must be provided to update password', function (): void {
31 | $user = User::factory()->create();
32 |
33 | $this->actingAs($user);
34 |
35 | $component = Volt::test('components.profile.update-password-form')
36 | ->set('current_password', 'wrong-password')
37 | ->set('password', 'new-password')
38 | ->set('password_confirmation', 'new-password')
39 | ->call('updatePassword');
40 |
41 | $component
42 | ->assertHasErrors(['current_password'])
43 | ->assertNoRedirect();
44 | });
45 | })->group('auth');
46 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/cart/element.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'item',
3 | ])
4 |
5 | @php
6 | $price = shopper_money_format(
7 | amount: $item->price * $item->quantity,
8 | currency: current_currency(),
9 | );
10 |
11 | $model = $item->associatedModel instanceof \App\Models\ProductVariant ? $item->associatedModel->product : $item->associatedModel;
12 | @endphp
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ $item->name }}
20 |
21 |
22 |
23 |
24 | {{ __(':qty x :price', ['qty' => $item->quantity, 'price' => shopper_money_format($item->price, current_currency())]) }}
25 |
26 |
27 | @if($item->attributes->isNotEmpty())
28 |
29 | @foreach($item->attributes as $name => $value)
30 |
31 | {{ $name }} :
32 | {{ $value }}
33 |
34 | @endforeach
35 |
36 | @endif
37 |
38 |
39 | {{ $price }}
40 |
41 |
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ## Starter Kits
10 |
11 | This repository is a base forked of Laravel Breeze, and is give as an example way to build a front-end with Shopper Headless e-commerce.
12 |
13 | > [!IMPORTANT]
14 | > As this starter kit is already based on Laravel Breeze, you no longer need to install it to have authentication, as it offers the same features as Laravel breeze.
15 |
16 | ## Installation
17 |
18 | After install and setup Shopper, you need to run the following command to install the starter kit
19 |
20 | ```bash
21 | composer require shopper/starter-kits --dev
22 | ```
23 |
24 | After installing the package, you may execute the `shopper:starter-kit:install` Artisan command. This command accepts the name of the stack you prefer (livewire, blade or inertia)
25 |
26 | ## Documentation
27 |
28 | Documentation for Starter kits can be found on the [Documentation](https://laravelshopper.dev/docs/starter-kits).
29 |
30 | ## License
31 |
32 | Starter Kits is open-sourced software licensed under the [MIT license](LICENSE.md).
33 |
--------------------------------------------------------------------------------
/stubs/shared/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import colors from 'tailwindcss/colors';
2 | import defaultTheme from 'tailwindcss/defaultTheme';
3 | import aspectRatio from '@tailwindcss/aspect-ratio';
4 | import forms from '@tailwindcss/forms';
5 | import typography from '@tailwindcss/typography';
6 | import preset from './vendor/filament/support/tailwind.config.preset';
7 |
8 | /** @type {import('tailwindcss').Config} */
9 | export default {
10 | presets: [preset],
11 | content: [
12 | './storage/framework/views/*.php',
13 | './resources/views/**/*.blade.php',
14 | './vendor/laravelcm/livewire-slide-overs/resources/views/*.blade.php',
15 | './vendor/wire-elements/modal/resources/views/*.blade.php',
16 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
17 | ],
18 | safelist: [
19 | {
20 | pattern: /max-w-(sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl)/,
21 | variants: ['sm', 'md', 'lg', 'xl', '2xl']
22 | }
23 | ],
24 | theme: {
25 | extend: {
26 | animation: {
27 | blink: 'blink 1.4s both infinite',
28 | },
29 | keyframes: {
30 | blink: {
31 | '0%': { opacity: 0.2 },
32 | '20%': { opacity: 1 },
33 | '100% ': { opacity: 0.2 },
34 | },
35 | },
36 | colors: {
37 | primary: colors.teal,
38 | },
39 | fontFamily: {
40 | sans: ['Figtree', ...defaultTheme.fontFamily.sans],
41 | heading: ['Space Grotesk', ...defaultTheme.fontFamily.sans],
42 | },
43 | maxWidth: {
44 | '8xl': '98rem',
45 | }
46 | },
47 | },
48 | plugins: [aspectRatio, forms, typography],
49 | };
50 |
--------------------------------------------------------------------------------
/stubs/livewire/routes/auth.php:
--------------------------------------------------------------------------------
1 | group(function (): void {
11 | Volt::route('register', 'pages.auth.register')
12 | ->name('register');
13 |
14 | Volt::route('login', 'pages.auth.login')
15 | ->name('login');
16 |
17 | Volt::route('forgot-password', 'pages.auth.forgot-password')
18 | ->name('password.request');
19 |
20 | Volt::route('reset-password/{token}', 'pages.auth.reset-password')
21 | ->name('password.reset');
22 | });
23 |
24 | Route::middleware('auth')->group(function (): void {
25 | Volt::route('verify-email', 'pages.auth.verify-email')
26 | ->name('verification.notice');
27 |
28 | Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
29 | ->middleware(['signed', 'throttle:6,1'])
30 | ->name('verification.verify');
31 |
32 | Volt::route('confirm-password', 'pages.auth.confirm-password')
33 | ->name('password.confirm');
34 |
35 | Volt::route('/account', 'pages.account.index')->name('account');
36 |
37 | Route::prefix('account')->as('account.')->group(function (): void {
38 | Volt::route('profile', 'pages.account.profile')->name('profile');
39 | Route::get('addresses', Pages\Account\Addresses::class)->name('addresses');
40 | Route::get('orders', Pages\Account\Orders::class)->name('orders');
41 | Volt::route('orders/{number}', 'pages.account.orders.detail')->name('orders.detail');
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/layouts/header.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | @auth
13 |
14 | @else
15 |
16 | {{ __('Login') }}
17 |
18 |
19 |
20 | {{ __('Register') }}
21 |
22 | @endauth
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Modals/ShoppingCart.php:
--------------------------------------------------------------------------------
1 | getId();
29 |
30 | $this->sessionKey = $sessionKey;
31 | $this->items = CartFacade::session($sessionKey)->getContent();
32 | $this->subtotal = CartFacade::session($sessionKey)->getSubTotal();
33 | }
34 |
35 | public function cartUpdated(): void
36 | {
37 | $this->items = CartFacade::session($this->sessionKey)->getContent();
38 | $this->subtotal = CartFacade::session($this->sessionKey)->getSubTotal();
39 | }
40 |
41 | public function removeToCart(int $id): void
42 | {
43 | CartFacade::session($this->sessionKey)->remove($id);
44 |
45 | Notification::make()
46 | ->title(__('Cart updated'))
47 | ->body(__('The product has been removed from your cart !'))
48 | ->success()
49 | ->send();
50 |
51 | $this->dispatch('cartUpdated');
52 | $this->dispatch('closePanel');
53 | }
54 |
55 | public function render(): View
56 | {
57 | return view('livewire.modals.shopping-cart');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Modals/ZoneSelector.php:
--------------------------------------------------------------------------------
1 | handle();
29 | }
30 |
31 | public function selectZone(int $countryId): void
32 | {
33 | /** @var CountryByZoneData $selectedZone */
34 | $selectedZone = $this->countries->firstWhere('countryId', $countryId);
35 |
36 | if ($selectedZone->countryId !== ZoneSessionManager::getSession()?->countryId) {
37 | ZoneSessionManager::setSession($selectedZone);
38 |
39 | session()->forget('checkout');
40 |
41 | $this->dispatch('zoneChanged');
42 | }
43 |
44 | $this->redirectIntended();
45 | }
46 |
47 | public function placeholder(): string
48 | {
49 | return <<<'Blade'
50 |
51 |
52 |
53 |
54 | Blade;
55 | }
56 |
57 | public function render(): View
58 | {
59 | return view('livewire.modals.zone-selector');
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/modal.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'formAction' => false,
3 | 'headerClasses' => '',
4 | 'contentClasses' => 'p-4 sm:p-6',
5 | 'footerClasses' => 'p-4 sm:flex sm:p-6',
6 | ])
7 |
8 | twMerge(['class' => 'h-full bg-white']) }}>
9 | @if ($formAction)
10 | {{-- format-ignore-start --}}
{{-- format-ignore-end --}}
44 | @endif
45 |
46 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/attributes/color.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'option',
3 | ])
4 |
5 |
6 |
7 |
{{ $option->attribute->name }}
8 |
9 |
10 | {{ __('Choose a color') }}
11 |
22 | @foreach ($option->values as $value)
23 |
32 | {{ $value->value }}
33 |
38 |
39 | @endforeach
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/shopping-cart-button.blade.php:
--------------------------------------------------------------------------------
1 | 0,
11 | 'sessionKey' => session()->getId(),
12 | ]);
13 |
14 | mount(function (): void {
15 | $this->cartTotalItems = CartFacade::session($this->sessionKey)->getTotalQuantity();
16 | });
17 |
18 | on(['cartUpdated' => function () {
19 | $this->cartTotalItems = CartFacade::session($this->sessionKey)->getTotalQuantity();
20 | }]);
21 |
22 | ?>
23 |
24 |
25 |
30 |
38 |
43 |
44 |
45 | {{ $cartTotalItems }}
46 |
47 | {{ __('items in cart, view cart') }}
48 |
49 |
50 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/buttons/default.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'href' => null,
3 | 'whiteBorder' => false,
4 | ])
5 |
6 | @if ($href)
7 | twMerge(['class' => 'group relative inline-flex py-2.5 border border-gray-300 text-sm font-medium text-gray-500 bg-white shadow-sm hover:bg-gray-50 hover:text-gray-700 focus:outline-none']) }}
10 | >
11 | $whiteBorder,
15 | 'border-gray-300' => ! $whiteBorder,
16 | ])
17 | >
18 |
19 |
20 | {{ $slot }}
21 |
22 |
23 | @else
24 | twMerge(['class' => 'group relative inline-flex py-2.5 border border-gray-300 text-sm font-medium text-gray-500 bg-white shadow-sm hover:bg-gray-50 hover:text-gray-700 focus:outline-none']) }}
26 | >
27 | $whiteBorder,
31 | 'border-gray-300' => ! $whiteBorder,
32 | ])
33 | >
34 |
35 |
36 | {{ $slot }}
37 |
38 |
39 | @endif
40 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/auth-oauth.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | {{ __('Or continue with') }}
9 |
10 |
11 |
12 |
13 |
26 |
27 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/attributes/index.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'option',
3 | ])
4 |
5 |
6 |
7 |
8 |
{{ $option->attribute->name }}
9 |
10 |
11 |
12 | {{ __('Choose a value') }}
13 |
24 | @foreach ($option->values as $value)
25 |
35 | {{ $value->value }}
36 |
37 | @endforeach
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/layouts/templates/account.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ __('My account') }}
7 |
8 |
9 |
10 |
15 |
20 |
25 |
30 |
31 |
32 |
33 |
{{ $slot }}
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Auth/EmailVerificationTest.php:
--------------------------------------------------------------------------------
1 | create([
13 | 'email_verified_at' => null,
14 | ]);
15 |
16 | $response = $this->actingAs($user)->get('/verify-email');
17 |
18 | $response->assertStatus(200);
19 | });
20 |
21 | test('email can be verified', function (): void {
22 | $user = User::factory()->create([
23 | 'email_verified_at' => null,
24 | ]);
25 |
26 | Event::fake();
27 |
28 | $verificationUrl = URL::temporarySignedRoute(
29 | 'verification.verify',
30 | now()->addMinutes(60),
31 | ['id' => $user->id, 'hash' => sha1($user->email)]
32 | );
33 |
34 | $response = $this->actingAs($user)->get($verificationUrl);
35 |
36 | Event::assertDispatched(Verified::class);
37 |
38 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
39 |
40 | $response->assertRedirect('/account' . '?verified=1');
41 | });
42 |
43 | test('email is not verified with invalid hash', function (): void {
44 | $user = User::factory()->create([
45 | 'email_verified_at' => null,
46 | ]);
47 |
48 | $verificationUrl = URL::temporarySignedRoute(
49 | 'verification.verify',
50 | now()->addMinutes(60),
51 | ['id' => $user->id, 'hash' => sha1('wrong-email')]
52 | );
53 |
54 | $this->actingAs($user)->get($verificationUrl);
55 |
56 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
57 | });
58 | })->group('auth');
59 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Auth/AuthenticationTest.php:
--------------------------------------------------------------------------------
1 | get('/login');
12 |
13 | $response
14 | ->assertOk()
15 | ->assertSeeVolt('pages.auth.login');
16 | });
17 |
18 | test('users can authenticate using the login screen', function (): void {
19 | $user = User::factory()->create();
20 |
21 | $component = Volt::test('pages.auth.login')
22 | ->set('form.email', $user->email)
23 | ->set('form.password', 'password');
24 |
25 | $component->call('login');
26 |
27 | $component
28 | ->assertHasNoErrors()
29 | ->assertRedirect('/account');
30 |
31 | $this->assertAuthenticated();
32 | });
33 |
34 | test('users can not authenticate with invalid password', function (): void {
35 | $user = User::factory()->create();
36 |
37 | $component = Volt::test('pages.auth.login')
38 | ->set('form.email', $user->email)
39 | ->set('form.password', 'wrong-password');
40 |
41 | $component->call('login');
42 |
43 | $component
44 | ->assertHasErrors()
45 | ->assertNoRedirect();
46 |
47 | $this->assertGuest();
48 | });
49 |
50 | test('users can logout', function (): void {
51 | $user = User::factory()->create();
52 |
53 | $this->actingAs($user);
54 |
55 | $component = Volt::test('components.account-menu');
56 |
57 | $component->call('logout');
58 |
59 | $component
60 | ->assertHasNoErrors()
61 | ->assertRedirect('/');
62 |
63 | $this->assertGuest();
64 | });
65 | })->group('auth');
66 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/checkout-steps.blade.php:
--------------------------------------------------------------------------------
1 |
2 | @foreach ($steps as $step)
3 | $step->complete,
7 | ])
8 | @if ($step->complete)
9 | wire:click="{{ $step->show() }}"
10 | @endif
11 | >
12 | $step->complete,
16 | "border border-gray-300 bg-white text-gray-500" => ! $step->complete,
17 | ])
18 | >
19 | @if ($step->complete)
20 |
21 | @else
22 | {{ $loop->index + 1 }}
23 | @endif
24 |
25 | $step->complete,
29 | "font-medium text-gray-900" => $step->isCurrent(),
30 | "text-gray-500" => ! (
31 | $step->isCurrent() || $step->complete
32 | ),
33 | ])
34 | >
35 | {{ $step->label }}
36 |
37 | @if (! $loop->last)
38 |
39 |
44 |
45 | @endif
46 |
47 | @endforeach
48 |
49 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/attributes/size.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'option',
3 | ])
4 |
5 |
6 |
7 |
8 |
{{ $option->attribute->name }}
9 |
10 | {{ __('Size guide') }}
11 |
12 |
13 |
14 |
15 | {{ __('Choose a size') }}
16 |
27 | @foreach ($option->values as $value)
28 |
38 | {{ $value->value }}
39 |
40 | @endforeach
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/stats.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{ __('Free shipping') }}
7 |
8 | {{ __('From :amount', ['amount' => shopper_money_format(5000, current_currency())]) }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
{{ __('24/7 customer support') }}
16 |
{{ __('Friendly 24/7 customer support') }}
17 |
18 |
19 |
20 |
21 |
22 |
{{ __('Secure payment') }}
23 |
{{ __('On all orders') }}
24 |
25 |
26 |
27 |
28 |
29 |
{{ __('Return when you\'re ready') }}
30 |
{{ __('60 days of free returns') }}
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Forms/LoginForm.php:
--------------------------------------------------------------------------------
1 | ensureIsNotRateLimited();
34 |
35 | if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
36 | RateLimiter::hit($this->throttleKey());
37 |
38 | throw ValidationException::withMessages([
39 | 'form.email' => trans('auth.failed'),
40 | ]);
41 | }
42 |
43 | RateLimiter::clear($this->throttleKey());
44 | }
45 |
46 | /**
47 | * Ensure the authentication request is not rate limited.
48 | */
49 | protected function ensureIsNotRateLimited(): void
50 | {
51 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
52 | return;
53 | }
54 |
55 | event(new Lockout(request()));
56 |
57 | $seconds = RateLimiter::availableIn($this->throttleKey());
58 |
59 | throw ValidationException::withMessages([
60 | 'form.email' => trans('auth.throttle', [
61 | 'seconds' => $seconds,
62 | 'minutes' => ceil($seconds / 60),
63 | ]),
64 | ]);
65 | }
66 |
67 | /**
68 | * Get the authentication rate limiting throttle key.
69 | */
70 | protected function throttleKey(): string
71 | {
72 | return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/account/index.blade.php:
--------------------------------------------------------------------------------
1 | __('My orders'),
16 | 'description' => __('Track your orders, return them or buy them again'),
17 | 'href' => route('account.orders'),
18 | 'icon' => 'untitledui-shopping-bag-03'
19 | ],
20 | [
21 | 'title' => __('Personal information'),
22 | 'description' => __('Change of e-mail address, name and telephone number'),
23 | 'href' => route('account.profile'),
24 | 'icon' => 'untitledui-shield-tick'
25 | ],
26 | [
27 | 'title' => __('My addresses'),
28 | 'description' => __('Billing and delivery preferences for orders'),
29 | 'href' => route('account.addresses'),
30 | 'icon' => 'untitledui-globe-05'
31 | ],
32 | [
33 | 'title' => __('Contact us'),
34 | 'description' => __('Contact our customer service department by telephone or e-mail'),
35 | 'href' => route('account'),
36 | 'icon' => 'untitledui-phone'
37 | ],
38 | ];
39 | }
40 | }; ?>
41 |
42 |
43 |
47 |
48 |
49 | @foreach($this->links as $link)
50 |
56 | @endforeach
57 |
58 |
59 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/product/additionnal-infos.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'product',
3 | 'categories',
4 | ])
5 |
6 |
7 |
8 |
{{ __('Measurements and specifications') }}
9 |
10 |
11 |
12 |
13 | {{ __('Category') }} :
14 |
15 | {{ $categories }}
16 |
17 |
18 |
19 | {{ __('Height') }} :
20 |
21 | {{ \Illuminate\Support\Number::format($product->height_value ?? 0) }} {{ $product->height_unit->value }}
22 |
23 |
24 |
25 | {{ __('Width') }} :
26 |
27 | {{ \Illuminate\Support\Number::format($product->width_value ?? 0) }} {{ $product->width_unit->value }}
28 |
29 |
30 |
31 | {{ __('Depth') }} :
32 |
33 | {{ \Illuminate\Support\Number::format($product->depth_value ?? 0) }} {{ $product->depth_unit->value }}
34 |
35 |
36 |
37 | {{ __('Weight') }} :
38 |
39 | {{ \Illuminate\Support\Number::format($product->weight_value ?? 0) }} {{ $product->weight_unit->value }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Components/Checkout/Delivery.php:
--------------------------------------------------------------------------------
1 | get('checkout'), 'shipping_address.country_id');
28 | $this->currentSelected = data_get(session()->get('checkout'), 'shipping_option')
29 | ? data_get(session()->get('checkout'), 'shipping_option')[0]['id']
30 | : null;
31 |
32 | $country = Country::query()->with('zones')->find($countryId);
33 | /** @var ?Zone $zone */
34 | // @phpstan-ignore-next-line
35 | $zone = $country->zones()
36 | ->with('shippingOptions')
37 | ->where('is_enabled', true)
38 | ->first();
39 |
40 | $this->options = $zone
41 | ? $zone->shippingOptions()->where('is_enabled', true)->get()
42 | : [];
43 | }
44 |
45 | public function save(): void
46 | {
47 | $this->validate();
48 |
49 | session()->forget('checkout.shipping_option');
50 |
51 | session()->push('checkout.shipping_option', CarrierOption::query()->find($this->currentSelected)->toArray());
52 |
53 | $this->dispatch('cart-price-update');
54 |
55 | $this->nextStep();
56 | }
57 |
58 | public function stepInfo(): array
59 | {
60 | return [
61 | 'label' => __('Delivery method'),
62 | 'complete' => session()->exists('checkout')
63 | && data_get(session()->get('checkout'), 'shipping_option') !== null,
64 | ];
65 | }
66 |
67 | public function render(): View
68 | {
69 | return view('livewire.components.checkout.delivery');
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/cart/item.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'item',
3 | ])
4 |
5 | @php
6 | $price = shopper_money_format(
7 | amount: $item->price * $item->quantity,
8 | currency: current_currency(),
9 | );
10 |
11 | $model = $item->associatedModel instanceof \App\Models\ProductVariant ? $item->associatedModel->loadMissing('product')->product : $item->associatedModel;
12 | @endphp
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ $item->name }}
22 |
23 |
24 |
25 | @if($item->attributes->isNotEmpty())
26 |
27 | @foreach($item->attributes as $name => $value)
28 |
29 | {{ $name }} :
30 | {{ $value }}
31 |
32 | @endforeach
33 |
34 | @endif
35 |
36 |
37 |
38 | {{ $price }}
39 |
40 |
41 |
42 |
43 | {{ __('Quantity: :qty', ['qty' => $item->quantity]) }}
44 |
45 |
46 |
47 |
52 |
53 | {{ __('Remove') }}
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/stubs/shared/app/Filament/Components/Form/AddressFields.php:
--------------------------------------------------------------------------------
1 | label(__('shopper::forms.label.street_address'))
28 | ->placeholder('Akwa Avenue 34...')
29 | ->columnSpan('full')
30 | ->required(),
31 | Components\TextInput::make(self::getPrefix($prefix) . 'street_address_plus')
32 | ->label(__('shopper::forms.label.street_address_plus'))
33 | ->columnSpan('full'),
34 | Components\TextInput::make(self::getPrefix($prefix) . 'city')
35 | ->label(__('shopper::forms.label.city'))
36 | ->required(),
37 | Components\TextInput::make(self::getPrefix($prefix) . 'postal_code')
38 | ->label(__('shopper::forms.label.postal_code'))
39 | ->required(),
40 | Components\Select::make(self::getPrefix($prefix) . 'country_id')
41 | ->label(__('shopper::forms.label.country'))
42 | ->options(
43 | Country::query()
44 | ->whereIn(
45 | column: 'id',
46 | values: (new CountriesWithZone())
47 | ->handle()
48 | ->where('zoneId', ZoneSessionManager::getSession()->zoneId)->pluck('countryId')
49 | )
50 | ->pluck('name', 'id')
51 | )
52 | ->searchable()
53 | ->required()
54 | ->columnSpan('full'),
55 | Components\TextInput::make(self::getPrefix($prefix) . 'phone_number')
56 | ->label(__('shopper::forms.label.phone_number'))
57 | ->tel()
58 | ->columnSpan('full'),
59 | ];
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Components/Checkout/Payment.php:
--------------------------------------------------------------------------------
1 | get('checkout'), 'shipping_address.country_id');
31 | $this->currentSelected = data_get(session()->get('checkout'), 'payment')
32 | ? data_get(session()->get('checkout'), 'payment')[0]['id']
33 | : null;
34 |
35 | $country = Country::query()->with('zones')->find($countryId);
36 | /** @var ?Zone $zone */
37 | $zone = $country->zones()
38 | ->where('is_enabled', true)
39 | ->with('paymentMethods', function ($query) {
40 | $query->where('is_enabled', true);
41 | })
42 | ->first();
43 |
44 | $this->methods = $zone ? $zone->paymentMethods : [];
45 | }
46 |
47 | public function save(): void
48 | {
49 | $this->validate();
50 |
51 | session()->forget('checkout.payment');
52 |
53 | session()->push('checkout.payment', PaymentMethod::query()->find($this->currentSelected)->toArray());
54 |
55 | $order = (new CreateOrder)->handle();
56 |
57 | match (data_get(session()->get('checkout'), 'payment')[0]['slug']) {
58 | PaymentType::Cash() => (new PayWithCash)->handle($order),
59 | };
60 | }
61 |
62 | public function stepInfo(): array
63 | {
64 | return [
65 | 'label' => __('Payment'),
66 | 'complete' => session()->exists('checkout')
67 | && data_get(session()->get('checkout'), 'payment') !== null,
68 | ];
69 | }
70 |
71 | public function render(): View
72 | {
73 | return view('livewire.components.checkout.payment');
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Components/Checkout/Shipping.php:
--------------------------------------------------------------------------------
1 | get('checkout');
29 |
30 | $this->shippingAddressId = data_get($checkout, 'shipping_address.id');
31 | $this->billingAddressId = data_get($checkout, 'billing_address.id');
32 | $this->sameAsShipping = (bool) data_get($checkout, 'same_as_shipping');
33 | }
34 |
35 | public function save(): void
36 | {
37 | $this->validate();
38 |
39 | if (session()->exists('checkout')) {
40 | session()->forget('checkout');
41 | }
42 |
43 | session()->put('checkout', [
44 | 'shipping_address' => $shippingAddress = Address::query()->find($this->shippingAddressId)->toArray(),
45 | 'same_as_shipping' => $this->sameAsShipping,
46 | 'billing_address' => $this->sameAsShipping
47 | ? $shippingAddress
48 | : Address::query()->find($this->billingAddressId)->toArray(),
49 | ]);
50 |
51 | $this->nextStep();
52 | }
53 |
54 | public function stepInfo(): array
55 | {
56 | return [
57 | 'label' => __('Address'),
58 | 'complete' => session()->exists('checkout')
59 | && data_get(session()->get('checkout'), 'shipping_address') !== null,
60 | ];
61 | }
62 |
63 | #[On('addresses-updated')]
64 | public function render(): View
65 | {
66 | $countryId = ZoneSessionManager::getSession()?->countryId;
67 | $addresses = Auth::user()->addresses()
68 | ->where('country_id', $countryId)
69 | ->get()
70 | ->groupBy('type');
71 |
72 | return view('livewire.components.checkout.shipping', [
73 | 'addresses' => $addresses,
74 | ]);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/product/images.blade.php:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | BLADE;
33 | }
34 |
35 | #[On('variant.selected')]
36 | public function variantSelected(?int $variantId = null): void
37 | {
38 | if ($variantId) {
39 | $variant = ProductVariant::with('media', 'product.media')
40 | ->select('product_id', 'id')
41 | ->find($variantId);
42 |
43 | $this->thumbnail = $variant->getMedia(config('shopper.media.storage.thumbnail_collection'))->isNotEmpty()
44 | ? $variant->getFirstMediaUrl(config('shopper.media.storage.thumbnail_collection'))
45 | : $variant->product->getFirstMediaUrl(config('shopper.media.storage.thumbnail_collection'));
46 |
47 | $this->images = $variant->getMedia(config('shopper.media.storage.collection_name'))->isNotEmpty()
48 | ? $variant->getMedia(config('shopper.media.storage.collection_name'))
49 | ->map(fn ($media) => $media->getUrl())
50 | ->toArray()
51 | : $variant->product->getMedia(config('shopper.media.storage.collection_name'))
52 | ->map(fn ($media) => $media->getUrl())
53 | ->toArray();
54 | }
55 | }
56 | } ?>
57 |
58 |
59 |
60 |
65 |
66 |
67 | @if (count($images))
68 |
69 | @endif
70 |
71 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Auth/PasswordResetTest.php:
--------------------------------------------------------------------------------
1 | get('/forgot-password');
15 |
16 | $response
17 | ->assertSeeVolt('pages.auth.forgot-password')
18 | ->assertStatus(200);
19 | });
20 |
21 | test('reset password link can be requested', function (): void {
22 | Notification::fake();
23 |
24 | $user = User::factory()->create();
25 |
26 | Volt::test('pages.auth.forgot-password')
27 | ->set('email', $user->email)
28 | ->call('sendPasswordResetLink');
29 |
30 | Notification::assertSentTo($user, ResetPassword::class);
31 | })->skip();
32 |
33 | test('reset password screen can be rendered', function (): void {
34 | Notification::fake();
35 |
36 | $user = User::factory()->create();
37 |
38 | Volt::test('pages.auth.forgot-password')
39 | ->set('email', $user->email)
40 | ->call('sendPasswordResetLink');
41 |
42 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
43 | $response = $this->get('/reset-password/' . $notification->token);
44 |
45 | $response
46 | ->assertSeeVolt('pages.auth.reset-password')
47 | ->assertStatus(200);
48 |
49 | return true;
50 | });
51 | })->skip();
52 |
53 | test('password can be reset with valid token', function (): void {
54 | Notification::fake();
55 |
56 | $user = User::factory()->create();
57 |
58 | Volt::test('pages.auth.forgot-password')
59 | ->set('email', $user->email)
60 | ->call('sendPasswordResetLink');
61 |
62 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
63 | $component = Volt::test('pages.auth.reset-password', ['token' => $notification->token])
64 | ->set('email', $user->email)
65 | ->set('password', 'password')
66 | ->set('password_confirmation', 'password');
67 |
68 | $component->call('resetPassword');
69 |
70 | $component
71 | ->assertRedirect('/login')
72 | ->assertHasNoErrors();
73 |
74 | return true;
75 | });
76 | })->skip();
77 | })->group('auth');
78 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/profile/delete-user-form.blade.php:
--------------------------------------------------------------------------------
1 | validate([
19 | 'password' => ['required', 'string', 'current_password'],
20 | ]);
21 |
22 | tap(Auth::user(), $logout(...))->delete();
23 |
24 | $this->redirect('/', navigate: true);
25 | }
26 | }; ?>
27 |
28 |
29 |
30 |
31 | {{ __('Delete Account') }}
32 |
33 |
34 |
35 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
36 |
37 |
38 |
39 | {{ __('Delete Account') }}
43 |
44 |
45 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/modals/zone-selector.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ __('Please select your Country / Zone') }}
7 |
8 |
9 |
14 | {{ __('Close panel') }}
15 |
16 |
17 |
18 |
19 | @if(\App\Actions\ZoneSessionManager::getSession())
20 |
21 | {{ __('Where you shop now') }} :
22 |
23 | {{ \App\Actions\ZoneSessionManager::getSession()->countryName }}
24 |
25 |
26 | @endif
27 |
28 | {{ __("Please note that if you change zone / country while shopping, all the contents of your basket will be deleted.") }}
29 |
30 |
31 |
32 | @foreach($this->countries->groupBy('zoneName') as $zone => $countries)
33 |
34 |
35 | {{ $zone }}
36 |
37 |
38 | @foreach($countries as $country)
39 |
40 |
41 |
42 | {{ $country->countryName }}
43 | , {{ __('Select zone') }}
44 |
45 |
46 | @endforeach
47 |
48 |
49 | @endforeach
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/stubs/shared/config/shopper/models.php:
--------------------------------------------------------------------------------
1 | Models\Brand::class,
21 |
22 | /*
23 | |--------------------------------------------------------------------------
24 | | Category Model
25 | |--------------------------------------------------------------------------
26 | |
27 | | Eloquent model should be used to retrieve your categories. Of course,
28 | | If you want to use a custom model, your model needs to extends the
29 | | \Shopper\Core\Models\Category Model.
30 | |
31 | */
32 |
33 | 'category' => Models\Category::class,
34 |
35 | /*
36 | |--------------------------------------------------------------------------
37 | | Collection Model
38 | |--------------------------------------------------------------------------
39 | |
40 | | Eloquent model should be used to retrieve your collections. Of course,
41 | | If you want to use a custom model, your model needs to extends the
42 | | \Shopper\Core\Models\Collection Model.
43 | |
44 | */
45 |
46 | 'collection' => Models\Collection::class,
47 |
48 | /*
49 | |--------------------------------------------------------------------------
50 | | Product Model
51 | |--------------------------------------------------------------------------
52 | |
53 | | Eloquent model should be used to retrieve your products. Of course,
54 | | If you want to use a custom model, your model needs to extends the
55 | | \Shopper\Core\Models\Product Model.
56 | |
57 | */
58 |
59 | 'product' => Models\Product::class,
60 |
61 | /*
62 | |--------------------------------------------------------------------------
63 | | Product Variant Model
64 | |--------------------------------------------------------------------------
65 | |
66 | | Eloquent model should be used to retrieve your product variants. Of course,
67 | | If you want to use a custom model, your model needs to extends the
68 | | \Shopper\Core\Models\ProductVariant Model.
69 | |
70 | */
71 |
72 | 'variant' => Models\ProductVariant::class,
73 |
74 | /*
75 | |--------------------------------------------------------------------------
76 | | Channel Model
77 | |--------------------------------------------------------------------------
78 | |
79 | | Eloquent model should be used to retrieve channels. Of course,
80 | | If you want to use a custom model, your model needs to extends the
81 | | \Shopper\Core\Models\Channel Model.
82 | |
83 | */
84 |
85 | 'channel' => Models\Channel::class,
86 |
87 | ];
88 |
--------------------------------------------------------------------------------
/stubs/livewire/tests/Feature/Livewire/Modals/AddressTest.php:
--------------------------------------------------------------------------------
1 | user = User::factory()->create();
15 | $this->countries = Country::factory()
16 | ->count(3)
17 | ->create();
18 |
19 | $this->actingAs($this->user);
20 | });
21 |
22 | describe(Addresses::class, function (): void {
23 | it('user can create address', function (): void {
24 | Livewire::actingAs($this->user)
25 | ->test(AddressForm::class)
26 | ->set('first_name', 'John')
27 | ->set('last_name', 'Doe')
28 | ->set('street_address', 'Ndokoti')
29 | ->set('street_address_plus', 'Adress street plus')
30 | ->set('type', AddressType::Billing)
31 | ->set('country_id', $this->countries->first()->id)
32 | ->set('postal_code', '33790')
33 | ->set('city', 'Douala')
34 | ->set('phone_number', '99007788')
35 | ->call('save')
36 | ->assertDispatched('addresses-updated');
37 |
38 | expect(Address::query()->count())
39 | ->toBe(1);
40 | });
41 |
42 | it('user can update address', function (): void {
43 | /** @var Address $address */
44 | $address = Address::factory([
45 | 'first_name' => 'John',
46 | 'type' => AddressType::Billing,
47 | 'user_id' => $this->user->id,
48 | 'country_id' => $this->countries->first()->id,
49 | ])->create();
50 |
51 | Livewire::test(AddressForm::class, ['addressId' => $address->id])
52 | ->set('first_name', 'Jane')
53 | ->set('last_name', $address->last_name)
54 | ->set('street_address', $address->street_address)
55 | ->set('street_address_plus', $address->street_address_plus)
56 | ->set('type', $address->type)
57 | ->set('country_id', $address->country_id)
58 | ->set('postal_code', $address->postal_code)
59 | ->set('city', $address->city)
60 | ->set('phone_number', $address->phone_number)
61 | ->call('save')
62 | ->assertDispatched('addresses-updated');
63 |
64 | /** @var Address $updatedAddress */
65 | $updatedAddress = Address::query()->findOrFail($address->id);
66 |
67 | expect($updatedAddress->first_name)
68 | ->toBe('Jane');
69 | });
70 |
71 | it('user can delete address', function (): void {
72 | $address = Address::factory()->create([
73 | 'first_name' => 'Old first name',
74 | 'type' => AddressType::Billing,
75 | 'user_id' => $this->user->id,
76 | ]);
77 |
78 | Livewire::test(Addresses::class)
79 | ->call('removeAddress', $address->id);
80 |
81 | expect(Address::query()->count())
82 | ->toBe(0);
83 | });
84 | })->group('account');
85 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/order/index.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'order',
3 | ])
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ __('Order N°') }}
11 |
12 |
13 | {{ $order->number }}
14 |
15 |
16 |
17 |
18 | {{ __('Placed on') }}
19 |
20 |
21 |
22 | {{ $order->created_at->translatedFormat('j F Y') }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ __('Total') }}
31 |
32 |
33 | {{ shopper_money_format($order->total() + $order->shippingOption?->price, $order->currency_code) }}
34 |
35 |
36 |
37 |
{{ __('Status') }}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | @foreach($order->items->take(5) as $item)
47 |
48 | @if($order->items->count() > 5 && $loop->index === 4)
49 |
50 |
51 | + {{ $order->items->count() - 5 }}
52 |
53 |
54 | @endif
55 |
56 |
57 | @endforeach
58 |
59 |
60 |
61 | {{ __('View details') }}
62 |
63 |
64 | {{ __('Invoice') }}
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/auth/confirm-password.blade.php:
--------------------------------------------------------------------------------
1 | validate([
20 | 'password' => ['required', 'string'],
21 | ]);
22 |
23 | if (! Auth::guard('web')->validate([
24 | 'email' => Auth::user()->email,
25 | 'password' => $this->password,
26 | ])) {
27 | throw ValidationException::withMessages([
28 | 'password' => __('auth.password'),
29 | ]);
30 | }
31 |
32 | session(['auth.password_confirmed_at' => time()]);
33 |
34 | $this->redirectIntended(default: route('account'), navigate: true);
35 | }
36 | }; ?>
37 |
38 |
39 |
43 |
44 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
62 |
63 |
64 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/auth/verify-email.blade.php:
--------------------------------------------------------------------------------
1 | hasVerifiedEmail()) {
19 | $this->redirectIntended(default: route('account', absolute: false), navigate: true);
20 |
21 | return;
22 | }
23 |
24 | Auth::user()->sendEmailVerificationNotification();
25 |
26 | Session::flash('status', 'verification-link-sent');
27 | }
28 |
29 | /**
30 | * Log the current user out of the application.
31 | */
32 | public function logout(Logout $logout): void
33 | {
34 | $logout();
35 |
36 | $this->redirect('/', navigate: true);
37 | }
38 | }; ?>
39 |
40 |
41 |
45 |
46 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
64 |
65 |
66 | @if (session('status') == 'verification-link-sent')
67 |
68 | {{ __('A new verification link has been sent to the email address you provided during registration.') }}
69 |
70 | @endif
71 |
72 |
73 |
78 |
79 |
80 | {{ __('Reset Password') }}
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/profile/update-password-form.blade.php:
--------------------------------------------------------------------------------
1 | validate([
24 | 'current_password' => ['required', 'string', 'current_password'],
25 | 'password' => ['required', 'string', Password::defaults(), 'confirmed'],
26 | ]);
27 | } catch (ValidationException $e) {
28 | $this->reset('current_password', 'password', 'password_confirmation');
29 |
30 | throw $e;
31 | }
32 |
33 | Auth::user()->update([
34 | 'password' => Hash::make($validated['password']),
35 | ]);
36 |
37 | $this->reset('current_password', 'password', 'password_confirmation');
38 |
39 | $this->dispatch('password-updated');
40 | }
41 | }; ?>
42 |
43 |
78 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Modals/Account/AddressForm.php:
--------------------------------------------------------------------------------
1 | address = $addressId
55 | ? Address::query()->findOrFail($addressId)
56 | : new Address;
57 |
58 | $this->countries = Country::query()
59 | ->whereIn(
60 | column: 'id',
61 | values: (new CountriesWithZone)
62 | ->handle()
63 | ->where('zoneId', ZoneSessionManager::getSession()?->zoneId)->pluck('countryId')
64 | )
65 | ->pluck('name', 'id');
66 |
67 | if ($addressId && $this->address->id) {
68 | $this->fill(array_merge($this->address->toArray(), ['type' => $this->address->type]));
69 | }
70 | }
71 |
72 | public static function modalMaxWidth(): string
73 | {
74 | return '2xl';
75 | }
76 |
77 | public function save(): void
78 | {
79 | $this->validate();
80 |
81 | if ($this->address->id) {
82 | $this->address->update(array_merge($this->validate(), ['user_id' => Auth::id()]));
83 | } else {
84 | Address::query()->create(array_merge($this->validate(), ['user_id' => Auth::id()]));
85 | }
86 |
87 | Notification::make()
88 | ->title(__('The address has been successfully saved'))
89 | ->success()
90 | ->send();
91 |
92 | $this->dispatch('addresses-updated');
93 |
94 | $this->closeModal();
95 | }
96 |
97 | public function render(): View
98 | {
99 | return view('livewire.modals.account.address-form', [
100 | 'title' => $this->address->id
101 | ? __('Update address')
102 | : __('Add new address'),
103 | ]);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/variants-selector.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $product->name }}
5 |
6 | @if ($this->variant)
7 | {{ $this->variant->name }}
8 | @endif
9 |
10 |
11 |
15 |
16 |
17 |
74 |
75 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/account/orders/detail.blade.php:
--------------------------------------------------------------------------------
1 | null]);
12 |
13 | mount(function (string $number): void {
14 | $this->order = Order::with([
15 | 'items',
16 | 'items.product',
17 | 'shippingOption',
18 | 'shippingAddress',
19 | 'paymentMethod',
20 | ])
21 | ->where('number', $number)
22 | ->firstOrFail();
23 | });
24 |
25 | title(__('Details of your order'));
26 |
27 | ?>
28 |
29 |
30 |
31 | {{ __('Details of your order') }}
32 |
33 |
34 |
35 |
36 |
37 | {{ __('Order N° ') }}
38 |
39 |
40 | {{ __('#:number', ['number' => $order->number]) }}
41 |
42 |
43 |
44 |
45 | {{ __('Passed the') }}
46 |
47 |
48 |
49 | {{ $order->created_at->translatedFormat('j F Y') }}
50 |
51 |
52 |
53 |
54 |
55 | {{ __('Total') }}
56 |
57 |
58 | {{ shopper_money_format($order->total() + $order->shippingOption->price, $order->currency_code) }}
59 |
60 |
61 |
62 |
{{ __('status') }}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | {{ __('order summary') }}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | {{ __('Do you have a problem with your order? Our customer service is here to help') }}
83 |
84 |
85 |
86 | {{ __('Contact us') }}
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/modals/shopping-cart.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 |
22 | @if ($items->isNotempty())
23 |
24 |
25 | @foreach ($items as $item)
26 |
27 | @endforeach
28 |
29 |
30 | @else
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{ __('Your cart is empty') }}
38 |
39 |
40 | {{ __('Browse our product catalog to find your perfect match.') }}
41 |
42 |
43 |
44 | @endif
45 |
46 |
47 |
48 |
49 |
50 |
{{ __('Tax') }}
51 |
52 | {{ shopper_money_format(0, currency: current_currency()) }}
53 |
54 |
55 |
56 |
{{ __('Delivery') }}
57 |
{{ __('Calculated at the time of payment') }}
58 |
59 |
60 |
{{ __('Subtotal') }}
61 |
62 | {{ shopper_money_format($subtotal, currency: current_currency()) }}
63 |
64 |
65 |
66 |
67 | {{ __('Proceed to checkout') }}
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/stubs/blade-common/resources/views/components/order/summary.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'order',
3 | ])
4 |
5 |
6 |
7 |
8 |
{{ __('Sub total') }}
9 |
10 | {{ shopper_money_format($order->total(), $order->currency_code) }}
11 |
12 |
13 |
14 |
{{ __('Shipping') }}
15 |
16 | {{ shopper_money_format($order->shippingOption->price, $order->currency_code) }}
17 |
18 |
19 |
20 |
{{ __('Tax') }}
21 |
22 | {{ shopper_money_format(0, $order->currency_code) }}
23 |
24 |
25 |
26 |
{{ __('Total') }}
27 |
28 | {{ shopper_money_format($order->total() + $order->shippingOption->price, $order->currency_code) }}
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ __('Shipping Address') }}
36 |
37 |
38 |
39 | {{ $order->shippingAddress->first_name }} {{ $order->shippingAddress->last_name }}
40 |
41 | {{ $order->shippingAddress->street_address }}
42 |
43 | {{ $order->shippingAddress->city }}, {{ $order->shippingAddress->postal_code }}
44 |
45 | {{ $order->shippingAddress->country_name }}
46 |
47 |
48 |
49 |
50 | {{ __('Billing address') }}
51 |
52 |
53 |
54 | {{ $order->billingAddress->first_name }} {{ $order->billingAddress->last_name }}
55 |
56 | {{ $order->billingAddress->street_address }}
57 |
58 | {{ $order->billingAddress->city }}, {{ $order->billingAddress->postal_code }}
59 |
60 | {{ $order->billingAddress->country_name }}
61 |
62 |
63 |
64 |
65 |
66 | {{ __('Payment method') }}
67 |
68 |
69 |
70 | {{ $order->paymentMethod->title }}
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/components/address/edit-address.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'address',
3 | ])
4 |
5 |
6 | @if ($address->type === \Shopper\Core\Enum\AddressType::Billing)
7 |
8 |
9 |
10 | {{ __('Billing') }}
11 |
12 |
13 | @endif
14 |
15 |
16 |
17 |
18 | {{ $address->first_name }} {{ $address->last_name }}
19 |
20 |
21 |
22 | {{ $address->street_address }}
23 | @if ($address->street_address_plus)
24 | , {{ $address->street_address_plus }}
25 | @endif
26 |
27 |
28 | {{ $address->postal_code }}, {{ $address->city }}
29 |
30 |
31 | {{ $address->country?->name }}
32 |
33 |
34 |
35 | @if ($address->isShippingDefault())
36 |
37 |
38 |
39 | {{ __('Default shipping address') }}
40 |
41 |
42 | @endif
43 | @if ($address->isBillingDefault())
44 |
45 |
46 |
47 | {{ __('Default billing address') }}
48 |
49 |
50 | @endif
51 |
52 |
53 |
54 |
59 |
60 | {{ __('Delete') }}
61 |
62 |
66 |
71 | {{ __('Edit') }}
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/auth/forgot-password.blade.php:
--------------------------------------------------------------------------------
1 | validate([
19 | 'email' => ['required', 'string', 'email'],
20 | ]);
21 |
22 | // We will send the password reset link to this user. Once we have attempted
23 | // to send the link, we will examine the response then see the message we
24 | // need to show to the user. Finally, we'll send out a proper response.
25 | $status = Password::sendResetLink(
26 | $this->only('email')
27 | );
28 |
29 | if ($status != Password::RESET_LINK_SENT) {
30 | $this->addError('email', __($status));
31 |
32 | return;
33 | }
34 |
35 | $this->reset('email');
36 |
37 | session()->flash('status', __($status));
38 | }
39 | }; ?>
40 |
41 |
42 |
46 |
47 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {{ __('Forgot your password') }}
66 |
67 |
68 | {{ __('No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
69 |
70 |
71 |
72 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/single-product.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {{ __('Our privacy') }}
40 |
41 |
42 |
43 |
44 |
45 | {{ __('International delivery') }}
46 |
47 |
48 | {{ __('Get your order in 2 weeks') }}
49 |
50 |
51 |
52 |
53 |
54 | {{ __('Loyalty rewards') }}
55 |
56 |
57 | {{ __('Don\'t look at other tees') }}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/stubs/shared/app/Actions/CreateOrder.php:
--------------------------------------------------------------------------------
1 | get('checkout');
19 | $sessionId = session()->getId();
20 | $customer = Auth::user();
21 |
22 | /** @var OrderAddress $shippingAddress */
23 | $shippingAddress = OrderAddress::query()->create([
24 | 'customer_id' => data_get($checkout, 'shipping_address.user_id'),
25 | 'last_name' => data_get($checkout, 'shipping_address.last_name'),
26 | 'first_name' => data_get($checkout, 'shipping_address.first_name'),
27 | 'street_address' => data_get($checkout, 'shipping_address.street_address'),
28 | 'street_address_plus' => data_get($checkout, 'shipping_address.street_address_plus'),
29 | 'city' => data_get($checkout, 'shipping_address.city'),
30 | 'postal_code' => data_get($checkout, 'shipping_address.postal_code'),
31 | 'phone' => data_get($checkout, 'shipping_address.phone_number'),
32 | // @phpstan-ignore-next-line
33 | 'country_name' => Country::query()
34 | ->find(data_get($checkout, 'shipping_address.country_id'))
35 | ->name,
36 | ]);
37 | /** @var OrderAddress $billingAddress */
38 | $billingAddress = ! data_get($checkout, 'same_as_shipping')
39 | ? OrderAddress::query()->create([
40 | 'customer_id' => data_get($checkout, 'billing_address.user_id'),
41 | 'last_name' => data_get($checkout, 'billing_address.last_name'),
42 | 'first_name' => data_get($checkout, 'billing_address.first_name'),
43 | 'street_address' => data_get($checkout, 'billing_address.street_address'),
44 | 'street_address_plus' => data_get($checkout, 'billing_address.street_address_plus'),
45 | 'city' => data_get($checkout, 'billing_address.city'),
46 | 'postal_code' => data_get($checkout, 'billing_address.postal_code'),
47 | 'phone' => data_get($checkout, 'billing_address.phone_number'),
48 | // @phpstan-ignore-next-line
49 | 'country_name' => Country::query()
50 | ->find(data_get($checkout, 'billing_address.country_id'))
51 | ->name,
52 | ])
53 | : $shippingAddress;
54 |
55 | /** @var Order $order */
56 | $order = Order::query()->create([
57 | 'number' => generate_number(),
58 | 'customer_id' => $customer->id,
59 | 'zone_id' => ZoneSessionManager::getSession()->zoneId,
60 | 'currency_code' => current_currency(),
61 | 'shipping_address_id' => $shippingAddress->id,
62 | 'billing_address_id' => $billingAddress->id,
63 | 'shipping_option_id' => data_get($checkout, 'shipping_option')[0]['id'],
64 | 'payment_method_id' => data_get($checkout, 'payment')[0]['id'],
65 | ]);
66 |
67 | // @phpstan-ignore-next-line
68 | foreach (CartFacade::session($sessionId)->getContent() as $item) {
69 | OrderItem::query()->create([
70 | 'order_id' => $order->id,
71 | 'quantity' => $item->quantity,
72 | 'unit_price_amount' => $item->price,
73 | 'name' => $item->name,
74 | 'sku' => $item->associatedModel->sku,
75 | 'product_id' => $item->associatedModel->id,
76 | 'product_type' => $item->associatedModel->getMorphClass(),
77 | ]);
78 | }
79 |
80 | CartFacade::session($sessionId)->clear(); // @phpstan-ignore-line
81 |
82 | return $order;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/checkout/payment.blade.php:
--------------------------------------------------------------------------------
1 |
2 | @include('components.checkout-steps')
3 |
4 |
75 |
76 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/home.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 | {{ __('The new arrivals have, well, newly arrived. Check out the latest options from our summer small-batch release while they\'re still in stock.') }}
29 |
30 |
31 |
32 |
33 | {{ __('Shop now') }}
34 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | @if($collections->isNotEmpty())
52 |
53 |
56 |
57 | {{ __('Explore our curated furniture collections, designed to elevate every space. From modern minimalism to classic elegance, find timeless pieces that blend style, comfort, and functionality for your home.') }}
58 |
59 |
60 |
61 | @foreach($collections as $collection)
62 |
63 |
68 |
69 | {{ $collection->name }}
70 |
71 |
72 | @endforeach
73 |
74 |
75 | @endif
76 |
77 |
78 |
79 | {{ __('Trending products') }}
80 |
81 |
82 |
83 | @foreach ($products as $product)
84 |
85 | @endforeach
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/profile/update-profile-information-form.blade.php:
--------------------------------------------------------------------------------
1 | first_name = Auth::user()->first_name;
23 | $this->last_name = Auth::user()->last_name;
24 | $this->email = Auth::user()->email;
25 | }
26 |
27 | /**
28 | * Update the profile information for the currently authenticated user.
29 | */
30 | public function updateProfileInformation(): void
31 | {
32 | $user = Auth::user();
33 |
34 | $validated = $this->validate([
35 | 'first_name' => ['required', 'string', 'max:255'],
36 | 'last_name' => ['required', 'string', 'max:255'],
37 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($user->id)],
38 | ]);
39 |
40 | $user->fill($validated);
41 |
42 | if ($user->isDirty('email')) {
43 | $user->email_verified_at = null;
44 | }
45 |
46 | $user->save();
47 |
48 | $this->dispatch('profile-updated', name: $user->full_name);
49 | }
50 |
51 | /**
52 | * Send an email verification notification to the current user.
53 | */
54 | public function sendVerification(): void
55 | {
56 | $user = Auth::user();
57 |
58 | if ($user->hasVerifiedEmail()) {
59 | $this->redirectIntended(default: route('account'));
60 |
61 | return;
62 | }
63 |
64 | $user->sendEmailVerificationNotification();
65 |
66 | Session::flash('status', 'verification-link-sent');
67 | }
68 | }; ?>
69 |
70 |
123 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/pages/auth/login.blade.php:
--------------------------------------------------------------------------------
1 | validate();
17 |
18 | $this->form->authenticate();
19 |
20 | Session::regenerate();
21 |
22 | $this->redirectIntended(default: route('account', absolute: false), navigate: true);
23 | }
24 | }; ?>
25 |
26 |
27 |
31 |
32 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{ __('I already have an account') }}
50 |
51 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {{ __('New customer') }}
98 |
99 |
100 | {{ __('Create your own space for an enhanced shopping experience.') }}
101 |
102 |
103 |
104 |
105 | {{ __('Create account') }}
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/stubs/livewire/app/Livewire/Components/VariantsSelector.php:
--------------------------------------------------------------------------------
1 | variant ?? $this->product;
35 |
36 | if (! $model->getPrice()) {
37 | Notification::make()
38 | ->title(__('Cart Error'))
39 | ->body(__('You cannot add product without price in you cart'))
40 | ->danger()
41 | ->send();
42 |
43 | return;
44 | }
45 |
46 | CartFacade::session(session()->getId())->add([
47 | 'id' => $model->created_at->timestamp * $model->id,
48 | 'name' => $this->product->name,
49 | 'price' => $model->getPrice()->value->amount,
50 | 'quantity' => 1,
51 | 'attributes' => $this->variant
52 | ? $this->getVariantAttributes()
53 | : [],
54 | 'associatedModel' => $model,
55 | ]);
56 |
57 | Notification::make()
58 | ->title(__('Cart updated'))
59 | ->body(__('Product / variant has been added to your cart'))
60 | ->success()
61 | ->send();
62 |
63 | $this->dispatch('cartUpdated');
64 | }
65 |
66 | #[Computed(persist: true)]
67 | public function variants(): Collection
68 | {
69 | return Cache::remember(
70 | key: 'product.'. $this->product->id .'.variants',
71 | ttl: now()->addWeek(),
72 | callback: fn () => $this->product
73 | ->variants
74 | ->load([
75 | 'inventoryHistories',
76 | 'media',
77 | 'values',
78 | 'values.attribute',
79 | 'prices.currency',
80 | ])
81 | );
82 | }
83 |
84 | #[Computed]
85 | public function variant(): ?ProductVariant
86 | {
87 | if ($this->productOptions->isNotEmpty()) {
88 | return $this->selectVariantUsingOption();
89 | }
90 |
91 | if ($this->selectedVariantId) {
92 | $this->dispatch('variant.selected', variantId: $this->selectedVariantId);
93 |
94 | return $this->variants->firstWhere('id', $this->selectedVariantId);
95 | }
96 |
97 | return null;
98 | }
99 |
100 | #[Computed]
101 | public function productOptionValues(): Collection
102 | {
103 | return $this->variants->pluck('values')->flatten();
104 | }
105 |
106 | #[Computed]
107 | public function productOptions(): Collection
108 | {
109 | return $this->productOptionValues
110 | ->unique('id')
111 | ->groupBy('attribute_id')
112 | ->map(function ($values) {
113 | return new OptionData(
114 | attribute: $values->first()->attribute,
115 | values: $values,
116 | );
117 | })->values();
118 | }
119 |
120 | protected function selectVariantUsingOption(): ?ProductVariant
121 | {
122 | if (! count($this->selectedOptionValues)) {
123 | return null;
124 | }
125 |
126 | $variant = $this->variants
127 | ->first(
128 | fn ($variant) => ! $variant->values->pluck('id')
129 | ->diff(collect($this->selectedOptionValues)->values())
130 | ->count()
131 | );
132 |
133 | if ($variant) {
134 | $this->dispatch('variant.selected', variantId: $variant->id);
135 | }
136 |
137 | return $variant;
138 | }
139 |
140 | protected function getVariantAttributes(): array
141 | {
142 | if ($this->productOptions->isNotEmpty()) {
143 | return $this->variant->values->mapWithKeys(fn ($value): array => [
144 | $value->attribute->name => $value->value,
145 | ])->toArray();
146 | }
147 |
148 | return [
149 | 'Variant' => $this->variant?->name,
150 | ];
151 | }
152 |
153 | public function render(): View
154 | {
155 | return view('livewire.components.variants-selector');
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/stubs/livewire/resources/views/livewire/components/checkout/delivery.blade.php:
--------------------------------------------------------------------------------
1 |
2 | @include('components.checkout-steps')
3 |
4 | @if(count($options) === 0)
5 |
6 |
7 |
8 | {{ __('No delivery option available for your address.') }}
9 |
10 |
11 | @else
12 |
85 | @endif
86 |
87 |
--------------------------------------------------------------------------------