├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── .styleci.yml ├── README.md ├── app ├── Console │ └── Commands │ │ └── DeleteUnpaidOrders.php ├── Enums │ ├── AddressType.php │ ├── CustomerStatus.php │ ├── OrderStatus.php │ └── PaymentStatus.php ├── Helpers │ └── Cart.php ├── Http │ ├── Controllers │ │ ├── Api │ │ │ ├── AuthController.php │ │ │ ├── CategoryController.php │ │ │ ├── CustomerController.php │ │ │ ├── DashboardController.php │ │ │ ├── OrderController.php │ │ │ ├── ProductController.php │ │ │ └── UserController.php │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── NewPasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── VerifyEmailController.php │ │ ├── CartController.php │ │ ├── CheckoutController.php │ │ ├── Controller.php │ │ ├── OrderController.php │ │ ├── ProductController.php │ │ ├── ProfileController.php │ │ └── ReportController.php │ ├── Middleware │ │ ├── Admin.php │ │ └── GuestOrVerified.php │ ├── Requests │ │ ├── Auth │ │ │ └── LoginRequest.php │ │ ├── CreateUserRequest.php │ │ ├── CustomerRequest.php │ │ ├── PasswordUpdateRequest.php │ │ ├── ProductRequest.php │ │ ├── ProfileRequest.php │ │ ├── StoreCategoryRequest.php │ │ ├── UpdateCategoryRequest.php │ │ └── UpdateUserRequest.php │ └── Resources │ │ ├── CategoryResource.php │ │ ├── CategoryTreeResource.php │ │ ├── CountryResource.php │ │ ├── CustomerListResource.php │ │ ├── CustomerResource.php │ │ ├── Dashboard │ │ └── OrderResource.php │ │ ├── OrderListResource.php │ │ ├── OrderResource.php │ │ ├── ProductListResource.php │ │ ├── ProductResource.php │ │ └── UserResource.php ├── Mail │ ├── NewOrderEmail.php │ └── OrderUpdateEmail.php ├── Models │ ├── Api │ │ ├── Product.php │ │ └── User.php │ ├── CartItem.php │ ├── Category.php │ ├── Country.php │ ├── Customer.php │ ├── CustomerAddress.php │ ├── Order.php │ ├── OrderDetail.php │ ├── OrderItem.php │ ├── Payment.php │ ├── Product.php │ ├── ProductCategory.php │ ├── ProductImage.php │ └── User.php ├── Providers │ ├── AppServiceProvider.php │ └── TelescopeServiceProvider.php ├── Traits │ └── ReportTrait.php └── View │ └── Components │ ├── AppLayout.php │ └── GuestLayout.php ├── artisan ├── backend ├── .env.example ├── .env.production ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── favicon.ico ├── src │ ├── App.vue │ ├── assets │ │ ├── logo.png │ │ └── noimage.png │ ├── axios.js │ ├── components │ │ ├── AppLayout.vue │ │ ├── GuestLayout.vue │ │ ├── ImagePreview.vue │ │ ├── Navbar.vue │ │ ├── Sidebar.vue │ │ └── core │ │ │ ├── Charts │ │ │ ├── Bar.vue │ │ │ ├── Doughnut.vue │ │ │ └── Line.vue │ │ │ ├── CustomInput.vue │ │ │ ├── Spinner.vue │ │ │ ├── Table │ │ │ └── TableHeaderCell.vue │ │ │ └── Toast.vue │ ├── constants.js │ ├── filters │ │ └── currency.js │ ├── index.css │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── actions.js │ │ ├── index.js │ │ ├── mutations.js │ │ └── state.js │ └── views │ │ ├── Categories │ │ ├── Categories.vue │ │ ├── CategoriesTable.vue │ │ └── CategoryModal.vue │ │ ├── Customers │ │ ├── CustomerView.vue │ │ ├── Customers.vue │ │ └── CustomersTable.vue │ │ ├── Dashboard.vue │ │ ├── Login.vue │ │ ├── NotFound.vue │ │ ├── Orders │ │ ├── OrderStatus.vue │ │ ├── OrderView.vue │ │ ├── Orders.vue │ │ └── OrdersTable.vue │ │ ├── Products │ │ ├── ProductForm.vue │ │ ├── Products.vue │ │ └── ProductsTable.vue │ │ ├── Reports │ │ ├── CustomersReport.vue │ │ ├── OrdersReport.vue │ │ └── Report.vue │ │ ├── RequestPassword.vue │ │ ├── ResetPassword.vue │ │ └── Users │ │ ├── UserModal.vue │ │ ├── Users.vue │ │ └── UsersTable.vue ├── tailwind.config.js └── vite.config.js ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── filesystems.php ├── logging.php ├── mail.php ├── queue.php ├── sanctum.php ├── services.php ├── session.php └── telescope.php ├── database ├── .gitignore ├── factories │ ├── ProductFactory.php │ └── UserFactory.php ├── migrations │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2022_07_09_004121_create_products_table.php │ ├── 2022_07_09_004135_create_orders_table.php │ ├── 2022_07_09_004342_create_countries_table.php │ ├── 2022_07_09_004403_create_cart_items_table.php │ ├── 2022_07_09_004417_create_order_details_table.php │ ├── 2022_07_09_004430_create_order_items_table.php │ ├── 2022_07_09_004446_create_payments_table.php │ ├── 2022_07_09_004505_create_customers_table.php │ ├── 2022_07_09_004515_create_customer_addresses_table.php │ ├── 2022_07_11_043258_add_is_admin_column_to_users_table.php │ ├── 2022_09_11_142434_rename_customer_id_column.php │ ├── 2022_09_17_025414_change_countries_states_column_into_json.php │ ├── 2022_10_01_142356_add_session_id_to_payments_table.php │ ├── 2022_10_09_171628_add_published_column_to_products.php │ ├── 2022_11_28_194915_update_order_items_order_id.php │ ├── 2022_11_28_194929_update_payments_order_id.php │ ├── 2023_02_26_194708_add_expires_at_column_to_personal_access_tokens.php │ ├── 2023_08_29_144700_add_quantity_column_to_products_table.php │ ├── 2023_09_01_145113_create_product_images_table.php │ ├── 2023_09_22_145051_create_categories_table.php │ └── 2023_10_16_151019_create_product_categories_table.php └── seeders │ ├── AdminUserSeeder.php │ ├── CountrySeeder.php │ ├── DatabaseSeeder.php │ └── ProductSeeder.php ├── lang └── en │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .htaccess ├── build │ ├── assets │ │ ├── app-ca70469a.css │ │ └── app-e41bd908.js │ └── manifest.json ├── favicon.ico ├── img │ └── noimage.png ├── index.php ├── robots.txt └── vendor │ └── telescope │ ├── app-dark.css │ ├── app.css │ ├── app.js │ ├── favicon.ico │ └── mix-manifest.json ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ ├── bootstrap.js │ └── http.js └── views │ ├── auth │ ├── confirm-password.blade.php │ ├── forgot-password.blade.php │ ├── login.blade.php │ ├── register.blade.php │ ├── reset-password.blade.php │ └── verify-email.blade.php │ ├── cart │ └── index.blade.php │ ├── checkout │ ├── failure.blade.php │ └── success.blade.php │ ├── components │ ├── application-logo.blade.php │ ├── auth-card.blade.php │ ├── auth-session-status.blade.php │ ├── auth-validation-errors.blade.php │ ├── button.blade.php │ ├── category-list.blade.php │ ├── dropdown-link.blade.php │ ├── dropdown.blade.php │ ├── input.blade.php │ ├── label.blade.php │ ├── nav-link.blade.php │ └── responsive-nav-link.blade.php │ ├── dashboard.blade.php │ ├── layouts │ ├── app.blade.php │ ├── guest.blade.php │ └── navigation.blade.php │ ├── mail │ ├── new-order.blade.php │ └── update-order.blade.php │ ├── order │ ├── index.blade.php │ └── view.blade.php │ ├── product │ ├── index.blade.php │ └── view.blade.php │ ├── profile │ └── view.blade.php │ ├── vendor │ └── mail │ │ ├── html │ │ ├── button.blade.php │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ ├── layout.blade.php │ │ ├── message.blade.php │ │ ├── panel.blade.php │ │ ├── subcopy.blade.php │ │ ├── table.blade.php │ │ └── themes │ │ │ └── default.css │ │ └── text │ │ ├── button.blade.php │ │ ├── footer.blade.php │ │ ├── header.blade.php │ │ ├── layout.blade.php │ │ ├── message.blade.php │ │ ├── panel.blade.php │ │ ├── subcopy.blade.php │ │ └── table.blade.php │ └── welcome.blade.php ├── routes ├── api.php ├── auth.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── debugbar │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── CreatesApplication.php ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ └── RegistrationTest.php │ └── ExampleTest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml,js,css,html,vue}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=stack 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE=laravel_vue_ecommerce 15 | DB_USERNAME=root 16 | DB_PASSWORD= 17 | 18 | BROADCAST_DRIVER=log 19 | CACHE_DRIVER=file 20 | FILESYSTEM_DISK=local 21 | QUEUE_CONNECTION=sync 22 | SESSION_DRIVER=file 23 | SESSION_LIFETIME=120 24 | 25 | MEMCACHED_HOST=127.0.0.1 26 | 27 | REDIS_HOST=127.0.0.1 28 | REDIS_PASSWORD=null 29 | REDIS_PORT=6379 30 | 31 | MAIL_MAILER=smtp 32 | MAIL_HOST=mailhog 33 | MAIL_PORT=1025 34 | MAIL_USERNAME=null 35 | MAIL_PASSWORD=null 36 | MAIL_ENCRYPTION=null 37 | MAIL_FROM_ADDRESS="hello@example.com" 38 | MAIL_FROM_NAME="${APP_NAME}" 39 | 40 | AWS_ACCESS_KEY_ID= 41 | AWS_SECRET_ACCESS_KEY= 42 | AWS_DEFAULT_REGION=us-east-1 43 | AWS_BUCKET= 44 | AWS_USE_PATH_STYLE_ENDPOINT=false 45 | 46 | PUSHER_APP_ID= 47 | PUSHER_APP_KEY= 48 | PUSHER_APP_SECRET= 49 | PUSHER_HOST= 50 | PUSHER_PORT=443 51 | PUSHER_SCHEME=https 52 | PUSHER_APP_CLUSTER=mt1 53 | 54 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 55 | VITE_PUSHER_HOST="${PUSHER_HOST}" 56 | VITE_PUSHER_PORT="${PUSHER_PORT}" 57 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 58 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 59 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /vendor 6 | .env 7 | .env.backup 8 | .phpunit.result.cache 9 | Homestead.json 10 | Homestead.yaml 11 | npm-debug.log 12 | yarn-error.log 13 | /.idea 14 | /.vscode 15 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | php: 2 | preset: laravel 3 | disabled: 4 | - no_unused_imports 5 | finder: 6 | not-name: 7 | - index.php 8 | js: 9 | finder: 10 | not-name: 11 | - vite.config.js 12 | css: true 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel E-commerce Website 2 | E-commerce application built with Laravel, Vue.js, Tailwind.css and Alpine.js.
3 | 4 | > If you want to see every single step how this E-commerce application is build and learn how to build your own Full Stack applications, check my website [thecodeholic.com](https://thecodeholic.com) 5 | 6 | ## Demo 7 | Admin Panel: https://admin.lcommerce.net 8 | ``` 9 | Email: admin@example.com 10 | Password: admin123 11 | ``` 12 | 13 | Website: https://lcommerce.net 14 | 15 | ``` 16 | Email: user1@example.com 17 | Password: useruser1 18 | 19 | 20 | Email: user2@example.com 21 | Password: useruser2 22 | ``` 23 | 24 | ## Installation 25 | Make sure you have environment setup properly. You will need MySQL, PHP8.1, Node.js and composer. 26 | 27 | ### Install Laravel Website + API 28 | 1. Download the project (or clone using GIT) 29 | 2. Copy `.env.example` into `.env` and configure database credentials 30 | 3. Navigate to the project's root directory using terminal 31 | 4. Run `composer install` 32 | 5. Set the encryption key by executing `php artisan key:generate --ansi` 33 | 6. Run migrations `php artisan migrate --seed` 34 | 7. Start local server by executing `php artisan serve` 35 | 8. Open new terminal and navigate to the project root directory 36 | 9. Run `npm install` 37 | 10. Run `npm run dev` to start vite server for Laravel frontend 38 | 39 | ### Install Vue.js Admin Panel 40 | 1. Navigate to `backend` folder 41 | 2. Run `npm install` 42 | 3. Copy `backend/.env.example` into `backend/.env` 43 | 4. Make sure `VITE_API_BASE_URL` key in `backend/.env` is set to your Laravel API host (Default: http://localhost:8000) 44 | 5. Run `npm run dev` 45 | 6. Open Vue.js Admin Panel in browser and login with 46 | ``` 47 | admin@example.com 48 | admin123 49 | ``` 50 | -------------------------------------------------------------------------------- /app/Console/Commands/DeleteUnpaidOrders.php: -------------------------------------------------------------------------------- 1 | argument('hours'); 32 | $count = Order::deleteUnpaidOrders($hours); 33 | $this->info("$count unpaid orders were deleted"); 34 | return Command::SUCCESS; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Enums/AddressType.php: -------------------------------------------------------------------------------- 1 | 15 | * @package App\Enums 16 | */ 17 | enum AddressType: string 18 | { 19 | case Shipping = 'shipping'; 20 | case Billing = 'billing'; 21 | } 22 | -------------------------------------------------------------------------------- /app/Enums/CustomerStatus.php: -------------------------------------------------------------------------------- 1 | 15 | * @package App\Enums 16 | */ 17 | enum CustomerStatus: string 18 | { 19 | case Active = 'active'; 20 | case Disabled = 'disabled'; 21 | } 22 | -------------------------------------------------------------------------------- /app/Enums/OrderStatus.php: -------------------------------------------------------------------------------- 1 | 15 | * @package App\Enums 16 | */ 17 | enum OrderStatus: string 18 | { 19 | case Unpaid = 'unpaid'; 20 | case Paid = 'paid'; 21 | case Cancelled = 'cancelled'; 22 | case Shipped = 'shipped'; 23 | case Completed = 'completed'; 24 | 25 | public static function getStatuses() 26 | { 27 | return [ 28 | self::Paid, self::Unpaid, self::Cancelled, self::Shipped, self::Completed 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Enums/PaymentStatus.php: -------------------------------------------------------------------------------- 1 | 15 | * @package App\Enums 16 | */ 17 | enum PaymentStatus: string 18 | { 19 | case Pending = 'pending'; 20 | case Paid = 'paid'; 21 | case Failed = 'failed'; 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/AuthController.php: -------------------------------------------------------------------------------- 1 | validate([ 15 | 'email'=> ['required', 'email'], 16 | 'password' => 'required', 17 | 'remember' => 'boolean' 18 | ]); 19 | $remember = $credentials['remember'] ?? false; 20 | unset($credentials['remember']); 21 | if (!Auth::attempt($credentials, $remember)) { 22 | return response([ 23 | 'message' => 'Email or password is incorrect' 24 | ], 422); 25 | } 26 | 27 | /** @var \App\Models\User $user */ 28 | $user = Auth::user(); 29 | if (!$user->is_admin) { 30 | Auth::logout(); 31 | return response([ 32 | 'message' => 'You don\'t have permission to authenticate as admin' 33 | ], 403); 34 | } 35 | if (!$user->email_verified_at) { 36 | Auth::logout(); 37 | return response([ 38 | 'message' => 'Your email address is not verified' 39 | ], 403); 40 | } 41 | $token = $user->createToken('main')->plainTextToken; 42 | return response([ 43 | 'user' => new UserResource($user), 44 | 'token' => $token 45 | ]); 46 | 47 | } 48 | 49 | public function logout() 50 | { 51 | /** @var \App\Models\User $user */ 52 | $user = Auth::user(); 53 | $user->currentAccessToken()->delete(); 54 | 55 | return response('', 204); 56 | } 57 | 58 | public function getUser(Request $request) 59 | { 60 | return new UserResource($request->user()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/CategoryController.php: -------------------------------------------------------------------------------- 1 | orderBy($sortField, $sortDirection) 24 | ->latest() 25 | ->get(); 26 | 27 | return CategoryResource::collection($categories); 28 | } 29 | 30 | public function getAsTree() 31 | { 32 | return Category::getActiveAsTree(CategoryTreeResource::class); 33 | } 34 | 35 | /** 36 | * Store a newly created resource in storage. 37 | */ 38 | public function store(StoreCategoryRequest $request) 39 | { 40 | $data = $request->validated(); 41 | $data['created_by'] = $request->user()->id; 42 | $data['updated_by'] = $request->user()->id; 43 | $category = Category::create($data); 44 | 45 | return new CategoryResource($category); 46 | } 47 | 48 | /** 49 | * Update the specified resource in storage. 50 | */ 51 | public function update(UpdateCategoryRequest $request, Category $category) 52 | { 53 | $data = $request->validated(); 54 | $data['updated_by'] = $request->user()->id; 55 | $category->update($data); 56 | 57 | return new CategoryResource($category); 58 | } 59 | 60 | /** 61 | * Remove the specified resource from storage. 62 | */ 63 | public function destroy(Category $category) 64 | { 65 | $category->delete(); 66 | 67 | return response()->noContent(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/OrderController.php: -------------------------------------------------------------------------------- 1 | withCount('items') 33 | ->with('user.customer') 34 | ->where('id', 'like', "%{$search}%") 35 | ->orderBy($sortField, $sortDirection) 36 | ->paginate($perPage); 37 | 38 | return OrderListResource::collection($query); 39 | } 40 | 41 | public function view(Order $order) 42 | { 43 | $order->load('items.product'); 44 | 45 | return new OrderResource($order); 46 | } 47 | 48 | public function getStatuses() 49 | { 50 | return OrderStatus::getStatuses(); 51 | } 52 | 53 | public function changeStatus(Order $order, $status) 54 | { 55 | DB::beginTransaction(); 56 | try { 57 | $order->status = $status; 58 | $order->save(); 59 | 60 | if ($status === OrderStatus::Cancelled->value) { 61 | foreach ($order->items as $item) { 62 | $product = $item->product; 63 | if ($product && $product->quantity !== null) { 64 | $product->quantity += $item->quantity; 65 | $product->save(); 66 | } 67 | } 68 | } 69 | Mail::to($order->user)->send(new OrderUpdateEmail($order)); 70 | } catch (\Exception $e) { 71 | DB::rollBack(); 72 | throw $e; 73 | } 74 | 75 | DB::commit(); 76 | 77 | return response('', 200); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | authenticate(); 35 | 36 | $request->session()->regenerate(); 37 | 38 | Cart::moveCartItemsIntoDb(); 39 | 40 | return redirect()->intended(RouteServiceProvider::HOME); 41 | } 42 | 43 | /** 44 | * Destroy an authenticated session. 45 | * 46 | * @param \Illuminate\Http\Request $request 47 | * @return \Illuminate\Http\RedirectResponse 48 | */ 49 | public function destroy(Request $request) 50 | { 51 | Auth::guard('web')->logout(); 52 | 53 | $request->session()->invalidate(); 54 | 55 | $request->session()->regenerateToken(); 56 | 57 | return redirect('/'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 32 | 'email' => $request->user()->email, 33 | 'password' => $request->password, 34 | ])) { 35 | throw ValidationException::withMessages([ 36 | 'password' => __('auth.password'), 37 | ]); 38 | } 39 | 40 | $request->session()->put('auth.password_confirmed_at', time()); 41 | 42 | return redirect()->intended(RouteServiceProvider::HOME); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 20 | return redirect()->intended(RouteServiceProvider::HOME); 21 | } 22 | 23 | $request->user()->sendEmailVerificationNotification(); 24 | 25 | return back()->with('status', 'verification-link-sent'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 20 | ? redirect()->intended(RouteServiceProvider::HOME) 21 | : view('auth.verify-email'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request]); 24 | } 25 | 26 | /** 27 | * Handle an incoming new password request. 28 | * 29 | * @param \Illuminate\Http\Request $request 30 | * @return \Illuminate\Http\RedirectResponse 31 | * 32 | * @throws \Illuminate\Validation\ValidationException 33 | */ 34 | public function store(Request $request) 35 | { 36 | $request->validate([ 37 | 'token' => ['required'], 38 | 'email' => ['required', 'email'], 39 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 40 | ]); 41 | 42 | // Here we will attempt to reset the user's password. If it is successful we 43 | // will update the password on an actual user model and persist it to the 44 | // database. Otherwise we will parse the error and return the response. 45 | $status = Password::reset( 46 | $request->only('email', 'password', 'password_confirmation', 'token'), 47 | function ($user) use ($request) { 48 | $user->forceFill([ 49 | 'password' => Hash::make($request->password), 50 | 'remember_token' => Str::random(60), 51 | ])->save(); 52 | 53 | event(new PasswordReset($user)); 54 | } 55 | ); 56 | 57 | // If the password was successfully reset, we will redirect the user back to 58 | // the application's home authenticated view. If there is an error we can 59 | // redirect them back to where they came from with their error message. 60 | return $status == Password::PASSWORD_RESET 61 | ? redirect()->route('login')->with('status', __($status)) 62 | : back()->withInput($request->only('email')) 63 | ->withErrors(['email' => __($status)]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | validate([ 32 | 'email' => ['required', 'email'], 33 | ]); 34 | 35 | // We will send the password reset link to this user. Once we have attempted 36 | // to send the link, we will examine the response then see the message we 37 | // need to show to the user. Finally, we'll send out a proper response. 38 | $status = Password::sendResetLink( 39 | $request->only('email') 40 | ); 41 | 42 | return $status == Password::RESET_LINK_SENT 43 | ? back()->with('status', __($status)) 44 | : back()->withInput($request->only('email')) 45 | ->withErrors(['email' => __($status)]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 40 | 'name' => ['required', 'string', 'max:255'], 41 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 42 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 43 | ]); 44 | 45 | DB::beginTransaction(); 46 | try { 47 | $user = User::create([ 48 | 'name' => $request->name, 49 | 'email' => $request->email, 50 | 'password' => Hash::make($request->password), 51 | ]); 52 | 53 | event(new Registered($user)); 54 | 55 | $customer = new Customer(); 56 | $names = explode(" ", $user->name); 57 | $customer->user_id = $user->id; 58 | $customer->first_name = $names[0]; 59 | $customer->last_name = $names[1] ?? ''; 60 | $customer->save(); 61 | 62 | Auth::login($user); 63 | } catch (\Exception $e) { 64 | DB::rollBack(); 65 | return redirect()->back()->withInput()->with('error', 'Unable to register right now.'); 66 | } 67 | 68 | DB::commit(); 69 | 70 | Cart::moveCartItemsIntoDb(); 71 | 72 | return redirect(RouteServiceProvider::HOME); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 22 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); 23 | } 24 | 25 | if ($request->user()->markEmailAsVerified()) { 26 | $customer = $request->user()->customer; 27 | $customer->status = CustomerStatus::Active->value; 28 | $customer->save(); 29 | event(new Verified($request->user())); 30 | } 31 | 32 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | user(); 14 | 15 | $orders = Order::withCount('items') 16 | ->where(['created_by' => $user->id]) 17 | ->orderBy('created_at', 'desc') 18 | ->paginate(10); 19 | 20 | return view('order.index', compact('orders')); 21 | } 22 | 23 | public function view(Order $order) 24 | { 25 | /** @var \App\Models\User $user */ 26 | $user = \request()->user(); 27 | if ($order->created_by !== $user->id) { 28 | return response("You don't have permission to view this order", 403); 29 | } 30 | 31 | return view('order.view', compact('order')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProductController.php: -------------------------------------------------------------------------------- 1 | renderProducts($query); 17 | } 18 | 19 | public function byCategory(Category $category) 20 | { 21 | $categories = Category::getAllChildrenByParent($category); 22 | 23 | $query = Product::query() 24 | ->select('products.*') 25 | ->join('product_categories AS pc', 'pc.product_id', 'products.id') 26 | ->whereIn('pc.category_id', array_map(fn($c) => $c->id, $categories)); 27 | 28 | return $this->renderProducts($query); 29 | } 30 | 31 | public function view(Product $product) 32 | { 33 | return view('product.view', ['product' => $product]); 34 | } 35 | 36 | private function renderProducts(Builder $query) 37 | { 38 | $search = \request()->get('search'); 39 | $sort = \request()->get('sort', '-updated_at'); 40 | 41 | if ($sort) { 42 | $sortDirection = 'asc'; 43 | if ($sort[0] === '-') { 44 | $sortDirection = 'desc'; 45 | } 46 | $sortField = preg_replace('/^-?/', '', $sort); 47 | 48 | $query->orderBy($sortField, $sortDirection); 49 | } 50 | $products = $query 51 | ->where('published', '=', 1) 52 | ->where(function ($query) use ($search) { 53 | /** @var $query \Illuminate\Database\Eloquent\Builder */ 54 | $query->where('products.title', 'like', "%$search%") 55 | ->orWhere('products.description', 'like', "%$search%"); 56 | }) 57 | 58 | ->paginate(5); 59 | 60 | return view('product.index', [ 61 | 'products' => $products 62 | ]); 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Http/Controllers/ReportController.php: -------------------------------------------------------------------------------- 1 | prepareDataForBarChart($query, 'Orders By Day'); 23 | } 24 | 25 | public function customers() 26 | { 27 | $query = Customer::query(); 28 | 29 | return $this->prepareDataForBarChart($query, 'Customers By Day'); 30 | } 31 | 32 | private function prepareDataForBarChart($query, $label) 33 | { 34 | $fromDate = $this->getFromDate() ?: Carbon::now()->subDay(30); 35 | $query 36 | ->select([DB::raw('CAST(created_at as DATE) AS day'), DB::raw('COUNT(created_at) AS count')]) 37 | ->groupBy(DB::raw('CAST(created_at as DATE)')); 38 | if ($fromDate) { 39 | $query->where('created_at', '>', $fromDate); 40 | } 41 | $records = $query->get()->keyBy('day'); 42 | 43 | // Process for chartjs 44 | $days = []; 45 | $labels = []; 46 | $now = Carbon::now(); 47 | while ($fromDate < $now) { 48 | $key = $fromDate->format('Y-m-d'); 49 | $labels[] = $key; 50 | $fromDate = $fromDate->addDay(1); 51 | $days[] = isset($records[$key]) ? $records[$key]['count'] : 0; 52 | } 53 | 54 | return [ 55 | 'labels' => $labels, 56 | 'datasets' => [[ 57 | 'label' => $label, 58 | 'backgroundColor' => '#f87979', 59 | 'data' => $days 60 | ]] 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Http/Middleware/Admin.php: -------------------------------------------------------------------------------- 1 | is_admin == 1) { 21 | return $next($request); 22 | } 23 | return response([ 24 | 'message' => 'You don\'t have permission to perform this action' 25 | ], 403); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Middleware/GuestOrVerified.php: -------------------------------------------------------------------------------- 1 | user()) { 20 | return $next($request); 21 | } 22 | return parent::handle($request, $next, $redirectToRoute); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Requests/CreateUserRequest.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function rules() 26 | { 27 | return [ 28 | 'name' => ['required', 'max:55'], 29 | 'email' => ['required', 'email'], 30 | 'password' => ['required', Password::min(8)->numbers()->letters()->symbols()] 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Requests/CustomerRequest.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | public function rules() 27 | { 28 | return [ 29 | 'first_name' => ['required'], 30 | 'last_name' => ['required'], 31 | 'phone' => ['required', 'min:7'], 32 | 'email' => ['required', 'email'], 33 | 'status' => ['required', 'boolean'], 34 | 35 | 'shippingAddress.address1' => ['required'], 36 | 'shippingAddress.address2' => ['required'], 37 | 'shippingAddress.city' => ['required'], 38 | 'shippingAddress.state' => ['required'], 39 | 'shippingAddress.zipcode' => ['required'], 40 | 'shippingAddress.country_code' => ['required', 'exists:countries,code'], 41 | 42 | 'billingAddress.address1' => ['required'], 43 | 'billingAddress.address2' => ['required'], 44 | 'billingAddress.city' => ['required'], 45 | 'billingAddress.state' => ['required'], 46 | 'billingAddress.zipcode' => ['required'], 47 | 'billingAddress.country_code' => ['required', 'exists:countries,code'], 48 | 49 | ]; 50 | } 51 | 52 | public function attributes() 53 | { 54 | return [ 55 | 'billingAddress.address1' => 'address 1', 56 | 'billingAddress.address2' => 'address 2', 57 | 'billingAddress.city' => 'city', 58 | 'billingAddress.state' => 'state', 59 | 'billingAddress.zipcode' => 'zip code', 60 | 'billingAddress.country_code' => 'country', 61 | 'shippingAddress.address1' => 'address 1', 62 | 'shippingAddress.address2' => 'address 2', 63 | 'shippingAddress.city' => 'city', 64 | 'shippingAddress.state' => 'state', 65 | 'shippingAddress.zipcode' => 'zip code', 66 | 'shippingAddress.country_code' => 'country', 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Requests/PasswordUpdateRequest.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function rules() 26 | { 27 | return [ 28 | 'old_password' => 'current_password', 29 | 'new_password' => ['required', 'confirmed', Password::min(8)->letters()->numbers()->symbols()] 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Requests/ProductRequest.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function rules() 25 | { 26 | return [ 27 | 'title' => ['required', 'max:2000'], 28 | 'images.*' => ['nullable', 'image'], 29 | 'deleted_images.*' => ['nullable', 'int'], 30 | 'image_positions.*' => ['nullable', 'int'], 31 | 'categories.*' => ['nullable', 'int', 'exists:categories,id'], 32 | 'price' => ['required', 'numeric', 'min:0.01'], 33 | 'quantity' => ['nullable', 'numeric', 'min:0'], 34 | 'description' => ['nullable', 'string'], 35 | 'published' => ['required', 'boolean'] 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileRequest.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function rules() 25 | { 26 | return [ 27 | 'first_name' => ['required'], 28 | 'last_name' => ['required'], 29 | 'phone' => ['required', 'min:7'], 30 | 'email' => ['required', 'email'], 31 | 32 | 'shipping.address1' => ['required'], 33 | 'shipping.address2' => ['required'], 34 | 'shipping.city' => ['required'], 35 | 'shipping.state' => ['required'], 36 | 'shipping.zipcode' => ['required'], 37 | 'shipping.country_code' => ['required', 'exists:countries,code'], 38 | 39 | 'billing.address1' => ['required'], 40 | 'billing.address2' => ['required'], 41 | 'billing.city' => ['required'], 42 | 'billing.state' => ['required'], 43 | 'billing.zipcode' => ['required'], 44 | 'billing.country_code' => ['required', 'exists:countries,code'], 45 | 46 | ]; 47 | } 48 | 49 | public function attributes() 50 | { 51 | return [ 52 | 'billing.address1' => 'address 1', 53 | 'billing.address2' => 'address 2', 54 | 'billing.city' => 'city', 55 | 'billing.state' => 'state', 56 | 'billing.zipcode' => 'zip code', 57 | 'billing.country_code' => 'country', 58 | 'shipping.address1' => 'address 1', 59 | 'shipping.address2' => 'address 2', 60 | 'shipping.city' => 'city', 61 | 'shipping.state' => 'state', 62 | 'shipping.zipcode' => 'zip code', 63 | 'shipping.country_code' => 'country', 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreCategoryRequest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'name' => ['required', 'string'], 26 | 'parent_id' => ['nullable', 'exists:categories,id'], 27 | 'active' => ['required', 'boolean'] 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateCategoryRequest.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | 'name' => ['required', 'string'], 27 | 'parent_id' => [ 28 | 'nullable', 'exists:categories,id', 29 | function(string $attribute, $value, \Closure $fail) { 30 | $id = $this->get('id'); 31 | $category = Category::where('id', $id)->first(); 32 | 33 | $children = Category::getAllChildrenByParent($category); 34 | $ids = array_map(fn($c) => $c->id, $children); 35 | 36 | if (in_array($value, $ids)) { 37 | return $fail('You cannot choose category as parent which is already a child of the category.'); 38 | } 39 | } 40 | ], 41 | 'active' => ['required', 'boolean'] 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateUserRequest.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function rules() 26 | { 27 | return [ 28 | 'name' => ['max:55'], 29 | 'email' => ['email'], 30 | 'password' => ['nullable', Password::min(8)->numbers()->letters()->symbols()] 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Resources/CategoryResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return [ 18 | 'id' => $this->id, 19 | 'name' => $this->name, 20 | 'slug' => $this->slug, 21 | 'active' => $this->active, 22 | 'parent_id' => $this->parent_id, 23 | 'parent' => $this->parent ? new CategoryResource($this->parent) : null, 24 | 'created_at' => $this->created_at->format('Y-m-d H:i:s'), 25 | 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/CategoryTreeResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | $data = [ 18 | 'id' => $this->id, 19 | 'label' => $this->name, 20 | ]; 21 | 22 | if ($this->children ?? false) { 23 | $data['children'] = $this->children; 24 | } 25 | 26 | return $data; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/CountryResource.php: -------------------------------------------------------------------------------- 1 | $this->code, 23 | 'name' => $this->name, 24 | 'states' => json_decode($this->states, true), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Resources/CustomerListResource.php: -------------------------------------------------------------------------------- 1 | $this->user_id, 23 | 'first_name' => $this->first_name, 24 | 'last_name' => $this->last_name, 25 | 'email' => $this->user->email, 26 | 'phone' => $this->phone, 27 | 'status' => $this->status, 28 | 'created_at' => (new \DateTime($this->created_at))->format('Y-m-d H:i:s'), 29 | 'updated_at' => (new \DateTime($this->updated_at))->format('Y-m-d H:i:s'), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Resources/CustomerResource.php: -------------------------------------------------------------------------------- 1 | shippingAddress; 23 | $billing = $this->billingAddress; 24 | return [ 25 | 'id' => $this->user_id, 26 | 'first_name' => $this->first_name, 27 | 'last_name' => $this->last_name, 28 | 'email' => $this->user->email, 29 | 'phone' => $this->phone, 30 | 'status' => $this->status === CustomerStatus::Active->value, 31 | 'created_at' => (new \DateTime($this->created_at))->format('Y-m-d H:i:s'), 32 | 'updated_at' => (new \DateTime($this->updated_at))->format('Y-m-d H:i:s'), 33 | 34 | 'shippingAddress' => [ 35 | 'id' => $shipping?->id, 36 | 'address1' => $shipping?->address1, 37 | 'address2' => $shipping?->address2, 38 | 'city' => $shipping?->city, 39 | 'state' => $shipping?->state, 40 | 'zipcode' => $shipping?->zipcode, 41 | 'country_code' => $shipping?->country->code, 42 | ], 43 | 'billingAddress' => [ 44 | 'id' => $billing?->id, 45 | 'address1' => $billing?->address1, 46 | 'address2' => $billing?->address2, 47 | 'city' => $billing?->city, 48 | 'state' => $billing?->state, 49 | 'zipcode' => $billing?->zipcode, 50 | 'country_code' => $billing?->country->code, 51 | ] 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Http/Resources/Dashboard/OrderResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 21 | 'total_price' => $this->total_price, 22 | 'created_at' => $this->created_at->diffForHumans(), 23 | 'items' => $this->items, 24 | 'user_id' => $this->user_id, 25 | 'first_name' => $this->first_name, 26 | 'last_name' => $this->last_name, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Resources/OrderListResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 21 | 'status' => $this->status, 22 | 'total_price' => $this->total_price, 23 | 'number_of_items' => $this->items_count, 24 | 'customer' => [ 25 | 'id' => $this->user->id, 26 | 'first_name' => $this->user->customer->first_name, 27 | 'last_name' => $this->user->customer->last_name, 28 | ], 29 | 'created_at' => (new \DateTime($this->created_at))->format('Y-m-d H:i:s'), 30 | 'updated_at' => (new \DateTime($this->updated_at))->format('Y-m-d H:i:s'), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Resources/ProductListResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 21 | 'title' => $this->title, 22 | 'image_url' => $this->image, 23 | 'price' => $this->price, 24 | 'quantity' => $this->quantity, 25 | 'updated_at' => ( new \DateTime($this->updated_at) )->format('Y-m-d H:i:s'), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/ProductResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 23 | 'title' => $this->title, 24 | 'slug' => $this->slug, 25 | 'description' => $this->description, 26 | 'image_url' => $this->image, 27 | 'images' => $this->images, 28 | 'price' => $this->price, 29 | 'quantity' => $this->quantity, 30 | 'published' => (bool)$this->published, 31 | 'categories' => $this->categories->map(fn($c) => $c->id), 32 | 'created_at' => (new \DateTime($this->created_at))->format('Y-m-d H:i:s'), 33 | 'updated_at' => (new \DateTime($this->updated_at))->format('Y-m-d H:i:s'), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 22 | 'name' => $this->name, 23 | 'email' => $this->email, 24 | 'created_at' => (new DateTime($this->created_at))->format('Y-m-d H:i:s'), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Mail/NewOrderEmail.php: -------------------------------------------------------------------------------- 1 | subject('New Order') 34 | ->view('mail.new-order'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Mail/OrderUpdateEmail.php: -------------------------------------------------------------------------------- 1 | subject('Order Status was updated') 34 | ->view('mail.update-order'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/Api/Product.php: -------------------------------------------------------------------------------- 1 | generateSlugsFrom('name') 23 | ->saveSlugsTo('slug'); 24 | } 25 | 26 | public function parent() 27 | { 28 | return $this->belongsTo(Category::class); 29 | } 30 | 31 | public function products() 32 | { 33 | return $this->belongsToMany(Product::class); // product_category 34 | } 35 | 36 | public static function getActiveAsTree($resourceClassName = null) 37 | { 38 | $categories = Category::where('active', true)->orderBy('parent_id')->get(); 39 | return self::buildCategoryTree($categories, null, $resourceClassName); 40 | } 41 | 42 | public static function getAllChildrenByParent(Category $category) 43 | { 44 | $categories = Category::where('active', true)->orderBy('parent_id')->get(); 45 | $result[] = $category; 46 | self::getCategoriesArray($categories, $category->id, $result); 47 | 48 | return $result; 49 | } 50 | 51 | private static function buildCategoryTree($categories, $parentId = null, $resourceClassName = null) 52 | { 53 | $categoryTree = []; 54 | 55 | foreach ($categories as $category) { 56 | if ($category->parent_id === $parentId) { 57 | $children = self::buildCategoryTree($categories, $category->id, $resourceClassName); 58 | if ($children) { 59 | $category->setAttribute('children', $children); 60 | } 61 | $categoryTree[] = $resourceClassName ? new $resourceClassName($category) : $category; 62 | } 63 | } 64 | 65 | return $categoryTree; 66 | } 67 | 68 | private static function getCategoriesArray($categories, $parentId, &$result) 69 | { 70 | foreach ($categories as $category) { 71 | if ($category->parent_id === $parentId) { 72 | $result[] = $category; 73 | self::getCategoriesArray($categories, $category->id, $result); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/Models/Country.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 21 | } 22 | 23 | private function _getAddresses(): HasOne 24 | { 25 | return $this->hasOne(CustomerAddress::class, 'customer_id', 'user_id'); 26 | } 27 | 28 | public function shippingAddress(): HasOne 29 | { 30 | return $this->_getAddresses()->where('type', '=', AddressType::Shipping->value); 31 | } 32 | 33 | public function billingAddress(): HasOne 34 | { 35 | return $this->_getAddresses()->where('type', '=', AddressType::Billing->value); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Models/CustomerAddress.php: -------------------------------------------------------------------------------- 1 | belongsTo(Customer::class); 19 | } 20 | 21 | public function country(): BelongsTo 22 | { 23 | return $this->belongsTo(Country::class, 'country_code', 'code'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Models/Order.php: -------------------------------------------------------------------------------- 1 | status === OrderStatus::Paid->value; 22 | } 23 | 24 | public function payment(): HasOne 25 | { 26 | return $this->hasOne(Payment::class); 27 | } 28 | 29 | public function user() 30 | { 31 | return $this->belongsTo(User::class, 'created_by'); 32 | } 33 | 34 | public function items(): HasMany 35 | { 36 | return $this->hasMany(OrderItem::class); 37 | } 38 | 39 | public static function deleteUnpaidOrders($hours) 40 | { 41 | return Order::query()->where('status', OrderStatus::Unpaid->value) 42 | ->where('created_at', '<', Carbon::now()->subHours($hours)) 43 | ->delete(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Models/OrderDetail.php: -------------------------------------------------------------------------------- 1 | belongsTo(Order::class); 18 | } 19 | 20 | public function product(): BelongsTo 21 | { 22 | return $this->belongsTo(Product::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Models/Payment.php: -------------------------------------------------------------------------------- 1 | hasOne(Order::class, 'id', 'order_id'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Models/Product.php: -------------------------------------------------------------------------------- 1 | generateSlugsFrom('title') 26 | ->saveSlugsTo('slug'); 27 | } 28 | 29 | public function getRouteKeyName() 30 | { 31 | return 'slug'; 32 | } 33 | 34 | public function images() 35 | { 36 | return $this->hasMany(ProductImage::class)->orderBy('position'); 37 | } 38 | 39 | public function getImageAttribute() 40 | { 41 | return $this->images->count() > 0 ? $this->images->get(0)->url : null; 42 | } 43 | 44 | public function categories() 45 | { 46 | return $this->belongsToMany(Category::class, 'product_categories'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Models/ProductCategory.php: -------------------------------------------------------------------------------- 1 | belongsTo(Product::class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | 'email_verified_at', 25 | 'is_admin' 26 | ]; 27 | 28 | /** 29 | * The attributes that should be hidden for serialization. 30 | * 31 | * @var array 32 | */ 33 | protected $hidden = [ 34 | 'password', 35 | 'remember_token', 36 | ]; 37 | 38 | /** 39 | * The attributes that should be cast. 40 | * 41 | * @var array 42 | */ 43 | protected $casts = [ 44 | 'email_verified_at' => 'datetime', 45 | ]; 46 | 47 | public function customer() 48 | { 49 | return $this->hasOne(Customer::class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->environment('local')) { 17 | $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); 18 | $this->app->register(TelescopeServiceProvider::class); 19 | } 20 | } 21 | 22 | /** 23 | * Bootstrap any application services. 24 | * 25 | * @return void 26 | */ 27 | public function boot() 28 | { 29 | // 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Providers/TelescopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | hideSensitiveRequestDetails(); 20 | 21 | Telescope::filter(function (IncomingEntry $entry) { 22 | if ($this->app->environment('local')) { 23 | return true; 24 | } 25 | 26 | return $entry->isReportableException() || 27 | $entry->isFailedRequest() || 28 | $entry->isFailedJob() || 29 | $entry->isScheduledTask() || 30 | $entry->hasMonitoredTag(); 31 | }); 32 | } 33 | 34 | /** 35 | * Prevent sensitive request details from being logged by Telescope. 36 | */ 37 | protected function hideSensitiveRequestDetails(): void 38 | { 39 | if ($this->app->environment('local')) { 40 | return; 41 | } 42 | 43 | Telescope::hideRequestParameters(['_token']); 44 | 45 | Telescope::hideRequestHeaders([ 46 | 'cookie', 47 | 'x-csrf-token', 48 | 'x-xsrf-token', 49 | ]); 50 | } 51 | 52 | /** 53 | * Register the Telescope gate. 54 | * 55 | * This gate determines who can access Telescope in non-local environments. 56 | */ 57 | protected function gate(): void 58 | { 59 | Gate::define('viewTelescope', function ($user) { 60 | return in_array($user->email, [ 61 | // 62 | ]); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Traits/ReportTrait.php: -------------------------------------------------------------------------------- 1 | get('d'); 18 | $array = [ 19 | '1d' => Carbon::now()->subDays(1), 20 | '1k' => Carbon::now()->subDays(7), 21 | '2k' => Carbon::now()->subDays(14), 22 | '1m' => Carbon::now()->subDays(30), 23 | '3m' => Carbon::now()->subDays(60), 24 | '6m' => Carbon::now()->subDays(180), 25 | ]; 26 | 27 | return $array[$paramDate] ?? null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/View/Components/AppLayout.php: -------------------------------------------------------------------------------- 1 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL = http://localhost:8000 2 | -------------------------------------------------------------------------------- /backend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL = https://lcommerce.net 2 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | -------------------------------------------------------------------------------- /backend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite --port=3000", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@ckeditor/ckeditor5-build-classic": "^39.0.1", 12 | "@ckeditor/ckeditor5-vue": "^5.1.0", 13 | "axios": "^1.6.8", 14 | "chart.js": "^3.9.1", 15 | "sortablejs": "^1.15.2", 16 | "sortablejs-vue3": "^1.2.11", 17 | "uuid": "^9.0.1", 18 | "vue": "^3.4.21", 19 | "vue-chartjs": "^4.1.2", 20 | "vue-router": "^4.3.0", 21 | "vue3-treeselect": "^0.1.10", 22 | "vuex": "^4.1.0" 23 | }, 24 | "devDependencies": { 25 | "@headlessui/vue": "^1.7.19", 26 | "@heroicons/vue": "^1.0.6", 27 | "@tailwindcss/forms": "^0.5.7", 28 | "@vitejs/plugin-vue": "^5.0.4", 29 | "autoprefixer": "^10.4.19", 30 | "postcss": "^8.4.38", 31 | "tailwindcss": "^3.4.1", 32 | "vite": "^5.2.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /backend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeholic/laravel-vue-ecommerce/dd10302167419a04dfd486c55ffc6cfb797b5897/backend/public/favicon.ico -------------------------------------------------------------------------------- /backend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /backend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeholic/laravel-vue-ecommerce/dd10302167419a04dfd486c55ffc6cfb797b5897/backend/src/assets/logo.png -------------------------------------------------------------------------------- /backend/src/assets/noimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeholic/laravel-vue-ecommerce/dd10302167419a04dfd486c55ffc6cfb797b5897/backend/src/assets/noimage.png -------------------------------------------------------------------------------- /backend/src/axios.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Zura on 12/25/2021. 3 | */ 4 | import axios from "axios"; 5 | import store from "./store"; 6 | import router from "./router/index.js"; 7 | 8 | const axiosClient = axios.create({ 9 | baseURL: `${import.meta.env.VITE_API_BASE_URL}/api` 10 | }) 11 | 12 | axiosClient.interceptors.request.use(config => { 13 | config.headers.Authorization = `Bearer ${store.state.user.token}` 14 | return config; 15 | }) 16 | 17 | axiosClient.interceptors.response.use(response => { 18 | return response; 19 | }, error => { 20 | if (error.response.status === 401) { 21 | store.commit('setToken', null) 22 | router.push({name: 'login'}) 23 | } 24 | throw error; 25 | }) 26 | 27 | export default axiosClient; 28 | -------------------------------------------------------------------------------- /backend/src/components/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 56 | 57 | 60 | -------------------------------------------------------------------------------- /backend/src/components/GuestLayout.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /backend/src/components/core/Charts/Bar.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /backend/src/components/core/Charts/Doughnut.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /backend/src/components/core/Charts/Line.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /backend/src/components/core/Spinner.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 41 | 42 | 45 | -------------------------------------------------------------------------------- /backend/src/components/core/Table/TableHeaderCell.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /backend/src/components/core/Toast.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 81 | 82 | 85 | -------------------------------------------------------------------------------- /backend/src/constants.js: -------------------------------------------------------------------------------- 1 | export const PRODUCTS_PER_PAGE = 10 2 | export const USERS_PER_PAGE = 10 3 | export const CUSTOMERS_PER_PAGE = 10 4 | -------------------------------------------------------------------------------- /backend/src/filters/currency.js: -------------------------------------------------------------------------------- 1 | export default function currencyUSD(value) { 2 | return new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}) 3 | .format(value); 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .ck-content > blockquote, 6 | .ck-content > dl, 7 | .ck-content > dd, 8 | .ck-content > h1, 9 | .ck-content > h2, 10 | .ck-content > h3, 11 | .ck-content > h4, 12 | .ck-content > h5, 13 | .ck-content > h6, 14 | .ck-content > hr, 15 | .ck-content > figure, 16 | .ck-content > p, 17 | .ck-content > pre { 18 | margin: revert; 19 | } 20 | 21 | .ck-content > ol, 22 | .ck-content > ul { 23 | list-style: revert; 24 | margin: revert; 25 | padding: revert; 26 | } 27 | 28 | .ck-content > table { 29 | border-collapse: revert; 30 | } 31 | 32 | .ck-content > h1, 33 | .ck-content > h2, 34 | .ck-content > h3, 35 | .ck-content > h4, 36 | .ck-content > h5, 37 | .ck-content > h6 { 38 | font-size: revert; 39 | font-weight: revert; 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import CKEditor from '@ckeditor/ckeditor5-vue' 3 | import store from './store' 4 | import router from './router' 5 | import './index.css'; 6 | import currencyUSD from './filters/currency.js' 7 | 8 | import App from './App.vue' 9 | 10 | const app = createApp(App); 11 | 12 | app 13 | .use(store) 14 | .use(router) 15 | .use(CKEditor) 16 | .mount('#app') 17 | ; 18 | 19 | app.config.globalProperties.$filters = { 20 | currencyUSD 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import {createStore} from "vuex"; 2 | import state from './state' 3 | import * as actions from './actions' 4 | import * as mutations from './mutations' 5 | 6 | const store = createStore({ 7 | state, 8 | getters: {}, 9 | actions, 10 | mutations, 11 | }) 12 | 13 | export default store 14 | -------------------------------------------------------------------------------- /backend/src/store/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | user: { 3 | token: sessionStorage.getItem('TOKEN'), 4 | data: {} 5 | }, 6 | products: { 7 | loading: false, 8 | data: [], 9 | links: [], 10 | from: null, 11 | to: null, 12 | page: 1, 13 | limit: null, 14 | total: null 15 | }, 16 | users: { 17 | loading: false, 18 | data: [], 19 | links: [], 20 | from: null, 21 | to: null, 22 | page: 1, 23 | limit: null, 24 | total: null 25 | }, 26 | customers: { 27 | loading: false, 28 | data: [], 29 | links: [], 30 | from: null, 31 | to: null, 32 | page: 1, 33 | limit: null, 34 | total: null 35 | }, 36 | countries: [], 37 | orders: { 38 | loading: false, 39 | data: [], 40 | links: [], 41 | from: null, 42 | to: null, 43 | page: 1, 44 | limit: null, 45 | total: null 46 | }, 47 | toast: { 48 | show: false, 49 | message: '', 50 | delay: 5000 51 | }, 52 | dateOptions: [ 53 | {key: '1d', text: 'Last Day'}, 54 | {key: '1k', text: 'Last Week'}, 55 | {key: '2k', text: 'Last 2 Weeks'}, 56 | {key: '1m', text: 'Last Month'}, 57 | {key: '3m', text: 'Last 3 Months'}, 58 | {key: '6m', text: 'Last 6 Months'}, 59 | {key: 'all', text: 'All Time'}, 60 | ], 61 | categories: { 62 | loading: false, 63 | data: [], 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /backend/src/views/Categories/Categories.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | 48 | 51 | -------------------------------------------------------------------------------- /backend/src/views/Customers/Customers.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /backend/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /backend/src/views/Orders/OrderStatus.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /backend/src/views/Orders/Orders.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /backend/src/views/Products/Products.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /backend/src/views/Reports/CustomersReport.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /backend/src/views/Reports/OrdersReport.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /backend/src/views/Reports/Report.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /backend/src/views/RequestPassword.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 39 | -------------------------------------------------------------------------------- /backend/src/views/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 45 | -------------------------------------------------------------------------------- /backend/src/views/Users/Users.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | 48 | 51 | -------------------------------------------------------------------------------- /backend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | keyframes: { 10 | 'fade-in-down': { 11 | "from": { 12 | transform: "translateY(-0.75rem)", 13 | opacity: '0' 14 | }, 15 | "to": { 16 | transform: "translateY(0rem)", 17 | opacity: '1' 18 | }, 19 | }, 20 | }, 21 | animation: { 22 | 'fade-in-down': "fade-in-down 0.2s ease-in-out both", 23 | }, 24 | }, 25 | plugins: [ 26 | require('@tailwindcss/forms'), 27 | ], 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 11 | web: __DIR__.'/../routes/web.php', 12 | api: __DIR__.'/../routes/api.php', 13 | commands: __DIR__.'/../routes/console.php', 14 | health: '/up', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware) { 17 | $middleware->alias([ 18 | 'guestOrVerified' => GuestOrVerified::class, 19 | 'admin' => Admin::class 20 | ]); 21 | }) 22 | ->withExceptions(function (Exceptions $exceptions) { 23 | // 24 | })->create(); 25 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 19 | '%s%s', 20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 21 | Sanctum::currentApplicationUrlWithPort() 22 | ))), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Sanctum Guards 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This array contains the authentication guards that will be checked when 30 | | Sanctum is trying to authenticate a request. If none of these guards 31 | | are able to authenticate the request, Sanctum will use the bearer 32 | | token that's present on an incoming request for authentication. 33 | | 34 | */ 35 | 36 | 'guard' => ['web'], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Expiration Minutes 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This value controls the number of minutes until an issued token will be 44 | | considered expired. If this value is null, personal access tokens do 45 | | not expire. This won't tweak the lifetime of first-party sessions. 46 | | 47 | */ 48 | 49 | 'expiration' => null, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Sanctum Middleware 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When authenticating your first-party SPA with Sanctum you may need to 57 | | customize some of the middleware Sanctum uses while processing the 58 | | request. You may change the middleware listed below as required. 59 | | 60 | */ 61 | 62 | 'middleware' => [ 63 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 64 | 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, 65 | 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, 66 | ], 67 | 68 | ]; 69 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/ProductFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ProductFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition() 18 | { 19 | return [ 20 | 'title' => fake()->text(), 21 | 'image' => fake()->imageUrl(), 22 | 'description' => fake()->realText(2000), 23 | 'price' => fake()->randomFloat(2, 2, 5), 24 | 'created_at' => now(), 25 | 'updated_at' => now(), 26 | 'created_by' => 1, 27 | 'updated_by' => 1, 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UserFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition() 19 | { 20 | return [ 21 | 'name' => fake()->name(), 22 | 'email' => fake()->safeEmail(), 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | } 28 | 29 | /** 30 | * Indicate that the model's email address should be unverified. 31 | * 32 | * @return static 33 | */ 34 | public function unverified() 35 | { 36 | return $this->state(function (array $attributes) { 37 | return [ 38 | 'email_verified_at' => null, 39 | ]; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->rememberToken(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('users'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('uuid')->unique(); 19 | $table->text('connection'); 20 | $table->text('queue'); 21 | $table->longText('payload'); 22 | $table->longText('exception'); 23 | $table->timestamp('failed_at')->useCurrent(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('failed_jobs'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->morphs('tokenable'); 19 | $table->string('name'); 20 | $table->string('token', 64)->unique(); 21 | $table->text('abilities')->nullable(); 22 | $table->timestamp('last_used_at')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('personal_access_tokens'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004121_create_products_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('title', 2000); 20 | $table->string('slug', 2000); 21 | $table->string('image', 2000)->nullable(); 22 | $table->string('image_mime')->nullable(); 23 | $table->integer('image_size')->nullable(); 24 | $table->longText('description')->nullable(); 25 | $table->decimal('price', 10, 2); 26 | $table->foreignIdFor(User::class, 'created_by')->nullable(); 27 | $table->foreignIdFor(User::class, 'updated_by')->nullable(); 28 | $table->softDeletes(); 29 | $table->foreignIdFor(User::class, 'deleted_by')->nullable(); 30 | $table->timestamps(); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('products'); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004135_create_orders_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->decimal('total_price', 20, 2); 20 | $table->string('status', 45); 21 | $table->timestamps(); 22 | $table->foreignIdFor(User::class, 'created_by')->nullable(); 23 | $table->foreignIdFor(User::class, 'updated_by')->nullable(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('orders'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004342_create_countries_table.php: -------------------------------------------------------------------------------- 1 | string('code', 3)->primary(); 18 | $table->string('name', 255); 19 | $table->jsonb('states')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('countries'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004403_create_cart_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignIdFor(\App\Models\User::class, 'user_id'); 19 | $table->foreignId('product_id')->references('id')->on('products'); 20 | $table->integer('quantity'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('cart_items'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004417_create_order_details_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('first_name'); 19 | $table->string('last_name'); 20 | $table->string('phone')->nullable(); 21 | $table->string('address1', 255); 22 | $table->string('address2', 255); 23 | $table->string('city', 255); 24 | $table->string('state', 45)->nullable(); 25 | $table->string('zipcode', 45); 26 | $table->string('country_code', 3); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('order_details'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004430_create_order_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('order_id')->references('id')->on('orders'); 19 | $table->foreignId('product_id')->references('id')->on('products'); 20 | $table->integer('quantity'); 21 | $table->decimal('unit_price'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('order_items'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004446_create_payments_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->foreignId('order_id')->references('id')->on('orders'); 20 | $table->decimal('amount', 10, 2); 21 | $table->string('status', 45); 22 | $table->string('type', 45); 23 | $table->timestamps(); 24 | $table->foreignIdFor(User::class, 'created_by')->nullable(); 25 | $table->foreignIdFor(User::class, 'updated_by')->nullable(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('payments'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004505_create_customers_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('first_name'); 20 | $table->string('last_name'); 21 | $table->string('phone')->nullable(); 22 | $table->string('status', 45)->nullable(); 23 | $table->timestamps(); 24 | $table->foreignIdFor(User::class, 'created_by')->nullable(); 25 | $table->foreignIdFor(User::class, 'updated_by')->nullable(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('customers'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /database/migrations/2022_07_09_004515_create_customer_addresses_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('type', 45); 19 | $table->string('address1', 255); 20 | $table->string('address2', 255); 21 | $table->string('city', 255); 22 | $table->string('state', 45)->nullable(); 23 | $table->string('zipcode', 45); 24 | $table->string('country_code', 3); 25 | $table->foreignId('customer_id')->references('id')->on('customers'); 26 | $table->timestamps(); 27 | $table->foreign('country_code')->references('code')->on('countries'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('customer_addresses'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/migrations/2022_07_11_043258_add_is_admin_column_to_users_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_admin')->default(false); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('users', function (Blueprint $table) { 29 | $table->dropColumn('is_admin'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_09_11_142434_rename_customer_id_column.php: -------------------------------------------------------------------------------- 1 | renameColumn('id', 'user_id'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('customers', function (Blueprint $table) { 29 | $table->renameColumn('user_id', 'id'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_09_17_025414_change_countries_states_column_into_json.php: -------------------------------------------------------------------------------- 1 | dropColumn('states'); 17 | }); 18 | Schema::table('countries', function (Blueprint $table) { 19 | $table->json('states')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('countries', function (Blueprint $table) { 31 | $table->longText('states')->nullable()->change(); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2022_10_01_142356_add_session_id_to_payments_table.php: -------------------------------------------------------------------------------- 1 | string('session_id', 255)->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('payments', function (Blueprint $table) { 29 | $table->dropColumn('session_id'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_10_09_171628_add_published_column_to_products.php: -------------------------------------------------------------------------------- 1 | boolean('published')->default(false); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('products', function (Blueprint $table) { 29 | $table->dropColumn('published'); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_11_28_194915_update_order_items_order_id.php: -------------------------------------------------------------------------------- 1 | dropForeign(['order_id']); 18 | $table->foreign('order_id')->references('id')->on('orders')->cascadeOnDelete(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('order_items', function(Blueprint $table) { 30 | $table->dropForeign(['order_id']); 31 | $table->foreign('order_id')->references('id')->on('orders'); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2022_11_28_194929_update_payments_order_id.php: -------------------------------------------------------------------------------- 1 | dropForeign(['order_id']); 18 | $table->foreign('order_id')->references('id')->on('orders')->cascadeOnDelete(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::table('payments', function(Blueprint $table) { 30 | $table->dropForeign(['order_id']); 31 | $table->foreign('order_id')->references('id')->on('orders'); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_02_26_194708_add_expires_at_column_to_personal_access_tokens.php: -------------------------------------------------------------------------------- 1 | timestamp('expires_at')->nullable()->after('last_used_at'); 15 | }); 16 | } 17 | 18 | /** 19 | * Reverse the migrations. 20 | */ 21 | public function down(): void 22 | { 23 | Schema::table('personal_access_tokens', function (Blueprint $table) { 24 | $table->dropColumn('expires_at'); 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2023_08_29_144700_add_quantity_column_to_products_table.php: -------------------------------------------------------------------------------- 1 | integer('quantity')->nullable()->after('price'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('products', function (Blueprint $table) { 25 | $table->dropColumn('quantity'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2023_09_22_145051_create_categories_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('slug'); 18 | $table->boolean('active'); 19 | $table->foreignId('parent_id')->nullable()->constrained('categories'); 20 | $table->foreignIdFor(\App\Models\User::class, 'created_by'); 21 | $table->foreignIdFor(\App\Models\User::class, 'updated_by'); 22 | $table->timestamp('deleted_at')->nullable(); 23 | $table->foreignIdFor(\App\Models\User::class, 'deleted_by')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('categories'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2023_10_16_151019_create_product_categories_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('product_id')->constrained('products'); 17 | $table->foreignId('category_id')->constrained('categories'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('product_categories'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/seeders/AdminUserSeeder.php: -------------------------------------------------------------------------------- 1 | 'Admin', 20 | 'email' => 'admin@example.com', 21 | 'password' => bcrypt('admin123'), 22 | 'email_verified_at' => now(), 23 | 'is_admin' => true 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/seeders/CountrySeeder.php: -------------------------------------------------------------------------------- 1 | 'Alabama', 22 | "AK" => 'Alaska', 23 | "AZ" => 'Arizona', 24 | "AR" => 'Arkansas', 25 | "CA" => 'California', 26 | ]; 27 | $countries = [ 28 | ['code' => 'geo', 'name' => 'Georgia', 'states' => null], 29 | ['code' => 'ind', 'name' => 'India', 'states' => null], 30 | ['code' => 'usa', 'name' => 'United States of America', 'states' => json_encode($usaStates)], 31 | ['code' => 'ger', 'name' => 'Germany', 'states' => null], 32 | ]; 33 | Country::insert($countries); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call([ 18 | AdminUserSeeder::class, 19 | CountrySeeder::class 20 | ]); 21 | // \App\Models\User::factory(10)->create(); 22 | 23 | // \App\Models\User::factory()->create([ 24 | // 'name' => 'Test User', 25 | // 'email' => 'test@example.com', 26 | // ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/seeders/ProductSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite --port=3001", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@alpinejs/collapse": "^3.13.7", 10 | "@tailwindcss/aspect-ratio": "^0.4.2", 11 | "@tailwindcss/forms": "^0.5.7", 12 | "alpinejs": "^3.13.7", 13 | "autoprefixer": "^10.4.19", 14 | "axios": "^1.6.8", 15 | "laravel-vite-plugin": "^1.0", 16 | "lodash": "^4.17.21", 17 | "postcss": "^8.4.38", 18 | "tailwindcss": "^3.4.1", 19 | "vite": "^5.2.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources/css/app.css": { 3 | "file": "assets/app-ca70469a.css", 4 | "src": "resources/css/app.css", 5 | "isEntry": true 6 | }, 7 | "resources/js/app.js": { 8 | "file": "assets/app-e41bd908.js", 9 | "isEntry": true, 10 | "src": "resources/js/app.js" 11 | } 12 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeholic/laravel-vue-ecommerce/dd10302167419a04dfd486c55ffc6cfb797b5897/public/favicon.ico -------------------------------------------------------------------------------- /public/img/noimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeholic/laravel-vue-ecommerce/dd10302167419a04dfd486c55ffc6cfb797b5897/public/img/noimage.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeholic/laravel-vue-ecommerce/dd10302167419a04dfd486c55ffc6cfb797b5897/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=7049e92a398e816f8cd53a915eaea592", 3 | "/app-dark.css": "/app-dark.css?id=b11fa9a28e9d3aeb8c92986f319b3c44", 4 | "/app.css": "/app.css?id=b3ccfbe68f24cff776f83faa8dead721" 5 | } 6 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | .py-navbar-item { 7 | @apply py-6 8 | } 9 | 10 | .pt-navbar-item { 11 | @apply pt-6 12 | } 13 | 14 | .pb-navbar-item { 15 | @apply pb-6 16 | } 17 | 18 | .px-navbar-item { 19 | @apply px-3 20 | } 21 | 22 | .pl-navbar-item { 23 | @apply pl-3 24 | } 25 | 26 | .pr-navbar-item { 27 | @apply pr-3 28 | } 29 | } 30 | 31 | @layer components { 32 | .btn-primary { 33 | @apply text-white bg-purple-600 py-2 px-4 rounded shadow-md hover:bg-purple-700 active:bg-purple-800 transition-colors; 34 | } 35 | } 36 | 37 | body { 38 | background-color: #e5e7eb; 39 | } 40 | 41 | 42 | .wysiwyg-content > blockquote, 43 | .wysiwyg-content > dl, 44 | .wysiwyg-content > dd, 45 | .wysiwyg-content > h1, 46 | .wysiwyg-content > h2, 47 | .wysiwyg-content > h3, 48 | .wysiwyg-content > h4, 49 | .wysiwyg-content > h5, 50 | .wysiwyg-content > h6, 51 | .wysiwyg-content > hr, 52 | .wysiwyg-content > figure, 53 | .wysiwyg-content > p, 54 | .wysiwyg-content > pre { 55 | margin: revert; 56 | } 57 | 58 | .wysiwyg-content > ol, 59 | .wysiwyg-content > ul { 60 | list-style: revert; 61 | margin: revert; 62 | padding: revert; 63 | } 64 | 65 | .wysiwyg-content > table { 66 | border-collapse: revert; 67 | } 68 | 69 | .wysiwyg-content > h1, 70 | .wysiwyg-content > h2, 71 | .wysiwyg-content > h3, 72 | .wysiwyg-content > h4, 73 | .wysiwyg-content > h5, 74 | .wysiwyg-content > h6 { 75 | font-size: revert; 76 | font-weight: revert; 77 | } 78 | 79 | .table > tbody > tr > td, 80 | .table > thead > tr > th { 81 | padding: 0.5rem; 82 | } 83 | 84 | .table-sm > tbody > tr > td, 85 | .table-sm > thead > tr > th { 86 | padding: 0.25rem; 87 | } 88 | 89 | .category-list > .category-item:hover > .category-list { 90 | display: flex; 91 | } 92 | 93 | .category-list > .category-item > .category-list > .category-item > .category-list { 94 | left: 100%; 95 | top: 0; 96 | } 97 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | window._ = _; 3 | 4 | /** 5 | * We'll load the axios HTTP library which allows us to easily issue requests 6 | * to our Laravel back-end. This library automatically handles sending the 7 | * CSRF token as a header based on the value of the "XSRF" token cookie. 8 | */ 9 | 10 | import axios from 'axios'; 11 | window.axios = axios; 12 | 13 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 14 | 15 | /** 16 | * Echo exposes an expressive API for subscribing to channels and listening 17 | * for events that are broadcast by Laravel. Echo and event broadcasting 18 | * allows your team to easily build robust real-time web applications. 19 | */ 20 | 21 | // import Echo from 'laravel-echo'; 22 | 23 | // import Pusher from 'pusher-js'; 24 | // window.Pusher = Pusher; 25 | 26 | // window.Echo = new Echo({ 27 | // broadcaster: 'pusher', 28 | // key: import.meta.env.VITE_PUSHER_APP_KEY, 29 | // wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_CLUSTER}.pusher.com`, 30 | // wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, 31 | // wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, 32 | // forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', 33 | // enabledTransports: ['ws', 'wss'], 34 | // }); 35 | -------------------------------------------------------------------------------- /resources/js/http.js: -------------------------------------------------------------------------------- 1 | 2 | export function request(method, url, data = {}) { 3 | return fetch(url, { 4 | method, 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | 'Accept': 'application/json', 8 | 'X-CSRF-TOKEN': document.head.querySelector('meta[name=csrf-token]').content 9 | // 'Content-Type': 'application/x-www-form-urlencoded', 10 | }, 11 | ...(method === 'get' ? {}: {body: JSON.stringify(data)}) 12 | }).then(async (response) => { 13 | if (response.status >=200 && response.status <300) { 14 | return response.json() 15 | } 16 | throw await response.json(); 17 | }) 18 | } 19 | 20 | export function get(url) { 21 | return request('get', url) 22 | } 23 | 24 | export function post(url, data) { 25 | return request('post', url, data) 26 | } 27 | -------------------------------------------------------------------------------- /resources/views/auth/confirm-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 6 |
7 | 8 | 9 | 10 | 11 |
12 | @csrf 13 | 14 | 15 |
16 | 17 | 18 | 22 |
23 | 24 |
25 | 26 | {{ __('Confirm') }} 27 | 28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /resources/views/auth/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | @csrf 5 |

6 | Enter your Email to reset password 7 |

8 | 9 | 10 | 11 | 12 |

13 | or 14 | 18 | login with existing account 19 | 20 |

21 | 22 |
23 | 25 |
26 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /resources/views/auth/login.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Login to your account 5 |

6 |

7 | or 8 | 12 | create new account 13 | 14 |

15 | 16 | 17 | 18 | 19 | @csrf 20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | 33 | 34 |
35 | @if (Route::has('password.request')) 36 | 37 | Forgot Password? 38 | 39 | @endif 40 |
41 | 46 | 47 |
48 | -------------------------------------------------------------------------------- /resources/views/auth/register.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
7 | @csrf 8 | 9 |

Create an account

10 |

11 | or 12 | 16 | login with existing account 17 | 18 |

19 | 20 | @if (session('error')) 21 |
22 | {{ session('error') }} 23 |
24 | @endif 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 | 47 | 48 |
49 | -------------------------------------------------------------------------------- /resources/views/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

5 | Enter your new password 6 |

7 | 8 | 9 | 10 |
11 | @csrf 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 |
29 | 30 | 31 |
32 | 33 | 34 | 37 |
38 | 39 |
40 | 41 | {{ __('Reset Password') }} 42 | 43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /resources/views/auth/verify-email.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} 6 |
7 | 8 | @if (session('status') == 'verification-link-sent') 9 |
10 | {{ __('A new verification link has been sent to the email address you provided during registration.') }} 11 |
12 | @endif 13 | 14 |
15 |
16 | @csrf 17 | 18 |
19 | 20 | {{ __('Resend Verification Email') }} 21 | 22 |
23 |
24 | 25 |
26 | @csrf 27 | 28 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /resources/views/checkout/failure.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Your payment was not successful!!

