├── public ├── favicon.ico ├── robots.txt ├── vendor │ └── telescope │ │ ├── favicon.ico │ │ └── mix-manifest.json ├── build │ └── manifest.json ├── .htaccess └── index.php ├── database ├── .gitignore ├── seeders │ ├── ProductSeeder.php │ ├── AdminUserSeeder.php │ ├── DatabaseSeeder.php │ └── CountrySeeder.php ├── migrations │ ├── 2022_07_09_004342_create_countries_table.php │ ├── 2022_07_11_043258_add_is_admin_column_to_users_table.php │ ├── 2022_09_11_142434_rename_customer_id_column.php │ ├── 2022_10_01_142356_add_session_id_to_payments_table.php │ ├── 2022_10_09_171628_add_published_column_to_products.php │ ├── 2023_02_26_194708_add_expires_at_column_to_personal_access_tokens.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2022_07_09_004403_create_cart_items_table.php │ ├── 2022_09_17_025414_change_countries_states_column_into_json.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2022_11_28_194929_update_payments_order_id.php │ ├── 2022_11_28_194915_update_order_items_order_id.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2022_07_09_004430_create_order_items_table.php │ ├── 2022_07_09_004135_create_orders_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2022_07_09_004505_create_customers_table.php │ ├── 2022_07_09_004446_create_payments_table.php │ ├── 2022_07_09_004417_create_order_details_table.php │ ├── 2022_07_09_004515_create_customer_addresses_table.php │ └── 2022_07_09_004121_create_products_table.php └── factories │ ├── ProductFactory.php │ └── UserFactory.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore ├── debugbar │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── backend ├── .env.example ├── .env.production ├── .vscode │ └── extensions.json ├── src │ ├── index.css │ ├── assets │ │ └── logo.png │ ├── constants.js │ ├── App.vue │ ├── filters │ │ └── currency.js │ ├── store │ │ ├── index.js │ │ ├── state.js │ │ └── mutations.js │ ├── main.js │ ├── views │ │ ├── Orders │ │ │ ├── OrderStatus.vue │ │ │ └── Orders.vue │ │ ├── Reports │ │ │ ├── OrdersReport.vue │ │ │ ├── CustomersReport.vue │ │ │ └── Report.vue │ │ ├── NotFound.vue │ │ ├── Customers │ │ │ └── Customers.vue │ │ ├── Users │ │ │ └── Users.vue │ │ ├── Products │ │ │ └── Products.vue │ │ ├── RequestPassword.vue │ │ └── ResetPassword.vue │ ├── components │ │ ├── GuestLayout.vue │ │ ├── core │ │ │ ├── Spinner.vue │ │ │ ├── Table │ │ │ │ └── TableHeaderCell.vue │ │ │ ├── Charts │ │ │ │ ├── Bar.vue │ │ │ │ ├── Line.vue │ │ │ │ └── Doughnut.vue │ │ │ └── Toast.vue │ │ ├── AppLayout.vue │ │ └── Sidebar.vue │ └── axios.js ├── public │ └── favicon.ico ├── postcss.config.js ├── vite.config.js ├── .gitignore ├── README.md ├── index.html ├── tailwind.config.js └── package.json ├── resources ├── views │ ├── vendor │ │ └── mail │ │ │ ├── text │ │ │ ├── footer.blade.php │ │ │ ├── panel.blade.php │ │ │ ├── subcopy.blade.php │ │ │ ├── table.blade.php │ │ │ ├── button.blade.php │ │ │ ├── header.blade.php │ │ │ ├── layout.blade.php │ │ │ └── message.blade.php │ │ │ └── html │ │ │ ├── table.blade.php │ │ │ ├── subcopy.blade.php │ │ │ ├── footer.blade.php │ │ │ ├── header.blade.php │ │ │ ├── panel.blade.php │ │ │ ├── button.blade.php │ │ │ ├── message.blade.php │ │ │ └── layout.blade.php │ ├── welcome.blade.php │ ├── components │ │ ├── label.blade.php │ │ ├── button.blade.php │ │ ├── dropdown-link.blade.php │ │ ├── auth-session-status.blade.php │ │ ├── auth-card.blade.php │ │ ├── auth-validation-errors.blade.php │ │ ├── nav-link.blade.php │ │ ├── responsive-nav-link.blade.php │ │ ├── dropdown.blade.php │ │ └── input.blade.php │ ├── checkout │ │ ├── success.blade.php │ │ └── failure.blade.php │ ├── mail │ │ ├── update-order.blade.php │ │ └── new-order.blade.php │ ├── dashboard.blade.php │ ├── layouts │ │ ├── guest.blade.php │ │ └── app.blade.php │ ├── auth │ │ ├── confirm-password.blade.php │ │ ├── forgot-password.blade.php │ │ ├── verify-email.blade.php │ │ ├── register.blade.php │ │ ├── reset-password.blade.php │ │ └── login.blade.php │ └── product │ │ └── index.blade.php ├── js │ ├── http.js │ └── bootstrap.js └── css │ └── app.css ├── app ├── Models │ ├── Api │ │ ├── User.php │ │ └── Product.php │ ├── Country.php │ ├── OrderDetail.php │ ├── CartItem.php │ ├── Payment.php │ ├── OrderItem.php │ ├── CustomerAddress.php │ ├── Product.php │ ├── Customer.php │ ├── Order.php │ └── User.php ├── Enums │ ├── AddressType.php │ ├── CustomerStatus.php │ ├── PaymentStatus.php │ └── OrderStatus.php ├── Http │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrustHosts.php │ │ ├── TrimStrings.php │ │ ├── Authenticate.php │ │ ├── TrustProxies.php │ │ ├── GuestOrVerified.php │ │ ├── Admin.php │ │ └── RedirectIfAuthenticated.php │ ├── Controllers │ │ ├── Controller.php │ │ ├── ProductController.php │ │ ├── Auth │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── VerifyEmailController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── AuthenticatedSessionController.php │ │ │ └── RegisteredUserController.php │ │ ├── OrderController.php │ │ ├── Api │ │ │ ├── OrderController.php │ │ │ └── AuthController.php │ │ └── ReportController.php │ ├── Resources │ │ ├── CountryResource.php │ │ ├── UserResource.php │ │ ├── ProductListResource.php │ │ ├── Dashboard │ │ │ └── OrderResource.php │ │ ├── CustomerListResource.php │ │ ├── ProductResource.php │ │ ├── OrderListResource.php │ │ └── CustomerResource.php │ └── Requests │ │ ├── PasswordUpdateRequest.php │ │ ├── UpdateUserRequest.php │ │ ├── CreateUserRequest.php │ │ ├── ProductRequest.php │ │ ├── ProfileRequest.php │ │ └── CustomerRequest.php ├── View │ └── Components │ │ ├── AppLayout.php │ │ └── GuestLayout.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ ├── RouteServiceProvider.php │ └── TelescopeServiceProvider.php ├── Traits │ └── ReportTrait.php ├── Mail │ ├── NewOrderEmail.php │ └── OrderUpdateEmail.php ├── Console │ ├── Kernel.php │ └── Commands │ │ └── DeleteUnpaidOrders.php └── Exceptions │ └── Handler.php ├── postcss.config.js ├── .gitattributes ├── tests ├── TestCase.php ├── Unit │ └── ExampleTest.php ├── Feature │ ├── ExampleTest.php │ └── Auth │ │ ├── RegistrationTest.php │ │ ├── AuthenticationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── EmailVerificationTest.php │ │ └── PasswordResetTest.php └── CreatesApplication.php ├── .styleci.yml ├── .gitignore ├── .editorconfig ├── vite.config.js ├── package.json ├── lang └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── routes ├── channels.php ├── console.php └── web.php ├── tailwind.config.js ├── config ├── cors.php ├── services.php ├── view.php ├── hashing.php ├── broadcasting.php └── sanctum.php ├── README.md ├── phpunit.xml ├── .env.example ├── artisan └── composer.json /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL = http://localhost:8000 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /backend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL = https://lcommerce.net 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /backend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | 2 | Test 3 | 4 | -------------------------------------------------------------------------------- /backend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /backend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midas95/Laravel-ecommerce/HEAD/backend/public/favicon.ico -------------------------------------------------------------------------------- /backend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midas95/Laravel-ecommerce/HEAD/backend/src/assets/logo.png -------------------------------------------------------------------------------- /app/Models/Api/User.php: -------------------------------------------------------------------------------- 1 | 2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 | 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /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/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/components/label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /resources/views/components/button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/Models/Api/Product.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ Illuminate\Mail\Markdown::parse($slot) }} 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/checkout/success.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{$customer->name}}, Your order has been completed!! 4 |
5 |
6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /app/Models/Country.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Your payment was not successful!!

