diff --git a/app/Http/Controllers/Admin/MenuItemController.php b/app/Http/Controllers/Admin/MenuItemController.php index e00e4d8..ec60810 100644 --- a/app/Http/Controllers/Admin/MenuItemController.php +++ b/app/Http/Controllers/Admin/MenuItemController.php @@ -64,7 +64,9 @@ public function store(StoreMenuItemRequest $request, Menu $menu) $menu->menuItems()->create($request->except(['roles'])); $roles = $request->roles ?? []; - $menu->assignRole($roles); + if(!empty($roles)) { + $menu->assignRole($roles); + } return redirect()->route('admin.menu.item.index', $menu->id) ->with('message', 'Menu created successfully.'); diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 0d36b02..d44fe97 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -4,20 +4,19 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Auth\LoginRequest; -use App\Providers\RouteServiceProvider; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; use Inertia\Inertia; +use Inertia\Response; class AuthenticatedSessionController extends Controller { /** * Display the login view. - * - * @return \Inertia\Response */ - public function create() + public function create(): Response { return Inertia::render('Auth/Login', [ 'canResetPassword' => Route::has('password.request'), @@ -27,24 +26,20 @@ public function create() /** * Handle an incoming authentication request. - * - * @return \Illuminate\Http\RedirectResponse */ - public function store(LoginRequest $request) + public function store(LoginRequest $request): RedirectResponse { $request->authenticate(); $request->session()->regenerate(); - return redirect()->intended(RouteServiceProvider::HOME); + return redirect()->intended(route('dashboard', absolute: false)); } /** * Destroy an authenticated session. - * - * @return \Illuminate\Http\RedirectResponse */ - public function destroy(Request $request) + public function destroy(Request $request): RedirectResponse { Auth::guard('web')->logout(); diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php index f1e72f3..d2b1f14 100644 --- a/app/Http/Controllers/Auth/ConfirmablePasswordController.php +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -3,30 +3,27 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; use Inertia\Inertia; +use Inertia\Response; class ConfirmablePasswordController extends Controller { /** * Show the confirm password view. - * - * @return \Inertia\Response */ - public function show() + public function show(): Response { return Inertia::render('Auth/ConfirmPassword'); } /** * Confirm the user's password. - * - * @return mixed */ - public function store(Request $request) + public function store(Request $request): RedirectResponse { if (! Auth::guard('web')->validate([ 'email' => $request->user()->email, @@ -39,6 +36,6 @@ public function store(Request $request) $request->session()->put('auth.password_confirmed_at', time()); - return redirect()->intended(RouteServiceProvider::HOME); + return redirect()->intended(route('dashboard', absolute: false)); } } diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php index 6c7fa80..f64fa9b 100644 --- a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php +++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -3,20 +3,18 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class EmailVerificationNotificationController extends Controller { /** * Send a new email verification notification. - * - * @return \Illuminate\Http\RedirectResponse */ - public function store(Request $request) + public function store(Request $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(RouteServiceProvider::HOME); + return redirect()->intended(route('dashboard', absolute: false)); } $request->user()->sendEmailVerificationNotification(); diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php index b02dd81..b42e0d5 100644 --- a/app/Http/Controllers/Auth/EmailVerificationPromptController.php +++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -3,21 +3,20 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Inertia; +use Inertia\Response; class EmailVerificationPromptController extends Controller { /** * Display the email verification prompt. - * - * @return mixed */ - public function __invoke(Request $request) + public function __invoke(Request $request): RedirectResponse|Response { return $request->user()->hasVerifiedEmail() - ? redirect()->intended(RouteServiceProvider::HOME) + ? redirect()->intended(route('dashboard', absolute: false)) : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]); } } diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php index c34f27b..394cc4a 100644 --- a/app/Http/Controllers/Auth/NewPasswordController.php +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use Illuminate\Auth\Events\PasswordReset; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Password; @@ -11,15 +12,14 @@ use Illuminate\Validation\Rules; use Illuminate\Validation\ValidationException; use Inertia\Inertia; +use Inertia\Response; class NewPasswordController extends Controller { /** * Display the password reset view. - * - * @return \Inertia\Response */ - public function create(Request $request) + public function create(Request $request): Response { return Inertia::render('Auth/ResetPassword', [ 'email' => $request->email, @@ -30,11 +30,9 @@ public function create(Request $request) /** * Handle an incoming new password request. * - * @return \Illuminate\Http\RedirectResponse - * * @throws \Illuminate\Validation\ValidationException */ - public function store(Request $request) + public function store(Request $request): RedirectResponse { $request->validate([ 'token' => 'required', diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php new file mode 100644 index 0000000..57a82b5 --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordController.php @@ -0,0 +1,29 @@ +validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back(); + } +} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php index 790c44a..b22c544 100644 --- a/app/Http/Controllers/Auth/PasswordResetLinkController.php +++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -3,19 +3,19 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Password; use Illuminate\Validation\ValidationException; use Inertia\Inertia; +use Inertia\Response; class PasswordResetLinkController extends Controller { /** * Display the password reset link request view. - * - * @return \Inertia\Response */ - public function create() + public function create(): Response { return Inertia::render('Auth/ForgotPassword', [ 'status' => session('status'), @@ -25,11 +25,9 @@ public function create() /** * Handle an incoming password reset link request. * - * @return \Illuminate\Http\RedirectResponse - * * @throws \Illuminate\Validation\ValidationException */ - public function store(Request $request) + public function store(Request $request): RedirectResponse { $request->validate([ 'email' => 'required|email', diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index e074f08..53a546b 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -4,22 +4,21 @@ use App\Http\Controllers\Controller; use App\Models\User; -use App\Providers\RouteServiceProvider; use Illuminate\Auth\Events\Registered; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules; use Inertia\Inertia; +use Inertia\Response; class RegisteredUserController extends Controller { /** * Display the registration view. - * - * @return \Inertia\Response */ - public function create() + public function create(): Response { return Inertia::render('Auth/Register'); } @@ -27,15 +26,13 @@ public function create() /** * Handle an incoming registration request. * - * @return \Illuminate\Http\RedirectResponse - * * @throws \Illuminate\Validation\ValidationException */ - public function store(Request $request) + public function store(Request $request): RedirectResponse { $request->validate([ 'name' => 'required|string|max:255', - 'email' => 'required|string|email|max:255|unique:users', + 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); @@ -49,6 +46,6 @@ public function store(Request $request) Auth::login($user); - return redirect(RouteServiceProvider::HOME); + return redirect(route('dashboard', absolute: false)); } } diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php index 7729a5f..784765e 100644 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -3,27 +3,25 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; use Illuminate\Auth\Events\Verified; use Illuminate\Foundation\Auth\EmailVerificationRequest; +use Illuminate\Http\RedirectResponse; class VerifyEmailController extends Controller { /** * Mark the authenticated user's email address as verified. - * - * @return \Illuminate\Http\RedirectResponse */ - public function __invoke(EmailVerificationRequest $request) + public function __invoke(EmailVerificationRequest $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); } if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); } - return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..873b4f7 --- /dev/null +++ b/app/Http/Controllers/ProfileController.php @@ -0,0 +1,63 @@ + $request->user() instanceof MustVerifyEmail, + 'status' => session('status'), + ]); + } + + /** + * Update the user's profile information. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $request->user()->fill($request->validated()); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return Redirect::route('profile.edit'); + } + + /** + * Delete the user's account. + */ + public function destroy(Request $request): RedirectResponse + { + $request->validate([ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return Redirect::to('/'); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 4c4f01f..0c5b182 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -5,7 +5,6 @@ use BalajiDharma\LaravelMenu\Models\Menu; use Illuminate\Http\Request; use Inertia\Middleware; -use Tightenco\Ziggy\Ziggy; class HandleInertiaRequests extends Middleware { @@ -18,10 +17,8 @@ class HandleInertiaRequests extends Middleware /** * Determine the current asset version. - * - * @return string|null */ - public function version(Request $request) + public function version(Request $request): string|null { return parent::version($request); } @@ -29,25 +26,21 @@ public function version(Request $request) /** * Define the props that are shared by default. * - * @return array + * @return array */ - public function share(Request $request) + public function share(Request $request): array { - return array_merge(parent::share($request), [ + return [ + ...parent::share($request), 'auth' => [ 'user' => $request->user(), ], - 'ziggy' => function () use ($request) { - return array_merge((new Ziggy)->toArray(), [ - 'location' => $request->url(), - ]); - }, 'flash' => [ 'message' => fn () => $request->session()->get('message'), ], 'navigation' => [ 'menu' => Menu::getMenuTree('admin', false, true), ], - ]); + ]; } } diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 940a2d4..2b92f65 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -13,10 +13,8 @@ class LoginRequest extends FormRequest { /** * Determine if the user is authorized to make this request. - * - * @return bool */ - public function authorize() + public function authorize(): bool { return true; } @@ -24,9 +22,9 @@ public function authorize() /** * Get the validation rules that apply to the request. * - * @return array + * @return array */ - public function rules() + public function rules(): array { return [ 'email' => ['required', 'string', 'email'], @@ -37,11 +35,9 @@ public function rules() /** * Attempt to authenticate the request's credentials. * - * @return void - * * @throws \Illuminate\Validation\ValidationException */ - public function authenticate() + public function authenticate(): void { $this->ensureIsNotRateLimited(); @@ -59,11 +55,9 @@ public function authenticate() /** * Ensure the login request is not rate limited. * - * @return void - * * @throws \Illuminate\Validation\ValidationException */ - public function ensureIsNotRateLimited() + public function ensureIsNotRateLimited(): void { if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { return; @@ -83,11 +77,9 @@ public function ensureIsNotRateLimited() /** * Get the rate limiting throttle key for the request. - * - * @return string */ - public function throttleKey() + public function throttleKey(): string { - return Str::lower($this->input('email')).'|'.$this->ip(); + return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); } } diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php new file mode 100644 index 0000000..93b0022 --- /dev/null +++ b/app/Http/Requests/ProfileUpdateRequest.php @@ -0,0 +1,23 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], + ]; + } +} diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php deleted file mode 100644 index 395c518..0000000 --- a/app/Providers/BroadcastServiceProvider.php +++ /dev/null @@ -1,21 +0,0 @@ -configureRateLimiting(); - - $this->routes(function () { - Route::middleware('api') - ->prefix('api') - ->group(base_path('routes/api.php')); - - Route::middleware('web') - ->group(base_path('routes/web.php')); - - Route::middleware('web') - ->namespace($this->namespace) - ->group(base_path('routes/admin.php')); - }); - } - - /** - * Configure the rate limiters for the application. - * - * @return void - */ - protected function configureRateLimiting() - { - RateLimiter::for('api', function (Request $request) { - return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); - }); - } -} diff --git a/bootstrap/app.php b/bootstrap/app.php index 037e17d..b5e4d4e 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,55 +1,28 @@ singleton( - Illuminate\Contracts\Http\Kernel::class, - App\Http\Kernel::class -); - -$app->singleton( - Illuminate\Contracts\Console\Kernel::class, - App\Console\Kernel::class -); - -$app->singleton( - Illuminate\Contracts\Debug\ExceptionHandler::class, - App\Exceptions\Handler::class -); - -/* -|-------------------------------------------------------------------------- -| Return The Application -|-------------------------------------------------------------------------- -| -| This script returns the application instance. The instance is given to -| the calling script so we can separate the building of the instances -| from the actual running of the application and sending responses. -| -*/ - -return $app; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Configuration\Exceptions; +use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Support\Facades\Route; + +return Application::configure(basePath: dirname(__DIR__)) + ->withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + then: function () { + Route::middleware('web') + ->group(base_path('routes/admin.php')); + }, + ) + ->withMiddleware(function (Middleware $middleware) { + $middleware->web(append: [ + \App\Http\Middleware\HandleInertiaRequests::class, + \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, + ]); + + // + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..3943ffe --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,8 @@ + env('APP_URL', 'http://localhost'), - 'asset_url' => env('ASSET_URL'), - /* |-------------------------------------------------------------------------- | Application Timezone |-------------------------------------------------------------------------- | | Here you may specify the default timezone for your application, which - | will be used by the PHP date and date-time functions. We have gone - | ahead and set this to a sensible default for you out of the box. + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. | */ - 'timezone' => 'UTC', + 'timezone' => env('APP_TIMEZONE', 'UTC'), /* |-------------------------------------------------------------------------- @@ -77,53 +73,37 @@ |-------------------------------------------------------------------------- | | The application locale determines the default locale that will be used - | by the translation service provider. You are free to set this value - | to any of the locales which will be supported by the application. + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. | */ - 'locale' => 'en', + 'locale' => env('APP_LOCALE', 'en'), - /* - |-------------------------------------------------------------------------- - | Application Fallback Locale - |-------------------------------------------------------------------------- - | - | The fallback locale determines the locale to use when the current one - | is not available. You may change the value to correspond to any of - | the language folders that are provided through your application. - | - */ + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), - 'fallback_locale' => 'en', - - /* - |-------------------------------------------------------------------------- - | Faker Locale - |-------------------------------------------------------------------------- - | - | This locale will be used by the Faker PHP library when generating fake - | data for your database seeds. For example, this will be used to get - | localized telephone numbers, street address information and more. - | - */ - - 'faker_locale' => 'en_US', + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), /* |-------------------------------------------------------------------------- | Encryption Key |-------------------------------------------------------------------------- | - | This key is used by the Illuminate encrypter service and should be set - | to a random, 32 character string, otherwise these encrypted strings - | will not be safe. Please do this before deploying an application! + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. | */ + 'cipher' => 'AES-256-CBC', + 'key' => env('APP_KEY'), - 'cipher' => 'AES-256-CBC', + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], /* |-------------------------------------------------------------------------- @@ -139,77 +119,8 @@ */ 'maintenance' => [ - 'driver' => 'file', - // 'store' => 'redis', + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], - /* - |-------------------------------------------------------------------------- - | Autoloaded Service Providers - |-------------------------------------------------------------------------- - | - | The service providers listed here will be automatically loaded on the - | request to your application. Feel free to add your own services to - | this array to grant expanded functionality to your applications. - | - */ - - 'providers' => [ - - /* - * Laravel Framework Service Providers... - */ - Illuminate\Auth\AuthServiceProvider::class, - Illuminate\Broadcasting\BroadcastServiceProvider::class, - Illuminate\Bus\BusServiceProvider::class, - Illuminate\Cache\CacheServiceProvider::class, - Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, - Illuminate\Cookie\CookieServiceProvider::class, - Illuminate\Database\DatabaseServiceProvider::class, - Illuminate\Encryption\EncryptionServiceProvider::class, - Illuminate\Filesystem\FilesystemServiceProvider::class, - Illuminate\Foundation\Providers\FoundationServiceProvider::class, - Illuminate\Hashing\HashServiceProvider::class, - Illuminate\Mail\MailServiceProvider::class, - Illuminate\Notifications\NotificationServiceProvider::class, - Illuminate\Pagination\PaginationServiceProvider::class, - Illuminate\Pipeline\PipelineServiceProvider::class, - Illuminate\Queue\QueueServiceProvider::class, - Illuminate\Redis\RedisServiceProvider::class, - Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, - Illuminate\Session\SessionServiceProvider::class, - Illuminate\Translation\TranslationServiceProvider::class, - Illuminate\Validation\ValidationServiceProvider::class, - Illuminate\View\ViewServiceProvider::class, - - /* - * Package Service Providers... - */ - - /* - * Application Service Providers... - */ - App\Providers\AppServiceProvider::class, - App\Providers\AuthServiceProvider::class, - // App\Providers\BroadcastServiceProvider::class, - App\Providers\EventServiceProvider::class, - App\Providers\RouteServiceProvider::class, - - ], - - /* - |-------------------------------------------------------------------------- - | Class Aliases - |-------------------------------------------------------------------------- - | - | This array of class aliases will be registered when this application - | is started. However, feel free to register as many as you wish as - | the aliases are "lazy" loaded so they don't hinder performance. - | - */ - - 'aliases' => Facade::defaultAliases()->merge([ - // 'ExampleClass' => App\Example\ExampleClass::class, - ])->toArray(), - ]; diff --git a/jsconfig.json b/jsconfig.json index 97921a9..6269354 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@/*": ["resources/js/*"] + "@/*": ["resources/js/*"], + "ziggy-js": ["./vendor/tightenco/ziggy"] } }, "exclude": ["node_modules", "public"] diff --git a/package.json b/package.json index 6f58c40..1663b47 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,26 @@ { "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build" }, "devDependencies": { + "@inertiajs/vue3": "^1.0.0", "@mdi/js": "^7.0.96", - "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.0", - "@vitejs/plugin-vue": "^3.0.1", - "autoprefixer": "^10.4.2", - "axios": "^0.27", + "@vitejs/plugin-vue": "^5.0.0", + "autoprefixer": "^10.4.12", + "axios": "^1.6.4", "chart.js": "^3.9.1", - "laravel-vite-plugin": "^0.5.0", + "laravel-vite-plugin": "^1.0", "lodash": "^4.17.19", "numeral": "^2.0.6", "pinia": "^2.0.17", - "postcss": "^8.4.6", - "tailwindcss": "^3.1.0", - "vite": "^3.0.0", - "vue": "^3.2.31" - }, - "dependencies": { - "@inertiajs/vue3": "^1.0.0" + "postcss": "^8.4.31", + "tailwindcss": "^3.2.1", + "vite": "^5.0", + "vue": "^3.4.0" } } diff --git a/postcss.config.js b/postcss.config.js index 67cdf1a..49c0612 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { plugins: { tailwindcss: {}, autoprefixer: {}, diff --git a/resources/css/_checkbox-radio-switch.css b/resources/css/_checkbox-radio-switch.css index 5d5ef37..7509581 100644 --- a/resources/css/_checkbox-radio-switch.css +++ b/resources/css/_checkbox-radio-switch.css @@ -1,61 +1,73 @@ -.checkbox, .radio, .switch { - @apply inline-flex items-center cursor-pointer relative; -} - -.checkbox input[type=checkbox], .radio input[type=radio], .switch input[type=checkbox] { - @apply absolute left-0 opacity-0 -z-1; -} - -.checkbox input[type=checkbox]+.check, .radio input[type=radio]+.check, .switch input[type=checkbox]+.check { - @apply border-gray-700 border transition-colors duration-200 dark:bg-slate-800; -} - -.checkbox input[type=checkbox]:focus+.check, .radio input[type=radio]:focus+.check, .switch input[type=checkbox]:focus+.check { - @apply ring ring-blue-700; -} - -.checkbox input[type=checkbox]+.check, .radio input[type=radio]+.check { - @apply block w-5 h-5; -} - -.checkbox input[type=checkbox]+.check { - @apply rounded; -} - -.switch input[type=checkbox]+.check { - @apply flex items-center shrink-0 w-12 h-6 p-0.5 bg-gray-200; -} - -.radio input[type=radio]+.check, .switch input[type=checkbox]+.check, .switch input[type=checkbox]+.check:before { - @apply rounded-full; -} - -.checkbox input[type=checkbox]:checked+.check, .radio input[type=radio]:checked+.check { - @apply bg-no-repeat bg-center bg-blue-600 border-blue-600 border-4; -} - -.checkbox input[type=checkbox]:checked+.check { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E"); -} - -.radio input[type=radio]:checked+.check { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E"); -} - -.switch input[type=checkbox]:checked+.check { - @apply bg-blue-600 border-blue-600; -} - -.switch input[type=checkbox]+.check:before { - content: ''; - @apply block w-5 h-5 bg-white border border-gray-700; -} - -.switch input[type=checkbox]:checked+.check:before { - transform: translate3d(110%, 0 ,0); - @apply border-blue-600; -} - -.checkbox .control-label, .radio .control-label, .switch .control-label { - @apply pl-2; -} +@layer components { + .checkbox, + .radio, + .switch { + @apply inline-flex items-center cursor-pointer relative; + } + + .checkbox input[type='checkbox'], + .radio input[type='radio'], + .switch input[type='checkbox'] { + @apply absolute left-0 opacity-0 -z-1; + } + + .checkbox input[type='checkbox'] + .check, + .radio input[type='radio'] + .check, + .switch input[type='checkbox'] + .check { + @apply border-gray-700 border transition-colors duration-200 dark:bg-slate-800; + } + + .checkbox input[type='checkbox']:focus + .check, + .radio input[type='radio']:focus + .check, + .switch input[type='checkbox']:focus + .check { + @apply ring ring-blue-700; + } + + .checkbox input[type='checkbox'] + .check, + .radio input[type='radio'] + .check { + @apply block w-5 h-5; + } + + .checkbox input[type='checkbox'] + .check { + @apply rounded; + } + + .switch input[type='checkbox'] + .check { + @apply flex items-center shrink-0 w-12 h-6 p-0.5 bg-gray-200; + } + + .radio input[type='radio'] + .check, + .switch input[type='checkbox'] + .check, + .switch input[type='checkbox'] + .check:before { + @apply rounded-full; + } + + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply bg-no-repeat bg-center border-4; + } + + .checkbox input[type='checkbox']:checked + .check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E"); + } + + .radio input[type='radio']:checked + .check { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E"); + } + + .switch input[type='checkbox']:checked + .check, + .checkbox input[type='checkbox']:checked + .check, + .radio input[type='radio']:checked + .check { + @apply bg-blue-600 border-blue-600; + } + + .switch input[type='checkbox'] + .check:before { + content: ''; + @apply block w-5 h-5 bg-white border border-gray-700; + } + + .switch input[type='checkbox']:checked + .check:before { + transform: translate3d(110%, 0, 0); + @apply border-blue-600; + } +} \ No newline at end of file diff --git a/resources/css/_progress.css b/resources/css/_progress.css index 33bb821..fba9e16 100644 --- a/resources/css/_progress.css +++ b/resources/css/_progress.css @@ -1,19 +1,21 @@ -progress { - @apply h-3 rounded-full overflow-hidden; -} +@layer base { + progress { + @apply h-3 rounded-full overflow-hidden; + } -progress::-webkit-progress-bar { - @apply bg-blue-200; -} + progress::-webkit-progress-bar { + @apply bg-blue-200; + } -progress::-webkit-progress-value { - @apply bg-blue-500; -} + progress::-webkit-progress-value { + @apply bg-blue-500; + } -progress::-moz-progress-bar { - @apply bg-blue-500; -} + progress::-moz-progress-bar { + @apply bg-blue-500; + } -progress::-ms-fill { - @apply bg-blue-500 border-0; -} + progress::-ms-fill { + @apply bg-blue-500 border-0; + } +} \ No newline at end of file diff --git a/resources/css/_scrollbars.css b/resources/css/_scrollbars.css index 43ad367..dbcca7a 100644 --- a/resources/css/_scrollbars.css +++ b/resources/css/_scrollbars.css @@ -1,37 +1,41 @@ -html { - scrollbar-width: thin; - scrollbar-color: #9ca3af #e5e7eb; -} - -html::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -html::-webkit-scrollbar-track { - @apply bg-gray-200; -} - -html::-webkit-scrollbar-thumb { - @apply bg-gray-400 rounded; -} - -html::-webkit-scrollbar-thumb:hover { - @apply bg-gray-500; -} - -html.dark-scrollbars { - scrollbar-color: #374151 #111827; -} - -html.dark-scrollbars::-webkit-scrollbar-track { - @apply bg-gray-900; -} - -html.dark-scrollbars::-webkit-scrollbar-thumb { - @apply bg-gray-700; -} - -html.dark-scrollbars::-webkit-scrollbar-thumb:hover { - @apply bg-gray-600; -} +@layer base { + html { + scrollbar-width: thin; + scrollbar-color: rgb(156, 163, 175) rgb(249, 250, 251); + } + + body::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + body::-webkit-scrollbar-track { + @apply bg-gray-50; + } + + body::-webkit-scrollbar-thumb { + @apply bg-gray-400 rounded; + } + + body::-webkit-scrollbar-thumb:hover { + @apply bg-gray-500; + } +} + +@layer utilities { + .dark-scrollbars-compat { + scrollbar-color: rgb(71, 85, 105) rgb(30, 41, 59); + } + + .dark-scrollbars::-webkit-scrollbar-track { + @apply bg-slate-800; + } + + .dark-scrollbars::-webkit-scrollbar-thumb { + @apply bg-slate-600; + } + + .dark-scrollbars::-webkit-scrollbar-thumb:hover { + @apply bg-slate-500; + } +} \ No newline at end of file diff --git a/resources/css/_table.css b/resources/css/_table.css index 6a1a8b4..9787b57 100644 --- a/resources/css/_table.css +++ b/resources/css/_table.css @@ -1,46 +1,49 @@ -table { - @apply w-full; -} - -thead { - @apply hidden lg:table-header-group; -} - -tr { - @apply max-w-full block relative border-b-4 border-gray-100 - lg:table-row lg:border-b-0 dark:border-slate-800; -} - -tr:last-child { - @apply border-b-0; -} - -td:not(:first-child) { - @apply lg:border-l lg:border-t-0 lg:border-r-0 lg:border-b-0 lg:border-gray-100 lg:dark:border-slate-700; -} - -th { - @apply lg:text-left lg:p-3; -} - -td { - @apply flex justify-between text-right py-3 px-4 align-top border-b border-gray-100 - lg:table-cell lg:text-left lg:p-3 lg:align-middle lg:border-b-0 dark:border-slate-800; -} - -td:last-child { - @apply border-b-0; -} - -tbody tr, tbody tr:nth-child(odd) { - @apply lg:hover:bg-gray-100 lg:dark:hover:bg-slate-700/70; -} - -tbody tr:nth-child(odd) { - @apply lg:bg-gray-50 lg:dark:bg-slate-800; -} - -td:before { - content: attr(data-label); - @apply font-semibold pr-3 text-left lg:hidden; -} +@layer base { + table { + @apply w-full; + } + + thead { + @apply hidden lg:table-header-group; + } + + tr { + @apply max-w-full block relative border-b-4 border-gray-100 + lg:table-row lg:border-b-0 dark:border-slate-800; + } + + tr:last-child { + @apply border-b-0; + } + + td:not(:first-child) { + @apply lg:border-l lg:border-t-0 lg:border-r-0 lg:border-b-0 lg:border-gray-100 lg:dark:border-slate-700; + } + + th { + @apply lg:text-left lg:p-3; + } + + td { + @apply flex justify-between text-right py-3 px-4 align-top border-b border-gray-100 + lg:table-cell lg:text-left lg:p-3 lg:align-middle lg:border-b-0 dark:border-slate-800; + } + + td:last-child { + @apply border-b-0; + } + + tbody tr, + tbody tr:nth-child(odd) { + @apply lg:hover:bg-gray-100 lg:dark:hover:bg-slate-700/70; + } + + tbody tr:nth-child(odd) { + @apply lg:bg-gray-100/50 lg:dark:bg-slate-800/50; + } + + td:before { + content: attr(data-label); + @apply font-semibold pr-3 text-left lg:hidden; + } +} \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..5153810 --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,11 @@ +@import 'tailwind/_base.css'; +@import 'tailwind/_components.css'; +@import 'tailwind/_utilities.css'; + +@import '_checkbox-radio-switch.css'; +@import '_progress.css'; +@import '_scrollbars.css'; +@import '_table.css'; + +@import 'styles/_basic.css'; +@import 'styles/_white.css'; \ No newline at end of file diff --git a/resources/css/styles/_basic.css b/resources/css/styles/_basic.css new file mode 100644 index 0000000..4ee2296 --- /dev/null +++ b/resources/css/styles/_basic.css @@ -0,0 +1,31 @@ +@layer components { + .style-basic:not(.dark) { + .aside { + @apply bg-gray-800; + } + .aside-scrollbars { + @apply aside-scrollbars-gray; + } + .aside-brand { + @apply bg-gray-900 text-white; + } + .aside-menu-item { + @apply text-gray-300 hover:text-white; + } + .aside-menu-item-active { + @apply text-white; + } + .aside-menu-dropdown { + @apply bg-gray-700/50; + } + .navbar-item-label { + @apply text-black hover:text-blue-500; + } + .navbar-item-label-active { + @apply text-blue-600; + } + .overlay { + @apply from-gray-700 via-gray-900 to-gray-700; + } + } + } \ No newline at end of file diff --git a/resources/css/styles/_white.css b/resources/css/styles/_white.css new file mode 100644 index 0000000..0bc40a3 --- /dev/null +++ b/resources/css/styles/_white.css @@ -0,0 +1,31 @@ +@layer components { + .style-white:not(.dark) { + .aside { + @apply bg-white; + } + .aside-scrollbars { + @apply aside-scrollbars-light; + } + .aside-menu-item { + @apply text-blue-600 hover:text-black; + } + .aside-menu-item-active { + @apply text-black; + } + .aside-menu-dropdown { + @apply bg-gray-100/75; + } + .navbar-item-label { + @apply text-blue-600; + } + .navbar-item-label-hover { + @apply hover:text-black; + } + .navbar-item-label-active { + @apply text-black; + } + .overlay { + @apply from-white via-gray-100 to-white; + } + } + } \ No newline at end of file diff --git a/resources/js/Components/Admin/MenuItemList.vue b/resources/js/Components/Admin/MenuItemList.vue index 2f587ab..86c3a86 100644 --- a/resources/js/Components/Admin/MenuItemList.vue +++ b/resources/js/Components/Admin/MenuItemList.vue @@ -37,7 +37,6 @@ const props = defineProps({ :path="item.icon" class="flex-none" :class="activeInactiveStyle" - w="w-16" :size="18" /> {{ item.name }} diff --git a/resources/js/Components/AsideMenu.vue b/resources/js/Components/AsideMenu.vue index 53741df..8375e59 100644 --- a/resources/js/Components/AsideMenu.vue +++ b/resources/js/Components/AsideMenu.vue @@ -1,26 +1,39 @@ + + \ No newline at end of file diff --git a/resources/js/Components/AsideMenuItem.vue b/resources/js/Components/AsideMenuItem.vue index ba37bf1..5f13c9c 100644 --- a/resources/js/Components/AsideMenuItem.vue +++ b/resources/js/Components/AsideMenuItem.vue @@ -1,7 +1,6 @@ + \ No newline at end of file diff --git a/resources/js/Components/DangerButton.vue b/resources/js/Components/DangerButton.vue new file mode 100644 index 0000000..55a5c88 --- /dev/null +++ b/resources/js/Components/DangerButton.vue @@ -0,0 +1,7 @@ + diff --git a/resources/js/Components/InputLabel.vue b/resources/js/Components/InputLabel.vue new file mode 100644 index 0000000..c47ccff --- /dev/null +++ b/resources/js/Components/InputLabel.vue @@ -0,0 +1,14 @@ + + + diff --git a/resources/js/Components/Modal.vue b/resources/js/Components/Modal.vue new file mode 100644 index 0000000..ae2be41 --- /dev/null +++ b/resources/js/Components/Modal.vue @@ -0,0 +1,98 @@ + + + diff --git a/resources/js/Components/NavBar.vue b/resources/js/Components/NavBar.vue index 2402a77..30b12fa 100644 --- a/resources/js/Components/NavBar.vue +++ b/resources/js/Components/NavBar.vue @@ -1,201 +1,46 @@ + \ No newline at end of file diff --git a/resources/js/Components/NavBarItem.vue b/resources/js/Components/NavBarItem.vue index 218a753..d01d07b 100644 --- a/resources/js/Components/NavBarItem.vue +++ b/resources/js/Components/NavBarItem.vue @@ -1,83 +1,132 @@ + \ No newline at end of file diff --git a/resources/js/Components/NavBarItemPlain.vue b/resources/js/Components/NavBarItemPlain.vue new file mode 100644 index 0000000..e4f2142 --- /dev/null +++ b/resources/js/Components/NavBarItemPlain.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/resources/js/Components/NavBarMenuList.vue b/resources/js/Components/NavBarMenuList.vue new file mode 100644 index 0000000..3e4e52f --- /dev/null +++ b/resources/js/Components/NavBarMenuList.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/resources/js/Components/OverlayLayer.vue b/resources/js/Components/OverlayLayer.vue index 030b4c8..f15bff9 100644 --- a/resources/js/Components/OverlayLayer.vue +++ b/resources/js/Components/OverlayLayer.vue @@ -1,26 +1,26 @@ + \ No newline at end of file diff --git a/resources/js/Components/PrimaryButton.vue b/resources/js/Components/PrimaryButton.vue new file mode 100644 index 0000000..bda4c34 --- /dev/null +++ b/resources/js/Components/PrimaryButton.vue @@ -0,0 +1,7 @@ + diff --git a/resources/js/Components/SecondaryButton.vue b/resources/js/Components/SecondaryButton.vue new file mode 100644 index 0000000..567be02 --- /dev/null +++ b/resources/js/Components/SecondaryButton.vue @@ -0,0 +1,17 @@ + + + diff --git a/resources/js/Components/SectionFullScreen.vue b/resources/js/Components/SectionFullScreen.vue index a097a6a..73e102c 100644 --- a/resources/js/Components/SectionFullScreen.vue +++ b/resources/js/Components/SectionFullScreen.vue @@ -1,6 +1,6 @@ + + diff --git a/resources/js/Layouts/AuthenticatedLayout.vue b/resources/js/Layouts/AuthenticatedLayout.vue new file mode 100644 index 0000000..e3e5100 --- /dev/null +++ b/resources/js/Layouts/AuthenticatedLayout.vue @@ -0,0 +1,152 @@ + + + diff --git a/resources/js/Layouts/GuestLayout.vue b/resources/js/Layouts/GuestLayout.vue new file mode 100644 index 0000000..943dc86 --- /dev/null +++ b/resources/js/Layouts/GuestLayout.vue @@ -0,0 +1,20 @@ + + + diff --git a/resources/js/Layouts/LayoutAuthenticated.vue b/resources/js/Layouts/LayoutAuthenticated.vue index 5ddb3e7..497e690 100644 --- a/resources/js/Layouts/LayoutAuthenticated.vue +++ b/resources/js/Layouts/LayoutAuthenticated.vue @@ -1,25 +1,74 @@ + \ No newline at end of file diff --git a/resources/js/Layouts/LayoutGuest.vue b/resources/js/Layouts/LayoutGuest.vue index 1fe1661..3147513 100644 --- a/resources/js/Layouts/LayoutGuest.vue +++ b/resources/js/Layouts/LayoutGuest.vue @@ -1,13 +1,5 @@ - - + \ No newline at end of file diff --git a/resources/js/Pages/Admin/Dashboard.vue b/resources/js/Pages/Admin/Dashboard.vue new file mode 100644 index 0000000..37f1aae --- /dev/null +++ b/resources/js/Pages/Admin/Dashboard.vue @@ -0,0 +1,162 @@ + + + \ No newline at end of file diff --git a/resources/js/Pages/Dashboard.vue b/resources/js/Pages/Dashboard.vue index dc886e9..718c55e 100644 --- a/resources/js/Pages/Dashboard.vue +++ b/resources/js/Pages/Dashboard.vue @@ -1,162 +1,22 @@ \ No newline at end of file diff --git a/resources/js/Pages/Profile/Edit.vue b/resources/js/Pages/Profile/Edit.vue new file mode 100644 index 0000000..5ce984d --- /dev/null +++ b/resources/js/Pages/Profile/Edit.vue @@ -0,0 +1,46 @@ + + + diff --git a/resources/js/Pages/Profile/Partials/DeleteUserForm.vue b/resources/js/Pages/Profile/Partials/DeleteUserForm.vue new file mode 100644 index 0000000..9cd0be3 --- /dev/null +++ b/resources/js/Pages/Profile/Partials/DeleteUserForm.vue @@ -0,0 +1,95 @@ + + + diff --git a/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue new file mode 100644 index 0000000..4451539 --- /dev/null +++ b/resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue @@ -0,0 +1,105 @@ + + + diff --git a/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue new file mode 100644 index 0000000..8a99859 --- /dev/null +++ b/resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue @@ -0,0 +1,102 @@ + + + diff --git a/resources/js/Pages/Welcome.vue b/resources/js/Pages/Welcome.vue index 54d1e01..866635d 100644 --- a/resources/js/Pages/Welcome.vue +++ b/resources/js/Pages/Welcome.vue @@ -18,7 +18,7 @@ defineProps({
Dashboard diff --git a/resources/js/Stores/darkMode.js b/resources/js/Stores/darkMode.js new file mode 100644 index 0000000..2059840 --- /dev/null +++ b/resources/js/Stores/darkMode.js @@ -0,0 +1,29 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useDarkModeStore = defineStore('darkMode', () => { + const isEnabled = ref(false) + + function set(payload = null) { + isEnabled.value = payload !== null ? payload : !isEnabled.value + + if (typeof document !== 'undefined') { + document.body.classList[isEnabled.value ? 'add' : 'remove']('dark-scrollbars') + + document.documentElement.classList[isEnabled.value ? 'add' : 'remove']( + 'dark', + 'dark-scrollbars-compat' + ) + } + + // You can persist dark mode setting + if (typeof localStorage !== 'undefined') { + localStorage.setItem('darkMode', this.darkMode ? '1' : '0') + } + } + + return { + isEnabled, + set + } +}) \ No newline at end of file diff --git a/resources/js/Stores/layout.js b/resources/js/Stores/layout.js deleted file mode 100644 index 2af97e0..0000000 --- a/resources/js/Stores/layout.js +++ /dev/null @@ -1,14 +0,0 @@ -import { defineStore } from 'pinia' - -export const useLayoutStore = defineStore('layout', { - state: () => ({ - isAsideMobileExpanded: false, - isAsideLgActive: false - }), - - actions: { - asideMobileToggle () { - this.isAsideMobileExpanded = !this.isAsideMobileExpanded - } - } -}) diff --git a/resources/js/Stores/main.js b/resources/js/Stores/main.js index b07e30b..f42943f 100644 --- a/resources/js/Stores/main.js +++ b/resources/js/Stores/main.js @@ -1,44 +1,64 @@ import { defineStore } from 'pinia' +import { ref, computed } from 'vue' import axios from 'axios' -export const useMainStore = defineStore('main', { - state: () => ({ - /* User */ - userName: null, - userEmail: null, - userAvatar: null, - - /* Field focus with ctrl+k (to register only once) */ - isFieldFocusRegistered: false, - - /* Sample data (commonly used) */ - clients: [], - history: [] - }), - actions: { - setUser (payload) { - if (payload.name) { - this.userName = payload.name - } - if (payload.email) { - this.userEmail = payload.email - } - if (payload.avatar) { - this.userAvatar = payload.avatar - } - }, - - fetch (sampleDataKey) { - axios - .get(`/data-sources/${sampleDataKey}.json`) - .then(r => { - if (r.data && r.data.data) { - this[sampleDataKey] = r.data.data - } - }) - .catch(error => { - alert(error.message) - }) +export const useMainStore = defineStore('main', () => { + const userName = ref('John Doe') + const userEmail = ref('doe.doe.doe@example.com') + + const userAvatar = computed( + () => + `https://api.dicebear.com/7.x/avataaars/svg?seed=${userEmail.value.replace( + /[^a-z0-9]+/gi, + '-' + )}` + ) + + const isFieldFocusRegistered = ref(false) + + const clients = ref([]) + const history = ref([]) + + function setUser(payload) { + if (payload.name) { + userName.value = payload.name + } + if (payload.email) { + userEmail.value = payload.email } } -}) + + function fetchSampleClients() { + axios + .get(`data-sources/clients.json?v=3`) + .then((result) => { + clients.value = result?.data?.data + }) + .catch((error) => { + alert(error.message) + }) + } + + function fetchSampleHistory() { + axios + .get(`data-sources/history.json`) + .then((result) => { + history.value = result?.data?.data + }) + .catch((error) => { + alert(error.message) + }) + } + + return { + userName, + userEmail, + userAvatar, + isFieldFocusRegistered, + clients, + history, + setUser, + fetchSampleClients, + fetchSampleHistory + } +}) \ No newline at end of file diff --git a/resources/js/Stores/style.js b/resources/js/Stores/style.js deleted file mode 100644 index adff227..0000000 --- a/resources/js/Stores/style.js +++ /dev/null @@ -1,51 +0,0 @@ -import { defineStore } from 'pinia' -import * as styles from '@/styles' -import { darkModeKey, styleKey } from '@/config' - -export const useStyleStore = defineStore('style', { - state: () => ({ - /* Styles */ - asideStyle: '', - asideBrandStyle: '', - asideMenuItemStyle: '', - asideMenuItemActiveStyle: '', - asideMenuDropdownStyle: '', - navBarItemLabelStyle: '', - navBarItemLabelHoverStyle: '', - navBarItemLabelActiveColorStyle: '', - navBarMenuListUpperLabelStyle: '', - overlayStyle: '', - - /* Dark mode */ - darkMode: false, - }), - actions: { - setStyle (payload) { - if (!styles[payload]) { - return - } - - if (typeof localStorage !== 'undefined') { - localStorage.setItem(styleKey, payload) - } - - const style = styles[payload] - - for (const key in style) { - this[`${key}Style`] = style[key] - } - }, - - setDarkMode (payload = null) { - this.darkMode = payload !== null ? payload : !this.darkMode - - if (typeof localStorage !== 'undefined') { - localStorage.setItem(darkModeKey, this.darkMode ? '1' : '0') - } - - if (typeof document !== 'undefined') { - document.documentElement.classList[this.darkMode ? 'add' : 'remove']('dark-scrollbars') - } - } - } -}) diff --git a/resources/js/app.js b/resources/js/app.js index 334a0ae..0ca35ad 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,48 +1,40 @@ -import '../css/main.css'; - -import { createPinia } from 'pinia' -import { useStyleStore } from '@/Stores/style.js' -import { useLayoutStore } from '@/Stores/layout.js' - -import { darkModeKey, styleKey } from '@/config.js' +import './bootstrap'; +import '../css/app.css'; import { createApp, h } from 'vue'; import { createInertiaApp, router } from '@inertiajs/vue3'; +import { createPinia } from 'pinia' +import { useDarkModeStore } from '@/Stores/darkMode.js' +import { useMainStore } from '@/Stores/main.js' import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; -import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m'; +import { ZiggyVue } from '../../vendor/tightenco/ziggy'; -const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel'; +const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; -const pinia = createPinia() +const pinia = createPinia(); createInertiaApp({ - progress: { - color: '#4B5563', - }, title: (title) => `${title} - ${appName}`, resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), setup({ el, App, props, plugin }) { return createApp({ render: () => h(App, props) }) - .use(plugin) - .use(pinia) + .use(plugin) .use(ZiggyVue, Ziggy) .mount(el); }, + progress: { + color: '#4B5563', + }, }); -const styleStore = useStyleStore(pinia) -const layoutStore = useLayoutStore(pinia) - -/* App style */ -styleStore.setStyle(localStorage[styleKey] ?? 'basic') +// Init main store +const mainStore = useMainStore(pinia) -/* Dark mode */ -if ((!localStorage[darkModeKey] && window.matchMedia('(prefers-color-scheme: dark)').matches) || localStorage[darkModeKey] === '1') { - styleStore.setDarkMode(true) -} +const darkModeStore = useDarkModeStore(pinia) -/* Collapse mobile aside menu on route change */ -router.on('navigate', (event) => { - layoutStore.isAsideMobileExpanded = false - layoutStore.isAsideLgActive = false -}) +if ( + (!localStorage['darkMode'] && window.matchMedia('(prefers-color-scheme: dark)').matches) || + localStorage['darkMode'] === '1' + ) { + darkModeStore.set(true) +} \ No newline at end of file diff --git a/resources/js/colors.js b/resources/js/colors.js index 384d8b2..cf924d4 100644 --- a/resources/js/colors.js +++ b/resources/js/colors.js @@ -1,4 +1,4 @@ -const gradientBgBase = 'bg-gradient-to-tr' +export const gradientBgBase = 'bg-gradient-to-tr' export const gradientBgPurplePink = `${gradientBgBase} from-purple-400 via-pink-500 to-red-500` export const gradientBgDark = `${gradientBgBase} from-slate-700 via-slate-900 to-slate-800` export const gradientBgPinkRed = `${gradientBgBase} from-pink-400 via-red-500 to-yellow-500` @@ -33,51 +33,77 @@ export const colorsOutline = { info: [colorsText.info, 'border-blue-500'] } -export const getButtonColor = (color, isOutlined, hasHover) => { +export const getButtonColor = (color, isOutlined, hasHover, isActive = false) => { const colors = { + ring: { + white: 'ring-gray-200 dark:ring-gray-500', + whiteDark: 'ring-gray-200 dark:ring-gray-500', + lightDark: 'ring-gray-200 dark:ring-gray-500', + contrast: 'ring-gray-300 dark:ring-gray-400', + success: 'ring-emerald-300 dark:ring-emerald-700', + danger: 'ring-red-300 dark:ring-red-700', + warning: 'ring-yellow-300 dark:ring-yellow-700', + info: 'ring-blue-300 dark:ring-blue-700' + }, + active: { + white: 'bg-gray-100', + whiteDark: 'bg-gray-100 dark:bg-slate-800', + lightDark: 'bg-gray-200 dark:bg-slate-700', + contrast: 'bg-gray-700 dark:bg-slate-100', + success: 'bg-emerald-700 dark:bg-emerald-600', + danger: 'bg-red-700 dark:bg-red-600', + warning: 'bg-yellow-700 dark:bg-yellow-600', + info: 'bg-blue-700 dark:bg-blue-600' + }, bg: { white: 'bg-white text-black', + whiteDark: 'bg-white text-black dark:bg-slate-900 dark:text-white', + lightDark: 'bg-gray-100 text-black dark:bg-slate-800 dark:text-white', contrast: 'bg-gray-800 text-white dark:bg-white dark:text-black', - light: 'bg-gray-50 text-black', success: 'bg-emerald-600 dark:bg-emerald-500 text-white', danger: 'bg-red-600 dark:bg-red-500 text-white', warning: 'bg-yellow-600 dark:bg-yellow-500 text-white', info: 'bg-blue-600 dark:bg-blue-500 text-white' }, bgHover: { - white: 'hover:bg-gray-50', - contrast: 'hover:bg-gray-900 hover:dark:bg-slate-100', - light: 'hover:bg-gray-200', - success: 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-emerald-600 hover:dark:border-emerald-600', - danger: 'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600', - warning: 'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600', + white: 'hover:bg-gray-100', + whiteDark: 'hover:bg-gray-100 hover:dark:bg-slate-800', + lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700', + contrast: 'hover:bg-gray-700 hover:dark:bg-slate-100', + success: + 'hover:bg-emerald-700 hover:border-emerald-700 hover:dark:bg-emerald-600 hover:dark:border-emerald-600', + danger: + 'hover:bg-red-700 hover:border-red-700 hover:dark:bg-red-600 hover:dark:border-red-600', + warning: + 'hover:bg-yellow-700 hover:border-yellow-700 hover:dark:bg-yellow-600 hover:dark:border-yellow-600', info: 'hover:bg-blue-700 hover:border-blue-700 hover:dark:bg-blue-600 hover:dark:border-blue-600' }, borders: { - white: 'border-gray-100', - contrast: 'border-gray-900 dark:border-slate-100', - light: 'border-gray-100 dark:border-slate-700', + white: 'border-white', + whiteDark: 'border-white dark:border-slate-900', + lightDark: 'border-gray-100 dark:border-slate-800', + contrast: 'border-gray-800 dark:border-white', success: 'border-emerald-600 dark:border-emerald-500', danger: 'border-red-600 dark:border-red-500', warning: 'border-yellow-600 dark:border-yellow-500', info: 'border-blue-600 dark:border-blue-500' }, text: { - white: 'text-black dark:text-slate-100', contrast: 'dark:text-slate-100', - light: 'text-gray-700 dark:text-slate-400', success: 'text-emerald-600 dark:text-emerald-500', danger: 'text-red-600 dark:text-red-500', warning: 'text-yellow-600 dark:text-yellow-500', info: 'text-blue-600 dark:text-blue-500' }, outlineHover: { - white: 'hover:bg-gray-100 hover:text-gray-900 dark:hover:text-slate-900', - contrast: 'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black', - light: 'hover:bg-gray-100 hover:text-gray-900 dark:hover:text-slate-900', - success: 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-emerald-600', - danger: 'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600', - warning: 'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600', + contrast: + 'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black', + success: + 'hover:bg-emerald-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-emerald-600', + danger: + 'hover:bg-red-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-red-600', + warning: + 'hover:bg-yellow-600 hover:text-white hover:text-white hover:dark:text-white hover:dark:border-yellow-600', info: 'hover:bg-blue-600 hover:text-white hover:dark:text-white hover:dark:border-blue-600' } } @@ -86,14 +112,19 @@ export const getButtonColor = (color, isOutlined, hasHover) => { return color } - const base = [ - isOutlined ? colors.text[color] : colors.bg[color], - colors.borders[color] - ] + const isOutlinedProcessed = isOutlined && ['white', 'whiteDark', 'lightDark'].indexOf(color) < 0 + + const base = [colors.borders[color], colors.ring[color]] + + if (isActive) { + base.push(colors.active[color]) + } else { + base.push(isOutlinedProcessed ? colors.text[color] : colors.bg[color]) + } if (hasHover) { - base.push(isOutlined ? colors.outlineHover[color] : colors.bgHover[color]) + base.push(isOutlinedProcessed ? colors.outlineHover[color] : colors.bgHover[color]) } return base -} +} \ No newline at end of file diff --git a/resources/js/config.js b/resources/js/config.js index 3c5a9cb..1c815a7 100644 --- a/resources/js/config.js +++ b/resources/js/config.js @@ -1,5 +1 @@ -export const darkModeKey = 'darkMode' - -export const styleKey = 'style' - export const containerMaxW = 'xl:max-w-6xl xl:mx-auto' \ No newline at end of file diff --git a/resources/js/menu.js b/resources/js/menu.js deleted file mode 100644 index f565c20..0000000 --- a/resources/js/menu.js +++ /dev/null @@ -1,38 +0,0 @@ -import { - mdiAccountCircle, - mdiMonitor, - mdiGithub, - mdiAccountKey, - mdiAccountEye, - mdiAccountGroup, - mdiPalette -} from '@mdi/js' - -export default [ - { - route: 'dashboard', - icon: mdiMonitor, - label: 'Dashboard' - }, - { - route: 'permission.index', - icon: mdiAccountKey, - label: 'Permissions' - }, - { - route: 'role.index', - icon: mdiAccountEye, - label: 'Roles' - }, - { - route: 'user.index', - icon: mdiAccountGroup, - label: 'Users' - }, - { - href: 'https://github.com/balajidharma/laravel-vue-admin-panel', - label: 'GitHub', - icon: mdiGithub, - target: '_blank' - } -] diff --git a/resources/js/menuNavBar.js b/resources/js/menuNavBar.js new file mode 100644 index 0000000..a930315 --- /dev/null +++ b/resources/js/menuNavBar.js @@ -0,0 +1,81 @@ +import { + mdiMenu, + mdiClockOutline, + mdiCloud, + mdiCrop, + mdiAccount, + mdiCogOutline, + mdiEmail, + mdiLogout, + mdiThemeLightDark, + mdiGithub + } from '@mdi/js' + + export default [ + { + icon: mdiMenu, + label: 'Sample menu', + menu: [ + { + icon: mdiClockOutline, + label: 'Item One' + }, + { + icon: mdiCloud, + label: 'Item Two' + }, + { + isDivider: true + }, + { + icon: mdiCrop, + label: 'Item Last' + } + ] + }, + { + isCurrentUser: true, + menu: [ + { + icon: mdiAccount, + label: 'My Profile', + to: '/admin/edit-account-info' + }, + { + icon: mdiCogOutline, + label: 'Settings' + }, + { + icon: mdiEmail, + label: 'Messages' + }, + { + isDivider: true + }, + { + icon: mdiLogout, + label: 'Log Out', + isLogout: true + } + ] + }, + { + icon: mdiThemeLightDark, + label: 'Light/Dark', + isDesktopNoLabel: true, + isToggleLightDark: true + }, + { + icon: mdiGithub, + label: 'GitHub', + isDesktopNoLabel: true, + href: 'https://github.com/balajidharma/laravel-vue-admin-panel', + target: '_blank' + }, + { + icon: mdiLogout, + label: 'Log out', + isDesktopNoLabel: true, + isLogout: true + } + ] \ No newline at end of file diff --git a/resources/js/styles.js b/resources/js/styles.js deleted file mode 100644 index 21b7004..0000000 --- a/resources/js/styles.js +++ /dev/null @@ -1,27 +0,0 @@ -export const basic = { - aside: 'bg-gray-800', - asideScrollbars: 'aside-scrollbars-gray', - asideBrand: 'bg-gray-900 text-white', - asideMenuItem: 'text-gray-300 hover:text-white', - asideMenuItemActive: 'font-bold text-white', - asideMenuDropdown: 'bg-gray-700/50', - navBarItemLabel: 'text-black', - navBarItemLabelHover: 'hover:text-blue-500', - navBarItemLabelActiveColor: 'text-blue-600', - navBarMenuListUpperLabel: 'bg-gray-100', - overlay: 'from-gray-700 via-gray-900 to-gray-700' -} - -export const white = { - aside: 'bg-white', - asideScrollbars: 'aside-scrollbars-light', - asideBrand: '', - asideMenuItem: 'text-blue-600 hover:text-black dark:text-white', - asideMenuItemActive: 'font-bold text-black dark:text-white', - asideMenuDropdown: 'bg-gray-100/75', - navBarItemLabel: 'text-blue-600', - navBarItemLabelHover: 'hover:text-black', - navBarItemLabelActiveColor: 'text-black', - navBarMenuListUpperLabel: 'bg-gray-50', - overlay: 'from-white via-gray-100 to-white' -} diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 4041198..6810aeb 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -1,14 +1,18 @@ - + {{ config('app.name', 'Laravel') }} + + + + @routes - @vite('resources/js/app.js') + @vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) @inertiaHead diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index 007d5a2..7b626b1 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -12,129 +12,161 @@ - -
- @if (Route::has('login')) -
- @auth - Home - @else - Log in - - @if (Route::has('register')) - Register + +
+ +
+
+
+
+ +
+ @if (Route::has('login')) + @endif - @endauth -
- @endif - -
-
- - - -
- -
-
- -
-
- - - + + +
+
+ +
+ Laravel documentation screenshot + +
-

Documentation

+
+
+
+ +
-

- Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end. -

-
+
+

Documentation

- - - -
+

+ Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end. +

+
+
- - +
+

Laracasts

- - - -
+

+ Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process. +

+
- -
- - - - - + + -
-
-
- - +
+
+ + + +
-

Vibrant Ecosystem

+
+

Vibrant Ecosystem

-

- Laravel's robust library of first-party tools and libraries, such as Forge, Vapor, Nova, and Envoyer help you take your projects to the next level. Pair them with powerful open source libraries like Cashier, Dusk, Echo, Horizon, Sanctum, Telescope, and more. -

+

+ Laravel's robust library of first-party tools and libraries, such as Forge, Vapor, Nova, Envoyer, and Herd help you take your projects to the next level. Pair them with powerful open source libraries like Cashier, Dusk, Echo, Horizon, Sanctum, Telescope, and more. +

+
-
-
- -
-
+
Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }}) -
+
- \ No newline at end of file + diff --git a/routes/admin.php b/routes/admin.php index d5ac457..68f2f1c 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -9,7 +9,7 @@ 'as' => 'admin.', ], function () { Route::get('/', function () { - return Inertia::render('Dashboard'); + return Inertia::render('Admin/Dashboard'); })->name('dashboard'); Route::resource('user', 'UserController'); Route::resource('role', 'RoleController'); diff --git a/routes/api.php b/routes/api.php index eb6fa48..03c4e31 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,17 +3,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -/* -|-------------------------------------------------------------------------- -| API Routes -|-------------------------------------------------------------------------- -| -| Here is where you can register API routes for your application. These -| routes are loaded by the RouteServiceProvider within a group which -| is assigned the "api" middleware group. Enjoy building your API! -| -*/ - -Route::middleware('auth:sanctum')->get('/user', function (Request $request) { +Route::middleware(['auth:sanctum'])->get('/user', function (Request $request) { return $request->user(); -}); +}); \ No newline at end of file diff --git a/routes/auth.php b/routes/auth.php index e11fb1a..1040b51 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Auth\EmailVerificationNotificationController; use App\Http\Controllers\Auth\EmailVerificationPromptController; use App\Http\Controllers\Auth\NewPasswordController; +use App\Http\Controllers\Auth\PasswordController; use App\Http\Controllers\Auth\PasswordResetLinkController; use App\Http\Controllers\Auth\RegisteredUserController; use App\Http\Controllers\Auth\VerifyEmailController; @@ -12,45 +13,47 @@ Route::middleware('guest')->group(function () { Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); + ->name('register'); Route::post('register', [RegisteredUserController::class, 'store']); Route::get('login', [AuthenticatedSessionController::class, 'create']) - ->name('login'); + ->name('login'); Route::post('login', [AuthenticatedSessionController::class, 'store']); Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) - ->name('password.request'); + ->name('password.request'); Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) - ->name('password.email'); + ->name('password.email'); Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) - ->name('password.reset'); + ->name('password.reset'); Route::post('reset-password', [NewPasswordController::class, 'store']) - ->name('password.update'); + ->name('password.store'); }); Route::middleware('auth')->group(function () { - Route::get('verify-email', [EmailVerificationPromptController::class, '__invoke']) - ->name('verification.notice'); + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); - Route::get('verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke']) - ->middleware(['signed', 'throttle:6,1']) - ->name('verification.verify'); + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) - ->middleware('throttle:6,1') - ->name('verification.send'); + ->middleware('throttle:6,1') + ->name('verification.send'); Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) - ->name('password.confirm'); + ->name('password.confirm'); Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) - ->name('logout'); + ->name('logout'); }); diff --git a/routes/channels.php b/routes/channels.php deleted file mode 100644 index 5d451e1..0000000 --- a/routes/channels.php +++ /dev/null @@ -1,18 +0,0 @@ -id === (int) $id; -}); diff --git a/routes/web.php b/routes/web.php index 9add5d7..067c4f5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,20 +1,10 @@ Route::has('login'), @@ -24,4 +14,14 @@ ]); }); +Route::get('/dashboard', function () { + return Inertia::render('Dashboard'); +})->middleware(['auth', 'verified'])->name('dashboard'); + +Route::middleware('auth')->group(function () { + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +}); + require __DIR__.'/auth.php'; diff --git a/tailwind.config.js b/tailwind.config.js index 5e370b6..177cdc8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,7 +1,7 @@ const plugin = require('tailwindcss/plugin') /** @type {import('tailwindcss').Config} */ -module.exports = { +export default { content: [ './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', './storage/framework/views/*.php', @@ -75,7 +75,6 @@ module.exports = { }, { values: theme('asideScrollbars') } ) - }), - require('@tailwindcss/line-clamp') + }) ] } diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index 075a4c2..13dcb7c 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Auth; use App\Models\User; -use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -11,14 +10,14 @@ class AuthenticationTest extends TestCase { use RefreshDatabase; - public function test_login_screen_can_be_rendered() + public function test_login_screen_can_be_rendered(): void { $response = $this->get('/login'); $response->assertStatus(200); } - public function test_users_can_authenticate_using_the_login_screen() + public function test_users_can_authenticate_using_the_login_screen(): void { $user = User::factory()->create(); @@ -28,10 +27,10 @@ public function test_users_can_authenticate_using_the_login_screen() ]); $this->assertAuthenticated(); - $response->assertRedirect(RouteServiceProvider::HOME); + $response->assertRedirect(route('dashboard', absolute: false)); } - public function test_users_can_not_authenticate_with_invalid_password() + public function test_users_can_not_authenticate_with_invalid_password(): void { $user = User::factory()->create(); @@ -42,4 +41,14 @@ public function test_users_can_not_authenticate_with_invalid_password() $this->assertGuest(); } + + public function test_users_can_logout(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $this->assertGuest(); + $response->assertRedirect('/'); + } } diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index e61810e..31671bd 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Auth; use App\Models\User; -use App\Providers\RouteServiceProvider; use Illuminate\Auth\Events\Verified; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; @@ -14,7 +13,7 @@ class EmailVerificationTest extends TestCase { use RefreshDatabase; - public function test_email_verification_screen_can_be_rendered() + public function test_email_verification_screen_can_be_rendered(): void { $user = User::factory()->create([ 'email_verified_at' => null, @@ -25,7 +24,7 @@ public function test_email_verification_screen_can_be_rendered() $response->assertStatus(200); } - public function test_email_can_be_verified() + public function test_email_can_be_verified(): void { $user = User::factory()->create([ 'email_verified_at' => null, @@ -43,10 +42,10 @@ public function test_email_can_be_verified() Event::assertDispatched(Verified::class); $this->assertTrue($user->fresh()->hasVerifiedEmail()); - $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1'); + $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); } - public function test_email_is_not_verified_with_invalid_hash() + public function test_email_is_not_verified_with_invalid_hash(): void { $user = User::factory()->create([ 'email_verified_at' => null, diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php index d2072ff..ff85721 100644 --- a/tests/Feature/Auth/PasswordConfirmationTest.php +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -10,7 +10,7 @@ class PasswordConfirmationTest extends TestCase { use RefreshDatabase; - public function test_confirm_password_screen_can_be_rendered() + public function test_confirm_password_screen_can_be_rendered(): void { $user = User::factory()->create(); @@ -19,7 +19,7 @@ public function test_confirm_password_screen_can_be_rendered() $response->assertStatus(200); } - public function test_password_can_be_confirmed() + public function test_password_can_be_confirmed(): void { $user = User::factory()->create(); @@ -31,7 +31,7 @@ public function test_password_can_be_confirmed() $response->assertSessionHasNoErrors(); } - public function test_password_is_not_confirmed_with_invalid_password() + public function test_password_is_not_confirmed_with_invalid_password(): void { $user = User::factory()->create(); diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php index b2cd77a..aa50350 100644 --- a/tests/Feature/Auth/PasswordResetTest.php +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -12,14 +12,14 @@ class PasswordResetTest extends TestCase { use RefreshDatabase; - public function test_reset_password_link_screen_can_be_rendered() + public function test_reset_password_link_screen_can_be_rendered(): void { $response = $this->get('/forgot-password'); $response->assertStatus(200); } - public function test_reset_password_link_can_be_requested() + public function test_reset_password_link_can_be_requested(): void { Notification::fake(); @@ -30,7 +30,7 @@ public function test_reset_password_link_can_be_requested() Notification::assertSentTo($user, ResetPassword::class); } - public function test_reset_password_screen_can_be_rendered() + public function test_reset_password_screen_can_be_rendered(): void { Notification::fake(); @@ -47,7 +47,7 @@ public function test_reset_password_screen_can_be_rendered() }); } - public function test_password_can_be_reset_with_valid_token() + public function test_password_can_be_reset_with_valid_token(): void { Notification::fake(); @@ -63,7 +63,9 @@ public function test_password_can_be_reset_with_valid_token() 'password_confirmation' => 'password', ]); - $response->assertSessionHasNoErrors(); + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('login')); return true; }); diff --git a/tests/Feature/Auth/PasswordUpdateTest.php b/tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..bbf079d --- /dev/null +++ b/tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,51 @@ +create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + + public function test_correct_password_must_be_provided_to_update_password(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrors('current_password') + ->assertRedirect('/profile'); + } +} diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index 317a827..1489d0e 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature\Auth; -use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -10,14 +9,14 @@ class RegistrationTest extends TestCase { use RefreshDatabase; - public function test_registration_screen_can_be_rendered() + public function test_registration_screen_can_be_rendered(): void { $response = $this->get('/register'); $response->assertStatus(200); } - public function test_new_users_can_register() + public function test_new_users_can_register(): void { $response = $this->post('/register', [ 'name' => 'Test User', @@ -27,6 +26,6 @@ public function test_new_users_can_register() ]); $this->assertAuthenticated(); - $response->assertRedirect(RouteServiceProvider::HOME); + $response->assertRedirect(route('dashboard', absolute: false)); } } diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php new file mode 100644 index 0000000..49886c3 --- /dev/null +++ b/tests/Feature/ProfileTest.php @@ -0,0 +1,99 @@ +create(); + + $response = $this + ->actingAs($user) + ->get('/profile'); + + $response->assertOk(); + } + + public function test_profile_information_can_be_updated(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); + } + + public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => $user->email, + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertNotNull($user->refresh()->email_verified_at); + } + + public function test_user_can_delete_their_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->delete('/profile', [ + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); + } + + public function test_correct_password_must_be_provided_to_delete_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->delete('/profile', [ + 'password' => 'wrong-password', + ]); + + $response + ->assertSessionHasErrors('password') + ->assertRedirect('/profile'); + + $this->assertNotNull($user->fresh()); + } +} diff --git a/vite.config.js b/vite.config.js index b8aed82..20de738 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,11 +3,6 @@ import laravel from 'laravel-vite-plugin'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ - server: { - hmr: { - host: 'localhost', - }, - }, plugins: [ laravel({ input: 'resources/js/app.js',