4 |

{{$message ?? ''}}

5 |
6 |
7 | -------------------------------------------------------------------------------- /resources/views/checkout/success.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{$customer->name}}, Your order has been completed!! 4 |
5 |
6 | -------------------------------------------------------------------------------- /resources/views/components/auth-card.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ $logo }} 4 |
5 | 6 |
7 | {{ $slot }} 8 |
9 |
10 | -------------------------------------------------------------------------------- /resources/views/components/auth-session-status.blade.php: -------------------------------------------------------------------------------- 1 | @props(['status']) 2 | 3 | @if ($status) 4 |
merge(['class' => 'font-medium text-sm bg-emerald-500 py-3 px-4 text-white rounded']) }}> 5 | {{ $status }} 6 |
7 | @endif 8 | -------------------------------------------------------------------------------- /resources/views/components/auth-validation-errors.blade.php: -------------------------------------------------------------------------------- 1 | @props(['errors']) 2 | 3 | @if ($errors->any()) 4 |
merge(['class' => 'p-3 rounded-md bg-red-600 text-white']) }}> 5 |
6 | {{ __('Whoops! Something went wrong.') }} 7 |
8 | 9 |
    10 | @foreach ($errors->all() as $error) 11 |
  • {{ $error }}
  • 12 | @endforeach 13 |