4 |

{{$message ?? ''}}

5 |
6 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Models/CartItem.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/components/auth-card.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ $logo }} 4 |
5 | 6 |
7 | {{ $slot }} 8 |
9 |
10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/View/Components/AppLayout.php: -------------------------------------------------------------------------------- 1 | ` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) 8 | -------------------------------------------------------------------------------- /backend/src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import store from './store' 3 | import router from './router' 4 | import './index.css'; 5 | import currencyUSD from './filters/currency.js' 6 | 7 | import App from './App.vue' 8 | 9 | const app = createApp(App); 10 | 11 | app 12 | .use(store) 13 | .use(router) 14 | .mount('#app') 15 | ; 16 | 17 | app.config.globalProperties.$filters = { 18 | currencyUSD 19 | } 20 | -------------------------------------------------------------------------------- /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/Controller.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | '/webhook/stripe', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /backend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /database/seeders/ProductSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | 2 | {{ order.status }} 8 | 9 | 10 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /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 | 14 |
15 | @endif 16 | -------------------------------------------------------------------------------- /app/Models/Payment.php: -------------------------------------------------------------------------------- 1 | hasOne(Order::class, 'id', 'order_id'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/views/Orders/Orders.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /backend/src/components/GuestLayout.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build" 6 | }, 7 | "devDependencies": { 8 | "@alpinejs/collapse": "^3.10.3", 9 | "@tailwindcss/aspect-ratio": "^0.4.0", 10 | "@tailwindcss/forms": "^0.5.2", 11 | "alpinejs": "^3.4.2", 12 | "autoprefixer": "^10.4.2", 13 | "axios": "^0.25", 14 | "laravel-vite-plugin": "^0.2.1", 15 | "lodash": "^4.17.19", 16 | "postcss": "^8.4.6", 17 | "tailwindcss": "^3.1.0", 18 | "vite": "^2.9.11" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Models/OrderItem.php: -------------------------------------------------------------------------------- 1 | belongsTo(Order::class); 18 | } 19 | 20 | public function product(): BelongsTo 21 | { 22 | return $this->belongsTo(Product::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProductController.php: -------------------------------------------------------------------------------- 1 | where('published', '=', 1) 14 | ->orderBy('updated_at', 'desc') 15 | ->paginate(5); 16 | return view('product.index', [ 17 | 'products' => $products 18 | ]); 19 | } 20 | 21 | public function view(Product $product) 22 | { 23 | return view('product.view', ['product' => $product]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/views/Reports/OrdersReport.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /backend/src/views/Reports/CustomersReport.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 20 | ? redirect()->intended(RouteServiceProvider::HOME) 21 | : view('auth.verify-email'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.27.2", 12 | "chart.js": "^3.9.1", 13 | "vue": "^3.2.25", 14 | "vue-chartjs": "^4.1.2", 15 | "vue-router": "^4.0.13", 16 | "vuex": "^4.0.2" 17 | }, 18 | "devDependencies": { 19 | "@headlessui/vue": "^1.6.6", 20 | "@heroicons/vue": "^1.0.6", 21 | "@tailwindcss/forms": "^0.5.2", 22 | "@vitejs/plugin-vue": "^2.3.3", 23 | "autoprefixer": "^10.4.7", 24 | "postcss": "^8.4.14", 25 | "tailwindcss": "^3.1.5", 26 | "vite": "^2.9.9" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /app/Mail/NewOrderEmail.php: -------------------------------------------------------------------------------- 1 | subject('New Order') 34 | ->view('mail.new-order'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Middleware/GuestOrVerified.php: -------------------------------------------------------------------------------- 1 | user()) { 20 | return $next($request); 21 | } 22 | return parent::handle($request, $next, $redirectToRoute); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 19 | } 20 | 21 | /** 22 | * Register the commands for the application. 23 | * 24 | * @return void 25 | */ 26 | protected function commands() 27 | { 28 | $this->load(__DIR__.'/Commands'); 29 | 30 | require base_path('routes/console.php'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Mail/OrderUpdateEmail.php: -------------------------------------------------------------------------------- 1 | subject('Order Status was updated') 34 | ->view('mail.update-order'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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_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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/Resources/ProductListResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 21 | 'title' => $this->title, 22 | 'image_url' => $this->image ?: null, 23 | 'price' => $this->price, 24 | 'updated_at' => (new \DateTime($this->updated_at))->format('Y-m-d H:i:s'), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Requests/ProductRequest.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function rules() 25 | { 26 | return [ 27 | 'title' => ['required', 'max:2000'], 28 | 'image' => ['nullable', 'image'], 29 | 'price' => ['required', 'numeric'], 30 | 'description' => ['nullable', 'string'], 31 | 'published' => ['required', 'boolean'] 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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/Models/Product.php: -------------------------------------------------------------------------------- 1 | generateSlugsFrom('title') 26 | ->saveSlugsTo('slug'); 27 | } 28 | 29 | public function getRouteKeyName() 30 | { 31 | return 'slug'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Http/Controllers/OrderController.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 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /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/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/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/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/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/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_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 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 26 | return redirect(RouteServiceProvider::HOME); 27 | } 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/views/Customers/Customers.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /app/Models/Customer.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/components/core/Spinner.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 41 | 42 | 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | // 31 | } 32 | 33 | /** 34 | * Determine if events and listeners should be automatically discovered. 35 | * 36 | * @return bool 37 | */ 38 | public function shouldDiscoverEvents() 39 | { 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /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 ?: null, 27 | 'price' => $this->price, 28 | 'published' => (bool)$this->published, 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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | .wysiwyg-content h3, 42 | .wysiwyg-content h4 { 43 | font-weight: 600; 44 | margin-top: 1rem; 45 | } 46 | 47 | .wysiwyg-content table > tbody > tr > td { 48 | padding: 0.25rem 0.125rem; 49 | } 50 | 51 | .wysiwyg-content table > tbody > tr > td:first-child { 52 | font-weight: bold; 53 | } 54 | 55 | .table > tbody > tr > td, 56 | .table > thead > tr > th { 57 | padding: 0.5rem; 58 | } 59 | 60 | .table-sm > tbody > tr > td, 61 | .table-sm > thead > tr > th { 62 | padding: 0.25rem; 63 | } 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel E-commerce Website 2 | E-commerce application built with Laravel, Vue.js, Tailwind.css and Alpine.js.
3 | 4 | ## Installation 5 | Make sure you have environment setup properly. You will need MySQL, PHP8.1, Node.js and composer. 6 | 7 | ### Install Laravel Website + API 8 | 1. Download the project (or clone using GIT) 9 | 2. Copy `.env.example` into `.env` and configure database credentials 10 | 3. Navigate to the project's root directory using terminal 11 | 4. Run `composer install` 12 | 5. Set the encryption key by executing `php artisan key:generate --ansi` 13 | 6. Run migrations `php artisan migrate --seed` 14 | 7. Start local server by executing `php artisan serve` 15 | 8. Open new terminal and navigate to the project root directory 16 | 9. Run `npm install` 17 | 10. Run `npm run dev` to start vite server for Laravel frontend 18 | 19 | ### Install Vue.js Admin Panel 20 | 1. Navigate to `backend` folder 21 | 2. Run `npm install` 22 | 3. Copy `backend/.env.example` into `backend/.env` 23 | 4. Make sure `VITE_API_BASE_URL` key in `backend/.env` is set to your Laravel API host (Default: http://localhost:8000) 24 | 5. Run `npm run dev` 25 | 6. Open Vue.js Admin Panel in browser and login with 26 | ``` 27 | admin@example.com 28 | admin123 29 | ``` 30 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 14 | */ 15 | protected $levels = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * A list of the exception types that are not reported. 21 | * 22 | * @var array> 23 | */ 24 | protected $dontReport = [ 25 | // 26 | ]; 27 | 28 | /** 29 | * A list of the inputs that are never flashed to the session on validation exceptions. 30 | * 31 | * @var array 32 | */ 33 | protected $dontFlash = [ 34 | 'current_password', 35 | 'password', 36 | 'password_confirmation', 37 | ]; 38 | 39 | /** 40 | * Register the exception handling callbacks for the application. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->reportable(function (Throwable $e) { 47 | // 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /backend/src/components/core/Table/TableHeaderCell.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 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 | -------------------------------------------------------------------------------- /backend/src/views/Users/Users.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | 48 | 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/components/core/Charts/Bar.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /backend/src/components/core/Charts/Line.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /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 | 21 | 22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 | 36 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 30 | 31 | $this->routes(function () { 32 | Route::middleware('api') 33 | ->prefix('api') 34 | ->group(base_path('routes/api.php')); 35 | 36 | Route::middleware('web') 37 | ->group(base_path('routes/web.php')); 38 | }); 39 | } 40 | 41 | /** 42 | * Configure the rate limiters for the application. 43 | * 44 | * @return void 45 | */ 46 | protected function configureRateLimiting() 47 | { 48 | RateLimiter::for('api', function (Request $request) { 49 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /backend/src/views/Products/Products.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 50 | 51 | 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/views/Reports/Report.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /backend/src/components/core/Charts/Doughnut.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/components/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 56 | 57 | 60 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/OrderController.php: -------------------------------------------------------------------------------- 1 | withCount('items') 32 | ->with('user.customer') 33 | ->where('id', 'like', "%{$search}%") 34 | ->orderBy($sortField, $sortDirection) 35 | ->paginate($perPage); 36 | 37 | return OrderListResource::collection($query); 38 | } 39 | 40 | public function view(Order $order) 41 | { 42 | $order->load('items.product'); 43 | return new OrderResource($order); 44 | } 45 | 46 | public function getStatuses() 47 | { 48 | return OrderStatus::getStatuses(); 49 | } 50 | 51 | public function changeStatus(Order $order, $status) 52 | { 53 | $order->status = $status; 54 | $order->save(); 55 | 56 | Mail::to($order->user)->send(new OrderUpdateEmail($order)); 57 | 58 | return response('', 200); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/views/RequestPassword.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 39 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |
34 |
35 | 54 | 55 |
56 |
60 |
61 |
62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /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/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 39 | 'name' => ['required', 'string', 'max:255'], 40 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 41 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 42 | ]); 43 | 44 | $user = User::create([ 45 | 'name' => $request->name, 46 | 'email' => $request->email, 47 | 'password' => Hash::make($request->password), 48 | ]); 49 | 50 | event(new Registered($user)); 51 | 52 | $customer = new Customer(); 53 | $names = explode(" ",$user->name); 54 | $customer->user_id = $user->id; 55 | $customer->first_name = $names[0]; 56 | $customer->last_name = $names[1] ?? ''; 57 | $customer->save(); 58 | 59 | Auth::login($user); 60 | 61 | Cart::moveCartItemsIntoDb(); 62 | 63 | return redirect(RouteServiceProvider::HOME); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/components/core/Toast.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 81 | 82 | 85 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": [ 6 | "framework", 7 | "laravel" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": "^8.1", 12 | "doctrine/dbal": "^3.4", 13 | "guzzlehttp/guzzle": "^7.2", 14 | "laravel/framework": "^10.0", 15 | "laravel/sanctum": "^3.2", 16 | "laravel/tinker": "^2.7", 17 | "spatie/laravel-sluggable": "^3.4", 18 | "stripe/stripe-php": "^9.6" 19 | }, 20 | "require-dev": { 21 | "barryvdh/laravel-debugbar": "^3.8", 22 | "fakerphp/faker": "^1.9.1", 23 | "laravel/breeze": "^1.11", 24 | "laravel/sail": "^1.0.1", 25 | "laravel/telescope": "^4.14", 26 | "mockery/mockery": "^1.4.4", 27 | "nunomaduro/collision": "^6.1", 28 | "phpunit/phpunit": "^9.5.10", 29 | "spatie/laravel-ignition": "^2.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "App\\": "app/", 34 | "Database\\Factories\\": "database/factories/", 35 | "Database\\Seeders\\": "database/seeders/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Tests\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "post-autoload-dump": [ 45 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 46 | "@php artisan package:discover --ansi" 47 | ], 48 | "post-update-cmd": [ 49 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 50 | ], 51 | "post-root-package-install": [ 52 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 53 | ], 54 | "post-create-project-cmd": [ 55 | "@php artisan key:generate --ansi" 56 | ] 57 | }, 58 | "extra": { 59 | "laravel": { 60 | "dont-discover": [ 61 | "laravel/telescope" 62 | ] 63 | } 64 | }, 65 | "config": { 66 | "optimize-autoloader": true, 67 | "preferred-install": "dist", 68 | "sort-packages": true 69 | }, 70 | "minimum-stability": "stable", 71 | "prefer-stable": true 72 | } 73 | -------------------------------------------------------------------------------- /backend/src/views/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 45 | -------------------------------------------------------------------------------- /backend/src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'host' => env('PUSHER_HOST', 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 40 | 'port' => env('PUSHER_PORT', 443), 41 | 'scheme' => env('PUSHER_SCHEME', 'https'), 42 | 'encrypted' => true, 43 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 44 | ], 45 | 'client_options' => [ 46 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 47 | ], 48 | ], 49 | 50 | 'ably' => [ 51 | 'driver' => 'ably', 52 | 'key' => env('ABLY_KEY'), 53 | ], 54 | 55 | 'redis' => [ 56 | 'driver' => 'redis', 57 | 'connection' => 'default', 58 | ], 59 | 60 | 'log' => [ 61 | 'driver' => 'log', 62 | ], 63 | 64 | 'null' => [ 65 | 'driver' => 'null', 66 | ], 67 | 68 | ], 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /backend/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | 2 | export function setUser(state, user) { 3 | state.user.data = user; 4 | } 5 | 6 | export function setToken(state, token) { 7 | state.user.token = token; 8 | if (token) { 9 | sessionStorage.setItem('TOKEN', token); 10 | } else { 11 | sessionStorage.removeItem('TOKEN') 12 | } 13 | } 14 | 15 | export function setProducts(state, [loading, data = null]) { 16 | 17 | if (data) { 18 | state.products = { 19 | ...state.products, 20 | data: data.data, 21 | links: data.meta?.links, 22 | page: data.meta.current_page, 23 | limit: data.meta.per_page, 24 | from: data.meta.from, 25 | to: data.meta.to, 26 | total: data.meta.total, 27 | } 28 | } 29 | state.products.loading = loading; 30 | } 31 | 32 | export function setUsers(state, [loading, data = null]) { 33 | 34 | if (data) { 35 | state.users = { 36 | ...state.users, 37 | data: data.data, 38 | links: data.meta?.links, 39 | page: data.meta.current_page, 40 | limit: data.meta.per_page, 41 | from: data.meta.from, 42 | to: data.meta.to, 43 | total: data.meta.total, 44 | } 45 | } 46 | state.products.loading = loading; 47 | } 48 | 49 | export function setCustomers(state, [loading, data = null]) { 50 | 51 | if (data) { 52 | state.customers = { 53 | ...state.customers, 54 | data: data.data, 55 | links: data.meta?.links, 56 | page: data.meta.current_page, 57 | limit: data.meta.per_page, 58 | from: data.meta.from, 59 | to: data.meta.to, 60 | total: data.meta.total, 61 | } 62 | } 63 | state.products.loading = loading; 64 | } 65 | 66 | export function setOrders(state, [loading, data = null]) { 67 | 68 | if (data) { 69 | state.orders = { 70 | ...state.orders, 71 | data: data.data, 72 | links: data.meta?.links, 73 | page: data.meta.current_page, 74 | limit: data.meta.per_page, 75 | from: data.meta.from, 76 | to: data.meta.to, 77 | total: data.meta.total, 78 | } 79 | } 80 | state.orders.loading = loading; 81 | } 82 | 83 | export function showToast(state, message) { 84 | state.toast.show = true; 85 | state.toast.message = message; 86 | } 87 | 88 | export function hideToast(state) { 89 | state.toast.show = false; 90 | state.toast.message = ''; 91 | } 92 | 93 | export function setCountries(state, countries) { 94 | state.countries = countries.data; 95 | } 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/product/index.blade.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | count() === 0): ?> 7 |
8 | There are no products published 9 |
10 | 11 |
14 | @foreach($products as $product) 15 | 16 |
27 | 29 | 34 | 35 |
36 |

37 | 38 | {{$product->title}} 39 | 40 |

41 |
${{$product->price}}
42 |
43 |
44 | 47 |
48 |
49 | 50 | @endforeach 51 |
52 | {{$products->links()}} 53 | 54 |
55 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function () { 22 | Route::get('/', [ProductController::class, 'index'])->name('home'); 23 | Route::get('/product/{product:slug}', [ProductController::class, 'view'])->name('product.view'); 24 | 25 | Route::prefix('/cart')->name('cart.')->group(function () { 26 | Route::get('/', [CartController::class, 'index'])->name('index'); 27 | Route::post('/add/{product:slug}', [CartController::class, 'add'])->name('add'); 28 | Route::post('/remove/{product:slug}', [CartController::class, 'remove'])->name('remove'); 29 | Route::post('/update-quantity/{product:slug}', [CartController::class, 'updateQuantity'])->name('update-quantity'); 30 | }); 31 | }); 32 | 33 | Route::middleware(['auth', 'verified'])->group(function() { 34 | Route::get('/profile', [ProfileController::class, 'view'])->name('profile'); 35 | Route::post('/profile', [ProfileController::class, 'store'])->name('profile.update'); 36 | Route::post('/profile/password-update', [ProfileController::class, 'passwordUpdate'])->name('profile_password.update'); 37 | Route::post('/checkout', [CheckoutController::class, 'checkout'])->name('cart.checkout'); 38 | Route::post('/checkout/{order}', [CheckoutController::class, 'checkoutOrder'])->name('cart.checkout-order'); 39 | Route::get('/checkout/success', [CheckoutController::class, 'success'])->name('checkout.success'); 40 | Route::get('/checkout/failure', [CheckoutController::class, 'failure'])->name('checkout.failure'); 41 | Route::get('/orders', [OrderController::class, 'index'])->name('order.index'); 42 | Route::get('/orders/{order}', [OrderController::class, 'view'])->name('order.view'); 43 | }); 44 | 45 | Route::post('/webhook/stripe', [CheckoutController::class, 'webhook']); 46 | 47 | require __DIR__ . '/auth.php'; 48 | -------------------------------------------------------------------------------- /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 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 64 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 65 | ], 66 | 67 | ]; 68 | --------------------------------------------------------------------------------