Compare commits

...

23 Commits

Author SHA1 Message Date
Saufi
3b5454f55c Fix npm ci lockfile dependencies
Some checks failed
CI/CD / Test (push) Successful in 1m6s
CI/CD / Build (push) Failing after 1m6s
CI/CD / Deploy (push) Has been skipped
2026-05-13 14:07:24 +08:00
Saufi
8c162f7ebb test 8
Some checks failed
CI/CD / Test (push) Successful in 1m5s
CI/CD / Build (push) Failing after 57s
CI/CD / Deploy (push) Has been skipped
2026-05-13 13:59:25 +08:00
Saufi
e3ab4fcd4f test 7
Some checks failed
CI/CD / Test (push) Successful in 1m15s
CI/CD / Build (push) Failing after 55s
CI/CD / Deploy (push) Has been skipped
2026-05-13 12:46:27 +08:00
Saufi
726457a7ed test runner 6 codex
Some checks failed
CI/CD / Test (push) Successful in 1m5s
CI/CD / Build (push) Failing after 2m3s
CI/CD / Deploy (push) Has been skipped
2026-05-13 12:33:06 +08:00
Saufi
01c89c44a7 test runner 5 codex
Some checks failed
CI/CD / Test (push) Failing after 1m6s
CI/CD / Build (push) Has been skipped
CI/CD / Deploy (push) Has been skipped
2026-05-13 12:28:33 +08:00
Saufi
d7ced0c43d test runner 4
Some checks failed
CI/CD / Test (push) Failing after 1m15s
CI/CD / Build (push) Has been skipped
CI/CD / Deploy (push) Has been skipped
2026-05-13 12:17:51 +08:00
Saufi
7f55f6dbe9 test runner 3
Some checks failed
CI/CD / Test (push) Failing after 1m16s
CI/CD / Build (push) Has been skipped
CI/CD / Deploy (push) Has been skipped
2026-05-13 12:12:40 +08:00
Saufi
d046b6caeb retest runner 2
Some checks failed
CI/CD / Test (push) Failing after 1m29s
CI/CD / Build (push) Has been skipped
CI/CD / Deploy (push) Has been skipped
2026-05-13 12:03:13 +08:00
Saufi
b5de75189d retest runner
Some checks failed
CI/CD / Test (push) Failing after 1m4s
CI/CD / Build (push) Has been skipped
CI/CD / Deploy (push) Has been skipped
2026-05-13 11:53:50 +08:00
Saufi
5a43390296 try runner
Some checks failed
CI/CD / Test (push) Failing after 1m29s
CI/CD / Build (push) Has been skipped
CI/CD / Deploy (push) Has been skipped
CI / Tests (PHP 8.5) (push) Failing after 1m0s
2026-05-13 11:47:45 +08:00
Saufi
31c94cea87 try deploy sh
Some checks failed
CI / Tests (PHP 8.5) (push) Failing after 2m24s
2026-05-12 16:37:50 +08:00
Saufi
d50c2b88f4 test webhook
Some checks failed
CI / Tests (PHP 8.5) (push) Has been cancelled
2026-05-12 16:11:12 +08:00
Saufi
f560ec479c pipeline 2026-05-11 16:25:14 +08:00
Saufi
7351398114 Merge branch 'user-module' 2026-05-11 14:24:29 +08:00
Saufi
8f15610aab change logo 2026-05-11 14:20:00 +08:00
Saufi
e404aee576 tambah relation user-role 2026-05-11 14:13:25 +08:00
Saufi
dc48e38ba6 merge user dan role conflict resolved 2026-05-11 12:43:43 +08:00
Saufi
eaf0ba7a4d add new role functionality 2026-05-11 12:29:11 +08:00
Saufi
a300eced76 role listing 2026-05-11 12:26:07 +08:00
Saufi
3e79e74cb7 user pagination 2026-05-11 12:20:39 +08:00
Saufi
a461f0bb42 user list 2026-05-11 12:17:32 +08:00
Saufi
0b3705573c breeze 2026-05-11 12:09:56 +08:00
Saufi
aed07c19e4 tambah title 2026-05-11 11:59:58 +08:00
89 changed files with 4247 additions and 256 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"mcp__laravel-boost__database-schema",
"Bash(php artisan *)",
"Bash(vendor/bin/pint --dirty --format agent)",
"mcp__laravel-boost__search-docs"
]
}
}

View File

