├── database ├── .gitignore ├── seeders │ ├── BrandSeeder.php │ ├── PromoCodeSeeder.php │ ├── OrderSeeder.php │ ├── RoleSeeder.php │ ├── SettingSeeder.php │ ├── DatabaseSeeder.php │ ├── GoodSeeder.php │ └── UserSeeder.php ├── factories │ ├── BrandFactory.php │ ├── TagFactory.php │ ├── CategoryFactory.php │ ├── PropertyFactory.php │ ├── OrderItemFactory.php │ ├── ReviewFactory.php │ ├── UserAddressFactory.php │ ├── PromoCodeFactory.php │ ├── OrderRecipientFactory.php │ ├── OrderFactory.php │ ├── UserFactory.php │ └── GoodFactory.php └── migrations │ ├── 2023_02_22_220210_create_tags_table.php │ ├── 2023_02_22_220207_create_brands_table.php │ ├── 2023_02_22_220203_create_cities_table.php │ ├── 2023_02_22_220202_create_states_table.php │ ├── 2023_02_22_220213_create_good_tag_table.php │ ├── 2023_02_22_220214_create_good_user_table.php │ ├── 2023_02_22_220211_create_properties_table.php │ ├── 2023_02_22_220206_create_settings_table.php │ ├── 2023_02_22_220212_create_good_property_table.php │ ├── 2023_02_22_220216_create_cart_items_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 2019_05_03_000003_create_subscription_items_table.php │ ├── 2023_02_22_220205_create_user_socials_table.php │ ├── 2023_02_22_220219_create_order_items_table.php │ ├── 2023_02_22_220221_create_order_payments_table.php │ ├── 2023_02_22_220201_create_countries_table.php │ ├── 2019_05_03_000002_create_subscriptions_table.php │ ├── 2023_02_22_220222_create_media_table.php │ ├── 2023_02_22_220208_create_categories_table.php │ ├── 2023_02_22_220220_create_order_recipients_table.php │ ├── 2023_02_22_220215_create_reviews_table.php │ ├── 2023_02_22_220217_create_promo_codes_table.php │ ├── 2023_02_22_220204_create_user_addresses_table.php │ ├── 2023_02_22_220218_create_orders_table.php │ └── 2023_02_22_220209_create_goods_table.php ├── bootstrap ├── cache │ └── .gitignore ├── providers.php └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore ├── debugbar │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── public ├── js │ └── filament │ │ ├── tables │ │ └── tables.js │ │ └── forms │ │ └── components │ │ ├── textarea.js │ │ ├── tags-input.js │ │ └── key-value.js ├── robots.txt ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── static │ └── cards │ │ ├── 1.jpeg │ │ ├── 10.jpeg │ │ ├── 11.jpeg │ │ ├── 12.jpeg │ │ ├── 13.jpeg │ │ ├── 14.jpeg │ │ ├── 15.jpeg │ │ ├── 16.jpeg │ │ ├── 17.jpeg │ │ ├── 18.jpeg │ │ ├── 19.jpeg │ │ ├── 2.jpeg │ │ ├── 20.jpeg │ │ ├── 21.jpeg │ │ ├── 22.jpeg │ │ ├── 23.jpeg │ │ ├── 24.jpeg │ │ ├── 25.jpeg │ │ ├── 3.jpeg │ │ ├── 4.jpeg │ │ ├── 5.jpeg │ │ ├── 6.jpeg │ │ ├── 7.jpeg │ │ ├── 8.jpeg │ │ └── 9.jpeg ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── vendor │ ├── horizon │ │ ├── img │ │ │ └── favicon.png │ │ └── mix-manifest.json │ ├── log-viewer │ │ ├── img │ │ │ ├── bmc.png │ │ │ ├── bmc-logo.png │ │ │ ├── log-viewer-128.png │ │ │ ├── log-viewer-32.png │ │ │ └── log-viewer-64.png │ │ ├── mix-manifest.json │ │ └── app.js.LICENSE.txt │ └── telescope │ │ ├── favicon.ico │ │ └── mix-manifest.json ├── site.webmanifest ├── index.php └── .htaccess ├── resources ├── js │ ├── types │ │ ├── vite-env.d.ts │ │ ├── global.d.ts │ │ └── index.d.ts │ ├── Components │ │ ├── InputError.vue │ │ ├── InputLabel.vue │ │ ├── DropdownLink.vue │ │ ├── TextInput.vue │ │ ├── Checkbox.vue │ │ ├── PrimaryButton.vue │ │ ├── DangerButton.vue │ │ ├── NavLink.vue │ │ ├── SecondaryButton.vue │ │ ├── ResponsiveNavLink.vue │ │ └── Rating.vue │ ├── composables │ │ └── format.ts │ ├── Layouts │ │ └── GuestLayout.vue │ ├── ssr.ts │ ├── Pages │ │ ├── Profile │ │ │ └── Edit.vue │ │ └── Auth │ │ │ └── ConfirmPassword.vue │ ├── bootstrap.ts │ └── app.ts ├── css │ └── app.css └── views │ ├── mail │ └── user │ │ └── password.blade.php │ ├── filament │ ├── forms │ │ └── components │ │ │ └── _rating-item.blade.php │ ├── tables │ │ └── components │ │ │ └── rating-column.blade.php │ ├── widgets │ │ └── log-viewer-widget.blade.php │ └── pages │ │ ├── addresses.blade.php │ │ └── profile.blade.php │ └── app.blade.php ├── .prettierignore ├── tests ├── Unit │ └── ExampleTest.php ├── Feature │ ├── ExampleTest.php │ └── Auth │ │ ├── RegistrationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── AuthenticationTest.php │ │ ├── PasswordUpdateTest.php │ │ ├── EmailVerificationTest.php │ │ └── PasswordResetTest.php ├── TestCase.php ├── CreatesApplication.php └── Pest.php ├── postcss.config.js ├── docker ├── general │ ├── supervisord.conf │ └── cron ├── dev │ └── www.conf └── nginx │ └── Dockerfile ├── jsconfig.json ├── .gitattributes ├── routes ├── console.php └── api.php ├── app ├── Filament │ ├── Widgets │ │ ├── HorizonWidget.php │ │ └── LogViewerWidget.php │ └── Resources │ │ ├── GoodResource │ │ └── Pages │ │ │ ├── CreateGood.php │ │ │ ├── ListGoods.php │ │ │ └── EditGood.php │ │ ├── OrderResource │ │ └── Pages │ │ │ ├── CreateOrder.php │ │ │ ├── EditOrder.php │ │ │ └── ListOrders.php │ │ ├── ReviewResource │ │ └── Pages │ │ │ ├── CreateReview.php │ │ │ ├── EditReview.php │ │ │ └── ListReviews.php │ │ ├── PropertyResource │ │ └── Pages │ │ │ ├── CreateProperty.php │ │ │ ├── EditProperty.php │ │ │ └── ListProperties.php │ │ ├── PromoCodeResource │ │ └── Pages │ │ │ ├── CreatePromoCode.php │ │ │ ├── EditPromoCode.php │ │ │ └── ListPromoCodes.php │ │ ├── TagResource │ │ └── Pages │ │ │ ├── CreateTag.php │ │ │ ├── ListTags.php │ │ │ └── EditTag.php │ │ ├── BrandResource │ │ └── Pages │ │ │ ├── CreateBrand.php │ │ │ ├── ListBrands.php │ │ │ └── EditBrand.php │ │ ├── SettingResource │ │ └── Pages │ │ │ ├── CreateSetting.php │ │ │ ├── ListSettings.php │ │ │ └── EditSetting.php │ │ ├── CategoryResource │ │ └── Pages │ │ │ ├── CreateCategory.php │ │ │ ├── ListCategories.php │ │ │ └── EditCategory.php │ │ ├── CityResource │ │ └── Pages │ │ │ ├── ListCities.php │ │ │ └── ViewCity.php │ │ ├── StateResource │ │ └── Pages │ │ │ ├── ListStates.php │ │ │ └── ViewState.php │ │ ├── CountryResource │ │ └── Pages │ │ │ ├── ListCountries.php │ │ │ └── ViewCountry.php │ │ └── UserResource │ │ ├── Pages │ │ ├── ListUsers.php │ │ └── CreateUser.php │ │ ├── Widgets │ │ └── UsersOverview.php │ │ └── RelationManagers │ │ └── SocialsRelationManager.php ├── Models │ ├── GoodTag.php │ ├── GoodUser.php │ ├── Tag.php │ ├── OrderRecipient.php │ ├── GoodProperty.php │ ├── Setting.php │ ├── OrderItem.php │ ├── State.php │ ├── City.php │ ├── OrderPayment.php │ ├── Property.php │ ├── PromoCode.php │ ├── CartItem.php │ ├── Brand.php │ ├── Review.php │ ├── UserSocial.php │ ├── UserAddress.php │ ├── Country.php │ └── Order.php ├── Enums │ ├── PromoCode.php │ ├── UserProvider.php │ ├── UserGender.php │ ├── UserStatus.php │ ├── OrderPayment.php │ ├── UserRole.php │ ├── OrderDelivery.php │ ├── GoodStatus.php │ └── OrderStatus.php ├── Notifications │ └── SendVerifyWithQueueNotification.php ├── Http │ ├── Middleware │ │ ├── VerifyCsrfToken.php │ │ └── HandleInertiaRequests.php │ ├── Controllers │ │ ├── Controller.php │ │ ├── Main │ │ │ └── IndexController.php │ │ ├── Auth │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── PasswordController.php │ │ │ ├── VerifyEmailController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ └── RegisteredUserController.php │ │ └── ProfileController.php │ ├── Resources │ │ ├── StateResource.php │ │ ├── CountryResource.php │ │ ├── PropertyResource.php │ │ ├── CityResource.php │ │ ├── BrandResource.php │ │ ├── CategoryResource.php │ │ ├── CartResource.php │ │ ├── OrderItemResource.php │ │ ├── ReviewResource.php │ │ ├── UserAddressResource.php │ │ ├── OrderResource.php │ │ └── GoodResource.php │ └── Requests │ │ ├── ProfileUpdateRequest.php │ │ └── Profile │ │ ├── ProfileUpdateRequest.php │ │ └── AddressRequest.php ├── Console │ └── Kernel.php ├── Providers │ ├── AppServiceProvider.php │ ├── EventServiceProvider.php │ ├── HorizonServiceProvider.php │ └── RouteServiceProvider.php ├── Mail │ └── User │ │ └── PasswordMail.php └── Jobs │ └── StoreUserJob.php ├── .prettierrc.json ├── .editorconfig ├── .dockerignore ├── artisan ├── .gitignore ├── pint.json ├── lang └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── tsconfig.json ├── vite.config.js ├── phpunit.xml ├── config ├── filament.php └── services.php ├── .env.example └── package.json /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/js/filament/tables/tables.js: -------------------------------------------------------------------------------- 1 | (()=>{})(); 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /resources/js/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/vendor/horizon/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/vendor/horizon/img/favicon.png -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/bmc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/vendor/log-viewer/img/bmc.png -------------------------------------------------------------------------------- /public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/bmc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/vendor/log-viewer/img/bmc-logo.png -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/log-viewer-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/vendor/log-viewer/img/log-viewer-128.png -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/log-viewer-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/vendor/log-viewer/img/log-viewer-32.png -------------------------------------------------------------------------------- /public/vendor/log-viewer/img/log-viewer-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fordiquez/laravel-store/HEAD/public/vendor/log-viewer/img/log-viewer-64.png -------------------------------------------------------------------------------- /docker/general/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:cron] 5 | command=/usr/sbin/cron -l 2 -f 6 | autostart=true 7 | autorestart=true 8 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "public"] 9 | } 10 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | /proc/1/fd/1 2>/proc/1/fd/2 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote'); 9 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/textarea.js: -------------------------------------------------------------------------------- 1 | function t({initialHeight:e}){return{init:function(){this.render()},render:function(){this.$el.scrollHeight>0&&(this.$el.style.height=e+"rem",this.$el.style.height=this.$el.scrollHeight+"px")}}}export{t as default}; 2 | -------------------------------------------------------------------------------- /public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=7049e92a398e816f8cd53a915eaea592", 3 | "/app-dark.css": "/app-dark.css?id=1ea407db56c5163ae29311f1f38eb7b9", 4 | "/app.css": "/app.css?id=de4c978567bfd90b38d186937dee5ccf" 5 | } 6 | -------------------------------------------------------------------------------- /app/Filament/Widgets/HorizonWidget.php: -------------------------------------------------------------------------------- 1 | 2 | # Your account has been successfully created! Your password: {{ $password }} 3 | 4 | Log In 5 | 6 | Thanks for registration,
7 | {{ config('app.name') }} 8 | 9 | -------------------------------------------------------------------------------- /app/Enums/UserGender.php: -------------------------------------------------------------------------------- 1 | count(22)->create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /resources/js/Components/InputError.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /app/Enums/OrderPayment.php: -------------------------------------------------------------------------------- 1 | count(10)->create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /resources/views/filament/forms/components/_rating-item.blade.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /app/Filament/Resources/GoodResource/Pages/CreateGood.php: -------------------------------------------------------------------------------- 1 | count(10)->hasOrderItems(rand(1, 5))->create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/Filament/Resources/ReviewResource/Pages/CreateReview.php: -------------------------------------------------------------------------------- 1 | 2 | defineProps<{ 3 | value?: string 4 | }>() 5 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /app/Notifications/SendVerifyWithQueueNotification.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 'boolean', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /public/vendor/horizon/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=b4f3f08e60211bd6948ec35e5e9de9a1", 3 | "/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435", 4 | "/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5", 5 | "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f", 6 | "/img/horizon.svg": "/img/horizon.svg?id=904d5b5185fefb09035384e15bfca765", 7 | "/img/sprite.svg": "/img/sprite.svg?id=afc4952b74895bdef3ab4ebe9adb746f" 8 | } 9 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 18 | 19 | return $app; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Filament/Resources/TagResource/Pages/CreateTag.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /database/factories/BrandFactory.php: -------------------------------------------------------------------------------- 1 | fake()->company, 16 | 'slug' => fn (array $attributes) => str($attributes['name'])->slug(), 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Enums/GoodStatus.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/js/composables/format.ts: -------------------------------------------------------------------------------- 1 | export function useFormat() { 2 | const formatMoney = (value: any, format = 'ua', asCurrency = true, currency = 'USD') => { 3 | return new Intl.NumberFormat(format, { 4 | style: asCurrency ? 'currency' : 'decimal', 5 | currency 6 | }).format(value) 7 | } 8 | 9 | const formatCardNumber = (cardNumber: any) => (cardNumber ? cardNumber.match(/.{1,4}/g).join(' ') : '') 10 | 11 | return { formatMoney, formatCardNumber } 12 | } 13 | -------------------------------------------------------------------------------- /app/Filament/Resources/SettingResource/Pages/CreateSetting.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Filament/Resources/TagResource/Pages/ListTags.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Filament/Resources/CityResource/Pages/ListCities.php: -------------------------------------------------------------------------------- 1 | fake()->unique()->colorName(), 17 | 'slug' => fn (array $attributes) => Str::slug($attributes['name']), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/js/Components/DropdownLink.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /app/Filament/Resources/CountryResource/Pages/ListCountries.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /app/Http/Resources/StateResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'name' => $this->name, 17 | 18 | 'country' => new CountryResource($this->whenLoaded('country')), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Resources/CountryResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'name' => $this->name, 17 | 'iso2' => $this->iso2, 18 | 19 | 'flag' => $this->flag, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/views/filament/tables/components/rating-column.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
    3 | @for ($i = $getMinValue(); $i <= $getMaxValue(); $i++) 4 |
  • 7 | @include('filament.forms.components._rating-item', [ 8 | 'component' => $i <= $getState() ? $getSelectedIcon() : $getIcon(), 9 | ]) 10 |
  • 11 | @endfor 12 |