14 |
15 | @endif 16 | -------------------------------------------------------------------------------- /resources/views/components/button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/category-list.blade.php: -------------------------------------------------------------------------------- 1 | @props(['categoryList']) 2 | 3 |
merge(['class' => 'category-list flex text-white bg-slate-700']) }}> 4 | @if (!empty($categoryList)) 5 | @foreach($categoryList as $category) 6 | 12 | @endforeach 13 | @endif 14 |
15 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-link.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) 2 | 3 | @php 4 | switch ($align) { 5 | case 'left': 6 | $alignmentClasses = 'origin-top-left left-0'; 7 | break; 8 | case 'top': 9 | $alignmentClasses = 'origin-top'; 10 | break; 11 | case 'right': 12 | default: 13 | $alignmentClasses = 'origin-top-right right-0'; 14 | break; 15 | } 16 | 17 | switch ($width) { 18 | case '48': 19 | $width = 'w-48'; 20 | break; 21 | } 22 | @endphp 23 | 24 |
25 |
26 | {{ $trigger }} 27 |
28 | 29 | 43 |
44 | -------------------------------------------------------------------------------- /resources/views/components/input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false, 'errors', 'type' => 'text', 'label' => false]) 2 | 3 | 7 | 14 |
15 | @if ($label) 16 | 17 | @endif 18 | @if ($type === 'select') 19 | 25 | @else 26 | merge([ 27 | 'class' => 'border-gray-300 focus:border-purple-500 focus:outline-none focus:ring-purple-500 rounded-md w-full ' . 28 | ($errors->has($attributeName) ? $errorClasses : (old($attributeName) ? $successClasses :$defaultClasses)) 29 | ]) !!}> 30 | @endif 31 | @error($attributeName) 32 | {{ $message }} 33 | @enderror 34 |
35 | -------------------------------------------------------------------------------- /resources/views/components/label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /resources/views/components/nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'block pl-3 pr-4 py-2 border-l-4 border-indigo-400 text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Dashboard') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 | You're logged in! 13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel E-commerce Website') }} 9 | 10 | 11 | @vite(['resources/css/app.css', 'resources/js/app.js']) 12 | 17 | 18 | 19 | @include('layouts.navigation') 20 | 21 |
22 | {{ $slot }} 23 |
24 | 25 | 26 |
35 |
36 | 55 | 56 |
57 |
61 |
62 |
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /resources/views/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | @vite(['resources/css/app.css', 'resources/js/app.js']) 15 | 16 | 17 |
18 | {{ $slot }} 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/views/mail/new-order.blade.php: -------------------------------------------------------------------------------- 1 |