@@ -10,7 +10,7 @@ metadata:
## Documentation ## Documentation
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation. Use `search-docs` for detailed Tailwind CSS v3 patterns and documentation.
## Basic Usage ## Basic Usage
@@ -18,55 +18,22 @@ Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue). - Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically. - Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
## Tailwind CSS v4 Specifics ## Tailwind CSS v3 Specifics
- Always use Tailwind CSS v4 and avoid deprecated utilities. - Always use Tailwind CSS v3 and verify you're using only classes it supports.
- `corePlugins` is not supported in Tailwind v4. - Configuration is done in the `tailwind.config.js` file.
- Import using `@tailwind` directives:
### CSS-First Configuration <!-- v3 Import Syntax -->
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
<!-- CSS-First Config -->
```css ```css
@theme { @tailwind base;
--color-brand: oklch(0.72 0.11 178); @tailwind components;
} @tailwind utilities;
``` ```
### Import Syntax
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```
### Replaced Utilities
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
## Spacing ## Spacing
Use `gap` utilities instead of margins for spacing between siblings: When listing items, use gap utilities for spacing; don't use margins.
<!-- Gap Utilities --> <!-- Gap Utilities -->
```html ```html
@@ -110,10 +77,15 @@ If existing pages and components support dark mode, new pages and components mus
</div> </div>
``` ```
## Verification
1. Check browser for visual rendering
2. Test responsive breakpoints
3. Verify dark mode if project uses it
## Common Pitfalls ## Common Pitfalls
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities - Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode - Forgetting to add dark mode variants when the project uses dark mode
- Not checking existing project conventions before adding new utilities
- Overusing inline styles when Tailwind classes would suffice

165
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,165 @@
name: CI/CD
on:
push:
branches:
- master
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, dom, curl, sqlite3, pdo_sqlite
coverage: none
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Copy environment file
run: cp .env.example .env
- name: Generate application key
run: php artisan key:generate
- name: Create SQLite database file
run: touch database/database.sqlite
- name: Run tests
env:
APP_ENV: testing
CACHE_STORE: array
DB_CONNECTION: sqlite
DB_DATABASE: ':memory:'
MAIL_MAILER: array
QUEUE_CONNECTION: sync
SESSION_DRIVER: array
run: php artisan test --compact
build:
name: Build
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, dom, curl, sqlite3, pdo_sqlite
coverage: none
- name: Install Composer dependencies (production)
run: composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install Node dependencies
run: npm ci
- name: Build frontend assets
run: npm run build
- name: Create deployment archive
run: |
tar --exclude='.git' \
--exclude='.gitea' \
--exclude='node_modules' \
--exclude='tests' \
--exclude='.env' \
--exclude='*.md' \
-czf /tmp/deploy.tar.gz .
- name: Upload deployment artifact
uses: actions/upload-artifact@v4
with:
name: deploy
path: /tmp/deploy.tar.gz
retention-days: 7
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
steps:
- name: Download deployment artifact
uses: actions/download-artifact@v4
with:
name: deploy
path: /tmp
- name: Setup SSH known hosts
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT }}
run: |
mkdir -p ~/.ssh
ssh-keyscan -p "${SSH_PORT:-22}" "$SSH_HOST" >> ~/.ssh/known_hosts
- name: Install sshpass
run: sudo apt-get install -y sshpass
- name: Upload archive to server
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
SSH_PORT: ${{ secrets.SSH_PORT }}
SSH_USER: ${{ secrets.SSH_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: |
sshpass -p "$SSH_PASSWORD" \
rsync -az --no-perms \
-e "ssh -p ${SSH_PORT:-22}" \
/tmp/deploy.tar.gz \
"$SSH_USER@$SSH_HOST:$DEPLOY_PATH/deploy.tar.gz"
- name: Extract and deploy on server
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
SSH_PORT: ${{ secrets.SSH_PORT }}
SSH_USER: ${{ secrets.SSH_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
ENV_FILE: ${{ secrets.ENV_FILE }}
run: |
sshpass -p "$SSH_PASSWORD" \
ssh -p "${SSH_PORT:-22}" \
"$SSH_USER@$SSH_HOST" << ENDSSH
set -e
DEPLOY_PATH="$DEPLOY_PATH"
cd "$DEPLOY_PATH"
# Extract archive
tar -xzf deploy.tar.gz
rm deploy.tar.gz
# Write .env from secret
cat > .env << 'ENDENV'
$ENV_FILE
ENDENV
# Run post-deploy commands
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate --force --no-interaction
php artisan queue:restart
ENDSSH

View File

@@ -10,6 +10,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.5 - php - 8.5
- laravel/breeze (BREEZE) - v2
- laravel/framework (LARAVEL) - v13 - laravel/framework (LARAVEL) - v13
- laravel/prompts (PROMPTS) - v0 - laravel/prompts (PROMPTS) - v0
- laravel/boost (BOOST) - v2 - laravel/boost (BOOST) - v2
@@ -18,7 +19,8 @@ This application is a Laravel application and its main Laravel ecosystems packag
- laravel/pint (PINT) - v1 - laravel/pint (PINT) - v1
- pestphp/pest (PEST) - v4 - pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12 - phpunit/phpunit (PHPUNIT) - v12
- tailwindcss (TAILWINDCSS) - v4 - alpinejs (ALPINEJS) - v3
- tailwindcss (TAILWINDCSS) - v3
## Skills Activation ## Skills Activation
@@ -107,6 +109,13 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. - Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
=== tests rules ===
# Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
=== laravel/core rules === === laravel/core rules ===
# Do Things the Laravel Way # Do Things the Laravel Way

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@@ -0,0 +1,45 @@
<?php
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 Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
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 Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
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.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* 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')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreRoleRequest;
use App\Models\Role;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class RoleController extends Controller
{
public function index(Request $request)
{
$roles = Role::orderBy('name')->paginate(10);
if ($request->ajax()) {
return view('roles._table', compact('roles'));
}
return view('roles.index', compact('roles'));
}
public function create(): View
{
return view('roles.create');
}
public function store(StoreRoleRequest $request): RedirectResponse
{
Role::create($request->validated());
return redirect()->route('roles.index')
->with('status', 'role-created');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UpdateUserRolesRequest;
use App\Models\Role;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UserController extends Controller
{
public function index(Request $request)
{
$users = User::orderBy('name')->paginate(10);
if ($request->ajax()) {
return view('users._table', compact('users'));
}
return view('users.index', compact('users'));
}
public function edit(User $user): View
{
$roles = Role::orderBy('name')->get();
return view('users.edit', compact('user', 'roles'));
}
public function update(UpdateUserRolesRequest $request, User $user): RedirectResponse
{
$user->roles()->sync($request->validated()['roles'] ?? []);
return redirect()->route('users.index')
->with('status', 'user-updated');
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
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),
],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreRoleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255', 'unique:roles,name'],
'description' => ['nullable', 'string', 'max:500'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserRolesRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'roles' => ['nullable', 'array'],
'roles.*' => ['integer', 'exists:roles,id'],
];
}
}

21
app/Models/Role.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Database\Factories\RoleFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
/** @use HasFactory<RoleFactory> */
use HasFactory;
protected $fillable = ['name', 'description'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}

View File

@@ -7,6 +7,7 @@ use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -29,4 +30,9 @@ class User extends Authenticatable
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
} }

View File

@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}

View File

@@ -10,6 +10,7 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.3", "php": "^8.3",
"laravel/breeze": "^2.4",
"laravel/framework": "^13.7", "laravel/framework": "^13.7",
"laravel/tinker": "^3.0" "laravel/tinker": "^3.0"
}, },
@@ -89,4 +90,4 @@
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true "prefer-stable": true
} }

63
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2a673e530352295ff8331dc8031ebd18", "content-hash": "33b22d5f2ae7e6c33f459e27f7d1f18b",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -1053,6 +1053,67 @@
], ],
"time": "2025-08-22T14:27:06+00:00" "time": "2025-08-22T14:27:06+00:00"
}, },
{
"name": "laravel/breeze",
"version": "v2.4.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/breeze.git",
"reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/breeze/zipball/28cefeaf6af20177ddf5cc7b93e87e4ad79d533f",
"reference": "28cefeaf6af20177ddf5cc7b93e87e4ad79d533f",
"shasum": ""
},
"require": {
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/filesystem": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"illuminate/validation": "^11.0|^12.0|^13.0",
"php": "^8.2.0",
"symfony/console": "^7.0|^8.0"
},
"require-dev": {
"laravel/framework": "^11.0|^12.0|^13.0",
"orchestra/testbench-core": "^9.0|^10.0|^11.0",
"phpstan/phpstan": "^2.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Breeze\\BreezeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Breeze\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.",
"keywords": [
"auth",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/breeze/issues",
"source": "https://github.com/laravel/breeze"
},
"time": "2026-03-10T19:59:01+00:00"
},
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v13.8.0", "version": "v13.8.0",

View File

@@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Role;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Role>
*/
class RoleFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->unique()->word(),
'description' => fake()->sentence(),
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('description')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('roles');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->primary(['user_id', 'role_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('role_user');
}
};

View File

@@ -0,0 +1,22 @@
<?php
namespace Database\Seeders;
use App\Models\Role;
use Illuminate\Database\Seeder;
class RoleSeeder extends Seeder
{
public function run(): void
{
$roles = [
['name' => 'Admin', 'description' => 'Full access to all resources'],
['name' => 'Editor', 'description' => 'Can create and edit content'],
['name' => 'Viewer', 'description' => 'Read-only access'],
];
foreach ($roles as $role) {
Role::firstOrCreate(['name' => $role['name']], $role);
}
}
}

1390
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,17 @@
"dev": "vite" "dev": "vite"
}, },
"devDependencies": { "devDependencies": {
"@emnapi/core": "^1.10.0",
"@emnapi/runtime": "^1.10.0",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.16.0",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.1", "laravel-vite-plugin": "^3.1",
"tailwindcss": "^4.0.0", "postcss": "^8.4.31",
"tailwindcss": "^3.1.0",
"vite": "^8.0.0" "vite": "^8.0.0"
} }
} }

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,11 +1,3 @@
@import 'tailwindcss'; @tailwind base;
@tailwind components;
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @tailwind utilities;
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}

View File

@@ -1 +1,7 @@
// import './bootstrap';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

4
resources/js/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@@ -0,0 +1,27 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,25 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
</div>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('password.email') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,47 @@
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
<x-primary-button class="ms-3">
{{ __('Log in') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,52 @@
<x-guest-layout>
<form method="POST" action="{{ route('register') }}">
@csrf
<!-- Name -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<x-primary-button class="ms-4">
{{ __('Register') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,39 @@
<x-guest-layout>
<form method="POST" action="{{ route('password.store') }}">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Reset Password') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,31 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('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.') }}
</div>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
{{ __('Log Out') }}
</button>
</form>
</div>
</x-guest-layout>

View File

@@ -0,0 +1,4 @@
<!-- <svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg> -->
<img src="/images/MBIP-2024-300x300.png" {{ $attributes }}>

View File

@@ -0,0 +1,7 @@
@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600 dark:text-green-400']) }}>
{{ $status }}
</div>
@endif

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@@ -0,0 +1,35 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700'])
@php
$alignmentClasses = match ($align) {
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
'top' => 'origin-top',
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
};
$width = match ($width) {
'48' => 'w-48',
default => $width,
};
@endphp
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
{{ $content }}
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
@props(['messages'])
@if ($messages)
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}>
@foreach ((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
@endif

View File

@@ -0,0 +1,5 @@
@props(['value'])
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}>
{{ $value ?? $slot }}
</label>

View File

@@ -0,0 +1,78 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
</div>
<div
x-show="show"
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
{{ $slot }}
</div>
</div>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,3 @@
@props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) }}>

View File

@@ -0,0 +1,17 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
{{ __("You're logged in!") }}
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
@include('layouts.navigation')
<!-- Page Heading -->
@isset($header)
<header class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,112 @@
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
<x-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')">
{{ __('Users') }}
</x-nav-link>
<x-nav-link :href="route('roles.index')" :active="request()->routeIs('roles.*')">
{{ __('Roles') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('users.index')" :active="request()->routeIs('users.*')">
{{ __('Users') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('roles.index')" :active="request()->routeIs('roles.*')">
{{ __('Roles') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div class="px-4">
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>

View File

@@ -0,0 +1,29 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Profile') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-profile-information-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-password-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.delete-user-form')
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,55 @@
<section class="space-y-6">
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Delete Account') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
</p>
</header>
<x-danger-button
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
>{{ __('Delete Account') }}</x-danger-button>
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Are you sure you want to delete your account?') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>
<div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-danger-button class="ms-3">
{{ __('Delete Account') }}
</x-danger-button>
</div>
</form>
</x-modal>
</section>

View File

@@ -0,0 +1,48 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Update Password') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Ensure your account is using a long, random password to stay secure.') }}
</p>
</header>
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
@csrf
@method('put')
<div>
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password" :value="__('New Password')" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'password-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@@ -0,0 +1,64 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Profile Information') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __("Update your account's profile information and email address.") }}
</p>
</header>
<form id="send-verification" method="post" action="{{ route('verification.send') }}">
@csrf
</form>
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
@csrf
@method('patch')
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
<x-input-error class="mt-2" :messages="$errors->get('email')" />
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div>
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
{{ __('Your email address is unverified.') }}
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@if (session('status') === 'verification-link-sent')
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
{{ __('A new verification link has been sent to your email address.') }}
</p>
@endif
</div>
@endif
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'profile-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@@ -0,0 +1,34 @@
<table class="w-full text-sm text-left text-gray-900 dark:text-gray-100">
<thead class="text-xs text-gray-700 dark:text-gray-400 uppercase bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3">#</th>
<th class="px-6 py-3">{{ __('Name') }}</th>
<th class="px-6 py-3">{{ __('Description') }}</th>
<th class="px-6 py-3">{{ __('Created') }}</th>
</tr>
</thead>
<tbody>
@foreach ($roles as $role)
<tr class="{{ $loop->even ? 'bg-gray-50 dark:bg-gray-700' : 'bg-white dark:bg-gray-800' }} border-b border-gray-200 dark:border-gray-600">
<td class="px-6 py-4">{{ $roles->firstItem() + $loop->index }}</td>
<td class="px-6 py-4 font-medium">{{ $role->name }}</td>
<td class="px-6 py-4">{{ $role->description ?? '—' }}</td>
<td class="px-6 py-4">{{ $role->created_at->format('d M Y') }}</td>
</tr>
@endforeach
@if ($roles->isEmpty())
<tr>
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
{{ __('No roles found.') }}
</td>
</tr>
@endif
</tbody>
</table>
@if ($roles->hasPages())
<div class="mt-4">
{{ $roles->links() }}
</div>
@endif

View File

@@ -0,0 +1,56 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('New Role') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Role Details') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Create a new role to assign to users in the system.') }}
</p>
</header>
<form method="post" action="{{ route('roles.store') }}" class="mt-6 space-y-6">
@csrf
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name')" required autofocus autocomplete="off" />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
<div>
<x-input-label for="description" :value="__('Description')" />
<textarea
id="description"
name="description"
rows="3"
class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
>{{ old('description') }}</textarea>
<x-input-error class="mt-2" :messages="$errors->get('description')" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Create Role') }}</x-primary-button>
<a href="{{ route('roles.index') }}" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">
{{ __('Cancel') }}
</a>
</div>
</form>
</section>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,59 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Roles') }}
</h2>
<a href="{{ route('roles.create') }}">
<x-primary-button>{{ __('New Role') }}</x-primary-button>
</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
@if (session('status') === 'role-created')
<div
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 3000)"
class="mb-4 p-4 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-sm rounded-lg"
>
{{ __('Role created successfully.') }}
</div>
@endif
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div
class="p-6"
x-data="{
loading: false,
async paginate(url) {
this.loading = true;
const response = await fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
this.$refs.tableContainer.innerHTML = await response.text();
this.loading = false;
}
}"
@click.prevent="
const link = $event.target.closest('a[href]');
if (link && $refs.tableContainer.contains(link)) {
paginate(link.href);
}
"
>
<div x-show="loading" class="text-center py-4 text-gray-500 dark:text-gray-400">
{{ __('Loading...') }}
</div>
<div x-ref="tableContainer" x-show="!loading">
@include('roles._table')
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,40 @@
<table class="w-full text-sm text-left text-gray-900 dark:text-gray-100">
<thead class="text-xs text-gray-700 dark:text-gray-400 uppercase bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3">#</th>
<th class="px-6 py-3">{{ __('Name') }}</th>
<th class="px-6 py-3">{{ __('Email') }}</th>
<th class="px-6 py-3">{{ __('Joined') }}</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody>
@foreach ($users as $user)
<tr class="{{ $loop->even ? 'bg-gray-50 dark:bg-gray-700' : 'bg-white dark:bg-gray-800' }} border-b border-gray-200 dark:border-gray-600">
<td class="px-6 py-4">{{ $users->firstItem() + $loop->index }}</td>
<td class="px-6 py-4 font-medium">{{ $user->name }}</td>
<td class="px-6 py-4">{{ $user->email }}</td>
<td class="px-6 py-4">{{ $user->created_at->format('d M Y') }}</td>
<td class="px-6 py-4 text-right">
<a href="{{ route('users.edit', $user) }}" class="text-indigo-600 dark:text-indigo-400 hover:underline text-sm font-medium">
{{ __('Edit') }}
</a>
</td>
</tr>
@endforeach
@if ($users->isEmpty())
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
{{ __('No users found.') }}
</td>
</tr>
@endif
</tbody>
</table>
@if ($users->hasPages())
<div class="mt-4">
{{ $users->links() }}
</div>
@endif

View File

@@ -0,0 +1,65 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Edit User') }}: {{ $user->name }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Assign Roles') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Select the roles to assign to this user.') }}
</p>
</header>
<form method="post" action="{{ route('users.update', $user) }}" class="mt-6 space-y-6">
@csrf
@method('put')
<div class="space-y-3">
@forelse ($roles as $role)
<label class="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
name="roles[]"
value="{{ $role->id }}"
{{ $user->roles->contains($role) ? 'checked' : '' }}
class="mt-0.5 rounded border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900"
/>
<div>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $role->name }}</span>
@if ($role->description)
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $role->description }}</p>
@endif
</div>
</label>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('No roles available. Create one first.') }}</p>
@endforelse
</div>
<x-input-error class="mt-2" :messages="$errors->get('roles')" />
<x-input-error class="mt-2" :messages="$errors->get('roles.*')" />
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save Roles') }}</x-primary-button>
<a href="{{ route('users.index') }}" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">
{{ __('Cancel') }}
</a>
</div>
</form>
</section>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,43 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Users') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div
class="p-6"
x-data="{
loading: false,
async paginate(url) {
this.loading = true;
const response = await fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
this.$refs.tableContainer.innerHTML = await response.text();
this.loading = false;
}
}"
@click="
const link = $event.target.closest('a[href]');
if (link && link.closest('nav[role=navigation]') && $refs.tableContainer.contains(link)) {
$event.preventDefault();
paginate(link.href);
}
"
>
<div x-show="loading" class="text-center py-4 text-gray-500 dark:text-gray-400">
{{ __('Loading...') }}
</div>
<div x-ref="tableContainer" x-show="!loading">
@include('users._table')
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -47,174 +47,17 @@
</nav> </nav>
@endif @endif
</header> </header>
<div class="flex items-center justify-center w-full transition-opacity opacity-100 duration-750 lg:grow starting:opacity-0">
<main class="flex max-w-[335px] w-full flex-col-reverse lg:max-w-4xl lg:flex-row">
<div class="text-[13px] leading-[20px] flex-1 p-6 pb-6 lg:p-20 lg:pb-10 bg-white dark:bg-[#161615] dark:text-[#EDEDEC] shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d] rounded-bl-lg rounded-br-lg lg:rounded-tl-lg lg:rounded-br-none">
<h1 class="mb-1 font-medium">Let's get started</h1>
<p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">With so many options available to you,<br /> we suggest you start with the following:</p>
<ul class="flex flex-col mb-4 lg:mb-6">
<li class="flex items-center gap-4 py-2 relative before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A] before:top-1/2 before:bottom-0 before:left-[0.4rem] before:absolute">
<span class="relative py-1 bg-white dark:bg-[#161615]">
<span class="flex items-center justify-center rounded-full bg-[#FDFDFC] dark:bg-[#161615] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] w-3.5 h-3.5 border dark:border-[#3E3E3A] border-[#e3e3e0]">
<span class="rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A] w-1.5 h-1.5"></span>
</span>
</span>
<span>
Read the
<a href="https://laravel.com/docs" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1">
<span>Documentation</span>
<svg
width="10"
height="11"
viewBox="0 0 10 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-2.5 h-2.5"
>
<path
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
stroke="currentColor"
stroke-linecap="square"
/>
</svg>
</a>
</span>
</li>
<li class="flex items-center gap-4 py-2 relative before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A] before:bottom-1/2 before:top-0 before:left-[0.4rem] before:absolute">
<span class="relative py-1 bg-white dark:bg-[#161615]">
<span class="flex items-center justify-center rounded-full bg-[#FDFDFC] dark:bg-[#161615] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] w-3.5 h-3.5 border dark:border-[#3E3E3A] border-[#e3e3e0]">
<span class="rounded-full bg-[#dbdbd7] dark:bg-[#3E3E3A] w-1.5 h-1.5"></span>
</span>
</span>
<span>
Watch video tutorials at
<a href="https://laracasts.com" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1">
<span>Laracasts</span>
<svg
width="10"
height="11"
viewBox="0 0 10 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-2.5 h-2.5"
>
<path
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
stroke="currentColor"
stroke-linecap="square"
/>
</svg>
</a>
</span>
</li>
</ul>
<ul class="flex gap-3 text-sm leading-normal">
<li>
<a href="https://cloud.laravel.com" target="_blank" class="inline-block dark:bg-[#eeeeec] dark:border-[#eeeeec] dark:text-[#1C1C1A] dark:hover:bg-white dark:hover:border-white hover:bg-black hover:border-black px-5 py-1.5 bg-[#1b1b18] rounded-sm border border-black text-white text-sm leading-normal">
Deploy now
</a>
</li>
</ul>
<p class="mt-6 lg:mt-10 text-[#706f6c] dark:text-[#A1A09A]"> <h1 class="text-5xl text-center">BELAJAR GIT COURSE</h1>
v{{ app()->version() }} <h3>GIT webhook</h3>
<a href="https://github.com/laravel/framework/blob/13.x/CHANGELOG.md" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1">
<span>View changelog</span> <p>hello world</p>
<svg
width="10" <p>my name is saufi</p>
height="11"
viewBox="0 0 10 11" <p>this is git class to use gitea</p>
fill="none" <p>test runn</p>
xmlns="http://www.w3.org/2000/svg" <p>test runner</p>
class="w-2.5 h-2.5"
>
<path
d="M7.70833 6.95834V2.79167H3.54167M2.5 8L7.5 3.00001"
stroke="currentColor"
stroke-linecap="square"
/>
</svg>
</a>
</p>
</div>
<div class="bg-[#fff2f2] dark:bg-[#1D0002] relative lg:-ml-px -mb-px lg:mb-0 rounded-t-lg lg:rounded-t-none lg:rounded-r-lg aspect-[335/364] lg:aspect-auto w-full lg:w-[438px] shrink-0 overflow-hidden">
{{-- Laravel Logo --}}
<svg class="w-full text-[#F53003] dark:text-[#F61500] transition-all translate-y-0 opacity-100 max-w-none duration-750 starting:opacity-0 motion-safe:starting:translate-y-6" viewBox="0 0 438 104" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.2036 -3H0V102.197H49.5189V86.7187H17.2036V-3Z" fill="currentColor" />
<path d="M110.256 41.6337C108.061 38.1275 104.945 35.3731 100.905 33.3681C96.8667 31.3647 92.8016 30.3618 88.7131 30.3618C83.4247 30.3618 78.5885 31.3389 74.201 33.2923C69.8111 35.2456 66.0474 37.928 62.9059 41.3333C59.7643 44.7401 57.3198 48.6726 55.5754 53.1293C53.8287 57.589 52.9572 62.274 52.9572 67.1813C52.9572 72.1925 53.8287 76.8995 55.5754 81.3069C57.3191 85.7173 59.7636 89.6241 62.9059 93.0293C66.0474 96.4361 69.8119 99.1155 74.201 101.069C78.5885 103.022 83.4247 103.999 88.7131 103.999C92.8016 103.999 96.8667 102.997 100.905 100.994C104.945 98.9911 108.061 96.2359 110.256 92.7282V102.195H126.563V32.1642H110.256V41.6337ZM108.76 75.7472C107.762 78.4531 106.366 80.8078 104.572 82.8112C102.776 84.8161 100.606 86.4183 98.0637 87.6206C95.5202 88.823 92.7004 89.4238 89.6103 89.4238C86.5178 89.4238 83.7252 88.823 81.2324 87.6206C78.7388 86.4183 76.5949 84.8161 74.7998 82.8112C73.004 80.8078 71.6319 78.4531 70.6856 75.7472C69.7356 73.0421 69.2644 70.1868 69.2644 67.1821C69.2644 64.1758 69.7356 61.3205 70.6856 58.6154C71.6319 55.9102 73.004 53.5571 74.7998 51.5522C76.5949 49.5495 78.738 47.9451 81.2324 46.7427C83.7252 45.5404 86.5178 44.9396 89.6103 44.9396C92.7012 44.9396 95.5202 45.5404 98.0637 46.7427C100.606 47.9451 102.776 49.5487 104.572 51.5522C106.367 53.5571 107.762 55.9102 108.76 58.6154C109.756 61.3205 110.256 64.1758 110.256 67.1821C110.256 70.1868 109.756 73.0421 108.76 75.7472Z" fill="currentColor" />
<path d="M242.805 41.6337C240.611 38.1275 237.494 35.3731 233.455 33.3681C229.416 31.3647 225.351 30.3618 221.262 30.3618C215.974 30.3618 211.138 31.3389 206.75 33.2923C202.36 35.2456 198.597 37.928 195.455 41.3333C192.314 44.7401 189.869 48.6726 188.125 53.1293C186.378 57.589 185.507 62.274 185.507 67.1813C185.507 72.1925 186.378 76.8995 188.125 81.3069C189.868 85.7173 192.313 89.6241 195.455 93.0293C198.597 96.4361 202.361 99.1155 206.75 101.069C211.138 103.022 215.974 103.999 221.262 103.999C225.351 103.999 229.416 102.997 233.455 100.994C237.494 98.9911 240.611 96.2359 242.805 92.7282V102.195H259.112V32.1642H242.805V41.6337ZM241.31 75.7472C240.312 78.4531 238.916 80.8078 237.122 82.8112C235.326 84.8161 233.156 86.4183 230.614 87.6206C228.07 88.823 225.251 89.4238 222.16 89.4238C219.068 89.4238 216.275 88.823 213.782 87.6206C211.289 86.4183 209.145 84.8161 207.35 82.8112C205.554 80.8078 204.182 78.4531 203.236 75.7472C202.286 73.0421 201.814 70.1868 201.814 67.1821C201.814 64.1758 202.286 61.3205 203.236 58.6154C204.182 55.9102 205.554 53.5571 207.35 51.5522C209.145 49.5495 211.288 47.9451 213.782 46.7427C216.275 45.5404 219.068 44.9396 222.16 44.9396C225.251 44.9396 228.07 45.5404 230.614 46.7427C233.156 47.9451 235.326 49.5487 237.122 51.5522C238.917 53.5571 240.312 55.9102 241.31 58.6154C242.306 61.3205 242.806 64.1758 242.806 67.1821C242.805 70.1868 242.305 73.0421 241.31 75.7472Z" fill="currentColor" />
<path d="M438 -3H421.694V102.197H438V-3Z" fill="currentColor" />
<path d="M139.43 102.197H155.735V48.2834H183.712V32.1665H139.43V102.197Z" fill="currentColor" />
<path d="M324.49 32.1665L303.995 85.794L283.498 32.1665H266.983L293.748 102.197H314.242L341.006 32.1665H324.49Z" fill="currentColor" />
<path d="M376.571 30.3656C356.603 30.3656 340.797 46.8497 340.797 67.1828C340.797 89.6597 356.094 104 378.661 104C391.29 104 399.354 99.1488 409.206 88.5848L398.189 80.0226C398.183 80.031 389.874 90.9895 377.468 90.9895C363.048 90.9895 356.977 79.3111 356.977 73.269H411.075C413.917 50.1328 398.775 30.3656 376.571 30.3656ZM357.02 61.0967C357.145 59.7487 359.023 43.3761 376.442 43.3761C393.861 43.3761 395.978 59.7464 396.099 61.0967H357.02Z" fill="currentColor" />
</svg>
{{-- 13 --}}
<svg class="w-[438px] max-w-none relative -mt-[6.6rem] -ml-8 lg:ml-0 [--stroke-color:#1B1B18] dark:[--stroke-color:#FF750F]" viewBox="0 0 440 392" fill="none" xmlns="http://www.w3.org/2000/svg">
<g class="mix-blend-darken dark:mix-blend-normal transition-all delay-300 opacity-100 duration-750 starting:opacity-0 text-[#1B1B18] dark:text-black">
<mask id="path-1-mask" maskUnits="userSpaceOnUse" x="-0.328613" y="103" width="338" height="299" fill="black">
<rect fill="white" x="-0.328613" y="103" width="338" height="299"/>
<path d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z"/>
<path d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z"/>
</mask>
<path d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z" fill="currentColor"/>
<path d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z" fill="currentColor"/>
<path d="M234.936 400.8C204.136 400.8 178.936 392.4 159.336 375.6C140.136 358.8 130.536 337 130.536 310.2H200.736C200.736 318.2 203.736 324.8 209.736 330C215.736 335.2 223.736 337.8 233.736 337.8C243.336 337.8 251.136 335 257.136 329.4C263.536 323.8 266.736 316.6 266.736 307.8C266.736 299.8 263.936 293.2 258.336 288C252.736 282.8 245.536 280.2 236.736 280.2H199.536V218.4H236.736C243.536 218.4 249.336 216 254.136 211.2C258.936 206.4 261.336 200.4 261.336 193.2C261.336 184.8 258.736 178.2 253.536 173.4C248.336 168.6 241.736 166.2 233.736 166.2C226.536 166.2 220.336 168.4 215.136 172.8C210.336 177.2 207.936 182.8 207.936 189.6H141.336C141.336 164.8 150.136 144.6 167.736 129C185.336 113 207.936 105 235.536 105C263.136 105 285.536 112.2 302.736 126.6C320.336 141 329.136 160 329.136 183.6C329.136 200.8 324.536 214.8 315.336 225.6C306.136 236 294.336 243.2 279.936 247.2C297.136 252 310.736 260.2 320.736 271.8C331.136 283.4 336.336 298 336.336 315.6C336.336 340.4 326.936 360.8 308.136 376.8C289.336 392.8 264.936 400.8 234.936 400.8Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-1-mask)"/>
<path d="M26.8714 167.6H1.67139V105.2H94.6714V400.2H26.8714V167.6Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-1-mask)"/>
</g>
<g class="transition-all delay-400 opacity-100 duration-750 starting:opacity-0 motion-safe:starting:-translate-x-[26px] text-[#F3BEC7] dark:text-[#4B0600]">
<mask id="path-2-mask" maskUnits="userSpaceOnUse" x="25.3357" y="103" width="338" height="299" fill="black">
<rect fill="white" x="25.3357" y="103" width="338" height="299"/>
<path d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z"/>
<path d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z"/>
</mask>
<path d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z" fill="currentColor"/>
<path d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z" fill="currentColor"/>
<path d="M260.6 400.8C229.8 400.8 204.6 392.4 185 375.6C165.8 358.8 156.2 337 156.2 310.2H226.4C226.4 318.2 229.4 324.8 235.4 330C241.4 335.2 249.4 337.8 259.4 337.8C269 337.8 276.8 335 282.8 329.4C289.2 323.8 292.4 316.6 292.4 307.8C292.4 299.8 289.6 293.2 284 288C278.4 282.8 271.2 280.2 262.4 280.2H225.2V218.4H262.4C269.2 218.4 275 216 279.8 211.2C284.6 206.4 287 200.4 287 193.2C287 184.8 284.4 178.2 279.2 173.4C274 168.6 267.4 166.2 259.4 166.2C252.2 166.2 246 168.4 240.8 172.8C236 177.2 233.6 182.8 233.6 189.6H167C167 164.8 175.8 144.6 193.4 129C211 113 233.6 105 261.2 105C288.8 105 311.2 112.2 328.4 126.6C346 141 354.8 160 354.8 183.6C354.8 200.8 350.2 214.8 341 225.6C331.8 236 320 243.2 305.6 247.2C322.8 252 336.4 260.2 346.4 271.8C356.8 283.4 362 298 362 315.6C362 340.4 352.6 360.8 333.8 376.8C315 392.8 290.6 400.8 260.6 400.8Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-2-mask)"/>
<path d="M52.5357 167.6H27.3357V105.2H120.336V400.2H52.5357V167.6Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-2-mask)"/>
</g>
<g class="mix-blend-color dark:mix-blend-hard-light transition-all delay-400 opacity-100 duration-750 starting:opacity-0 motion-safe:starting:-translate-x-[51px] text-[#F8B803] dark:text-[#391800]">
<mask id="path-3-mask" maskUnits="userSpaceOnUse" x="51" y="103" width="338" height="299" fill="black">
<rect fill="white" x="51" y="103" width="338" height="299"/>
<path d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"/>
<path d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"/>
</mask>
<path d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z" fill="currentColor"/>
<path d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z" fill="currentColor"/>
<path d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-3-mask)"/>
<path d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-3-mask)"/>
</g>
<g class="mix-blend-multiply dark:mix-blend-normal transition-all delay-400 opacity-100 duration-750 starting:opacity-0 motion-safe:starting:-translate-x-[78px] text-[#F3BEC7] dark:text-[#733000]">
<mask id="path-4-mask" maskUnits="userSpaceOnUse" x="76.6643" y="103" width="338" height="299" fill="black">
<rect fill="white" x="76.6643" y="103" width="338" height="299"/>
<path d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z"/>
<path d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z"/>
</mask>
<path d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z" fill="currentColor"/>
<path d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z" fill="currentColor"/>
<path d="M311.929 400.8C281.129 400.8 255.929 392.4 236.329 375.6C217.129 358.8 207.529 337 207.529 310.2H277.729C277.729 318.2 280.729 324.8 286.729 330C292.729 335.2 300.729 337.8 310.729 337.8C320.329 337.8 328.129 335 334.129 329.4C340.529 323.8 343.729 316.6 343.729 307.8C343.729 299.8 340.929 293.2 335.329 288C329.729 282.8 322.529 280.2 313.729 280.2H276.529V218.4H313.729C320.529 218.4 326.329 216 331.129 211.2C335.929 206.4 338.329 200.4 338.329 193.2C338.329 184.8 335.729 178.2 330.529 173.4C325.329 168.6 318.729 166.2 310.729 166.2C303.529 166.2 297.329 168.4 292.129 172.8C287.329 177.2 284.929 182.8 284.929 189.6H218.329C218.329 164.8 227.129 144.6 244.729 129C262.329 113 284.929 105 312.529 105C340.129 105 362.529 112.2 379.729 126.6C397.329 141 406.129 160 406.129 183.6C406.129 200.8 401.529 214.8 392.329 225.6C383.129 236 371.329 243.2 356.929 247.2C374.129 252 387.729 260.2 397.729 271.8C408.129 283.4 413.329 298 413.329 315.6C413.329 340.4 403.929 360.8 385.129 376.8C366.329 392.8 341.929 400.8 311.929 400.8Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-4-mask)"/>
<path d="M103.864 167.6H78.6643V105.2H171.664V400.2H103.864V167.6Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-4-mask)"/>
</g>
<g class="mix-blend-hard-light transition-all delay-400 opacity-100 duration-750 starting:opacity-0 motion-safe:starting:-translate-x-[102px] text-[#F3BEC7] dark:text-[#4B0600]">
<mask id="path-5-mask" maskUnits="userSpaceOnUse" x="102.329" y="103" width="338" height="299" fill="black">
<rect fill="white" x="102.329" y="103" width="338" height="299"/>
<path d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z"/>
<path d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z"/>
</mask>
<path d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z" fill="currentColor"/>
<path d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z" fill="currentColor"/>
<path d="M337.593 400.8C306.793 400.8 281.593 392.4 261.993 375.6C242.793 358.8 233.193 337 233.193 310.2H303.393C303.393 318.2 306.393 324.8 312.393 330C318.393 335.2 326.393 337.8 336.393 337.8C345.993 337.8 353.793 335 359.793 329.4C366.193 323.8 369.393 316.6 369.393 307.8C369.393 299.8 366.593 293.2 360.993 288C355.393 282.8 348.193 280.2 339.393 280.2H302.193V218.4H339.393C346.193 218.4 351.993 216 356.793 211.2C361.593 206.4 363.993 200.4 363.993 193.2C363.993 184.8 361.393 178.2 356.193 173.4C350.993 168.6 344.393 166.2 336.393 166.2C329.193 166.2 322.993 168.4 317.793 172.8C312.993 177.2 310.593 182.8 310.593 189.6H243.993C243.993 164.8 252.793 144.6 270.393 129C287.993 113 310.593 105 338.193 105C365.793 105 388.193 112.2 405.393 126.6C422.993 141 431.793 160 431.793 183.6C431.793 200.8 427.193 214.8 417.993 225.6C408.793 236 396.993 243.2 382.593 247.2C399.793 252 413.393 260.2 423.393 271.8C433.793 283.4 438.993 298 438.993 315.6C438.993 340.4 429.593 360.8 410.793 376.8C391.993 392.8 367.593 400.8 337.593 400.8Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-5-mask)"/>
<path d="M129.529 167.6H104.329V105.2H197.329V400.2H129.529V167.6Z" stroke="var(--stroke-color)" stroke-width="2.4" mask="url(#path-5-mask)"/>
</g>
</svg>
<div class="absolute inset-0 rounded-t-lg lg:rounded-t-none lg:rounded-r-lg shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d]"></div>
</div>
</main>
</div>
@if (Route::has('login')) @if (Route::has('login'))
<div class="h-14.5 hidden lg:block"></div> <div class="h-14.5 hidden lg:block"></div>

59
routes/auth.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
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;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
->name('password.email');
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
->name('password.reset');
Route::post('reset-password', [NewPasswordController::class, 'store'])
->name('password.store');
});
Route::middleware('auth')->group(function () {
Route::get('verify-email', EmailVerificationPromptController::class)
->name('verification.notice');
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');
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
->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');
});

View File

@@ -1,7 +1,28 @@
<?php <?php
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\RoleController;
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/users/{user}/edit', [UserController::class, 'edit'])->name('users.edit');
Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update');
Route::get('/roles', [RoleController::class, 'index'])->name('roles.index');
Route::get('/roles/create', [RoleController::class, 'create'])->name('roles.create');
Route::post('/roles', [RoleController::class, 'store'])->name('roles.store');
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';

21
tailwind.config.js Normal file
View File

@@ -0,0 +1,21 @@
import defaultTheme from 'tailwindcss/defaultTheme';
import forms from '@tailwindcss/forms';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php',
'./resources/views/**/*.blade.php',
],
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [forms],
};

View File

@@ -0,0 +1,41 @@
<?php
use App\Models\User;
test('login screen can be rendered', function () {
$response = $this->get('/login');
$response->assertStatus(200);
});
test('users can authenticate using the login screen', function () {
$user = User::factory()->create();
$response = $this->post('/login', [
'email' => $user->email,
'password' => 'password',
]);
$this->assertAuthenticated();
$response->assertRedirect(route('dashboard', absolute: false));
});
test('users can not authenticate with invalid password', function () {
$user = User::factory()->create();
$this->post('/login', [
'email' => $user->email,
'password' => 'wrong-password',
]);
$this->assertGuest();
});
test('users can logout', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/logout');
$this->assertGuest();
$response->assertRedirect('/');
});

View File

@@ -0,0 +1,46 @@
<?php
use App\Models\User;
use Illuminate\Auth\Events\Verified;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;
test('email verification screen can be rendered', function () {
$user = User::factory()->unverified()->create();
$response = $this->actingAs($user)->get('/verify-email');
$response->assertStatus(200);
});
test('email can be verified', function () {
$user = User::factory()->unverified()->create();
Event::fake();
$verificationUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
['id' => $user->id, 'hash' => sha1($user->email)]
);
$response = $this->actingAs($user)->get($verificationUrl);
Event::assertDispatched(Verified::class);
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
});
test('email is not verified with invalid hash', function () {
$user = User::factory()->unverified()->create();
$verificationUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
['id' => $user->id, 'hash' => sha1('wrong-email')]
);
$this->actingAs($user)->get($verificationUrl);
expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
});

View File

@@ -0,0 +1,32 @@
<?php
use App\Models\User;
test('confirm password screen can be rendered', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/confirm-password');
$response->assertStatus(200);
});
test('password can be confirmed', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/confirm-password', [
'password' => 'password',
]);
$response->assertRedirect();
$response->assertSessionHasNoErrors();
});
test('password is not confirmed with invalid password', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/confirm-password', [
'password' => 'wrong-password',
]);
$response->assertSessionHasErrors();
});

View File

@@ -0,0 +1,60 @@
<?php
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Notification;
test('reset password link screen can be rendered', function () {
$response = $this->get('/forgot-password');
$response->assertStatus(200);
});
test('reset password link can be requested', function () {
Notification::fake();
$user = User::factory()->create();
$this->post('/forgot-password', ['email' => $user->email]);
Notification::assertSentTo($user, ResetPassword::class);
});
test('reset password screen can be rendered', function () {
Notification::fake();
$user = User::factory()->create();
$this->post('/forgot-password', ['email' => $user->email]);
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
$response = $this->get('/reset-password/'.$notification->token);
$response->assertStatus(200);
return true;
});
});
test('password can be reset with valid token', function () {
Notification::fake();
$user = User::factory()->create();
$this->post('/forgot-password', ['email' => $user->email]);
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
$response = $this->post('/reset-password', [
'token' => $notification->token,
'email' => $user->email,
'password' => 'password',
'password_confirmation' => 'password',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('login'));
return true;
});
});

View File

@@ -0,0 +1,40 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Hash;
test('password can be updated', function () {
$user = User::factory()->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));
});
test('correct password must be provided to update password', function () {
$user = User::factory()->create();
$response = $this
->actingAs($user)
->from('/profile')
->put('/password', [
'current_password' => 'wrong-password',
'password' => 'new-password',
'password_confirmation' => 'new-password',
]);
$response
->assertSessionHasErrorsIn('updatePassword', 'current_password')
->assertRedirect('/profile');
});

View File

@@ -0,0 +1,19 @@
<?php
test('registration screen can be rendered', function () {
$response = $this->get('/register');
$response->assertStatus(200);
});
test('new users can register', function () {
$response = $this->post('/register', [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password',
'password_confirmation' => 'password',
]);
$this->assertAuthenticated();
$response->assertRedirect(route('dashboard', absolute: false));
});

View File

@@ -0,0 +1,85 @@
<?php
use App\Models\Role;
use App\Models\User;
test('guests cannot access the edit user page', function () {
$user = User::factory()->create();
$this->get(route('users.edit', $user))->assertRedirect('/login');
});
test('authenticated users can access the edit user page', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get(route('users.edit', $user))
->assertOk()
->assertViewIs('users.edit')
->assertViewHas('user', $user)
->assertViewHas('roles');
});
test('edit page shows all available roles', function () {
$roles = Role::factory()->count(3)->create();
$user = User::factory()->create();
$this->actingAs($user)
->get(route('users.edit', $user))
->assertOk()
->assertViewHas('roles', fn ($viewRoles) => $viewRoles->count() === $roles->count());
});
test('can assign roles to a user', function () {
$user = User::factory()->create();
$roles = Role::factory()->count(2)->create();
$this->actingAs($user)
->put(route('users.update', $user), ['roles' => $roles->pluck('id')->all()])
->assertRedirect(route('users.index'))
->assertSessionHas('status', 'user-updated');
expect($user->roles()->count())->toBe(2);
});
test('can remove all roles from a user', function () {
$user = User::factory()->create();
$role = Role::factory()->create();
$user->roles()->attach($role);
$this->actingAs($user)
->put(route('users.update', $user), [])
->assertRedirect(route('users.index'));
expect($user->roles()->count())->toBe(0);
});
test('syncs roles replacing previous assignments', function () {
$user = User::factory()->create();
$oldRole = Role::factory()->create();
$newRole = Role::factory()->create();
$user->roles()->attach($oldRole);
$this->actingAs($user)
->put(route('users.update', $user), ['roles' => [$newRole->id]]);
expect($user->roles()->pluck('id')->all())->toBe([$newRole->id]);
});
test('role ids must exist in the database', function () {
$user = User::factory()->create();
$this->actingAs($user)
->put(route('users.update', $user), ['roles' => [999]])
->assertSessionHasErrors('roles.*');
});
test('guests cannot update user roles', function () {
$user = User::factory()->create();
$role = Role::factory()->create();
$this->put(route('users.update', $user), ['roles' => [$role->id]])
->assertRedirect('/login');
expect($user->roles()->count())->toBe(0);
});

View File

@@ -1,6 +1,6 @@
<?php <?php
test('the application returns a successful response', function () { it('returns a successful response', function () {
$response = $this->get('/'); $response = $this->get('/');
$response->assertStatus(200); $response->assertStatus(200);

View File

@@ -0,0 +1,85 @@
<?php
use App\Models\User;
test('profile page is displayed', function () {
$user = User::factory()->create();
$response = $this
->actingAs($user)
->get('/profile');
$response->assertOk();
});
test('profile information can be updated', function () {
$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);
});
test('email verification status is unchanged when the email address is unchanged', function () {
$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);
});
test('user can delete their account', function () {
$user = User::factory()->create();
$response = $this
->actingAs($user)
->delete('/profile', [
'password' => 'password',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect('/');
$this->assertGuest();
$this->assertNull($user->fresh());
});
test('correct password must be provided to delete account', function () {
$user = User::factory()->create();
$response = $this
->actingAs($user)
->from('/profile')
->delete('/profile', [
'password' => 'wrong-password',
]);
$response
->assertSessionHasErrorsIn('userDeletion', 'password')
->assertRedirect('/profile');
$this->assertNotNull($user->fresh());
});

View File

@@ -0,0 +1,41 @@
<?php
use App\Models\Role;
use App\Models\User;
test('guests are redirected to login from roles page', function () {
$this->get('/roles')->assertRedirect('/login');
});
test('authenticated users can view the roles page', function () {
$this->actingAs(User::factory()->create())
->get('/roles')
->assertOk()
->assertViewIs('roles.index');
});
test('roles page passes paginated roles to view', function () {
$this->actingAs(User::factory()->create())
->get('/roles')
->assertOk()
->assertViewHas('roles');
});
test('ajax request returns roles table partial', function () {
$this->actingAs(User::factory()->create())
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get('/roles')
->assertOk()
->assertViewIs('roles._table');
});
test('ajax pagination returns correct page', function () {
Role::factory()->count(15)->create();
$this->actingAs(User::factory()->create())
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get('/roles?page=2')
->assertOk()
->assertViewIs('roles._table')
->assertViewHas('roles', fn ($roles) => $roles->currentPage() === 2);
});

View File

@@ -0,0 +1,52 @@
<?php
use App\Models\Role;
use App\Models\User;
test('guests cannot access the create role page', function () {
$this->get('/roles/create')->assertRedirect('/login');
});
test('authenticated users can access the create role page', function () {
$this->actingAs(User::factory()->create())
->get('/roles/create')
->assertOk()
->assertViewIs('roles.create');
});
test('authenticated users can create a role', function () {
$this->actingAs(User::factory()->create())
->post('/roles', ['name' => 'Manager', 'description' => 'Manages things'])
->assertRedirect(route('roles.index'))
->assertSessionHas('status', 'role-created');
$this->assertDatabaseHas('roles', ['name' => 'Manager', 'description' => 'Manages things']);
});
test('role name is required', function () {
$this->actingAs(User::factory()->create())
->post('/roles', ['name' => '', 'description' => 'Some description'])
->assertSessionHasErrors('name');
});
test('role name must be unique', function () {
Role::factory()->create(['name' => 'Admin']);
$this->actingAs(User::factory()->create())
->post('/roles', ['name' => 'Admin'])
->assertSessionHasErrors('name');
});
test('description is optional', function () {
$this->actingAs(User::factory()->create())
->post('/roles', ['name' => 'Viewer'])
->assertRedirect(route('roles.index'));
$this->assertDatabaseHas('roles', ['name' => 'Viewer', 'description' => null]);
});
test('guests cannot create a role', function () {
$this->post('/roles', ['name' => 'Admin'])->assertRedirect('/login');
$this->assertDatabaseMissing('roles', ['name' => 'Admin']);
});

View File

@@ -0,0 +1,47 @@
<?php
use App\Models\User;
test('guests are redirected to login from users page', function () {
$this->get('/users')->assertRedirect('/login');
});
test('authenticated users can view the users page', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get('/users')
->assertOk()
->assertViewIs('users.index');
});
test('users page passes paginated users to view', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get('/users')
->assertOk()
->assertViewHas('users');
});
test('ajax request returns table partial', function () {
$user = User::factory()->create();
$this->actingAs($user)
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get('/users')
->assertOk()
->assertViewIs('users._table');
});
test('ajax pagination returns correct page', function () {
$user = User::factory()->create();
User::factory()->count(15)->create();
$this->actingAs($user)
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get('/users?page=2')
->assertOk()
->assertViewIs('users._table')
->assertViewHas('users', fn ($users) => $users->currentPage() === 2);
});

View File

@@ -10,12 +10,12 @@ use Tests\TestCase;
| |
| The closure you provide to your test functions is always bound to a specific PHPUnit test | The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may | case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind different classes or traits. | need to change it using the "pest()" function to bind a different classes or traits.
| |
*/ */
pest()->extend(TestCase::class) pest()->extend(TestCase::class)
// ->use(RefreshDatabase::class) ->use(RefreshDatabase::class)
->in('Feature'); ->in('Feature');
/* /*

View File

@@ -6,5 +6,10 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
// protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
}
} }

View File

@@ -1,24 +1,11 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin'; import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
laravel({ laravel({
input: ['resources/css/app.css', 'resources/js/app.js'], input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true, refresh: true,
fonts: [
bunny('Instrument Sans', {
weights: [400, 500, 600],
}),
],
}), }),
tailwindcss(),
], ],
server: {
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
}); });