13 |
14 | -------------------------------------------------------------------------------- /resources/views/filament/widgets/log-viewer-widget.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Log Viewer 5 | 6 | 7 |

8 | {{ \Composer\InstalledVersions::getPrettyVersion('opcodesio/log-viewer') }} 9 |

10 |
11 |
12 | -------------------------------------------------------------------------------- /app/Models/GoodProperty.php: -------------------------------------------------------------------------------- 1 | belongsTo(Property::class); 17 | } 18 | 19 | public function good(): BelongsTo 20 | { 21 | return $this->belongsTo(Good::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Filament/Resources/SettingResource/Pages/ListSettings.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ $this->form }} 4 | 5 |
6 | 7 | 8 | 9 | Cancel 10 | 11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /resources/views/filament/pages/profile.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ $this->form }} 4 | 5 |
6 | 7 | 8 | 9 | Cancel 10 | 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 7 | Route::get('countries', 'countries')->name('api.locations.countries'); 8 | Route::get('{country}/states', 'states')->name('api.locations.states'); 9 | Route::get('{state}/cities', 'cities')->name('api.locations.cities'); 10 | Route::get('categories', 'categories')->name('api.categories'); 11 | Route::post('verify-promo-code/key', 'verifyPromoCode')->name('api.verify-promo-code'); 12 | }); 13 | -------------------------------------------------------------------------------- /app/Filament/Resources/GoodResource/Pages/EditGood.php: -------------------------------------------------------------------------------- 1 | rand(), 16 | 'type' => $type, 17 | 'title' => $title, 18 | 'text' => $text, 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 16 | 'next' => 'Next »', 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Http/Resources/PropertyResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'name' => $this->name, 17 | 'slug' => $this->slug, 18 | 'filterable' => $this->filterable, 19 | 'value' => $this->pivot->value, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Resources/CityResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'name' => $this->name, 17 | 18 | 'country' => new CountryResource($this->whenLoaded('country')), 19 | 'state' => new StateResource($this->whenLoaded('state')), 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/factories/CategoryFactory.php: -------------------------------------------------------------------------------- 1 | fake()->unique()->jobTitle(), 16 | 'slug' => fn (array $attributes) => str($attributes['title'])->slug(), 17 | 'description' => fake()->sentence(rand(2, 10)), 18 | 'is_active' => true, 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/Pages/ListUsers.php: -------------------------------------------------------------------------------- 1 | get('/register'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('new users can register', function () { 12 | $response = $this->post('/register', [ 13 | 'name' => 'Test User', 14 | 'email' => 'test@example.com', 15 | 'password' => 'password', 16 | 'password_confirmation' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(RouteServiceProvider::HOME); 21 | }); 22 | -------------------------------------------------------------------------------- /database/seeders/RoleSeeder.php: -------------------------------------------------------------------------------- 1 | filter(fn (string $role) => $role !== UserRole::SUPER_ADMIN) 16 | ->each(fn (string $role) => Role::create(['name' => $role, 'guard_name' => 'web'])); 17 | 18 | User::where('id', '>', 1)->get()->each(fn (User $user) => $user->assignRole(UserRole::CUSTOMER)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Models/OrderItem.php: -------------------------------------------------------------------------------- 1 | 'integer', 14 | 'unit_price' => 'float', 15 | ]; 16 | 17 | public function order(): BelongsTo 18 | { 19 | return $this->belongsTo(Order::class); 20 | } 21 | 22 | public function good(): BelongsTo 23 | { 24 | return $this->belongsTo(Good::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Models/State.php: -------------------------------------------------------------------------------- 1 | 'boolean', 15 | ]; 16 | 17 | public function country(): BelongsTo 18 | { 19 | return $this->belongsTo(Country::class); 20 | } 21 | 22 | public function cities(): HasMany 23 | { 24 | return $this->hasMany(City::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Filament/Resources/TagResource/Pages/EditTag.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Resources/BrandResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'name' => $this->name, 17 | 'slug' => $this->slug, 18 | 'url' => $this->url, 19 | 'logo' => $this->logo, 20 | 21 | 'goods' => GoodResource::collection($this->whenLoaded('goods')), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "jsx": "preserve", 7 | "strict": true, 8 | "isolatedModules": true, 9 | "target": "ESNext", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "skipLibCheck": true, 14 | "paths": { 15 | "@/*": ["./resources/js/*"], 16 | "ziggy-js": ["./vendor/tightenco/ziggy"] 17 | } 18 | }, 19 | "include": ["resources/js/**/*.ts", "resources/js/**/*.d.ts", "resources/js/**/*.vue"] 20 | } 21 | -------------------------------------------------------------------------------- /app/Filament/Resources/BrandResource/Pages/EditBrand.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/Widgets/UsersOverview.php: -------------------------------------------------------------------------------- 1 | count()), 17 | Stat::make('Women', User::whereGender(UserGender::FEMALE)->count()), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/factories/PropertyFactory.php: -------------------------------------------------------------------------------- 1 | Category::inRandomOrder()->value('id'), 17 | 'filterable' => fake()->boolean(77), 18 | 'name' => fake()->unique()->sentence(rand(1, 5)), 19 | 'slug' => fn (array $attributes) => str($attributes['name'])->slug(), 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Filament/Resources/SettingResource/Pages/EditSetting.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/OrderItemFactory.php: -------------------------------------------------------------------------------- 1 | Good::inRandomOrder()->value('id'), 18 | 'order_id' => Order::inRandomOrder()->value('id'), 19 | 'quantity' => fake()->numberBetween(1, 5), 20 | 'unit_price' => fake()->numberBetween(100, 10000), 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Enums/OrderStatus.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import laravel, { refreshPaths } from 'laravel-vite-plugin' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | laravel({ 8 | input: ['resources/css/app.css', 'resources/js/app.ts'], 9 | ssr: ['resources/css/app.css', 'resources/js/app.ts'], 10 | refresh: [...refreshPaths, 'app/Livewire/**'] 11 | }), 12 | vue({ 13 | template: { 14 | transformAssetUrls: { 15 | base: null, 16 | includeAbsolute: false 17 | } 18 | } 19 | }) 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /app/Models/City.php: -------------------------------------------------------------------------------- 1 | 'boolean', 15 | ]; 16 | 17 | public function state(): BelongsTo 18 | { 19 | return $this->belongsTo(State::class); 20 | } 21 | 22 | public function country(): HasOneThrough 23 | { 24 | return $this->hasOneThrough(Country::class, State::class, 'id', 'id', 'state_id', 'country_id'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'name' => ['required', 'string', 'max:255'], 20 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 16 | 'password' => 'The provided password is incorrect.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | ]; 19 | -------------------------------------------------------------------------------- /resources/js/Components/TextInput.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 16 | $schedule->command('cache:prune-stale-tags')->hourly(); 17 | } 18 | 19 | /** 20 | * Register the commands for the application. 21 | */ 22 | protected function commands(): void 23 | { 24 | $this->load(__DIR__ . '/Commands'); 25 | 26 | require base_path('routes/console.php'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/OrderPayment.php: -------------------------------------------------------------------------------- 1 | 2 | import { computed } from 'vue' 3 | 4 | const emit = defineEmits(['update:checked']) 5 | 6 | const props = defineProps<{ 7 | checked: boolean 8 | value?: any 9 | }>() 10 | 11 | const proxyChecked = computed({ 12 | get() { 13 | return props.checked 14 | }, 15 | 16 | set(val) { 17 | emit('update:checked', val) 18 | } 19 | }) 20 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Main/IndexController.php: -------------------------------------------------------------------------------- 1 | subcategories)) { 19 | return to_route('goods.index', $category); 20 | } 21 | 22 | return inertia('Index/Category', [ 23 | 'category' => new CategoryResource($category), 24 | 'breadcrumbs' => Category::breadcrumbs($category), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 20 | ? redirect()->intended(RouteServiceProvider::HOME) 21 | : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220210_create_tags_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name', 50); 17 | $table->string('slug', 50)->unique(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('tags'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /app/Models/Property.php: -------------------------------------------------------------------------------- 1 | 'boolean', 18 | ]; 19 | 20 | public function category(): BelongsTo 21 | { 22 | return $this->belongsTo(Category::class); 23 | } 24 | 25 | public function goods(): BelongsToMany 26 | { 27 | return $this->belongsToMany(Good::class)->withPivot('value'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/js/Layouts/GuestLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 18 | return redirect()->intended(RouteServiceProvider::HOME); 19 | } 20 | 21 | $request->user()->sendEmailVerificationNotification(); 22 | 23 | return back()->with('status', 'verification-link-sent'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/seeders/SettingSeeder.php: -------------------------------------------------------------------------------- 1 | 'site', 'name' => 'Site currency', 'key' => 'currency', 'value' => '₴'], 14 | ['group' => 'delivery', 'name' => 'Courier', 'key' => 'courier', 'value' => 130], 15 | ['group' => 'delivery', 'name' => 'Meest', 'key' => 'meest', 'value' => 40], 16 | ['group' => 'delivery', 'name' => 'UkrPoshta', 'key' => 'ukrposhta', 'value' => 30], 17 | ['group' => 'delivery', 'name' => 'Nova Poshta', 'key' => 'nova_poshta', 'value' => 60], 18 | ], ['group', 'key'], ['value']); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 20 | SettingSeeder::class, 21 | LocationSeeder::class, 22 | UserSeeder::class, 23 | BrandSeeder::class, 24 | CategorySeeder::class, 25 | GoodSeeder::class, 26 | PromoCodeSeeder::class, 27 | OrderSeeder::class, 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 16 | 'sent' => 'We have emailed your password reset link!', 17 | 'throttled' => 'Please wait before retrying.', 18 | 'token' => 'This password reset token is invalid.', 19 | 'user' => "We can't find a user with that email address.", 20 | ]; 21 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220207_create_brands_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name', 100); 17 | $table->string('slug', 100)->unique(); 18 | $table->string('url')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('brands'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /app/Http/Resources/CategoryResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'title' => $this->title, 17 | 'slug' => $this->slug, 18 | 'description' => $this->description, 19 | 'thumbnail' => $this->thumbnail, 20 | 'is_active' => $this->is_active, 21 | 'is_navigational' => $this->is_navigational, 22 | 'subcategories' => $this->subcategories()->count() ? $this::loopCategories($this->subcategories) : null, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Resources/CartResource.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | [$goods, $cartItems] = $this->resource; 20 | 21 | return [ 22 | 'count' => Cart::getCount(), 23 | 'total' => $goods->reduce(fn (?float $carry, Good $good) => $carry + $good->price * $cartItems[$good->id]['quantity']), 24 | 'items' => $cartItems, 25 | 'goods' => GoodResource::collection($goods), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/vendor/log-viewer/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! #__NO_SIDE_EFFECTS__ */ 9 | 10 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 11 | 12 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 13 | 14 | /** 15 | * @license 16 | * Lodash 17 | * Copyright OpenJS Foundation and other contributors 18 | * Released under MIT license 19 | * Based on Underscore.js 1.8.3 20 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 21 | */ 22 | -------------------------------------------------------------------------------- /app/Models/PromoCode.php: -------------------------------------------------------------------------------- 1 | \App\Enums\PromoCode::class, 27 | 'used_times' => 'integer', 28 | 'starts_at' => 'datetime', 29 | 'expires_at' => 'datetime', 30 | 'greater_than' => 'float', 31 | 'is_active' => 'boolean', 32 | 'is_public' => 'boolean', 33 | ]; 34 | } 35 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/tags-input.js: -------------------------------------------------------------------------------- 1 | function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /app/Models/CartItem.php: -------------------------------------------------------------------------------- 1 | 'integer', 17 | 'unit_price' => 'float', 18 | ]; 19 | 20 | public function user(): BelongsTo 21 | { 22 | return $this->belongsTo(User::class); 23 | } 24 | 25 | public function good(): BelongsTo 26 | { 27 | return $this->belongsTo(Good::class); 28 | } 29 | 30 | public function unitPrice(): Attribute 31 | { 32 | return Attribute::get(fn () => $this->good->price); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/factories/ReviewFactory.php: -------------------------------------------------------------------------------- 1 | User::query()->inRandomOrder()->value('id'), 18 | 'good_id' => Good::query()->inRandomOrder()->value('id'), 19 | 'is_buyer' => fake()->boolean, 20 | 'content' => fake()->paragraph(10), 21 | 'advantages' => fake()->sentence(10), 22 | 'disadvantages' => fake()->sentence(10), 23 | 'rating' => fake()->numberBetween(1, 5), 24 | 'ip_address' => fake()->ipv4, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | # set main params 4 | ARG BUILD_ARGUMENT_ENV=dev 5 | ENV ENV=$BUILD_ARGUMENT_ENV 6 | 7 | RUN ln -sf /dev/stdout /var/log/nginx/access.log && \ 8 | ln -sf /dev/stderr /var/log/nginx/error.log && \ 9 | rm -rf /etc/nginx/conf.d/* 10 | 11 | # install openssl 12 | RUN apk add --update openssl && \ 13 | rm -rf /var/cache/apk/* 14 | 15 | # create folder for certificates 16 | RUN mkdir -p /etc/nginx/certificates 17 | 18 | # generate certificates 19 | # TODO: change it and make additional logic for production environment 20 | RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/certificates/key.pem -out /etc/nginx/certificates/cert.pem -subj "/C=AT/ST=Vienna/L=Vienna/O=Security/OU=Development/CN=example.com" 21 | 22 | # put nginx config 23 | COPY ./$BUILD_ARGUMENT_ENV/nginx.conf /etc/nginx/conf.d/default.conf 24 | -------------------------------------------------------------------------------- /resources/js/Components/PrimaryButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 19 | 'current_password' => ['required', 'current_password'], 20 | 'password' => ['required', Password::defaults(), 'confirmed'], 21 | ]); 22 | 23 | $request->user()->update([ 24 | 'password' => Hash::make($validated['password']), 25 | ]); 26 | 27 | return back(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Resources/OrderItemResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'quantity' => $this->quantity, 17 | 'unit_price' => $this->unit_price, 18 | 'created_at' => $this->created_at, 19 | 'updated_at' => $this->updated_at, 20 | 21 | 'order_id' => $this->order_id, 22 | 'good_id' => $this->good_id, 23 | 24 | 'good' => new GoodResource($this->whenLoaded('good')), 25 | 'order' => new OrderResource($this->whenLoaded('order')), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/Brand.php: -------------------------------------------------------------------------------- 1 | hasMany(Good::class); 23 | } 24 | 25 | protected function logo(): Attribute 26 | { 27 | return Attribute::get(fn () => $this->hasMedia('logo') ? $this->getFirstMediaUrl('logo') : url('static/not-found.svg')); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | user() && $request->user()->email === User::ADMIN_EMAIL && $request->user()->hasVerifiedEmail(); 20 | }); 21 | } 22 | 23 | /** 24 | * Bootstrap any application services. 25 | */ 26 | public function boot(): void 27 | { 28 | JsonResource::withoutWrapping(); 29 | Model::shouldBeStrict(false); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/seeders/GoodSeeder.php: -------------------------------------------------------------------------------- 1 | count(22)->create(); 15 | 16 | $properties = PropertyFactory::new()->count(10)->create(); 17 | 18 | GoodFactory::new()->count(222) 19 | ->hasReviews(rand(1, 3)) 20 | ->hasAttached($tags->toQuery()->inRandomOrder()->take(rand(1, 5))->get()) 21 | ->hasAttached($properties, fn () => [ 22 | 'property_id' => $properties->toQuery()->inRandomOrder()->value('id'), 23 | 'value' => fake()->unique()->sentence(rand(1, 3)), 24 | ]) 25 | ->create(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Models/Review.php: -------------------------------------------------------------------------------- 1 | 'boolean', 27 | 'rating' => 'integer', 28 | ]; 29 | 30 | public function user(): BelongsTo 31 | { 32 | return $this->belongsTo(User::class); 33 | } 34 | 35 | public function good(): BelongsTo 36 | { 37 | return $this->belongsTo(Good::class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220203_create_cities_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->foreignIdFor(State::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 18 | $table->string('name'); 19 | $table->boolean('is_active')->default(true); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('cities'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220202_create_states_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->foreignIdFor(Country::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 18 | $table->string('name'); 19 | $table->boolean('is_active')->default(true); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('states'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /app/Models/UserSocial.php: -------------------------------------------------------------------------------- 1 | UserProvider::class, 17 | ]; 18 | 19 | public function user(): BelongsTo 20 | { 21 | return $this->belongsTo(User::class); 22 | } 23 | 24 | public static function getGithubUsername(string $username): array 25 | { 26 | $username = Str::wordCount($username) > 1 ? Str::of($username) : Str::of($username)->append(' user'); 27 | 28 | return Arr::map($username->explode(' ', 2)->all(), fn ($value) => Str::limit($value, 50, null)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220213_create_good_tag_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(Good::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->foreignIdFor(Tag::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('good_tag'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220214_create_good_user_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(Good::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('good_user'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /resources/js/Components/DangerButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this->actingAs($user)->get('/confirm-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('password can be confirmed', function () { 14 | $user = User::factory()->create(); 15 | 16 | $response = $this->actingAs($user)->post('/confirm-password', [ 17 | 'password' => 'password', 18 | ]); 19 | 20 | $response->assertRedirect(); 21 | $response->assertSessionHasNoErrors(); 22 | }); 23 | 24 | test('password is not confirmed with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $response = $this->actingAs($user)->post('/confirm-password', [ 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $response->assertSessionHasErrors(); 32 | }); 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 19 | return redirect()->intended(RouteServiceProvider::HOME . '?verified=1'); 20 | } 21 | 22 | if ($request->user()->markEmailAsVerified()) { 23 | event(new Verified($request->user())); 24 | } 25 | 26 | return redirect()->intended(RouteServiceProvider::HOME . '?verified=1'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220211_create_properties_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->foreignIdFor(Category::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 18 | $table->string('name'); 19 | $table->string('slug')->unique(); 20 | $table->boolean('filterable'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('properties'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/key-value.js: -------------------------------------------------------------------------------- 1 | function r({state:i}){return{state:i,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=o=>o===null?0:Array.isArray(o)?o.length:typeof o!="object"?0:Object.keys(o).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows),s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.rows=e,this.updateState()},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220206_create_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('group', 50); 17 | $table->string('name', 50)->nullable(); 18 | $table->string('details')->nullable(); 19 | $table->string('key', 50); 20 | $table->text('value'); 21 | $table->timestamps(); 22 | 23 | $table->unique(['group', 'key']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('settings'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /app/Models/UserAddress.php: -------------------------------------------------------------------------------- 1 | 'boolean', 27 | ]; 28 | 29 | public function country(): BelongsTo 30 | { 31 | return $this->belongsTo(Country::class); 32 | } 33 | 34 | public function state(): BelongsTo 35 | { 36 | return $this->belongsTo(State::class); 37 | } 38 | 39 | public function city(): BelongsTo 40 | { 41 | return $this->belongsTo(City::class); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/factories/UserAddressFactory.php: -------------------------------------------------------------------------------- 1 | first(); 16 | $state = $country->has('states') ? $country->states()->inRandomOrder()->first() : null; 17 | 18 | return [ 19 | 'country_id' => $country->id, 20 | 'state_id' => $state?->id, 21 | 'city_id' => $state?->has('cities') ? $state->cities()->inRandomOrder()->value('id') : null, 22 | 'street' => fake()->streetName, 23 | 'house' => fake()->buildingNumber, 24 | 'flat' => fake()->boolean ?: fake()->numberBetween(0, 100), 25 | 'postal_code' => intval(fake()->postcode), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220212_create_good_property_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(Property::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->foreignIdFor(Good::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 20 | $table->string('value', 100); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('good_property'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220216_create_cart_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->foreignIdFor(Good::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 20 | $table->unsignedTinyInteger('quantity')->default(1); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('cart_items'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [SendEmailVerificationNotification::class], 19 | ]; 20 | 21 | /** 22 | * Register any events for your application. 23 | */ 24 | public function boot(): void 25 | { 26 | // 27 | } 28 | 29 | /** 30 | * Determine if events and listeners should be automatically discovered. 31 | */ 32 | public function shouldDiscoverEvents(): bool 33 | { 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /resources/js/Components/NavLink.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000003_create_subscription_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('subscription_id'); 17 | $table->string('stripe_id')->unique(); 18 | $table->string('stripe_product'); 19 | $table->string('stripe_price'); 20 | $table->integer('quantity')->nullable(); 21 | $table->timestamps(); 22 | 23 | $table->unique(['subscription_id', 'stripe_price']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('subscription_items'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /resources/js/ssr.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp, h, DefineComponent } from 'vue' 2 | import { renderToString } from '@vue/server-renderer' 3 | import { createInertiaApp } from '@inertiajs/vue3' 4 | import createServer from '@inertiajs/vue3/server' 5 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' 6 | import { ZiggyVue } from 'ziggy-js' 7 | 8 | const appName = import.meta.env.VITE_APP_NAME || 'brandford.' 9 | 10 | createServer((page) => 11 | createInertiaApp({ 12 | page, 13 | render: renderToString, 14 | title: (title) => `${title} - ${appName}`, 15 | resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), 16 | setup({ App, props, plugin }) { 17 | return createSSRApp({ render: () => h(App, props) }) 18 | .use(plugin) 19 | .use(ZiggyVue, { 20 | ...page.props.ziggy, 21 | location: new URL(page.props.ziggy.location) 22 | }) 23 | } 24 | }) 25 | ) 26 | -------------------------------------------------------------------------------- /app/Http/Resources/ReviewResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'is_buyer' => $this->is_buyer, 17 | 'content' => $this->content, 18 | 'advantages' => $this->advantages, 19 | 'disadvantages' => $this->disadvantages, 20 | 'rating' => $this->rating, 21 | 'video_src' => $this->video_src, 22 | 'created_at' => $this->created_at->format('F d, Y'), 23 | 'updated_at' => $this->updated_at, 24 | 25 | 'user_id' => $this->user_id, 26 | 'good_id' => $this->good_id, 27 | 28 | 'username' => $this->user->full_name, 29 | 30 | 'good' => new GoodResource($this->whenLoaded('good')), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/factories/PromoCodeFactory.php: -------------------------------------------------------------------------------- 1 | boolean ? 'fixed' : 'percentage'; 16 | 17 | return [ 18 | 'key' => Str::random(8), 19 | 'type' => $type, 20 | 'value' => fake()->numberBetween(1, $type === 'percentage' ? 99 : 1000), 21 | 'description' => fake()->sentence(10), 22 | 'used_times' => fake()->numberBetween(0, 1000), 23 | 'starts_at' => fake()->dateTimeThisYear(), 24 | 'expires_at' => fake()->dateTimeThisYear(), 25 | 'greater_than' => fake()->boolean ? fake()->numberBetween(0, 1000) : null, 26 | 'is_active' => fake()->boolean(77), 27 | 'is_public' => fake()->boolean(99), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Providers/HorizonServiceProvider.php: -------------------------------------------------------------------------------- 1 | email, [ 32 | // 33 | ]); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220205_create_user_socials_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->enum('provider', UserProvider::getValues()); 20 | $table->string('provider_id'); 21 | $table->string('provider_token'); 22 | $table->timestamps(); 23 | 24 | $table->unique(['provider', 'provider_id']); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('user_socials'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220219_create_order_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(Order::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->foreignIdFor(Good::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 20 | $table->unsignedTinyInteger('quantity')->default(1); 21 | $table->decimal('unit_price')->unsigned()->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('order_items'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220221_create_order_payments_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(Order::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->decimal('amount')->unsigned(); 20 | $table->string('status')->nullable(); 21 | $table->enum('type', OrderPayment::$types); 22 | $table->uuid('session_id')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('order_payments'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/seeders/UserSeeder.php: -------------------------------------------------------------------------------- 1 | has(OrderRecipientFactory::new()->count(rand(1, 3))) 18 | ->has(UserAddressFactory::new()->count(rand(1, 3)), 'addresses') 19 | ->create([ 20 | 'first_name' => 'Gerald', 21 | 'last_name' => 'Ford', 22 | 'birth_date' => '2001-02-22', 23 | 'gender' => 'male', 24 | 'email' => User::ADMIN_EMAIL, 25 | 'password' => Hash::make('brandford22'), 26 | ]); 27 | 28 | $user->addAvatarMedia(config('services.multiavatar.url') . $user->getFilamentName() . '.svg?apikey=' . config('services.multiavatar.key')); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/factories/OrderRecipientFactory.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function definition(): array 21 | { 22 | return [ 23 | 'user_id' => User::inRandomOrder()->value('id'), 24 | 'title' => fake()->unique()->name, 25 | 'first_name' => fake()->firstName(), 26 | 'last_name' => fake()->lastName(), 27 | 'phone' => '+380' . $this->networkCodes[rand(0, count($this->networkCodes) - 1)] . fake()->unique()->numberBetween(1000000, 9999999), 28 | 'description' => fake()->text(50), 29 | 'is_default' => fake()->boolean, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Requests/Profile/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | merge([ 14 | 'phone' => str($this->get('phone'))->replace(' ', '')->value(), 15 | ]); 16 | } 17 | 18 | /** 19 | * Get the validation rules that apply to the request. 20 | */ 21 | public function rules(): array 22 | { 23 | return [ 24 | 'first_name' => ['string', 'max:255'], 25 | 'last_name' => ['string', 'max:255'], 26 | 'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], 27 | 'phone' => ['required', 'string', 'regex:/^\+\d{7,12}$/', Rule::unique(User::class)->ignore($this->user()->id)], 28 | ]; 29 | } 30 | 31 | public function authorize(): bool 32 | { 33 | return boolval($this->user()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220201_create_countries_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('capital')->nullable(); 18 | $table->char('iso2', 2)->unique(); 19 | $table->char('iso3', 3)->unique(); 20 | $table->char('phone_code'); 21 | $table->char('currency', 3); 22 | $table->string('region')->nullable(); 23 | $table->string('subregion')->nullable(); 24 | $table->boolean('is_active')->default(true); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists('countries'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/factories/OrderFactory.php: -------------------------------------------------------------------------------- 1 | fake()->unique()->uuid, 21 | 'user_id' => User::query()->inRandomOrder()->value('id'), 22 | 'user_address_id' => UserAddress::query()->inRandomOrder()->value('id'), 23 | 'delivery_method' => OrderDelivery::getRandomValue(), 24 | 'payment_method' => OrderPayment::getRandomValue(), 25 | 'goods_cost' => fake()->numberBetween(100, 10000), 26 | 'delivery_cost' => fake()->numberBetween(0, 100), 27 | 'total_cost' => fn (array $attributes) => $attributes['goods_cost'] + $attributes['delivery_cost'], 28 | 'status' => OrderStatus::getRandomValue(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 8 | 9 | $response->assertStatus(200); 10 | }); 11 | 12 | test('users can authenticate using the login screen', function () { 13 | $user = User::factory()->create(); 14 | 15 | $response = $this->post('/login', [ 16 | 'email' => $user->email, 17 | 'password' => 'password', 18 | ]); 19 | 20 | $this->assertAuthenticated(); 21 | $response->assertRedirect(RouteServiceProvider::HOME); 22 | }); 23 | 24 | test('users can not authenticate with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $this->post('/login', [ 28 | 'email' => $user->email, 29 | 'password' => 'wrong-password', 30 | ]); 31 | 32 | $this->assertGuest(); 33 | }); 34 | 35 | test('users can logout', function () { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this->actingAs($user)->post('/logout'); 39 | 40 | $this->assertGuest(); 41 | $response->assertRedirect('/'); 42 | }); 43 | -------------------------------------------------------------------------------- /app/Http/Requests/Profile/AddressRequest.php: -------------------------------------------------------------------------------- 1 | merge([ 13 | 'is_main' => !$this->user()->addresses()->count(), 14 | ]); 15 | } 16 | 17 | public function rules(): array 18 | { 19 | return [ 20 | 'country_id' => ['required', 'integer', Rule::exists('countries', 'id')], 21 | 'state_id' => ['nullable', 'integer', Rule::exists('states', 'id')], 22 | 'city_id' => ['nullable', 'integer', Rule::exists('cities', 'id')], 23 | 'street' => ['required', 'string', 'max:255'], 24 | 'house' => ['required', 'string', 'max:10'], 25 | 'flat' => ['nullable', 'string', 'max:10'], 26 | 'postal_code' => ['nullable', 'string', 'max:10'], 27 | 'is_main' => ['required', 'boolean'], 28 | ]; 29 | } 30 | 31 | public function authorize(): bool 32 | { 33 | return boolval($this->user()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000002_create_subscriptions_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id'); 17 | $table->string('type'); 18 | $table->string('stripe_id')->unique(); 19 | $table->string('stripe_status'); 20 | $table->string('stripe_price')->nullable(); 21 | $table->integer('quantity')->nullable(); 22 | $table->timestamp('trial_ends_at')->nullable(); 23 | $table->timestamp('ends_at')->nullable(); 24 | $table->timestamps(); 25 | 26 | $table->index(['user_id', 'stripe_status']); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('subscriptions'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220222_create_media_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->morphs('model'); 15 | $table->uuid()->nullable()->unique(); 16 | $table->string('collection_name'); 17 | $table->string('name'); 18 | $table->string('file_name'); 19 | $table->string('mime_type')->nullable(); 20 | $table->string('disk'); 21 | $table->string('conversions_disk')->nullable(); 22 | $table->unsignedBigInteger('size'); 23 | $table->json('manipulations'); 24 | $table->json('custom_properties'); 25 | $table->json('generated_conversions'); 26 | $table->json('responsive_images'); 27 | $table->unsignedInteger('order_column')->nullable()->index(); 28 | 29 | $table->nullableTimestamps(); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220208_create_categories_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('title', 100); 17 | $table->string('slug', 100)->unique(); 18 | $table->string('description')->nullable(); 19 | $table->boolean('is_active')->default(true); 20 | $table->boolean('is_navigational')->default(true); 21 | $table->foreignId('parent_id')->nullable()->constrained('categories')->cascadeOnUpdate()->cascadeOnDelete(); 22 | $table->string('manual_url')->nullable(); 23 | $table->timestamps(); 24 | $table->softDeletes(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('categories'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /resources/js/Components/SecondaryButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/Mail/User/PasswordMail.php: -------------------------------------------------------------------------------- 1 | password = $password; 26 | } 27 | 28 | /** 29 | * Get the message envelope. 30 | */ 31 | public function envelope(): Envelope 32 | { 33 | return new Envelope(subject: 'Password Mail'); 34 | } 35 | 36 | /** 37 | * Get the message content definition. 38 | */ 39 | public function content(): Content 40 | { 41 | return new Content(markdown: 'mail.user.password'); 42 | } 43 | 44 | /** 45 | * Get the attachments for the message. 46 | */ 47 | public function attachments(): array 48 | { 49 | return []; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 8 | 9 | $response = $this 10 | ->actingAs($user) 11 | ->from('/profile') 12 | ->put('/password', [ 13 | 'current_password' => 'password', 14 | 'password' => 'new-password', 15 | 'password_confirmation' => 'new-password', 16 | ]); 17 | 18 | $response 19 | ->assertSessionHasNoErrors() 20 | ->assertRedirect('/profile'); 21 | 22 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 23 | }); 24 | 25 | test('correct password must be provided to update password', function () { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->from('/profile') 31 | ->put('/password', [ 32 | 'current_password' => 'wrong-password', 33 | 'password' => 'new-password', 34 | 'password_confirmation' => 'new-password', 35 | ]); 36 | 37 | $response 38 | ->assertSessionHasErrors('current_password') 39 | ->assertRedirect('/profile'); 40 | }); 41 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220220_create_order_recipients_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 18 | $table->string('title'); 19 | $table->string('first_name', 50); 20 | $table->string('last_name', 50); 21 | $table->string('second_name', 50)->nullable(); 22 | $table->string('phone', 13); 23 | $table->string('description')->nullable(); 24 | $table->boolean('is_default')->default(false); 25 | $table->timestamps(); 26 | 27 | $table->unique(['user_id', 'title']); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('order_recipients'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /resources/js/Components/ResponsiveNavLink.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 30 | 'email' => $request->user()->email, 31 | 'password' => $request->password])) { 32 | throw ValidationException::withMessages([ 33 | 'password' => __('auth.password'), 34 | ]); 35 | } 36 | 37 | $request->session()->put('auth.password_confirmed_at', time()); 38 | 39 | return redirect()->intended(RouteServiceProvider::HOME); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/filament.php: -------------------------------------------------------------------------------- 1 | [ 18 | 19 | // 'echo' => [ 20 | // 'broadcaster' => 'pusher', 21 | // 'key' => env('VITE_PUSHER_APP_KEY'), 22 | // 'cluster' => env('VITE_PUSHER_APP_CLUSTER'), 23 | // 'forceTLS' => true, 24 | // ], 25 | 26 | ], 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Default Filesystem Disk 31 | |-------------------------------------------------------------------------- 32 | | 33 | | This is the storage disk Filament will use to put media. You may use any 34 | | of the disks defined in the `config/filesystems.php`. 35 | | 36 | */ 37 | 38 | 'default_filesystem_disk' => env('FILAMENT_FILESYSTEM_DISK', 'public'), 39 | 40 | ]; 41 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220215_create_reviews_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 19 | $table->foreignIdFor(Good::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 20 | $table->boolean('is_buyer')->default(true); 21 | $table->text('content'); 22 | $table->tinyText('advantages'); 23 | $table->tinyText('disadvantages'); 24 | $table->unsignedTinyInteger('rating'); 25 | $table->string('video_src')->nullable(); 26 | $table->ipAddress()->nullable(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('reviews'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ config('app.name', 'brandford.') }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @routes 19 | @vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"]) 20 | @inertiaHead 21 | 22 | 23 | @inertia 24 | 25 | 26 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220217_create_promo_codes_table.php: -------------------------------------------------------------------------------- 1 | id(); 17 | $table->string('key', 50)->unique(); 18 | $table->enum('type', PromoCode::getValues()); 19 | $table->unsignedSmallInteger('value'); 20 | $table->string('description'); 21 | $table->integer('used_times')->default(0); 22 | $table->dateTime('starts_at')->nullable(); 23 | $table->dateTime('expires_at')->nullable(); 24 | $table->decimal('greater_than')->unsigned()->nullable(); 25 | $table->boolean('is_active')->default(true); 26 | $table->boolean('is_public')->default(false); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('promo_codes'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /resources/js/Pages/Profile/Edit.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /app/Http/Resources/UserAddressResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'is_main' => $this->is_main, 17 | 'street' => $this->street, 18 | 'house' => $this->house, 19 | 'flat' => $this->flat, 20 | 'postal_code' => $this->postal_code, 21 | 'created_at' => $this->created_at, 22 | 'updated_at' => $this->updated_at, 23 | 24 | 'country' => [ 25 | 'id' => $this->country->id, 26 | 'name' => $this->country->name, 27 | 'iso2' => $this->country->iso2, 28 | ], 29 | 'state' => [ 30 | 'id' => $this->state->id, 31 | 'uuid' => $this->state->uuid, 32 | 'name' => $this->state->name, 33 | ], 34 | 'city' => [ 35 | 'id' => $this->city->id, 36 | 'uuid' => $this->city->uuid, 37 | 'name' => $this->city->name, 38 | ], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Models/Country.php: -------------------------------------------------------------------------------- 1 | 'boolean', 30 | ]; 31 | 32 | protected $appends = ['flag']; 33 | 34 | public function states(): HasMany 35 | { 36 | return $this->hasMany(State::class); 37 | } 38 | 39 | public function cities(): HasManyThrough 40 | { 41 | return $this->hasManyThrough(City::class, State::class); 42 | } 43 | 44 | public function getRouteKeyName(): string 45 | { 46 | return 'iso2'; 47 | } 48 | 49 | protected function flag(): Attribute 50 | { 51 | return Attribute::get(fn () => $this->getFirstMediaUrl('flag')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resources/js/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * We'll load the axios HTTP library which allows us to easily issue requests 3 | * to our Laravel back-end. This library automatically handles sending the 4 | * CSRF token as a header based on the value of the "XSRF" token cookie. 5 | */ 6 | 7 | import axios from 'axios' 8 | window.axios = axios 9 | 10 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' 11 | 12 | /** 13 | * Echo exposes an expressive API for subscribing to channels and listening 14 | * for events that are broadcast by Laravel. Echo and event broadcasting 15 | * allows your team to easily build robust real-time web applications. 16 | */ 17 | 18 | // import Echo from 'laravel-echo'; 19 | 20 | // import Pusher from 'pusher-js'; 21 | // window.Pusher = Pusher; 22 | 23 | // window.Echo = new Echo({ 24 | // broadcaster: 'pusher', 25 | // key: import.meta.env.VITE_PUSHER_APP_KEY, 26 | // cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', 27 | // wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, 28 | // wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, 29 | // wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, 30 | // forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', 31 | // enabledTransports: ['ws', 'wss'], 32 | // }); 33 | -------------------------------------------------------------------------------- /resources/js/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'ziggy-js' 2 | 3 | export interface User { 4 | id: number 5 | first_name: string 6 | last_name: string 7 | email: string 8 | email_verified_at: string 9 | avatar: string 10 | phone: string 11 | birth_date: string 12 | gender: string 13 | } 14 | 15 | export type Address = { 16 | id: number 17 | user_id: number 18 | is_main: boolean 19 | country_id: number 20 | country: Country 21 | state_id: number | null 22 | state: State 23 | city_id: number | null 24 | city: City 25 | street: string 26 | house: string 27 | flat: string | null 28 | postal_code: string | null 29 | } 30 | 31 | export type Country = { 32 | id: number 33 | name: string 34 | iso2: string 35 | } 36 | 37 | export type State = { 38 | id: number 39 | country_id: number 40 | name: string 41 | } 42 | 43 | export type City = { 44 | id: number 45 | state_id: number 46 | name: string 47 | } 48 | 49 | export type Cart = { 50 | count: number 51 | total: number 52 | items: any 53 | goods: any 54 | } 55 | 56 | export type PageProps = Record> = T & { 57 | user: User 58 | categories: any[] 59 | breadcrumbs: any[] 60 | cart: Cart 61 | notification: any 62 | ziggy: Config & { location: string } 63 | } 64 | -------------------------------------------------------------------------------- /app/Jobs/StoreUserJob.php: -------------------------------------------------------------------------------- 1 | data = $data; 29 | } 30 | 31 | /** 32 | * Execute the job. 33 | * 34 | * @throws LimiterTimeoutException 35 | */ 36 | public function handle(): void 37 | { 38 | Redis::throttle('mailtrap')->allow(2)->every(1)->then(function () { 39 | Mail::to($this->data['email'])->send(new PasswordMail($this->data['password'])); 40 | 41 | $user = User::where('email', $this->data['email'])->firstOrFail(); 42 | event(new Registered($user)); 43 | }, function () { 44 | $this->release(2); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | boolean ? 'male' : 'female'; 20 | 21 | return [ 22 | 'first_name' => fake()->firstName($gender), 23 | 'last_name' => fake()->lastName(), 24 | 'birth_date' => fake()->dateTimeBetween('-30 years', '-14 years'), 25 | 'gender' => $gender, 26 | 'phone' => '+380' . $this->networkCodes[rand(0, count($this->networkCodes) - 1)] . fake()->unique()->numberBetween(1000000, 9999999), 27 | 'email' => fake()->unique()->safeEmail(), 28 | 'email_verified_at' => now(), 29 | 'password' => bcrypt(static::$password ??= fake()->password()), 30 | 'remember_token' => Str::random(10), 31 | ]; 32 | } 33 | 34 | public function unverified(): static 35 | { 36 | return $this->state( 37 | fn (array $attributes) => [ 38 | 'email_verified_at' => null, 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/factories/GoodFactory.php: -------------------------------------------------------------------------------- 1 | fake()->unique()->numberBetween(1000000000), 20 | 'title' => fake()->unique()->text(rand(5, 20)), 21 | 'slug' => fn (array $attributes) => Str::slug($attributes['title']), 22 | 'category_id' => Category::whereDoesntHave('subcategories')->inRandomOrder()->value('id'), 23 | 'brand_id' => Brand::inRandomOrder()->value('id'), 24 | 'description' => fake()->paragraph(rand(5, 10)), 25 | 'short_description' => fake()->sentence(rand(25, 75)), 26 | 'warning_description' => fake()->sentence(rand(10, 50)), 27 | 'old_price' => fake()->boolean ? fake()->numberBetween(100, 100000) : null, 28 | 'price' => fn (array $attributes) => $attributes['old_price'] ? $attributes['old_price'] - ($attributes['old_price'] * rand(1, 80)) / 100 : rand(100, 100000), 29 | 'quantity' => fake()->numberBetween(0, 100), 30 | 'status' => GoodStatus::getRandomValue(), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/js/app.ts: -------------------------------------------------------------------------------- 1 | import './bootstrap' 2 | import '../css/app.css' 3 | 4 | import { createApp, DefineComponent, h } from 'vue' 5 | import { createInertiaApp } from '@inertiajs/vue3' 6 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers' 7 | import { ZiggyVue } from 'ziggy-js'; 8 | import { library, dom } from '@fortawesome/fontawesome-svg-core' 9 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 10 | import { fas } from '@fortawesome/free-solid-svg-icons' 11 | import { far } from '@fortawesome/free-regular-svg-icons' 12 | import { fab } from '@fortawesome/free-brands-svg-icons' 13 | import Vue3Toasity, { type ToastContainerOptions } from 'vue3-toastify' 14 | import 'vue3-toastify/dist/index.css' 15 | 16 | library.add(fas, far, fab) 17 | dom.watch() 18 | 19 | const appName = import.meta.env.VITE_APP_NAME || 'brandford.' 20 | 21 | createInertiaApp({ 22 | title: (title) => `${title} - ${appName}`, 23 | resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), 24 | setup({ el, App, props, plugin }) { 25 | createApp({ render: () => h(App, props) }) 26 | .component('font-awesome-icon', FontAwesomeIcon) 27 | .use(plugin) 28 | .use(ZiggyVue) 29 | .use(Vue3Toasity, { autoClose: 3000 } as ToastContainerOptions) 30 | .mount(el) 31 | }, 32 | progress: { 33 | color: '#6366f1' 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 28 | 29 | $this->routes(function () { 30 | Route::middleware('api') 31 | ->prefix('api') 32 | ->group(base_path('routes/api.php')); 33 | 34 | Route::middleware('web')->group(base_path('routes/web.php')); 35 | }); 36 | } 37 | 38 | /** 39 | * Configure the rate limiters for the application. 40 | */ 41 | protected function configureRateLimiting(): void 42 | { 43 | RateLimiter::for('api', function (Request $request) { 44 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220204_create_user_addresses_table.php: -------------------------------------------------------------------------------- 1 | id(); 20 | $table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 21 | $table->boolean('is_main')->default(false); 22 | $table->foreignIdFor(Country::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 23 | $table->foreignIdFor(State::class)->nullable()->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 24 | $table->foreignIdFor(City::class)->nullable()->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 25 | $table->string('street'); 26 | $table->string('house', 10); 27 | $table->string('flat', 10)->nullable(); 28 | $table->string('postal_code', 10)->nullable(); 29 | $table->timestamps(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | */ 36 | public function down(): void 37 | { 38 | Schema::dropIfExists('user_addresses'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /app/Filament/Resources/StateResource/Pages/ViewState.php: -------------------------------------------------------------------------------- 1 | schema([ 19 | Components\Section::make()->schema([ 20 | Components\TextEntry::make('id')->label('ID'), 21 | Components\TextEntry::make('name'), 22 | Components\TextEntry::make('country.name') 23 | ->url(fn (State $record) => route('filament.admin.resources.countries.view', $record->country->iso2), true) 24 | ->badge() 25 | ->columnSpanFull(), 26 | Components\IconEntry::make('is_active')->boolean(), 27 | Components\TextEntry::make('cities_count')->default($this->record->cities()->count())->label('Cities')->badge(), 28 | Components\TextEntry::make('created_at')->label('Created Date')->dateTime(), 29 | Components\TextEntry::make('updated_at')->label('Last Modified Date')->dateTime(), 30 | ])->columns(), 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function share(Request $request): array 36 | { 37 | return [ 38 | ...parent::share($request), 39 | 'user' => $request->user(), 40 | 'ziggy' => fn () => [ 41 | ...(new Ziggy)->toArray(), 42 | 'location' => $request->url(), 43 | ], 44 | 'categories' => $request->routeIs('livewire.message') ?: CategoryResource::collection(Category::whereParentId(null)->get()), 45 | 'cart' => new CartResource(Cart::getGoodsAndCartItems()), 46 | 'notification' => $request->session()->get('notification'), 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Http/Resources/OrderResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'uuid' => $this->uuid, 20 | 'delivery_method' => OrderDelivery::fromValue($this->delivery_method)->description, 21 | 'payment_method' => OrderPayment::fromValue($this->payment_method)->description, 22 | 'goods_cost' => $this->goods_cost, 23 | 'delivery_cost' => $this->delivery_cost, 24 | 'total_cost' => $this->total_cost, 25 | 'status' => OrderStatus::fromValue($this->status)->description, 26 | 'status_history' => $this->status_history, 27 | 'created_at' => $this->created_at->format('d.m.y h:i:s'), 28 | 'updated_at' => $this->updated_at->format('d.m.y h:i:s'), 29 | 30 | 'user_id' => $this->user_id, 31 | 'user_address_id' => $this->user_address_id, 32 | 'promo_code_id' => $this->promo_code_id, 33 | 34 | 'userAddress' => new UserAddressResource($this->whenLoaded('userAddress')), 35 | 'orderItems' => OrderItemResource::collection($this->whenLoaded('orderItems')), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Filament/Resources/CityResource/Pages/ViewCity.php: -------------------------------------------------------------------------------- 1 | schema([ 19 | Components\Section::make()->schema([ 20 | Components\TextEntry::make('id')->label('ID'), 21 | Components\TextEntry::make('name'), 22 | Components\TextEntry::make('country.name') 23 | ->url(fn (City $record) => route('filament.admin.resources.countries.view', $record->country->iso2), true) 24 | ->badge(), 25 | Components\TextEntry::make('state.name') 26 | ->url(fn (City $record) => route('filament.admin.resources.states.view', $record->state_id), true) 27 | ->badge(), 28 | Components\IconEntry::make('is_active')->boolean()->columnSpanFull(), 29 | Components\TextEntry::make('created_at')->label('Created Date')->dateTime(), 30 | Components\TextEntry::make('updated_at')->label('Last Modified Date')->dateTime(), 31 | ])->columns(), 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Models/Order.php: -------------------------------------------------------------------------------- 1 | OrderDelivery::class, 32 | 'payment_method' => \App\Enums\OrderPayment::class, 33 | 'goods_cost' => 'float', 34 | 'delivery_cost' => 'float', 35 | 'total_cost' => 'float', 36 | 'status' => OrderStatus::class, 37 | ]; 38 | 39 | public function user(): BelongsTo 40 | { 41 | return $this->belongsTo(User::class); 42 | } 43 | 44 | public function userAddress(): BelongsTo 45 | { 46 | return $this->belongsTo(UserAddress::class); 47 | } 48 | 49 | public function promoCode(): BelongsTo 50 | { 51 | return $this->belongsTo(PromoCode::class); 52 | } 53 | 54 | public function orderItems(): HasMany 55 | { 56 | return $this->hasMany(OrderItem::class); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | Route::has('password.request'), 25 | 'status' => session('status'), 26 | ]); 27 | } 28 | 29 | /** 30 | * Handle an incoming authentication request. 31 | */ 32 | public function store(LoginRequest $request): RedirectResponse 33 | { 34 | $request->authenticate(); 35 | 36 | $request->session()->regenerate(); 37 | 38 | Cart::saveCookieCartItems(); 39 | 40 | return redirect()->intended(RouteServiceProvider::HOME); 41 | } 42 | 43 | /** 44 | * Destroy an authenticated session. 45 | */ 46 | public function destroy(Request $request): RedirectResponse 47 | { 48 | Auth::guard('web')->logout(); 49 | 50 | $request->session()->invalidate(); 51 | 52 | $request->session()->regenerateToken(); 53 | 54 | return to_route('index.dashboard'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | session('status'), 22 | ]); 23 | } 24 | 25 | /** 26 | * Handle an incoming password reset link request. 27 | * 28 | * @throws ValidationException 29 | */ 30 | public function store(Request $request): RedirectResponse 31 | { 32 | $request->validate([ 33 | 'email' => 'required|email', 34 | ]); 35 | 36 | // We will send the password reset link to this user. Once we have attempted 37 | // to send the link, we will examine the response then see the message we 38 | // need to show to the user. Finally, we'll send out a proper response. 39 | $status = Password::sendResetLink($request->only('email')); 40 | 41 | if ($status == Password::RESET_LINK_SENT) { 42 | return back()->with('status', __($status)); 43 | } 44 | 45 | throw ValidationException::withMessages([ 46 | 'email' => [trans($status)], 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | create([ 11 | 'email_verified_at' => null, 12 | ]); 13 | 14 | $response = $this->actingAs($user)->get('/verify-email'); 15 | 16 | $response->assertStatus(200); 17 | }); 18 | 19 | test('email can be verified', function () { 20 | $user = User::factory()->create([ 21 | 'email_verified_at' => null, 22 | ]); 23 | 24 | Event::fake(); 25 | 26 | $verificationUrl = URL::temporarySignedRoute( 27 | 'verification.verify', 28 | now()->addMinutes(60), 29 | ['id' => $user->id, 'hash' => sha1($user->email)] 30 | ); 31 | 32 | $response = $this->actingAs($user)->get($verificationUrl); 33 | 34 | Event::assertDispatched(Verified::class); 35 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 36 | $response->assertRedirect(RouteServiceProvider::HOME . '?verified=1'); 37 | }); 38 | 39 | test('email is not verified with invalid hash', function () { 40 | $user = User::factory()->create([ 41 | 'email_verified_at' => null, 42 | ]); 43 | 44 | $verificationUrl = URL::temporarySignedRoute( 45 | 'verification.verify', 46 | now()->addMinutes(60), 47 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 48 | ); 49 | 50 | $this->actingAs($user)->get($verificationUrl); 51 | 52 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 53 | }); 54 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CONTAINER_NAME=store 2 | APP_NAME="brandford." 3 | APP_ENV=local 4 | APP_KEY= 5 | APP_DEBUG=true 6 | APP_URL=http://laravel-store.test 7 | 8 | LOG_CHANNEL=stack 9 | LOG_DEPRECATIONS_CHANNEL=null 10 | LOG_LEVEL=debug 11 | 12 | DB_CONNECTION=sqlite 13 | #DB_HOST=mysql 14 | #DB_PORT=3306 15 | #DB_DATABASE=brandford 16 | #DB_USERNAME=root 17 | #DB_PASSWORD=root 18 | 19 | BROADCAST_DRIVER=log 20 | CACHE_DRIVER=database 21 | FILESYSTEM_DISK=public 22 | QUEUE_CONNECTION=database 23 | SESSION_DRIVER=database 24 | SESSION_LIFETIME=120 25 | 26 | MEMCACHED_HOST=127.0.0.1 27 | 28 | REDIS_HOST=redis 29 | REDIS_PASSWORD=null 30 | REDIS_PORT=6379 31 | 32 | MAIL_MAILER=smtp 33 | MAIL_HOST=sandbox.smtp.mailtrap.io 34 | MAIL_PORT=2525 35 | MAIL_USERNAME= 36 | MAIL_PASSWORD= 37 | MAIL_ENCRYPTION=tls 38 | MAIL_FROM_ADDRESS="no-reply@brandford.com" 39 | MAIL_FROM_NAME="${APP_NAME}" 40 | 41 | AWS_ACCESS_KEY_ID= 42 | AWS_SECRET_ACCESS_KEY= 43 | AWS_DEFAULT_REGION=us-east-1 44 | AWS_BUCKET= 45 | AWS_USE_PATH_STYLE_ENDPOINT=false 46 | 47 | PUSHER_APP_ID= 48 | PUSHER_APP_KEY= 49 | PUSHER_APP_SECRET= 50 | PUSHER_HOST= 51 | PUSHER_PORT=443 52 | PUSHER_SCHEME=https 53 | PUSHER_APP_CLUSTER=mt1 54 | 55 | VITE_APP_NAME="${APP_NAME}" 56 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 57 | VITE_PUSHER_HOST="${PUSHER_HOST}" 58 | VITE_PUSHER_PORT="${PUSHER_PORT}" 59 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 60 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 61 | 62 | MULTIAVATAR_API_KEY= 63 | 64 | CSC_API_URL=https://api.countrystatecity.in/v1/countries/ 65 | CSC_API_KEY= 66 | 67 | STRIPE_KEY= 68 | STRIPE_SECRET= 69 | STRIPE_WEBHOOK_SECRET= 70 | 71 | GITHUB_CLIENT_ID= 72 | GITHUB_CLIENT_SECRET= 73 | 74 | GOOGLE_CLIENT_ID= 75 | GOOGLE_CLIENT_SECRET= 76 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/RelationManagers/SocialsRelationManager.php: -------------------------------------------------------------------------------- 1 | schema([ 21 | Forms\Components\Section::make()->schema([ 22 | Forms\Components\TextInput::make('provider'), 23 | Forms\Components\TextInput::make('provider_id'), 24 | Forms\Components\TextInput::make('provider_token'), 25 | ]), 26 | ]); 27 | } 28 | 29 | /** 30 | * @throws \Exception 31 | */ 32 | public function table(Table $table): Table 33 | { 34 | return $table 35 | ->columns([ 36 | Tables\Columns\TextColumn::make('id')->label('ID')->sortable(), 37 | Tables\Columns\TextColumn::make('provider')->sortable()->searchable(), 38 | Tables\Columns\TextColumn::make('provider_id')->searchable(), 39 | Tables\Columns\TextColumn::make('provider_token')->searchable()->toggleable()->limit(50), 40 | ]) 41 | ->filters([ 42 | // 43 | ]) 44 | ->actions([ 45 | Tables\Actions\ViewAction::make(), 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withProviders([ 17 | AdminPanelProvider::class, 18 | HorizonServiceProvider::class, 19 | RouteServiceProvider::class, 20 | TelescopeServiceProvider::class, 21 | ]) 22 | ->withRouting( 23 | web: __DIR__ . '/../routes/web.php', 24 | api: __DIR__ . '/../routes/api.php', 25 | commands: __DIR__ . '/../routes/console.php', 26 | // channels: __DIR__.'/../routes/channels.php', 27 | health: '/up', 28 | ) 29 | ->withMiddleware(function (Middleware $middleware) { 30 | $middleware->web(append: [ 31 | HandleInertiaRequests::class, 32 | AddLinkHeadersForPreloadedAssets::class, 33 | ]); 34 | 35 | $middleware->redirectGuestsTo(fn () => route('login')); 36 | $middleware->redirectUsersTo('/'); 37 | 38 | $middleware->api('throttle:60,1'); 39 | 40 | $middleware->replaceInGroup('web', ValidateCsrfToken::class, VerifyCsrfToken::class); 41 | }) 42 | ->withExceptions(function (Exceptions $exceptions) { 43 | // 44 | })->create(); 45 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220218_create_orders_table.php: -------------------------------------------------------------------------------- 1 | id(); 22 | $table->uuid()->unique(); 23 | $table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 24 | $table->foreignIdFor(UserAddress::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 25 | $table->foreignIdFor(PromoCode::class)->nullable()->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 26 | $table->enum('delivery_method', OrderDelivery::getValues()); 27 | $table->enum('payment_method', OrderPayment::getValues()); 28 | $table->decimal('goods_cost')->unsigned(); 29 | $table->decimal('delivery_cost')->unsigned()->nullable()->default(0); 30 | $table->decimal('total_cost')->unsigned(); 31 | $table->enum('status', OrderStatus::getValues())->default(OrderStatus::UNPAID); 32 | $table->json('status_history')->nullable(); 33 | $table->timestamps(); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | */ 40 | public function down(): void 41 | { 42 | Schema::dropIfExists('orders'); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /resources/js/Components/Rating.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 47 | 48 | 57 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ConfirmPassword.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 49 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'domain' => env('MAILGUN_DOMAIN'), 18 | 'secret' => env('MAILGUN_SECRET'), 19 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 20 | 'scheme' => 'https', 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | 'multiavatar' => [ 34 | 'url' => 'https://api.multiavatar.com/', 35 | 'key' => env('MULTIAVATAR_API_KEY'), 36 | ], 37 | 38 | 'csc' => [ 39 | 'url' => env('CSC_API_URL'), 40 | 'key' => env('CSC_API_KEY'), 41 | ], 42 | 43 | 'github' => [ 44 | 'client_id' => env('GITHUB_CLIENT_ID'), 45 | 'client_secret' => env('GITHUB_CLIENT_SECRET'), 46 | 'redirect' => '/social/github/callback', 47 | ], 48 | 49 | 'google' => [ 50 | 'client_id' => env('GOOGLE_CLIENT_ID'), 51 | 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 52 | 'redirect' => '/social/google/callback', 53 | ], 54 | ]; 55 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | expect()->extend('toBeOne', function () { 31 | return $this->toBe(1); 32 | }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function something() 46 | { 47 | // .. 48 | } 49 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 37 | 'first_name' => ['required', 'string', 'max:255'], 38 | 'last_name' => ['required', 'string', 'max:255'], 39 | 'birth_date' => ['required', 'date'], 40 | 'gender' => ['required', 'string'], 41 | 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')], 42 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 43 | ]); 44 | 45 | $data['password'] = Hash::make($data['password']); 46 | 47 | $user = User::create($data); 48 | 49 | event(new Registered($user)); 50 | 51 | Auth::login($user); 52 | 53 | return redirect(RouteServiceProvider::HOME); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | $request->user() instanceof MustVerifyEmail, 23 | 'status' => session('status'), 24 | ]); 25 | } 26 | 27 | /** 28 | * Update the user's profile information. 29 | */ 30 | public function update(ProfileUpdateRequest $request): RedirectResponse 31 | { 32 | $request->user()->fill($request->validated()); 33 | 34 | if ($request->user()->isDirty('email')) { 35 | $request->user()->email_verified_at = null; 36 | } 37 | 38 | $request->user()->save(); 39 | 40 | return Redirect::route('profile.edit'); 41 | } 42 | 43 | /** 44 | * Delete the user's account. 45 | */ 46 | public function destroy(Request $request): RedirectResponse 47 | { 48 | $request->validate([ 49 | 'password' => ['required', 'current_password'], 50 | ]); 51 | 52 | $user = $request->user(); 53 | 54 | Auth::logout(); 55 | 56 | $user->delete(); 57 | 58 | $request->session()->invalidate(); 59 | $request->session()->regenerateToken(); 60 | 61 | return Redirect::to('/'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/Pages/CreateUser.php: -------------------------------------------------------------------------------- 1 | previousUrl ?? $this->getResource()::getUrl('index'); 19 | } 20 | 21 | public Collection $permissions; 22 | 23 | protected function mutateFormDataBeforeCreate(array $data): array 24 | { 25 | $nonPermissionsFilter = [ 26 | 'first_name', 27 | 'last_name', 28 | 'birth_date', 29 | 'gender', 30 | 'status', 31 | 'email', 32 | 'email_verified_at', 33 | 'phone', 34 | 'password', 35 | ]; 36 | 37 | $this->permissions = collect($data)->filter(function ($permission, $key) use ($nonPermissionsFilter) { 38 | return !in_array($key, $nonPermissionsFilter) && Str::contains($key, '_'); 39 | })->keys(); 40 | 41 | return Arr::only($data, $nonPermissionsFilter); 42 | } 43 | 44 | protected function afterCreate(): void 45 | { 46 | $permissionModels = collect(); 47 | $this->permissions->each(function ($permission) use ($permissionModels) { 48 | $permissionModels->push(Permission::firstOrCreate( 49 | ['name' => $permission], 50 | )); 51 | }); 52 | 53 | $this->record->syncPermissions($permissionModels); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /database/migrations/2023_02_22_220209_create_goods_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->bigInteger('vendor_code')->unique(); 20 | $table->string('title'); 21 | $table->string('slug')->unique(); 22 | $table->foreignIdFor(Category::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete(); 23 | $table->foreignIdFor(Brand::class)->nullable()->constrained()->cascadeOnUpdate()->nullOnDelete(); 24 | $table->text('description')->nullable(); 25 | $table->text('short_description')->nullable(); 26 | $table->text('warning_description')->nullable(); 27 | $table->decimal('old_price')->unsigned()->nullable(); 28 | $table->decimal('price')->unsigned(); 29 | $table->unsignedInteger('quantity')->default(0); 30 | $table->enum('status', GoodStatus::getValues()); 31 | $table->json('options')->nullable(); 32 | $table->timestamps(); 33 | 34 | $table->softDeletes(); 35 | if (config('database.default') !== 'sqlite') { 36 | $table->fullText(['title', 'description']); 37 | } 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | */ 44 | public function down(): void 45 | { 46 | Schema::dropIfExists('goods'); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('reset password link can be requested', function () { 14 | Notification::fake(); 15 | 16 | $user = User::factory()->create(); 17 | 18 | $this->post('/forgot-password', ['email' => $user->email]); 19 | 20 | Notification::assertSentTo($user, ResetPassword::class); 21 | }); 22 | 23 | test('reset password screen can be rendered', function () { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create(); 27 | 28 | $this->post('/forgot-password', ['email' => $user->email]); 29 | 30 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 31 | $response = $this->get('/reset-password/' . $notification->token); 32 | 33 | $response->assertStatus(200); 34 | 35 | return true; 36 | }); 37 | }); 38 | 39 | test('password can be reset with valid token', function () { 40 | Notification::fake(); 41 | 42 | $user = User::factory()->create(); 43 | 44 | $this->post('/forgot-password', ['email' => $user->email]); 45 | 46 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 47 | $response = $this->post('/reset-password', [ 48 | 'token' => $notification->token, 49 | 'email' => $user->email, 50 | 'password' => 'password', 51 | 'password_confirmation' => 'password', 52 | ]); 53 | 54 | $response->assertSessionHasNoErrors(); 55 | 56 | return true; 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /app/Filament/Resources/CountryResource/Pages/ViewCountry.php: -------------------------------------------------------------------------------- 1 | schema([ 18 | Components\Section::make()->schema([ 19 | Components\TextEntry::make('id')->label('ID'), 20 | Components\SpatieMediaLibraryImageEntry::make('flag')->collection('flag')->circular()->size(48), 21 | Components\TextEntry::make('name'), 22 | Components\TextEntry::make('capital'), 23 | Components\TextEntry::make('iso2'), 24 | Components\TextEntry::make('iso3'), 25 | Components\TextEntry::make('region'), 26 | Components\TextEntry::make('subregion'), 27 | Components\TextEntry::make('currency'), 28 | Components\IconEntry::make('is_active')->boolean(), 29 | Components\TextEntry::make('states_count')->default($this->record->states()->count())->label('States')->badge(), 30 | Components\TextEntry::make('cities_count')->default($this->record->cities()->count())->label('Cities')->badge(), 31 | Components\TextEntry::make('created_at')->label('Created Date')->dateTime(), 32 | Components\TextEntry::make('updated_at')->label('Last Modified Date')->dateTime(), 33 | ])->columns(), 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Resources/GoodResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'vendor_code' => $this->vendor_code, 17 | 'title' => $this->title, 18 | 'slug' => $this->slug, 19 | 'description' => $this->description, 20 | 'short_description' => $this->short_description, 21 | 'warning_description' => $this->warning_description, 22 | 'old_price' => $this->old_price, 23 | 'price' => $this->price, 24 | 'quantity' => $this->quantity, 25 | 'status' => $this->status, 26 | 'created_at' => $this->created_at, 27 | 'updated_at' => $this->updated_at, 28 | 29 | 'preview' => $this->preview, 30 | 'slides' => $this->getMedia('goods') 31 | ->pluck('file_name', 'id') 32 | ->map(fn (string $item, $key) => url("/storage/$key/$item")) 33 | ->all(), 34 | 35 | 'category_id' => $this->category_id, 36 | 'brand_id' => $this->brand_id, 37 | 38 | 'rating' => $this->reviews->avg('rating'), 39 | 'reviews_count' => $this->reviews->count(), 40 | 41 | 'brand' => new BrandResource($this->whenLoaded('brand')), 42 | 'category' => new CategoryResource($this->whenLoaded('category')), 43 | 'properties' => PropertyResource::collection($this->whenLoaded('properties')), 44 | 'reviews' => ReviewResource::collection($this->whenLoaded('reviews')), 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc && vite build && vite build --ssr", 7 | "prettier": "prettier --write ." 8 | }, 9 | "devDependencies": { 10 | "@inertiajs/vue3": "^1.0.0", 11 | "@tailwindcss/aspect-ratio": "^0.4.2", 12 | "@tailwindcss/forms": "^0.5.3", 13 | "@tailwindcss/typography": "^0.5.10", 14 | "@types/lodash": "^4.14.202", 15 | "@types/qs": "^6.9.11", 16 | "@vitejs/plugin-vue": "^5.0.0", 17 | "@vue/server-renderer": "^3.4.0", 18 | "autoprefixer": "^10.4.12", 19 | "axios": "^1.6.4", 20 | "laravel-vite-plugin": "^1.0.0", 21 | "lodash": "^4.17.21", 22 | "postcss": "^8.4.31", 23 | "prettier": "^3.2.4", 24 | "prettier-plugin-tailwindcss": "^0.5.11", 25 | "tailwind-scrollbar": "^3.0.5", 26 | "tailwindcss": "^3.2.1", 27 | "tippy.js": "^6.3.7", 28 | "typescript": "^5.0.2", 29 | "vite": "^5.0.0", 30 | "vue": "^3.4.0", 31 | "vue-tsc": "^1.8.27" 32 | }, 33 | "dependencies": { 34 | "@fortawesome/fontawesome-svg-core": "^6.5.1", 35 | "@fortawesome/free-brands-svg-icons": "^6.5.1", 36 | "@fortawesome/free-regular-svg-icons": "^6.5.1", 37 | "@fortawesome/free-solid-svg-icons": "^6.5.1", 38 | "@fortawesome/vue-fontawesome": "^3.0.5", 39 | "@headlessui/vue": "^1.7.17", 40 | "@kyvg/vue3-notification": "^3.1.3", 41 | "@splidejs/vue-splide": "^0.6.12", 42 | "@stripe/stripe-js": "^2.4.0", 43 | "@vueform/slider": "^2.1.10", 44 | "@vueuse/core": "^10.7.2", 45 | "flowbite": "^2.2.1", 46 | "qs": "^6.11.2", 47 | "vue-tel-input": "^8.3.1", 48 | "vue3-toastify": "^0.2.1", 49 | "ziggy-js": "^2.0.5" 50 | } 51 | } 52 | --------------------------------------------------------------------------------