2 | New order has been created 3 |

4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Order ID 9 | 10 | {{$order->id}} 11 | 12 |
Order Status{{ $order->status }}
Order Price${{$order->total_price}}
Order Date${{$order->created_at}}
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | @foreach($order->items as $item) 35 | 36 | 39 | 40 | 41 | 42 | 43 | @endforeach 44 |
ImageTitlePriceQuantity
37 | 38 | {{$item->product->title}}${{$item->unit_price * $item->quantity}}{{$item->quantity}}
45 | -------------------------------------------------------------------------------- /resources/views/mail/update-order.blade.php: -------------------------------------------------------------------------------- 1 |

2 | Your order status was changed into "{{$order->status}}" 3 |

4 |

5 | Link to your order: 6 | Order #{{$order->id}} 7 |

8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @if (trim($slot) === 'Laravel') 5 | 6 | @else 7 | {{ $slot }} 8 | @endif 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | 27 | 28 | 29 | 30 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.name') }} 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | @endcomponent 18 | @endslot 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | @slot('footer') 23 | @component('mail::footer') 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | @endcomponent 26 | @endslot 27 | @endcomponent 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/layout.blade.php: -------------------------------------------------------------------------------- 1 | {!! strip_tags($header) !!} 2 | 3 | {!! strip_tags($slot) !!} 4 | @isset($subcopy) 5 | 6 | {!! strip_tags($subcopy) !!} 7 | @endisset 8 | 9 | {!! strip_tags($footer) !!} 10 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.name') }} 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | @endcomponent 18 | @endslot 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | @slot('footer') 23 | @component('mail::footer') 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | @endcomponent 26 | @endslot 27 | @endcomponent 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | Test 3 | 4 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 14 | Route::get('register', [RegisteredUserController::class, 'create']) 15 | ->name('register'); 16 | 17 | Route::post('register', [RegisteredUserController::class, 'store']); 18 | 19 | Route::get('login', [AuthenticatedSessionController::class, 'create']) 20 | ->name('login'); 21 | 22 | Route::post('login', [AuthenticatedSessionController::class, 'store']); 23 | 24 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) 25 | ->name('password.request'); 26 | 27 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 28 | ->name('password.email'); 29 | 30 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) 31 | ->name('password.reset'); 32 | 33 | Route::post('reset-password', [NewPasswordController::class, 'store']) 34 | ->name('password.update'); 35 | }); 36 | 37 | Route::middleware('auth')->group(function () { 38 | Route::get('verify-email', [EmailVerificationPromptController::class, '__invoke']) 39 | ->name('verification.notice'); 40 | 41 | Route::get('verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke']) 42 | ->middleware(['signed', 'throttle:6,1']) 43 | ->name('verification.verify'); 44 | 45 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 46 | ->middleware('throttle:6,1') 47 | ->name('verification.send'); 48 | 49 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) 50 | ->name('password.confirm'); 51 | 52 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); 53 | 54 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 55 | ->name('logout'); 56 | }); 57 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function () { 22 | Route::get('/', [ProductController::class, 'index'])->name('home'); 23 | Route::get('/category/{category:slug}', [ProductController::class, 'byCategory'])->name('byCategory'); 24 | Route::get('/product/{product:slug}', [ProductController::class, 'view'])->name('product.view'); 25 | 26 | Route::prefix('/cart')->name('cart.')->group(function () { 27 | Route::get('/', [CartController::class, 'index'])->name('index'); 28 | Route::post('/add/{product:slug}', [CartController::class, 'add'])->name('add'); 29 | Route::post('/remove/{product:slug}', [CartController::class, 'remove'])->name('remove'); 30 | Route::post('/update-quantity/{product:slug}', [CartController::class, 'updateQuantity'])->name('update-quantity'); 31 | }); 32 | }); 33 | 34 | Route::middleware(['auth', 'verified'])->group(function() { 35 | Route::get('/profile', [ProfileController::class, 'view'])->name('profile'); 36 | Route::post('/profile', [ProfileController::class, 'store'])->name('profile.update'); 37 | Route::post('/profile/password-update', [ProfileController::class, 'passwordUpdate'])->name('profile_password.update'); 38 | Route::post('/checkout', [CheckoutController::class, 'checkout'])->name('cart.checkout'); 39 | Route::post('/checkout/{order}', [CheckoutController::class, 'checkoutOrder'])->name('cart.checkout-order'); 40 | Route::get('/checkout/success', [CheckoutController::class, 'success'])->name('checkout.success'); 41 | Route::get('/checkout/failure', [CheckoutController::class, 'failure'])->name('checkout.failure'); 42 | Route::get('/orders', [OrderController::class, 'index'])->name('order.index'); 43 | Route::get('/orders/{order}', [OrderController::class, 'view'])->name('order.view'); 44 | }); 45 | 46 | Route::post('/webhook/stripe', [CheckoutController::class, 'webhook']); 47 | 48 | require __DIR__ . '/auth.php'; 49 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 7 | './storage/framework/views/*.php', 8 | './resources/views/**/*.blade.php', 9 | ], 10 | 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ['Nunito', ...defaultTheme.fontFamily.sans], 15 | }, 16 | }, 17 | }, 18 | 19 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/aspect-ratio')], 20 | }; 21 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 17 | 18 | $response->assertStatus(200); 19 | } 20 | 21 | public function test_users_can_authenticate_using_the_login_screen() 22 | { 23 | $user = User::factory()->create(); 24 | 25 | $response = $this->post('/login', [ 26 | 'email' => $user->email, 27 | 'password' => 'password', 28 | ]); 29 | 30 | $this->assertAuthenticated(); 31 | $response->assertRedirect(RouteServiceProvider::HOME); 32 | } 33 | 34 | public function test_users_can_not_authenticate_with_invalid_password() 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $this->post('/login', [ 39 | 'email' => $user->email, 40 | 'password' => 'wrong-password', 41 | ]); 42 | 43 | $this->assertGuest(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | create([ 20 | 'email_verified_at' => null, 21 | ]); 22 | 23 | $response = $this->actingAs($user)->get('/verify-email'); 24 | 25 | $response->assertStatus(200); 26 | } 27 | 28 | public function test_email_can_be_verified() 29 | { 30 | $user = User::factory()->create([ 31 | 'email_verified_at' => null, 32 | ]); 33 | 34 | Event::fake(); 35 | 36 | $verificationUrl = URL::temporarySignedRoute( 37 | 'verification.verify', 38 | now()->addMinutes(60), 39 | ['id' => $user->id, 'hash' => sha1($user->email)] 40 | ); 41 | 42 | $response = $this->actingAs($user)->get($verificationUrl); 43 | 44 | Event::assertDispatched(Verified::class); 45 | $this->assertTrue($user->fresh()->hasVerifiedEmail()); 46 | $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1'); 47 | } 48 | 49 | public function test_email_is_not_verified_with_invalid_hash() 50 | { 51 | $user = User::factory()->create([ 52 | 'email_verified_at' => null, 53 | ]); 54 | 55 | $verificationUrl = URL::temporarySignedRoute( 56 | 'verification.verify', 57 | now()->addMinutes(60), 58 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 59 | ); 60 | 61 | $this->actingAs($user)->get($verificationUrl); 62 | 63 | $this->assertFalse($user->fresh()->hasVerifiedEmail()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this->actingAs($user)->get('/confirm-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_password_can_be_confirmed() 23 | { 24 | $user = User::factory()->create(); 25 | 26 | $response = $this->actingAs($user)->post('/confirm-password', [ 27 | 'password' => 'password', 28 | ]); 29 | 30 | $response->assertRedirect(); 31 | $response->assertSessionHasNoErrors(); 32 | } 33 | 34 | public function test_password_is_not_confirmed_with_invalid_password() 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this->actingAs($user)->post('/confirm-password', [ 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $response->assertSessionHasErrors(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_reset_password_link_can_be_requested() 23 | { 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); 31 | } 32 | 33 | public function test_reset_password_screen_can_be_rendered() 34 | { 35 | Notification::fake(); 36 | 37 | $user = User::factory()->create(); 38 | 39 | $this->post('/forgot-password', ['email' => $user->email]); 40 | 41 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 42 | $response = $this->get('/reset-password/'.$notification->token); 43 | 44 | $response->assertStatus(200); 45 | 46 | return true; 47 | }); 48 | } 49 | 50 | public function test_password_can_be_reset_with_valid_token() 51 | { 52 | Notification::fake(); 53 | 54 | $user = User::factory()->create(); 55 | 56 | $this->post('/forgot-password', ['email' => $user->email]); 57 | 58 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 59 | $response = $this->post('/reset-password', [ 60 | 'token' => $notification->token, 61 | 'email' => $user->email, 62 | 'password' => 'password', 63 | 'password_confirmation' => 'password', 64 | ]); 65 | 66 | $response->assertSessionHasNoErrors(); 67 | 68 | return true; 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | 20 | public function test_new_users_can_register() 21 | { 22 | $response = $this->post('/register', [ 23 | 'name' => 'Test User', 24 | 'email' => 'test@example.com', 25 | 'password' => 'password', 26 | 'password_confirmation' => 'password', 27 | ]); 28 | 29 | $this->assertAuthenticated(); 30 | $response->assertRedirect(RouteServiceProvider::HOME); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | laravel({ 7 | input: [ 8 | 'resources/css/app.css', 9 | 'resources/js/app.js', 10 | ], 11 | refresh: true, 12 | }), 13 | ], 14 | }); 15 | --------------------------------------------------------------------------------