├── 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 |
6 |
7 |
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 | merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
4 | {{ $value ?? $slot }}
5 |
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 | merge(['type' => 'submit', 'class' => 'btn-primary bg-emerald-500 hover:bg-emerald-600 active:bg-emerald-700 w-full']) }}>
2 | {{ $slot }}
3 |
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 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/header.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
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 |
4 |
5 |
6 |
7 | {{ Illuminate\Mail\Markdown::parse($slot) }}
8 |
9 |
10 |
11 |
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 |
10 | @foreach ($errors->all() as $error)
11 | {{ $error }}
12 | @endforeach
13 |
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 |
2 |
3 |
Orders
4 |
5 |
6 |
7 |
8 |
19 |
20 |
23 |
--------------------------------------------------------------------------------
/backend/src/components/GuestLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
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 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
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 |
2 |
3 |
4 |
5 |
26 |
27 |
30 |
--------------------------------------------------------------------------------
/backend/src/views/Reports/CustomersReport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
4 |
404
5 |
6 |
Page Not Found
7 |
Please check the url in address bar and try again
8 |
9 |
11 | Go back home
12 |
13 |
14 |
15 |
16 |
17 |
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 |
2 |
3 |
Customers
4 |
5 |
6 |
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 |
2 |
3 |
9 |
17 |
22 |
23 |
{{text}}
24 |
25 |
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 |
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 |
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 | Order ID
8 |
9 |
10 | {{$order->id}}
11 |
12 |
13 |
14 |
15 | Order Status
16 | {{ $order->status }}
17 |
18 |
19 | Order Price
20 | ${{$order->total_price}}
21 |
22 |
23 | Order Date
24 | ${{$order->created_at}}
25 |
26 |
27 |
28 |
29 | Image
30 | Title
31 | Price
32 | Quantity
33 |
34 | @foreach($order->items as $item)
35 |
36 |
37 |
38 |
39 | {{$item->product->title}}
40 | ${{$item->unit_price * $item->quantity}}
41 | {{$item->quantity}}
42 |
43 | @endforeach
44 |
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 |
2 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
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 |
2 |
3 |
Users
4 |
8 | Add new User
9 |
10 |
11 |
12 |
13 |
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 |
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 |
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 |
31 |
32 | {{ $header ?? '' }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{ Illuminate\Mail\Markdown::parse($slot) }}
42 |
43 | {{ $subcopy ?? '' }}
44 |
45 |
46 |
47 |
48 |
49 |
50 | {{ $footer ?? '' }}
51 |
52 |
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 |
39 |
40 | {{ $content }}
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/backend/src/views/Products/Products.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Products
4 |
8 | Add new Product
9 |
10 |
11 |
12 |
13 |
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 |
2 |
3 |
4 |
5 | Orders Report
8 |
9 | Customers Report
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 | {{$label}}
17 | @endif
18 | @if ($type === 'select')
19 | merge([
20 | 'class' => 'border-gray-300 focus:border-purple-500 focus:outline-none focus:ring-purple-500 rounded-md w-full ' .
21 | ($errors->has($attributeName) ? $errorClasses : (old($attributeName) ? $successClasses :$defaultClasses))
22 | ]) !!}>
23 | {{ $slot }}
24 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
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 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/backend/src/views/RequestPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
33 |
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 |
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 |
39 |
47 |
52 |
53 |
54 |
55 |
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 |
2 |
6 |
{{ toast.message }}
7 |
11 |
19 |
24 |
25 |
26 |
27 |
33 |
34 |
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 |
2 |
3 |
38 |
39 |
40 |
41 |
45 |
--------------------------------------------------------------------------------
/backend/src/components/Sidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 | Dashboard
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 | Products
19 |
20 |
21 |
23 |
24 |
25 |
26 |
27 | Orders
28 |
29 |
30 |
32 |
33 |
34 |
35 |
36 | Users
37 |
38 |
39 |
41 |
42 |
43 |
44 |
45 | Customers
46 |
47 |
48 |
50 |
51 |
52 |
53 |
54 | Reports
55 |
56 |
57 |
58 |
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 |
41 |
${{$product->price}}
42 |
43 |
44 |
45 | Add to Cart
46 |
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 |
--------------------------------------------------------------------------------