initial commit
This commit is contained in:
parent
3764dad884
commit
aed6ca46c2
@ -1,3 +1,3 @@
|
||||
# laravel-stubs
|
||||
|
||||
Some helper files and stubs that I may include on various Laravel projects.
|
||||
Some helper files and stubs that I may include on various Laravel projects.
|
||||
|
3
scripts/bootstrap.sh
Normal file
3
scripts/bootstrap.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
composer require itsgoingd/clockwork
|
8
scripts/genres.sh
Normal file
8
scripts/genres.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
php artisan make:model ${1} -mf
|
||||
php artisan make:controller Dashboard/${1}Controller
|
||||
php artisan make:request ${1}/StoreRequest
|
||||
php artisan make:request ${1}/UpdateRequest
|
||||
mkdir -pv resources/js/Pages/${1}s
|
||||
touch resources/js/Pages/${1}s/{Index,Show,Create,Edit}.vue
|
53
src/.env.example
Normal file
53
src/.env.example
Normal file
@ -0,0 +1,53 @@
|
||||
APP_NAME=Invoicer
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:hSCTwZ507IdKQ5QJHJ+mQw0DSMgDdAspasjwHCdiB8Y=
|
||||
APP_DEBUG=true
|
||||
APP_URL=https://invoicer.test
|
||||
APP_UID_BYTES=8
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=invoicer_v3
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DRIVER=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=memcached
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailhog
|
||||
MAIL_PORT=1125
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
78
src/app/Console/Commands/ResetPassword.php
Normal file
78
src/app/Console/Commands/ResetPassword.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class ResetPassword extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'password:reset {--i|id= : The ID of the user} {--e|email= : The email address of the user} {--p|password}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Reset the password for a given user';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = $this->option('id');
|
||||
$email = $this->option('email');
|
||||
$password = $this->option('password');
|
||||
$column = null;
|
||||
$value = null;
|
||||
|
||||
if (! empty($id) && empty($email)) {
|
||||
$column = 'id';
|
||||
$value = $id;
|
||||
} elseif (empty($id) && ! empty($email)) {
|
||||
$column = 'email';
|
||||
$value = $email;
|
||||
} else {
|
||||
$column = strtolower($this->choice('What column would you like to search by?', ['ID', 'Email']));
|
||||
$value = $this->ask("Please provide an $column to search for");
|
||||
}
|
||||
|
||||
$user = User::where($column, $value)->first();
|
||||
if (! $user) {
|
||||
$this->error('Unable to find a user matching your input.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (empty($password)) {
|
||||
$password = $this->secret('What is the new password?');
|
||||
}
|
||||
try {
|
||||
$user->update(['password' => Hash::make($password)]);
|
||||
$this->info("User {$user->id} ({$user->email}) password update successful!");
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Unable to set the password!');
|
||||
$this->line($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
28
src/app/Http/Middleware/CheckCustomSessionData.php
Normal file
28
src/app/Http/Middleware/CheckCustomSessionData.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CheckCustomSessionData
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ((! session()->has('timezone_name') || empty(session('timezone_name'))) && $request->user()) {
|
||||
session()->put('timezone_name', $request->user()->timezone_name);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
59
src/app/Http/Middleware/HandleInertiaRequests.php
Normal file
59
src/app/Http/Middleware/HandleInertiaRequests.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function version(Request $request)
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
$notifications = [];
|
||||
$unreadNotifications = false;
|
||||
if (! is_null($request->user())) {
|
||||
$notifications = $request->user()->notifications;
|
||||
if (count($request->user()->unreadNotifications) > 0) {
|
||||
$unreadNotifications = true;
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge(parent::share($request), [
|
||||
'app_name' => config('app.name'),
|
||||
'notifications' => $notifications,
|
||||
'unreadNotifications' => $unreadNotifications,
|
||||
]);
|
||||
}
|
||||
}
|
160
src/app/Models/Address.php
Normal file
160
src/app/Models/Address.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Traits\FormattedAddressTrait;
|
||||
use App\Models\Traits\HasUidTrait;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Prunable;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Address extends Model
|
||||
{
|
||||
use FormattedAddressTrait;
|
||||
use HasFactory;
|
||||
use HasUidTrait;
|
||||
use Prunable;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'addresses';
|
||||
|
||||
/** @var string */
|
||||
protected $keyType = 'string';
|
||||
|
||||
/** @var bool */
|
||||
public $incrementing = false;
|
||||
|
||||
/** @var array */
|
||||
protected $fillable = [
|
||||
'addressable_type',
|
||||
'addressable_id',
|
||||
'team_id',
|
||||
'street',
|
||||
'unit',
|
||||
'city',
|
||||
'state',
|
||||
'postal_code',
|
||||
'country',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $hidden = [];
|
||||
|
||||
/** @var array */
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $dates = [];
|
||||
|
||||
/** @var array */
|
||||
protected $appends = [];
|
||||
|
||||
/** @var array */
|
||||
protected $touches = [];
|
||||
|
||||
/** @var array */
|
||||
protected $dispatchesEvents = [];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Constants
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom/Private Methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the prunable model query.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function prunable(): Builder
|
||||
{
|
||||
//return static::where('created_at', '<=', now()->subMonth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the model for pruning.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function pruning(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Accessors
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mutators
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Scopes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enforce that a given model belongs to a Team.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int|string $teamId
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeOnTeam($query, $teamId): Builder
|
||||
{
|
||||
return $query->where('team_id', $teamId);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Morphable relationship method.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function addressable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
35
src/app/Models/Traits/FormattedAddressTrait.php
Normal file
35
src/app/Models/Traits/FormattedAddressTrait.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
trait FormattedAddressTrait
|
||||
{
|
||||
/**
|
||||
* Combine the street and optionally a street suffix
|
||||
* e.g. Apt 1
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAddressStreetAttribute(): string
|
||||
{
|
||||
$street = $this->street;
|
||||
if (! empty($this->unit)) {
|
||||
$street .= " {$this->unit}";
|
||||
}
|
||||
return $street;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine the city and state together as a single line
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCityStateAttribute(): string
|
||||
{
|
||||
return "{$this->city}, {$this->state}";
|
||||
}
|
||||
}
|
171
src/app/Models/Traits/FormattedDateTrait.php
Normal file
171
src/app/Models/Traits/FormattedDateTrait.php
Normal file
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
trait FormattedDateTrait
|
||||
{
|
||||
/**
|
||||
* Return the created at datetime as mon D, year.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCreatedAtDateShortAttribute(): string
|
||||
{
|
||||
return $this->created_at->format('M j, Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the created at datetime as month day, year.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCreatedAtDateFullAttribute(): string
|
||||
{
|
||||
return $this->created_at->format('F jS, Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the created at datetime as month day, year hour:minute meridian.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCreatedAtFullAttribute(): string
|
||||
{
|
||||
return $this->created_at->format('F jS, Y g:i a');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the updated at datetime as mon day, year.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUpdatedAtDateShortAttribute(): string
|
||||
{
|
||||
if (empty($this->updated_at)) {
|
||||
return '--';
|
||||
}
|
||||
return $this->updated_at->format('M j, Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the updated at datetime as mon day, year.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUpdatedAtDateFullAttribute(): string
|
||||
{
|
||||
if (empty($this->updated_at)) {
|
||||
return '--';
|
||||
}
|
||||
return $this->updated_at->format('F jS, Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the updated at datetime as mon day, year hour:minute meridian.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUpdatedAtFullAttribute(): string
|
||||
{
|
||||
if (empty($this->updated_at)) {
|
||||
return '--';
|
||||
}
|
||||
return $this->updated_at->format('F jS, Y g:i a');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datetime string as YYYY-MM-DD.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function dateStr(string $dateTimeString): string
|
||||
{
|
||||
return Carbon::parse($dateTimeString)->format('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datetime string as mon d, year.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function dateShort(string $dateTimeString): string
|
||||
{
|
||||
return Carbon::parse($dateTimeString)->format('M j, Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datetime string as month day, year.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function dateFull(string $dateTimeString): string
|
||||
{
|
||||
return Carbon::parse($dateTimeString)->format('F jS, Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datetime string as HH:MM (military time)
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function timeShort(string $dateTimeString): string
|
||||
{
|
||||
return Carbon::parse($dateTimeString)->format('H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datetime string as hour:minute meridian
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $dateTimeString
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function timeFull(string $dateTimeString): string
|
||||
{
|
||||
return Carbon::parse($dateTimeString)->format('g:i a');
|
||||
}
|
||||
}
|
55
src/app/Models/Traits/FormattedPhoneTrait.php
Normal file
55
src/app/Models/Traits/FormattedPhoneTrait.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
trait FormattedPhoneTrait
|
||||
{
|
||||
/**
|
||||
* Format a phone number to be human readable.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhoneNumberAttribute(): string
|
||||
{
|
||||
$phoneLength = strlen($this->phone);
|
||||
$phoneNumber = preg_replace('//', '', $this->phone);
|
||||
|
||||
if ($phoneLength > 10) {
|
||||
$countryCode = substr($phoneNumber, 0, $phoneLength - 10);
|
||||
$areaCode = substr($phoneNumber, -10, 3);
|
||||
$nextThree = substr($phoneNumber, -7, 3);
|
||||
$lastFour = substr($phoneNumber, -4, 4);
|
||||
|
||||
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
|
||||
} elseif ($phoneLength == 10) {
|
||||
$areaCode = substr($phoneNumber, 0, 3);
|
||||
$nextThree = substr($phoneNumber, 3, 3);
|
||||
$lastFour = substr($phoneNumber, 6, 4);
|
||||
|
||||
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
|
||||
} elseif ($phoneLength == 7) {
|
||||
$nextThree = substr($phoneNumber, 0, 3);
|
||||
$lastFour = substr($phoneNumber, 3, 4);
|
||||
|
||||
$phoneNumber = "{$nextThree}-{$lastFour}";
|
||||
}
|
||||
|
||||
return $phoneNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all non-numeric characters from the phone number.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPhoneNumberAttribute($value): void
|
||||
{
|
||||
$this->attributes['phone'] = preg_replace('/[^0-9]/', '', $value);
|
||||
}
|
||||
}
|
44
src/app/Models/Traits/HasUidTrait.php
Normal file
44
src/app/Models/Traits/HasUidTrait.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
trait HasUidTrait
|
||||
{
|
||||
/**
|
||||
* Ensure that when a model is saving, a unique ID
|
||||
* is set for the model.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function bootHasUidTrait(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize logic.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initializeHasUidTrait(): void
|
||||
{
|
||||
$this->id = $this->generateUid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically safe unique ID.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateUid(): string
|
||||
{
|
||||
$bytes = openssl_random_pseudo_bytes(env('APP_UID_BYTES', 8));
|
||||
return bin2hex($bytes);
|
||||
}
|
||||
}
|
182
src/app/Models/User.php
Normal file
182
src/app/Models/User.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Traits\HasUidTrait;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Prunable;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Jetstream\HasTeams;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens;
|
||||
use HasFactory;
|
||||
use HasProfilePhoto;
|
||||
use HasTeams;
|
||||
use HasUidTrait;
|
||||
use Notifiable;
|
||||
use Prunable;
|
||||
use TwoFactorAuthenticatable;
|
||||
|
||||
/** @var string */
|
||||
protected $table = 'users';
|
||||
|
||||
/** @var string */
|
||||
protected $keyType = 'string';
|
||||
|
||||
/** @var bool */
|
||||
public $incrementing = false;
|
||||
|
||||
/** @var array */
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'surname',
|
||||
'timezone_name',
|
||||
'current_team_id',
|
||||
'profile_photo_path',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $dates = [];
|
||||
|
||||
/** @var array */
|
||||
protected $appends = [
|
||||
'full_name',
|
||||
'name_full',
|
||||
'profile_photo_url',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $touches = [];
|
||||
|
||||
/** @var array */
|
||||
protected $dispatchesEvents = [];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Constants
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom/Private Methods
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the prunable model query.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function prunable()//: Builder
|
||||
{
|
||||
//return static::where('created_at', '<=', now()->subMonth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the model for pruning.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function pruning(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Accessors
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return the surname then (first)name separated by a comma.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return "{$this->surname}, {$this->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return both the (first)name and surname.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNameFullAttribute(): string
|
||||
{
|
||||
return "{$this->name} {$this->surname}";
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mutators
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Scopes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
//
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return the associated Address with a User.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function addresses(): MorphOne
|
||||
{
|
||||
return $this->morphOne(Address::class, 'addressable');
|
||||
}
|
||||
}
|
30
src/app/Providers/AppServiceProvider.php
Normal file
30
src/app/Providers/AppServiceProvider.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
// Disable lazy-loading when not in production to find N+1 problems
|
||||
Model::preventLazyLoading(! app()->isProduction());
|
||||
}
|
||||
}
|
84
src/app/Providers/JetstreamServiceProvider.php
Normal file
84
src/app/Providers/JetstreamServiceProvider.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Jetstream\AddTeamMember;
|
||||
use App\Actions\Jetstream\CreateTeam;
|
||||
use App\Actions\Jetstream\DeleteTeam;
|
||||
use App\Actions\Jetstream\DeleteUser;
|
||||
use App\Actions\Jetstream\InviteTeamMember;
|
||||
use App\Actions\Jetstream\RemoveTeamMember;
|
||||
use App\Actions\Jetstream\UpdateTeamName;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Fortify\Fortify;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
class JetstreamServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->configurePermissions();
|
||||
|
||||
Jetstream::createTeamsUsing(CreateTeam::class);
|
||||
Jetstream::updateTeamNamesUsing(UpdateTeamName::class);
|
||||
Jetstream::addTeamMembersUsing(AddTeamMember::class);
|
||||
Jetstream::inviteTeamMembersUsing(InviteTeamMember::class);
|
||||
Jetstream::removeTeamMembersUsing(RemoveTeamMember::class);
|
||||
|
||||
Jetstream::deleteTeamsUsing(DeleteTeam::class);
|
||||
|
||||
Jetstream::deleteUsersUsing(DeleteUser::class);
|
||||
|
||||
// Allow a user to authenticate with either email or username fields
|
||||
Fortify::authenticateUsing(function (Request $request) {
|
||||
$user = User::where('email', $request->username)
|
||||
->orWhere('username', $request->username)
|
||||
->first();
|
||||
|
||||
if ($user && Hash::check($request->password, $user->password)) {
|
||||
return $user;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the roles and permissions that are available within the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configurePermissions()
|
||||
{
|
||||
Jetstream::defaultApiTokenPermissions(['read']);
|
||||
|
||||
Jetstream::role('admin', 'Administrator', [
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
])->description('Administrator users can perform any action.');
|
||||
|
||||
Jetstream::role('editor', 'Editor', [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
])->description('Editor users have the ability to read, create, and update.');
|
||||
}
|
||||
}
|
35
src/database/factories/AddressFactory.php
Normal file
35
src/database/factories/AddressFactory.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Address;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class AddressFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Address::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'street' => $this->faker->buildingNumber() . ' ' . $this->faker->streetName(),
|
||||
'unit' => mt_rand(1, 10) === 10 ? $this->faker->secondaryAddress() : null,
|
||||
'city' => $this->faker->city(),
|
||||
'state' => $this->faker->state(),
|
||||
'postal_code' => $this->faker->postcode(),
|
||||
'country' => 'United States',
|
||||
'latitude' => $this->faker->latitude(),
|
||||
'longitude' => $this->faker->longitude(),
|
||||
];
|
||||
}
|
||||
}
|
72
src/database/factories/UserFactory.php
Normal file
72
src/database/factories/UserFactory.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Features;
|
||||
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = User::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->firstName(),
|
||||
'surname' => $this->faker->lastName(),
|
||||
'username' => $this->faker->unique()->userName(),
|
||||
'email' => $this->faker->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'timezone_name' => $this->faker->timezone(),
|
||||
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory
|
||||
*/
|
||||
public function unverified()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
'email_verified_at' => null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the user should have a personal team.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withPersonalTeam()
|
||||
{
|
||||
if (! Features::hasTeamFeatures()) {
|
||||
return $this->state([]);
|
||||
}
|
||||
|
||||
return $this->has(
|
||||
Team::factory()
|
||||
->state(function (array $attributes, User $user) {
|
||||
return ['name' => $user->name.'\'s Team', 'user_id' => $user->id, 'personal_team' => true];
|
||||
}),
|
||||
'ownedTeams'
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->string('id', 64)->primary();
|
||||
$table->string('name');
|
||||
$table->string('surname');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('timezone_name')->default('UTC');
|
||||
$table->string('current_team_id', 64)->nullable();
|
||||
$table->string('profile_photo_path', 2048)->nullable();
|
||||
$table->string('password');
|
||||
$table->text('two_factor_secret')->nullable();
|
||||
$table->text('two_factor_recovery_codes')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAddressesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('addresses', function (Blueprint $table) {
|
||||
$table->string('id', 64)->primary();
|
||||
$table->uuidMorphs('addressable');
|
||||
$table->string('street');
|
||||
$table->string('unit')->nullable();
|
||||
$table->string('city');
|
||||
$table->string('state');
|
||||
$table->string('postal_code'); // leave as string to accomodate 12345-6789 or Canada
|
||||
$table->string('country')->default('United States');
|
||||
$table->float('latitude', 12, 9)->index()->nullable();
|
||||
$table->float('longitude', 12, 9)->index()->nullable();
|
||||
$table->point('location')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
|
||||
|
||||
$table->spatialIndex('location');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('addresses');
|
||||
}
|
||||
}
|
11
src/helpers/constants/cache_ttl.php
Normal file
11
src/helpers/constants/cache_ttl.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
define('CACHE_TTL_FIVE_MINUTES', 300);
|
||||
define('CACHE_TTL_FIFTEEN_MINUTES', 900);
|
||||
|
||||
define('CACHE_TTL_HALF_HOUR', 1800);
|
||||
define('CACHE_TTL_ONE_HOUR', 3600);
|
||||
|
||||
define('CACHE_TTL_ONE_DAY', 86400);
|
||||
|
||||
define('CACHE_TTL_ONE_WEEK', 604800);
|
56
src/helpers/constants/http_status_codes.php
Normal file
56
src/helpers/constants/http_status_codes.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
$httpCodes = [
|
||||
// 200's (successful)
|
||||
200 => 'HTTP_SUCCESS',
|
||||
201 => 'HTTP_CREATED',
|
||||
202 => 'HTTP_ACCEPTED',
|
||||
204 => 'HTTP_NO_CONTENT',
|
||||
205 => 'HTTP_RESET_CONTENT',
|
||||
206 => 'HTTP_PARTIAL_CONTENT',
|
||||
208 => 'HTTP_ALREADY_REPORTED',
|
||||
|
||||
// 300's (redirections)
|
||||
301 => 'HTTP_MOVED_PERMANENTLY',
|
||||
302 => 'HTTP_REDIRECT_FOUND',
|
||||
304 => 'HTTP_NOT_MODIFIED',
|
||||
307 => 'HTTP_TEMP_REDIRECT',
|
||||
308 => 'HTTP_PERMANENT_REDIRECT',
|
||||
|
||||
// 400's (app got the request but couldn't process it successfully, coding error)
|
||||
400 => 'HTTP_BAD_REQUEST',
|
||||
401 => 'HTTP_UNAUTHORIZED',
|
||||
402 => 'HTTP_PAYMENT_REQUIRED',
|
||||
403 => 'HTTP_FORBIDDEN',
|
||||
404 => 'HTTP_NOT_FOUND',
|
||||
405 => 'HTTP_METHOD_NOT_ALLOWED',
|
||||
406 => 'HTTP_NOT_ACCEPTABLE',
|
||||
408 => 'HTTP_TIMEOUT',
|
||||
409 => 'HTTP_CONFLICT',
|
||||
410 => 'HTTP_GONE',
|
||||
412 => 'HTTP_PRECONDITION_FAILED',
|
||||
413 => 'HTTP_PAYLOAD_TOO_LARGE',
|
||||
415 => 'HTTP_UNSUPPORTED_MEDIA',
|
||||
413 => 'HTTP_TOO_LARGE',
|
||||
417 => 'HTTP_EXPECTATION_FAIL',
|
||||
418 => 'HTTP_TEAPOT',
|
||||
422 => 'HTTP_UNPROCESSABLE_ENTITY',
|
||||
423 => 'HTTP_LOCKED',
|
||||
424 => 'HTTP_FAILED_DEPENDENCY',
|
||||
236 => 'HTTP_UPGRADE_REQUIRED',
|
||||
428 => 'HTTP_PRECONDITION_REQUIRED',
|
||||
429 => 'HTTP_TOO_MANY_REQUESTS',
|
||||
451 => 'HTTP_GAG_ORDER',
|
||||
|
||||
// 500's (server-level problem, process died or configuration is incorrect)
|
||||
500 => 'HTTP_SERVER_ERROR',
|
||||
501 => 'HTTP_NOT_IMPLEMENTED',
|
||||
503 => 'HTTP_UNAVAILABLE',
|
||||
530 => 'HTTP_SUSPENDED',
|
||||
];
|
||||
|
||||
foreach ($httpCodes as $code => $slug) {
|
||||
if (! defined($slug)) {
|
||||
define($slug, $code);
|
||||
}
|
||||
}
|
43
src/helpers/global_functions.php
Normal file
43
src/helpers/global_functions.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('snake2Title')) {
|
||||
/**
|
||||
* Convert a snake case string to a title with spaces
|
||||
* and every word capitalized.
|
||||
*
|
||||
* @param string $stakeSlug A snake case string, commonly a slug
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function snake2Title(string $snakeSlug): string
|
||||
{
|
||||
$output = preg_replace('/\_/', ' ', $snakeSlug);
|
||||
return ucwords($output);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('carbon')) {
|
||||
function carbon(?string $timestring = null)
|
||||
{
|
||||
$carbon = Carbon\Carbon::now(session('timezone_name'));
|
||||
if (! empty($timestring)) {
|
||||
$carbon = Carbon\Carbon::parse($timestring, session('timezone_name'));
|
||||
}
|
||||
return $carbon;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('jddayofweek')) {
|
||||
function jddayofweek(?int $intDay = null, int $mode = 0)
|
||||
{
|
||||
if (is_null($intDay)) {
|
||||
$intDay = date('l');
|
||||
}
|
||||
|
||||
if ($mode === 0) {
|
||||
return $intDay;
|
||||
}
|
||||
|
||||
return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][$intDay];
|
||||
}
|
||||
}
|
0
src/public/copyright.html
Normal file
0
src/public/copyright.html
Normal file
BIN
src/public/fonts/Nunito/Nunito-Bold.woff2
Normal file
BIN
src/public/fonts/Nunito/Nunito-Bold.woff2
Normal file
Binary file not shown.
BIN
src/public/fonts/Nunito/Nunito-BoldItalic.woff2
Normal file
BIN
src/public/fonts/Nunito/Nunito-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
src/public/fonts/Nunito/Nunito-Italic.woff2
Normal file
BIN
src/public/fonts/Nunito/Nunito-Italic.woff2
Normal file
Binary file not shown.
BIN
src/public/fonts/Nunito/Nunito-Regular.woff2
Normal file
BIN
src/public/fonts/Nunito/Nunito-Regular.woff2
Normal file
Binary file not shown.
BIN
src/public/fonts/Nunito/Nunito-SemiBold.woff2
Normal file
BIN
src/public/fonts/Nunito/Nunito-SemiBold.woff2
Normal file
Binary file not shown.
BIN
src/public/fonts/Nunito/Nunito-SemiBoldItalic.woff2
Normal file
BIN
src/public/fonts/Nunito/Nunito-SemiBoldItalic.woff2
Normal file
Binary file not shown.
0
src/public/humans.txt
Normal file
0
src/public/humans.txt
Normal file
2
src/public/robots.txt
Normal file
2
src/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
108
src/resources/css/app.css
Normal file
108
src/resources/css/app.css
Normal file
@ -0,0 +1,108 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@import 'typography.css';
|
||||
|
||||
@import 'components/buttons.css';
|
||||
@import 'components/cards.css';
|
||||
@import 'components/tables.css';
|
||||
|
||||
body {
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.grid-container {
|
||||
@apply grid-cols-1;
|
||||
gap: 2em 0em;
|
||||
grid-template-cols: minmax(80px, 1fr);
|
||||
grid-template-areas:
|
||||
"nav"
|
||||
"main";
|
||||
}
|
||||
|
||||
.nav {
|
||||
@apply grid;
|
||||
grid-template-rows: minmax(80px, 1fr);
|
||||
grid-template-areas:
|
||||
"navh"
|
||||
"navc";
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
.navh {
|
||||
grid-area: navh;
|
||||
}
|
||||
|
||||
.navc {
|
||||
@apply flex flex-col;
|
||||
grid-area: navc;
|
||||
}
|
||||
|
||||
.main {
|
||||
@apply grid pb-8;
|
||||
grid-template-areas:
|
||||
"mainc";
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
.mainh {
|
||||
@apply hidden;
|
||||
grid-area: mainh;
|
||||
}
|
||||
|
||||
.mainc {
|
||||
grid-area: mainc;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.grid-container {
|
||||
@apply grid-cols-2;
|
||||
gap: 0em 3em;
|
||||
grid-template-columns: 300px 1fr;
|
||||
grid-template-areas:
|
||||
"nav main";
|
||||
}
|
||||
|
||||
.nav {
|
||||
@apply grid grid-cols-1 grid-flow-row;
|
||||
gap: 2em 0em;
|
||||
grid-template-rows: 80px 1fr;
|
||||
grid-template-areas:
|
||||
"navh"
|
||||
"navc";
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
.navh {
|
||||
grid-area: navh;
|
||||
}
|
||||
|
||||
.navc {
|
||||
grid-area: navc;
|
||||
}
|
||||
|
||||
.main {
|
||||
@apply grid grid-cols-1 grid-flow-row pb-8;
|
||||
gap: 2em 0em;
|
||||
grid-template-rows: 80px 1fr;
|
||||
grid-template-areas:
|
||||
"mainh"
|
||||
"mainc";
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
.mainh {
|
||||
@apply flex flex-row items-stretch justify-between;
|
||||
grid-area: mainh;
|
||||
}
|
||||
|
||||
.mainc {
|
||||
@apply grid grid-cols-6 gap-8;
|
||||
grid-template-rows: min-content 1fr;
|
||||
grid-area: mainc;
|
||||
}
|
||||
}
|
23
src/resources/css/components/buttons.css
Normal file
23
src/resources/css/components/buttons.css
Normal file
@ -0,0 +1,23 @@
|
||||
.button {
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
@apply border-blue-300 bg-gradient-to-br from-blue-400 to-blue-700;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
@apply border-indigo-300 bg-gradient-to-br from-indigo-400 to-indigo-700;
|
||||
}
|
||||
|
||||
.button-tertiary {
|
||||
@apply border-blue-300 bg-gradient-to-br from-green-400 to-blue-500;
|
||||
}
|
||||
|
||||
.button-success {
|
||||
@apply border-green-300 bg-gradient-to-br from-green-400 to-green-700;
|
||||
}
|
||||
|
||||
.button-danger {
|
||||
@apply border-red-300 bg-gradient-to-br from-red-400 to-red-700;
|
||||
}
|
11
src/resources/css/components/cards.css
Normal file
11
src/resources/css/components/cards.css
Normal file
@ -0,0 +1,11 @@
|
||||
.card {
|
||||
@apply flex flex-col nm-flat-white dark:nm-flat-gray-700 border border-gray-100 dark:border-gray-800 overflow-hidden rounded-lg;
|
||||
}
|
||||
|
||||
.card .card-header {
|
||||
@apply px-4 py-2 rounded-t-lg border-b-2 border-blue-600 font-semibold;
|
||||
}
|
||||
|
||||
.card .card-body {
|
||||
@apply px-4 py-3 rounded-b-lg;
|
||||
}
|
13
src/resources/css/components/tables.css
Normal file
13
src/resources/css/components/tables.css
Normal file
@ -0,0 +1,13 @@
|
||||
.table thead tr {
|
||||
@apply border-b-2 border-gray-700 uppercase text-left;
|
||||
}
|
||||
|
||||
.table-fuzzy-hover tbody:hover td {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 2px #999;
|
||||
}
|
||||
|
||||
.table-fuzzy-hover tbody:hover tr:hover td {
|
||||
color: #444;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
47
src/resources/css/typography.css
Normal file
47
src/resources/css/typography.css
Normal file
@ -0,0 +1,47 @@
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
src: url('/fonts/Nunito/Nunito-Regular.woff2') format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
src: url('/fonts/Nunito/Nunito-Italic.woff2') format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
src: url('/fonts/Nunito/Nunito-SemiBold.woff2') format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
src: url('/fonts/Nunito/Nunito-SemiBoldItalic.woff2') format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
src: url('/fonts/Nunito/Nunito-Bold.woff2') format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
src: url('/fonts/Nunito/Nunito-BoldItalic.woff2') format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
33
src/resources/js/Components/Card.vue
Normal file
33
src/resources/js/Components/Card.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="card" :class="class">
|
||||
<div class="card-header" v-if="hasHeader">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
class: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, {slots}) {
|
||||
const hasHeader = ref(false)
|
||||
|
||||
if (slots.header && slots.header().length) {
|
||||
hasHeader.value = true
|
||||
}
|
||||
|
||||
return { hasHeader }
|
||||
},
|
||||
})
|
||||
</script>
|
110
src/resources/js/Components/DropdownMenu.vue
Normal file
110
src/resources/js/Components/DropdownMenu.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<transition leave-active-class="duration-200">
|
||||
<div class="relative">
|
||||
<div class="hover:cursor-pointer" @click="show = !show">
|
||||
<slot name="trigger">
|
||||
<svg v-show="!show" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class=""><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
<svg v-show="show" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class=""><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-show="show" class="fixed inset-0 z-40" @click="show = false"></div>
|
||||
|
||||
<transition enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<div v-show="show" @click="show = false" class="flex flex-col absolute right-0 bg-white border border-gray-300 shadow-md w-max z-50">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: '2xl',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.closeOnEscape)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.closeOnEscape)
|
||||
},
|
||||
|
||||
computed: {
|
||||
maxWidthClass() {
|
||||
return {
|
||||
'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',
|
||||
}[this.maxWidth]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
closeOnEscape(event) {
|
||||
if (event.key === 'Escape' && this.show) {
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*.rotate { animation: rotation 2s infinite linear; }
|
||||
|
||||
@keyframes rotation {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(359deg); }
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: hsl(0, 0%, 100%);
|
||||
border: 1px solid hsl(240, 4.9%, 83.9%);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.dropdown-link {
|
||||
display: flex;
|
||||
padding: 1rem 0.5rem 1rem 0.5rem;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.dropdown-link:hover {
|
||||
background-color: hsl(240, 4.8%, 95.9%);
|
||||
}
|
||||
|
||||
.dropdown-link:focus {
|
||||
background-color: hsl(240, 4.8%, 95.9%);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}*/
|
||||
</style>
|
127
src/resources/js/Components/Paginator.vue
Normal file
127
src/resources/js/Components/Paginator.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<nav v-if="pagi !== undefined" class="flex items-center justify-between" role="navigation">
|
||||
<div class="flex justify-between flex-1 sm:hidden">
|
||||
<span v-if="onFirstPage" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-gray-100 border border-gray-300 cursor-default leading-5 rounded-md">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<Link v-else :href="previousPageUrl" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link v-if="hasMorePages" :href="nextPageUrl" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<span v-else class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-500 bg-gray-100 border border-gray-300 cursor-default leading-5 rounded-md">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div class="text-sm text-gray-700 leading-5">
|
||||
<p>Showing <span class="font-medium">{{ firstItem }}</span> to <span class="font-medium">{{ lastItem }}</span> of <span class="font-medium">{{ total }}</span> results</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="relative z-0 inline-flex shadow-sm rounded-md">
|
||||
<div v-if="onFirstPage" aria-disabled="true" aria-hidden="true" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-gray-100 border border-gray-300 cursor-default rounded-l-md leading-5">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<Link v-else :href="previousPageUrl" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" rel="prev">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<div v-for="(link, index) in pagi.links">
|
||||
<Link v-if="!isFirstOrLastOrDots(index, pagi.links.length, link.label)" :class="{ 'bg-blue-200' : link.active }" :href="link.url" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" v-html="link.label"></Link>
|
||||
<span v-else-if="link.label === '...'" aria-disabled="true" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5" v-html="link.label"></span>
|
||||
</div>
|
||||
|
||||
<Link v-if="hasMorePages" :href="nextPageUrl" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
<div v-else aria-disabled="true" aria-hidden="true" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-gray-100 border border-gray-300 cursor-default rounded-r-md leading-5">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path clip-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue"
|
||||
import { Link } from "@inertiajs/inertia-vue3"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
pagi: Object,
|
||||
},
|
||||
|
||||
components: {
|
||||
Link,
|
||||
},
|
||||
|
||||
computed: {
|
||||
onFirstPage() {
|
||||
return this.pagi.current_page === 1
|
||||
},
|
||||
|
||||
hasMorePages() {
|
||||
return this.pagi.current_page < this.pagi.last_page
|
||||
},
|
||||
|
||||
nextPageUrl() {
|
||||
return this.pagi.next_page_url
|
||||
},
|
||||
|
||||
previousPageUrl() {
|
||||
return this.pagi.prev_page_url
|
||||
},
|
||||
|
||||
firstItem() {
|
||||
if (this.pagi.from == null) {
|
||||
return '-'
|
||||
}
|
||||
return this.pagi.from
|
||||
},
|
||||
|
||||
lastItem() {
|
||||
if (this.pagi.to == null) {
|
||||
return '-'
|
||||
}
|
||||
return this.pagi.to
|
||||
},
|
||||
|
||||
total() {
|
||||
if (isNaN(this.pagi.total)) {
|
||||
return '0'
|
||||
}
|
||||
return this.pagi.total
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
isFirstOrLastOrDots(index, linksLength, label) {
|
||||
return index === 0 || index === linksLength - 1 || label.includes('...')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
85
src/resources/js/Components/ResponsiveNavLink.vue
Normal file
85
src/resources/js/Components/ResponsiveNavLink.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<a :href="href" :title="title" :class="classes">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
href: {
|
||||
type: String,
|
||||
default: "#",
|
||||
},
|
||||
|
||||
class: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
let classes = ""
|
||||
let classesArr = []
|
||||
let defaultClasses = []
|
||||
let dClasses = [
|
||||
"block",
|
||||
"pl-3",
|
||||
"pr-4",
|
||||
"py-2",
|
||||
"border-l-4",
|
||||
"text-base",
|
||||
"font-medium",
|
||||
"focus:outline-none",
|
||||
"transition",
|
||||
]
|
||||
|
||||
let tiveClasses = [
|
||||
"border-transparent",
|
||||
"text-gray-600",
|
||||
"hover:text-gray-800",
|
||||
"hover:bg-gray-50",
|
||||
"hover:border-gray-300",
|
||||
"focus:text-gray-800",
|
||||
"focus:bg-gray-50",
|
||||
"focus:border-gray-300",
|
||||
]
|
||||
|
||||
if (props.active) {
|
||||
tiveClasses = [
|
||||
"border-indigo-400",
|
||||
"text-indigo-700",
|
||||
"bg-indigo-50",
|
||||
"focus:text-indigo-800",
|
||||
"focus:bg-indigo-100",
|
||||
"focus:border-indigo-700",
|
||||
]
|
||||
}
|
||||
|
||||
defaultClasses = dClasses.concat(tiveClasses)
|
||||
|
||||
for (let elm of props.class.split(" ")) {
|
||||
elm = elm.trim()
|
||||
if (elm.length > 0 && defaultClasses.indexOf(elm) === -1) {
|
||||
classesArr.push(elm)
|
||||
}
|
||||
}
|
||||
|
||||
classes = defaultClasses.join(" ") + " " + classesArr.join(" ")
|
||||
|
||||
return { classes }
|
||||
},
|
||||
})
|
||||
</script>
|
74
src/resources/js/Components/SidenavLink.vue
Normal file
74
src/resources/js/Components/SidenavLink.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<a :href="href" :title="title" :class="classes">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
href: {
|
||||
type: String,
|
||||
default: "#",
|
||||
},
|
||||
|
||||
class: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
let classes = ""
|
||||
let classesArr = []
|
||||
let defaultClasses = []
|
||||
let dClasses = [
|
||||
"flex",
|
||||
"flex-row",
|
||||
"items-center",
|
||||
"px-4",
|
||||
"py-3",
|
||||
"transition",
|
||||
"rounded-lg",
|
||||
]
|
||||
|
||||
let tiveClasses = [
|
||||
"nm-flat-white",
|
||||
"hover:nm-concave-blue-300",
|
||||
"hover:text-white",
|
||||
]
|
||||
|
||||
if (props.active) {
|
||||
tiveClasses = [
|
||||
"nm-flat-blue-400",
|
||||
"text-white",
|
||||
]
|
||||
}
|
||||
|
||||
defaultClasses = dClasses.concat(tiveClasses)
|
||||
|
||||
for (let elm of props.class.split(" ")) {
|
||||
elm = elm.trim()
|
||||
if (elm.length > 0 && defaultClasses.indexOf(elm) === -1) {
|
||||
classesArr.push(elm)
|
||||
}
|
||||
}
|
||||
|
||||
classes = defaultClasses.join(" ") + " " + classesArr.join(" ")
|
||||
|
||||
return { classes }
|
||||
},
|
||||
})
|
||||
</script>
|
291
src/resources/js/Layouts/AppLayout.vue
Normal file
291
src/resources/js/Layouts/AppLayout.vue
Normal file
@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div>
|
||||
<Head :title="title" />
|
||||
|
||||
<div class="absolute top-0 mt-4 z-50">
|
||||
<success-notifications />
|
||||
<error-notifications />
|
||||
<generic-notifications />
|
||||
</div>
|
||||
|
||||
<jet-banner />
|
||||
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<div class="grid-container w-4/5 mx-auto grid">
|
||||
<div class="nav">
|
||||
<div class="navh flex items-center justify-between lg:justify-center">
|
||||
<Link :href="route('dashboard')" class="flex flex-row items-center justify-center">
|
||||
<ApplicationMark class="block h-9 w-auto" />
|
||||
<span class="ml-3 font-bold text-xl dark:text-white">{{ $page.props.app_name }}</span>
|
||||
</Link>
|
||||
|
||||
<div class="flex items-center lg:hidden">
|
||||
<button @click="showingNavigationDropdown = ! showingNavigationDropdown" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{'hidden': showingNavigationDropdown, 'inline-flex': ! showingNavigationDropdown }" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{'hidden': ! showingNavigationDropdown, 'inline-flex': showingNavigationDropdown }" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="navc">
|
||||
<div class="hidden lg:flex lg:flex-col">
|
||||
<SidenavLink :href="route('dashboard')" :active="route().current('dashboard')" :title="'Your Dashboard'">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" preserveAspectRatio="xMidYMid meet"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||
<span class="ml-2">Dashboard</span>
|
||||
</SidenavLink>
|
||||
</div>
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': showingNavigationDropdown, 'hidden': ! showingNavigationDropdown}" class="lg:hidden">
|
||||
<div class="pt-2 pb-3 flex flex-col space-y-1">
|
||||
<Link :href="route('dashboard')" :active="route().current('dashboard')">
|
||||
Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<div class="flex items-center px-4">
|
||||
<div v-if="$page.props.jetstream.managesProfilePhotos" class="flex-shrink-0 mr-3" >
|
||||
<img class="h-10 w-10 rounded-full object-cover" :src="$page.props.user.profile_photo_url" :alt="$page.props.user.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-base text-gray-800">{{ $page.props.user.name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500">{{ $page.props.user.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-col space-y-1">
|
||||
<ResponsiveNavLink :href="route('profile.show')" :active="route().current('profile.show')">
|
||||
Profile
|
||||
</ResponsiveNavLink>
|
||||
|
||||
<ResponsiveNavLink :href="route('api-tokens.index')" :active="route().current('api-tokens.index')" v-if="$page.props.jetstream.hasApiFeatures">
|
||||
API Tokens
|
||||
</ResponsiveNavLink>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" @submit.prevent="logout">
|
||||
<ResponsiveNavLink as="button">
|
||||
Log Out
|
||||
</ResponsiveNavLink>
|
||||
</form>
|
||||
|
||||
<!-- Team Management -->
|
||||
<template v-if="$page.props.jetstream.hasTeamFeatures">
|
||||
<div class="border-t border-gray-200"></div>
|
||||
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Manage Team
|
||||
</div>
|
||||
|
||||
<!-- Team Settings -->
|
||||
<ResponsiveNavLink :href="route('teams.show', $page.props.user.current_team)" :active="route().current('teams.show')">
|
||||
Team Settings
|
||||
</ResponsiveNavLink>
|
||||
|
||||
<ResponsiveNavLink :href="route('teams.create')" :active="route().current('teams.create')" v-if="$page.props.jetstream.canCreateTeams">
|
||||
Create New Team
|
||||
</ResponsiveNavLink>
|
||||
|
||||
<div class="border-t border-gray-200"></div>
|
||||
|
||||
<!-- Team Switcher -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Switch Teams
|
||||
</div>
|
||||
|
||||
<template v-for="team in $page.props.user.all_teams" :key="team.id">
|
||||
<form @submit.prevent="switchToTeam(team)">
|
||||
<ResponsiveNavLink as="button">
|
||||
<div class="flex items-center">
|
||||
<svg v-if="team.id == $page.props.user.current_team_id" class="mr-2 h-5 w-5 text-green-400" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<div>{{ team.name }}</div>
|
||||
</div>
|
||||
</ResponsiveNavLink>
|
||||
</form>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="mainh">
|
||||
<div class="flex items-center">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-white leading-tight">
|
||||
<slot name="header"></slot>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex flex-row items-center justify-end">
|
||||
<!-- <ThemeSwitcher></ThemeSwitcher> -->
|
||||
|
||||
<div class="ml-3 relative">
|
||||
<!-- Teams Dropdown -->
|
||||
<jet-dropdown align="right" width="60" v-if="$page.props.jetstream.hasTeamFeatures">
|
||||
<template #trigger>
|
||||
<span class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:bg-gray-50 hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition">
|
||||
{{ $page.props.user.current_team.name }}
|
||||
|
||||
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="w-60">
|
||||
<!-- Team Management -->
|
||||
<template v-if="$page.props.jetstream.hasTeamFeatures">
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Manage Team
|
||||
</div>
|
||||
|
||||
<!-- Team Settings -->
|
||||
<jet-dropdown-link :href="route('teams.show', $page.props.user.current_team)">
|
||||
Team Settings
|
||||
</jet-dropdown-link>
|
||||
|
||||
<jet-dropdown-link :href="route('teams.create')" v-if="$page.props.jetstream.canCreateTeams">
|
||||
Create New Team
|
||||
</jet-dropdown-link>
|
||||
|
||||
<div class="border-t border-gray-100"></div>
|
||||
|
||||
<!-- Team Switcher -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Switch Teams
|
||||
</div>
|
||||
|
||||
<template v-for="team in $page.props.user.all_teams" :key="team.id">
|
||||
<form @submit.prevent="switchToTeam(team)">
|
||||
<jet-dropdown-link as="button">
|
||||
<div class="flex items-center">
|
||||
<svg v-if="team.id == $page.props.user.current_team_id" class="mr-2 h-5 w-5 text-green-400" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" viewBox="0 0 24 24"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<div>{{ team.name }}</div>
|
||||
</div>
|
||||
</jet-dropdown-link>
|
||||
</form>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</jet-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="ml-3 relative">
|
||||
<jet-dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<button v-if="$page.props.jetstream.managesProfilePhotos" class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
|
||||
<img class="h-8 w-8 rounded-full object-cover" :src="$page.props.user.profile_photo_url" :alt="$page.props.user.name" />
|
||||
</button>
|
||||
|
||||
<span v-else class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition">
|
||||
{{ $page.props.user.name }}
|
||||
|
||||
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<!-- Account Management -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
Manage Account
|
||||
</div>
|
||||
|
||||
<jet-dropdown-link :href="route('profile.show')">
|
||||
Profile
|
||||
</jet-dropdown-link>
|
||||
|
||||
<jet-dropdown-link :href="route('profile.billing.show')">
|
||||
Billing
|
||||
</jet-dropdown-link>
|
||||
|
||||
<jet-dropdown-link :href="route('api-tokens.index')" v-if="$page.props.jetstream.hasApiFeatures">
|
||||
API Tokens
|
||||
</jet-dropdown-link>
|
||||
|
||||
<div class="border-t border-gray-100"></div>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form @submit.prevent="logout">
|
||||
<jet-dropdown-link as="button">
|
||||
Log Out
|
||||
</jet-dropdown-link>
|
||||
</form>
|
||||
</template>
|
||||
</jet-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main class="mainc">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue"
|
||||
import { Head, Link } from "@inertiajs/inertia-vue3"
|
||||
import SuccessNotifications from "@/Components/Notifications/SuccessNotifications.vue"
|
||||
import ErrorNotifications from "@/Components/Notifications/ErrorNotifications.vue"
|
||||
import GenericNotifications from "@/Components/Notifications/GenericNotifications.vue"
|
||||
import ApplicationMark from "@/Jetstream/ApplicationMark.vue"
|
||||
import ResponsiveNavLink from "@/Components/ResponsiveNavLink.vue"
|
||||
import SidenavLink from "@/Components/SidenavLink.vue"
|
||||
import JetBanner from "@/Jetstream/Banner.vue"
|
||||
import JetDropdown from "@/Jetstream/Dropdown.vue"
|
||||
import JetDropdownLink from "@/Jetstream/DropdownLink.vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
title: String,
|
||||
},
|
||||
|
||||
components: {
|
||||
Head,
|
||||
Link,
|
||||
SuccessNotifications,
|
||||
ErrorNotifications,
|
||||
GenericNotifications,
|
||||
ApplicationMark,
|
||||
ResponsiveNavLink,
|
||||
SidenavLink,
|
||||
JetBanner,
|
||||
JetDropdown,
|
||||
JetDropdownLink,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showingNavigationDropdown: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
switchToTeam(team) {
|
||||
this.$inertia.put(route("current-team.update"), {
|
||||
"team_id": team.id
|
||||
}, {
|
||||
preserveState: false
|
||||
})
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.$inertia.post(route("logout"));
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
67
src/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
67
src/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<Head title="Secure Area" />
|
||||
|
||||
<jet-authentication-card>
|
||||
<template #logo>
|
||||
<jet-authentication-card-logo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
This is a secure area of the application. Please confirm your password before continuing.
|
||||
</div>
|
||||
|
||||
<jet-validation-errors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<jet-label for="password" value="Password" />
|
||||
<jet-input id="password" type="password" class="mt-1 block w-full" v-model="form.password" required autocomplete="current-password" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<jet-button class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Confirm
|
||||
</jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</jet-authentication-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { Head } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue'
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Head,
|
||||
JetAuthenticationCard,
|
||||
JetAuthenticationCardLogo,
|
||||
JetButton,
|
||||
JetInput,
|
||||
JetLabel,
|
||||
JetValidationErrors
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form({
|
||||
password: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.form.post(this.route('password.confirm'), {
|
||||
onFinish: () => this.form.reset(),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
73
src/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
73
src/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<Head title="Forgot Password" />
|
||||
|
||||
<jet-authentication-card>
|
||||
<template #logo>
|
||||
<jet-authentication-card-logo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
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>
|
||||
|
||||
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<jet-validation-errors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<jet-label for="email" value="Email" />
|
||||
<jet-input id="email" type="email" class="mt-1 block w-full" v-model="form.email" required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Email Password Reset Link
|
||||
</jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</jet-authentication-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { Head } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue'
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Head,
|
||||
JetAuthenticationCard,
|
||||
JetAuthenticationCardLogo,
|
||||
JetButton,
|
||||
JetInput,
|
||||
JetLabel,
|
||||
JetValidationErrors
|
||||
},
|
||||
|
||||
props: {
|
||||
status: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form({
|
||||
email: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.form.post(this.route('password.email'))
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
98
src/resources/js/Pages/Auth/Login.vue
Normal file
98
src/resources/js/Pages/Auth/Login.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<Head title="Log in" />
|
||||
|
||||
<jet-authentication-card>
|
||||
<template #logo>
|
||||
<authentication-card-logo />
|
||||
</template>
|
||||
|
||||
<jet-validation-errors class="mb-4" />
|
||||
|
||||
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<jet-label for="email" value="Email" />
|
||||
<jet-input id="email" type="email" class="mt-1 block w-full" v-model="form.email" required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-label for="password" value="Password" />
|
||||
<jet-input id="password" type="password" class="mt-1 block w-full" v-model="form.password" required autocomplete="current-password" />
|
||||
</div>
|
||||
|
||||
<div class="block mt-4">
|
||||
<label class="flex items-center">
|
||||
<jet-checkbox name="remember" v-model:checked="form.remember" />
|
||||
<span class="ml-2 text-sm text-gray-600">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<Link v-if="canResetPassword" :href="route('password.request')" class="underline text-sm text-gray-600 hover:text-gray-900">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
|
||||
<jet-button class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Log in
|
||||
</jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</jet-authentication-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue'
|
||||
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetCheckbox from '@/Jetstream/Checkbox.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue'
|
||||
import { Head, Link } from '@inertiajs/inertia-vue3';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Head,
|
||||
JetAuthenticationCard,
|
||||
AuthenticationCardLogo,
|
||||
JetButton,
|
||||
JetInput,
|
||||
JetCheckbox,
|
||||
JetLabel,
|
||||
JetValidationErrors,
|
||||
Link,
|
||||
},
|
||||
|
||||
props: {
|
||||
canResetPassword: Boolean,
|
||||
status: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.form
|
||||
.transform(data => ({
|
||||
... data,
|
||||
remember: this.form.remember ? 'on' : ''
|
||||
}))
|
||||
.post(this.route('login'), {
|
||||
onFinish: () => this.form.reset('password'),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
114
src/resources/js/Pages/Auth/Register.vue
Normal file
114
src/resources/js/Pages/Auth/Register.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<Head title="Register" />
|
||||
|
||||
<jet-authentication-card>
|
||||
<template #logo>
|
||||
<authentication-card-logo />
|
||||
</template>
|
||||
|
||||
<jet-validation-errors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<jet-label for="name" value="Name" />
|
||||
<jet-input id="name" type="text" class="mt-1 block w-full" v-model="form.name" required autofocus autocomplete="given-name" />
|
||||
<jet-input-error :message="form.errors.name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-label for="surname" value="Surname" />
|
||||
<jet-input id="surname" type="text" class="mt-1 block w-full" v-model="form.surname" required autocomplete="family-name" />
|
||||
<jet-input-error :message="form.errors.surname" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-label for="email" value="Email" />
|
||||
<jet-input id="email" type="email" class="mt-1 block w-full" v-model="form.email" required />
|
||||
<jet-input-error :message="form.errors.email" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-label for="password" value="Password" />
|
||||
<jet-input id="password" type="password" class="mt-1 block w-full" v-model="form.password" required autocomplete="new-password" />
|
||||
<jet-input-error :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-label for="password_confirmation" value="Confirm Password" />
|
||||
<jet-input id="password_confirmation" type="password" class="mt-1 block w-full" v-model="form.password_confirmation" required autocomplete="new-password" />
|
||||
<jet-input-error :message="form.errors.password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4" v-if="$page.props.jetstream.hasTermsAndPrivacyPolicyFeature">
|
||||
<jet-label for="terms">
|
||||
<div class="flex items-center">
|
||||
<jet-checkbox name="terms" id="terms" v-model:checked="form.terms" />
|
||||
|
||||
<div class="ml-2">
|
||||
I agree to the <a target="_blank" :href="route('terms.show')" class="underline text-sm text-gray-600 hover:text-gray-900">Terms of Service</a> and <a target="_blank" :href="route('policy.show')" class="underline text-sm text-gray-600 hover:text-gray-900">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
</jet-label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<Link :href="route('login')" class="underline text-sm text-gray-600 hover:text-gray-900">
|
||||
Already registered?
|
||||
</Link>
|
||||
|
||||
<jet-button class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Register
|
||||
</jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</jet-authentication-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue'
|
||||
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetInputError from '@/Jetstream/InputError.vue'
|
||||
import JetCheckbox from '@/Jetstream/Checkbox.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue'
|
||||
import { Head, Link } from '@inertiajs/inertia-vue3';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Head,
|
||||
JetAuthenticationCard,
|
||||
AuthenticationCardLogo,
|
||||
JetButton,
|
||||
JetInput,
|
||||
JetInputError,
|
||||
JetCheckbox,
|
||||
JetLabel,
|
||||
JetValidationErrors,
|
||||
Link,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form({
|
||||
name: '',
|
||||
surname: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
terms: false,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.form.post(this.route('register'), {
|
||||
onFinish: () => this.form.reset('password', 'password_confirmation'),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
81
src/resources/js/Pages/Auth/ResetPassword.vue
Normal file
81
src/resources/js/Pages/Auth/ResetPassword.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<Head title="Reset Password" />
|
||||
|
||||
<jet-authentication-card>
|
||||
<template #logo>
|
||||
<jet-authentication-card-logo />
|
||||
</template>
|
||||
|
||||
<jet-validation-errors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<jet-label for="email" value="Email" />
|
||||
<jet-input id="email" type="email" class="mt-1 block w-full" v-model="form.email" required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-label for="password" value="Password" />
|
||||
<jet-input id="password" type="password" class="mt-1 block w-full" v-model="form.password" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-label for="password_confirmation" value="Confirm Password" />
|
||||
<jet-input id="password_confirmation" type="password" class="mt-1 block w-full" v-model="form.password_confirmation" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Reset Password
|
||||
</jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</jet-authentication-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { Head } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue'
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Head,
|
||||
JetAuthenticationCard,
|
||||
JetAuthenticationCardLogo,
|
||||
JetButton,
|
||||
JetInput,
|
||||
JetLabel,
|
||||
JetValidationErrors
|
||||
},
|
||||
|
||||
props: {
|
||||
email: String,
|
||||
token: String,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form({
|
||||
token: this.token,
|
||||
email: this.email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.form.post(this.route('password.update'), {
|
||||
onFinish: () => this.form.reset('password', 'password_confirmation'),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
102
src/resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
102
src/resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<Head title="Two-factor Confirmation" />
|
||||
|
||||
<jet-authentication-card>
|
||||
<template #logo>
|
||||
<jet-authentication-card-logo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
<template v-if="! recovery">
|
||||
Please confirm access to your account by entering the authentication code provided by your authenticator application.
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Please confirm access to your account by entering one of your emergency recovery codes.
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<jet-validation-errors class="mb-4" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div v-if="! recovery">
|
||||
<jet-label for="code" value="Code" />
|
||||
<jet-input ref="code" id="code" type="text" inputmode="numeric" class="mt-1 block w-full" v-model="form.code" autofocus autocomplete="one-time-code" />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<jet-label for="recovery_code" value="Recovery Code" />
|
||||
<jet-input ref="recovery_code" id="recovery_code" type="text" class="mt-1 block w-full" v-model="form.recovery_code" autocomplete="one-time-code" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<button type="button" class="text-sm text-gray-600 hover:text-gray-900 underline cursor-pointer" @click.prevent="toggleRecovery">
|
||||
<template v-if="! recovery">
|
||||
Use a recovery code
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Use an authentication code
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<jet-button class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Log in
|
||||
</jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</jet-authentication-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { Head } from '@inertiajs/inertia-vue3';
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue'
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Head,
|
||||
JetAuthenticationCard,
|
||||
JetAuthenticationCardLogo,
|
||||
JetButton,
|
||||
JetInput,
|
||||
JetLabel,
|
||||
JetValidationErrors,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
recovery: false,
|
||||
form: this.$inertia.form({
|
||||
code: '',
|
||||
recovery_code: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleRecovery() {
|
||||
this.recovery ^= true
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.recovery) {
|
||||
this.$refs.recovery_code.focus()
|
||||
this.form.code = '';
|
||||
} else {
|
||||
this.$refs.code.focus()
|
||||
this.form.recovery_code = ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.form.post(this.route('two-factor.login'))
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
67
src/resources/js/Pages/Auth/VerifyEmail.vue
Normal file
67
src/resources/js/Pages/Auth/VerifyEmail.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<Head title="Email Verification" />
|
||||
|
||||
<jet-authentication-card>
|
||||
<template #logo>
|
||||
<jet-authentication-card-logo />
|
||||
</template>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
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>
|
||||
|
||||
<div class="mb-4 font-medium text-sm text-green-600" v-if="verificationLinkSent" >
|
||||
A new verification link has been sent to the email address you provided during registration.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Resend Verification Email
|
||||
</jet-button>
|
||||
|
||||
<Link :href="route('logout')" method="post" as="button" class="underline text-sm text-gray-600 hover:text-gray-900">Log Out</Link>
|
||||
</div>
|
||||
</form>
|
||||
</jet-authentication-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetAuthenticationCard from '@/Jetstream/AuthenticationCard.vue'
|
||||
import JetAuthenticationCardLogo from '@/Jetstream/AuthenticationCardLogo.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import { Head, Link } from '@inertiajs/inertia-vue3';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Head,
|
||||
JetAuthenticationCard,
|
||||
JetAuthenticationCardLogo,
|
||||
JetButton,
|
||||
Link,
|
||||
},
|
||||
|
||||
props: {
|
||||
status: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
submit() {
|
||||
this.form.post(this.route('verification.send'))
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
verificationLinkSent() {
|
||||
return this.status === 'verification-link-sent';
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
36
src/resources/js/Pages/Dashboard.vue
Normal file
36
src/resources/js/Pages/Dashboard.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<app-layout title="Dashboard">
|
||||
<template #header>Dashboard</template>
|
||||
|
||||
<div class="col-span-6">
|
||||
<!-- content goes here -->
|
||||
</div>
|
||||
</app-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue"
|
||||
import AppLayout from "@/Layouts/AppLayout.vue"
|
||||
import Card from "@/Components/Card.vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {},
|
||||
|
||||
components: {
|
||||
AppLayout,
|
||||
Card,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
return {}
|
||||
},
|
||||
|
||||
computed: {},
|
||||
|
||||
methods: {},
|
||||
})
|
||||
</script>
|
107
src/resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
107
src/resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<jet-action-section>
|
||||
<template #title>
|
||||
Delete Account
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Permanently delete your account.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="max-w-xl text-sm text-gray-600">
|
||||
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.
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<jet-danger-button @click="confirmUserDeletion">
|
||||
Delete Account
|
||||
</jet-danger-button>
|
||||
</div>
|
||||
|
||||
<!-- Delete Account Confirmation Modal -->
|
||||
<jet-dialog-modal :show="confirmingUserDeletion" @close="closeModal">
|
||||
<template #title>
|
||||
Delete Account
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
Are you sure you want to delete your account? 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.
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-input type="password" class="mt-1 block w-3/4" placeholder="Password"
|
||||
ref="password"
|
||||
v-model="form.password"
|
||||
@keyup.enter="deleteUser" />
|
||||
|
||||
<jet-input-error :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<jet-secondary-button @click="closeModal">
|
||||
Cancel
|
||||
</jet-secondary-button>
|
||||
|
||||
<jet-danger-button class="ml-2" @click="deleteUser" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Delete Account
|
||||
</jet-danger-button>
|
||||
</template>
|
||||
</jet-dialog-modal>
|
||||
</template>
|
||||
</jet-action-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetActionSection from '@/Jetstream/ActionSection.vue'
|
||||
import JetDialogModal from '@/Jetstream/DialogModal.vue'
|
||||
import JetDangerButton from '@/Jetstream/DangerButton.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetInputError from '@/Jetstream/InputError.vue'
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
JetActionSection,
|
||||
JetDangerButton,
|
||||
JetDialogModal,
|
||||
JetInput,
|
||||
JetInputError,
|
||||
JetSecondaryButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
confirmingUserDeletion: false,
|
||||
|
||||
form: this.$inertia.form({
|
||||
password: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
confirmUserDeletion() {
|
||||
this.confirmingUserDeletion = true;
|
||||
|
||||
setTimeout(() => this.$refs.password.focus(), 250)
|
||||
},
|
||||
|
||||
deleteUser() {
|
||||
this.form.delete(route('current-user.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => this.closeModal(),
|
||||
onError: () => this.$refs.password.focus(),
|
||||
onFinish: () => this.form.reset(),
|
||||
})
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.confirmingUserDeletion = false
|
||||
|
||||
this.form.reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<jet-action-section>
|
||||
<template #title>
|
||||
Browser Sessions
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Manage and log out your active sessions on other browsers and devices.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="max-w-xl text-sm text-gray-600">
|
||||
If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.
|
||||
</div>
|
||||
|
||||
<!-- Other Browser Sessions -->
|
||||
<div class="mt-5 space-y-6" v-if="sessions.length > 0">
|
||||
<div class="flex items-center" v-for="(session, i) in sessions" :key="i">
|
||||
<div>
|
||||
<svg fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor" class="w-8 h-8 text-gray-500" v-if="session.agent.is_desktop">
|
||||
<path d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" class="w-8 h-8 text-gray-500" v-else>
|
||||
<path d="M0 0h24v24H0z" stroke="none"></path><rect x="7" y="4" width="10" height="16" rx="1"></rect><path d="M11 5h2M12 17v.01"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="ml-3">
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ session.agent.platform }} - {{ session.agent.browser }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ session.ip_address }},
|
||||
|
||||
<span class="text-green-500 font-semibold" v-if="session.is_current_device">This device</span>
|
||||
<span v-else>Last active {{ session.last_active }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-5">
|
||||
<jet-button @click="confirmLogout">
|
||||
Log Out Other Browser Sessions
|
||||
</jet-button>
|
||||
|
||||
<jet-action-message :on="form.recentlySuccessful" class="ml-3">
|
||||
Done.
|
||||
</jet-action-message>
|
||||
</div>
|
||||
|
||||
<!-- Log Out Other Devices Confirmation Modal -->
|
||||
<jet-dialog-modal :show="confirmingLogout" @close="closeModal">
|
||||
<template #title>
|
||||
Log Out Other Browser Sessions
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.
|
||||
|
||||
<div class="mt-4">
|
||||
<jet-input type="password" class="mt-1 block w-3/4" placeholder="Password"
|
||||
ref="password"
|
||||
v-model="form.password"
|
||||
@keyup.enter="logoutOtherBrowserSessions" />
|
||||
|
||||
<jet-input-error :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<jet-secondary-button @click="closeModal">
|
||||
Cancel
|
||||
</jet-secondary-button>
|
||||
|
||||
<jet-button class="ml-2" @click="logoutOtherBrowserSessions" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Log Out Other Browser Sessions
|
||||
</jet-button>
|
||||
</template>
|
||||
</jet-dialog-modal>
|
||||
</template>
|
||||
</jet-action-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetActionMessage from '@/Jetstream/ActionMessage.vue'
|
||||
import JetActionSection from '@/Jetstream/ActionSection.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetDialogModal from '@/Jetstream/DialogModal.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetInputError from '@/Jetstream/InputError.vue'
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: ['sessions'],
|
||||
|
||||
components: {
|
||||
JetActionMessage,
|
||||
JetActionSection,
|
||||
JetButton,
|
||||
JetDialogModal,
|
||||
JetInput,
|
||||
JetInputError,
|
||||
JetSecondaryButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
confirmingLogout: false,
|
||||
|
||||
form: this.$inertia.form({
|
||||
password: '',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
confirmLogout() {
|
||||
this.confirmingLogout = true
|
||||
|
||||
setTimeout(() => this.$refs.password.focus(), 250)
|
||||
},
|
||||
|
||||
logoutOtherBrowserSessions() {
|
||||
this.form.delete(route('other-browser-sessions.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => this.closeModal(),
|
||||
onError: () => this.$refs.password.focus(),
|
||||
onFinish: () => this.form.reset(),
|
||||
})
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.confirmingLogout = false
|
||||
|
||||
this.form.reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<jet-action-section>
|
||||
<template #title>
|
||||
Two Factor Authentication
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Add additional security to your account using two factor authentication.
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<h3 class="text-lg font-medium text-gray-900" v-if="twoFactorEnabled">
|
||||
You have enabled two factor authentication.
|
||||
</h3>
|
||||
|
||||
<h3 class="text-lg font-medium text-gray-900" v-else>
|
||||
You have not enabled two factor authentication.
|
||||
</h3>
|
||||
|
||||
<div class="mt-3 max-w-xl text-sm text-gray-600">
|
||||
<p>
|
||||
When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="twoFactorEnabled">
|
||||
<div v-if="qrCode">
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" v-html="qrCode">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="recoveryCodes.length > 0">
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
|
||||
<div v-for="code in recoveryCodes" :key="code">
|
||||
{{ code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<div v-if="! twoFactorEnabled">
|
||||
<jet-confirms-password @confirmed="enableTwoFactorAuthentication">
|
||||
<jet-button type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
|
||||
Enable
|
||||
</jet-button>
|
||||
</jet-confirms-password>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<jet-confirms-password @confirmed="regenerateRecoveryCodes">
|
||||
<jet-secondary-button class="mr-3"
|
||||
v-if="recoveryCodes.length > 0">
|
||||
Regenerate Recovery Codes
|
||||
</jet-secondary-button>
|
||||
</jet-confirms-password>
|
||||
|
||||
<jet-confirms-password @confirmed="showRecoveryCodes">
|
||||
<jet-secondary-button class="mr-3" v-if="recoveryCodes.length === 0">
|
||||
Show Recovery Codes
|
||||
</jet-secondary-button>
|
||||
</jet-confirms-password>
|
||||
|
||||
<jet-confirms-password @confirmed="disableTwoFactorAuthentication">
|
||||
<jet-danger-button
|
||||
:class="{ 'opacity-25': disabling }"
|
||||
:disabled="disabling">
|
||||
Disable
|
||||
</jet-danger-button>
|
||||
</jet-confirms-password>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</jet-action-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetActionSection from '@/Jetstream/ActionSection.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetConfirmsPassword from '@/Jetstream/ConfirmsPassword.vue'
|
||||
import JetDangerButton from '@/Jetstream/DangerButton.vue'
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
JetActionSection,
|
||||
JetButton,
|
||||
JetConfirmsPassword,
|
||||
JetDangerButton,
|
||||
JetSecondaryButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
enabling: false,
|
||||
disabling: false,
|
||||
|
||||
qrCode: null,
|
||||
recoveryCodes: [],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
enableTwoFactorAuthentication() {
|
||||
this.enabling = true
|
||||
|
||||
this.$inertia.post('/user/two-factor-authentication', {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => Promise.all([
|
||||
this.showQrCode(),
|
||||
this.showRecoveryCodes(),
|
||||
]),
|
||||
onFinish: () => (this.enabling = false),
|
||||
})
|
||||
},
|
||||
|
||||
showQrCode() {
|
||||
return axios.get('/user/two-factor-qr-code')
|
||||
.then(response => {
|
||||
this.qrCode = response.data.svg
|
||||
})
|
||||
},
|
||||
|
||||
showRecoveryCodes() {
|
||||
return axios.get('/user/two-factor-recovery-codes')
|
||||
.then(response => {
|
||||
this.recoveryCodes = response.data
|
||||
})
|
||||
},
|
||||
|
||||
regenerateRecoveryCodes() {
|
||||
axios.post('/user/two-factor-recovery-codes')
|
||||
.then(response => {
|
||||
this.showRecoveryCodes()
|
||||
})
|
||||
},
|
||||
|
||||
disableTwoFactorAuthentication() {
|
||||
this.disabling = true
|
||||
|
||||
this.$inertia.delete('/user/two-factor-authentication', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => (this.disabling = false),
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
twoFactorEnabled() {
|
||||
return ! this.enabling && this.$page.props.user.two_factor_enabled
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<jet-form-section @submitted="updatePassword">
|
||||
<template #title>
|
||||
Update Password
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Ensure your account is using a long, random password to stay secure.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<jet-label for="current_password" value="Current Password" />
|
||||
<jet-input id="current_password" type="password" class="mt-1 block w-full" v-model="form.current_password" ref="current_password" autocomplete="current-password" />
|
||||
<jet-input-error :message="form.errors.current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<jet-label for="password" value="New Password" />
|
||||
<jet-input id="password" type="password" class="mt-1 block w-full" v-model="form.password" ref="password" autocomplete="new-password" />
|
||||
<jet-input-error :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<jet-label for="password_confirmation" value="Confirm Password" />
|
||||
<jet-input id="password_confirmation" type="password" class="mt-1 block w-full" v-model="form.password_confirmation" autocomplete="new-password" />
|
||||
<jet-input-error :message="form.errors.password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<jet-action-message :on="form.recentlySuccessful" class="mr-3">
|
||||
Saved.
|
||||
</jet-action-message>
|
||||
|
||||
<jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</jet-button>
|
||||
</template>
|
||||
</jet-form-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetActionMessage from '@/Jetstream/ActionMessage.vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetFormSection from '@/Jetstream/FormSection.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetInputError from '@/Jetstream/InputError.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
JetActionMessage,
|
||||
JetButton,
|
||||
JetFormSection,
|
||||
JetInput,
|
||||
JetInputError,
|
||||
JetLabel,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updatePassword() {
|
||||
this.form.put(route('user-password.update'), {
|
||||
errorBag: 'updatePassword',
|
||||
preserveScroll: true,
|
||||
onSuccess: () => this.form.reset(),
|
||||
onError: () => {
|
||||
if (this.form.errors.password) {
|
||||
this.form.reset('password', 'password_confirmation')
|
||||
this.$refs.password.focus()
|
||||
}
|
||||
|
||||
if (this.form.errors.current_password) {
|
||||
this.form.reset('current_password')
|
||||
this.$refs.current_password.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<jet-form-section @submitted="updateProfileInformation">
|
||||
<template #title>
|
||||
Profile Information
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
Update your account's profile information and email address.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<!-- Profile Photo -->
|
||||
<div class="col-span-6 sm:col-span-4" v-if="$page.props.jetstream.managesProfilePhotos">
|
||||
<!-- Profile Photo File Input -->
|
||||
<input type="file" class="hidden"
|
||||
ref="photo"
|
||||
@change="updatePhotoPreview">
|
||||
|
||||
<jet-label for="photo" value="Photo" />
|
||||
|
||||
<!-- Current Profile Photo -->
|
||||
<div class="mt-2" v-show="! photoPreview">
|
||||
<img :src="user.profile_photo_url" :alt="user.name" class="rounded-full h-20 w-20 object-cover">
|
||||
</div>
|
||||
|
||||
<!-- New Profile Photo Preview -->
|
||||
<div class="mt-2" v-show="photoPreview">
|
||||
<span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
|
||||
:style="'background-image: url(\'' + photoPreview + '\');'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<jet-secondary-button class="mt-2 mr-2" type="button" @click.prevent="selectNewPhoto">
|
||||
Select A New Photo
|
||||
</jet-secondary-button>
|
||||
|
||||
<jet-secondary-button type="button" class="mt-2" @click.prevent="deletePhoto" v-if="user.profile_photo_path">
|
||||
Remove Photo
|
||||
</jet-secondary-button>
|
||||
|
||||
<jet-input-error :message="form.errors.photo" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<jet-label for="name" value="Name" />
|
||||
<jet-input id="name" type="text" class="mt-1 block w-full" v-model="form.name" autocomplete="given-name" />
|
||||
<jet-input-error :message="form.errors.name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Surname -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<jet-label for="surname" value="Surname" />
|
||||
<jet-input id="surname" type="text" class="mt-1 block w-full" v-model="form.surname" autocomplete="family-name" />
|
||||
<jet-input-error :message="form.errors.surname" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<jet-label for="email" value="Email" />
|
||||
<jet-input id="email" type="email" class="mt-1 block w-full" v-model="form.email" />
|
||||
<jet-input-error :message="form.errors.email" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Timezone -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<jet-label for="timezone_name" value="Timezone" />
|
||||
<jet-input id="timezone_name" type="text" list="" class="mt-1 block w-full" v-model="form.timezone_name" />
|
||||
<jet-input-error :message="form.errors.timezone_name" class="mt-2" />
|
||||
<datalist id="timezones">
|
||||
<option v-for="timezone in timezones" :key="timezone" :value="timezone"></option>
|
||||
</datalist>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<jet-action-message :on="form.recentlySuccessful" class="mr-3">
|
||||
Saved.
|
||||
</jet-action-message>
|
||||
|
||||
<jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Save
|
||||
</jet-button>
|
||||
</template>
|
||||
</jet-form-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import JetButton from '@/Jetstream/Button.vue'
|
||||
import JetFormSection from '@/Jetstream/FormSection.vue'
|
||||
import JetInput from '@/Jetstream/Input.vue'
|
||||
import JetInputError from '@/Jetstream/InputError.vue'
|
||||
import JetLabel from '@/Jetstream/Label.vue'
|
||||
import JetActionMessage from '@/Jetstream/ActionMessage.vue'
|
||||
import JetSecondaryButton from '@/Jetstream/SecondaryButton.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
JetActionMessage,
|
||||
JetButton,
|
||||
JetFormSection,
|
||||
JetInput,
|
||||
JetInputError,
|
||||
JetLabel,
|
||||
JetSecondaryButton,
|
||||
},
|
||||
|
||||
props: ['user', 'timezones'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
form: this.$inertia.form({
|
||||
_method: 'PUT',
|
||||
name: this.user.name,
|
||||
surname: this.user.surname,
|
||||
email: this.user.email,
|
||||
timezone_name: this.user.timezone_name,
|
||||
photo: null,
|
||||
}),
|
||||
|
||||
photoPreview: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateProfileInformation() {
|
||||
if (this.$refs.photo) {
|
||||
this.form.photo = this.$refs.photo.files[0]
|
||||
}
|
||||
|
||||
this.form.post(route('user-profile-information.update'), {
|
||||
errorBag: 'updateProfileInformation',
|
||||
preserveScroll: true,
|
||||
onSuccess: () => (this.clearPhotoFileInput()),
|
||||
});
|
||||
},
|
||||
|
||||
selectNewPhoto() {
|
||||
this.$refs.photo.click();
|
||||
},
|
||||
|
||||
updatePhotoPreview() {
|
||||
const photo = this.$refs.photo.files[0];
|
||||
|
||||
if (! photo) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
this.photoPreview = e.target.result;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(photo);
|
||||
},
|
||||
|
||||
deletePhoto() {
|
||||
this.$inertia.delete(route('current-user-photo.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
this.photoPreview = null;
|
||||
this.clearPhotoFileInput();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
clearPhotoFileInput() {
|
||||
if (this.$refs.photo?.value) {
|
||||
this.$refs.photo.value = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
68
src/resources/js/Pages/Profile/Show.vue
Normal file
68
src/resources/js/Pages/Profile/Show.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<app-layout title="Profile">
|
||||
<template #header>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Profile
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
<div v-if="$page.props.jetstream.canUpdateProfileInformation">
|
||||
<update-profile-information-form :user="$page.props.user" />
|
||||
|
||||
<jet-section-border />
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.canUpdatePassword">
|
||||
<update-password-form class="mt-10 sm:mt-0" />
|
||||
|
||||
<jet-section-border />
|
||||
</div>
|
||||
|
||||
<div v-if="$page.props.jetstream.canManageTwoFactorAuthentication">
|
||||
<two-factor-authentication-form class="mt-10 sm:mt-0" />
|
||||
|
||||
<jet-section-border />
|
||||
</div>
|
||||
|
||||
<logout-other-browser-sessions-form :sessions="sessions" class="mt-10 sm:mt-0" />
|
||||
|
||||
<template v-if="$page.props.jetstream.hasAccountDeletionFeatures">
|
||||
<jet-section-border />
|
||||
|
||||
<delete-user-form class="mt-10 sm:mt-0" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</app-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||
import DeleteUserForm from '@/Pages/Profile/Partials/DeleteUserForm.vue'
|
||||
import JetSectionBorder from '@/Jetstream/SectionBorder.vue'
|
||||
import LogoutOtherBrowserSessionsForm from '@/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue'
|
||||
import TwoFactorAuthenticationForm from '@/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue'
|
||||
import UpdatePasswordForm from '@/Pages/Profile/Partials/UpdatePasswordForm.vue'
|
||||
import UpdateProfileInformationForm from '@/Pages/Profile/Partials/UpdateProfileInformationForm.vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
sessions: Array,
|
||||
timezones: Array,
|
||||
allTimes: Array,
|
||||
},
|
||||
|
||||
components: {
|
||||
AppLayout,
|
||||
DeleteUserForm,
|
||||
JetSectionBorder,
|
||||
LogoutOtherBrowserSessionsForm,
|
||||
TwoFactorAuthenticationForm,
|
||||
UpdatePasswordForm,
|
||||
UpdateProfileInformationForm,
|
||||
},
|
||||
})
|
||||
</script>
|
26
src/resources/js/app.js
Normal file
26
src/resources/js/app.js
Normal file
@ -0,0 +1,26 @@
|
||||
require("./bootstrap");
|
||||
|
||||
import { createApp, h } from "vue";
|
||||
import { createInertiaApp } from "@inertiajs/inertia-vue3";
|
||||
import { InertiaProgress } from "@inertiajs/progress";
|
||||
import Notifications from "notiwind";
|
||||
|
||||
const appName = window.document.getElementsByTagName("title")[0]?.innerText || "Laravel";
|
||||
|
||||
window.genUid = function () {
|
||||
return [...Array(16)].map(() => Math.floor(Math.random() * 16).toString(16)).join("");
|
||||
};
|
||||
|
||||
createInertiaApp({
|
||||
title: (title) => `${title} - ${appName}`,
|
||||
resolve: (name) => require(`./Pages/${name}.vue`),
|
||||
setup({ el, app, props, plugin }) {
|
||||
return createApp({ render: () => h(app, props) })
|
||||
.use(plugin)
|
||||
.use(Notifications)
|
||||
.mixin({ methods: { route } })
|
||||
.mount(el);
|
||||
},
|
||||
});
|
||||
|
||||
InertiaProgress.init({ color: "#4b5563" });
|
27
src/resources/js/bootstrap.js
vendored
Normal file
27
src/resources/js/bootstrap.js
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
window._ = require('lodash');
|
||||
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
|
||||
window.axios = require('axios');
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
/**
|
||||
* Echo exposes an expressive API for subscribing to channels and listening
|
||||
* for events that are broadcast by Laravel. Echo and event broadcasting
|
||||
* allows your team to easily build robust real-time web applications.
|
||||
*/
|
||||
|
||||
// import Echo from 'laravel-echo';
|
||||
|
||||
// window.Pusher = require('pusher-js');
|
||||
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'pusher',
|
||||
// key: process.env.MIX_PUSHER_APP_KEY,
|
||||
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
// forceTLS: true
|
||||
// });
|
42
src/resources/lang/en/app.php
Normal file
42
src/resources/lang/en/app.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| App Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used throughout the application.
|
||||
|
|
||||
*/
|
||||
|
||||
// General language lines go here.
|
||||
//
|
||||
|
||||
// Text used as a page title or main heading.
|
||||
'titles_' => [
|
||||
//
|
||||
|
||||
'nav_' => [
|
||||
//
|
||||
],
|
||||
],
|
||||
|
||||
// Text used as a label of some sort, not restricted to
|
||||
// label HTML elements.
|
||||
'labels_' => [
|
||||
//
|
||||
],
|
||||
|
||||
// Text that may really only show up in a button.
|
||||
'buttons_' => [
|
||||
//
|
||||
],
|
||||
|
||||
// These will be longer status messages or short descriptions that serve
|
||||
// as an informational/warning notice to the user.
|
||||
'notices_' => [
|
||||
//
|
||||
],
|
||||
];
|
74
src/resources/views/app.blade.php
Normal file
74
src/resources/views/app.blade.php
Normal file
@ -0,0 +1,74 @@
|
||||
<!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="robots" content="index,follow">
|
||||
<meta name="googlebot" content="index,follow">
|
||||
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||
|
||||
<meta name="google" content="notranslate">
|
||||
<meta name="google" content="nositelinkssearchbox">
|
||||
|
||||
<meta name="rating" content="General">
|
||||
|
||||
<meta name="url" content="{{ url('/') }}">
|
||||
<meta name="subject" content="your website's subject">
|
||||
<meta name="description" content="A description of the page">
|
||||
|
||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Privacy -->
|
||||
<meta name="twitter:dnt" content="on">
|
||||
|
||||
<!-- analytics -->
|
||||
<!-- -->
|
||||
|
||||
<!-- Helps prevent duplicate content issues -->
|
||||
<link href="{{ url()->full() }}" rel="canonical">
|
||||
|
||||
<!-- Gives a reference to a location in your document that may be in another language -->
|
||||
{{-- <link href="https://de.example.com/2010/06/title-of-my-article" rel="alternate" hreflang="de"> --}}
|
||||
|
||||
<!-- Android web manifest file -->
|
||||
{{-- <link href="{{ url('/.webmanifest') }}" rel="manifest"> --}}
|
||||
|
||||
<!-- Files listing who was involved in this site and copyrights -->
|
||||
<link href="{{ url('/humans.txt') }}" rel="author">
|
||||
<link href="{{ url('/copyright.html') }}" rel="copyright">
|
||||
|
||||
<!-- Feeds -->
|
||||
{{-- <link href="https://feeds.feedburner.com/example" type="application/rss+xml" title="RSS" rel="alternate"> --}}
|
||||
{{-- <link href="{{ url('/feed.atom') }}" type="application/atom+xml" title="Atom 0.3" rel="alternate"> --}}
|
||||
|
||||
<!-- Favicon -->
|
||||
{{-- <link href="{{ asset('/favicon.ico') }}" rel="icon" sizes="16x16" type="image/icon"> --}}
|
||||
{{-- <link href="{{ asset('/favicon.svg') }}" rel="icon" type="image/svg+xml"> --}}
|
||||
{{-- <link href="{{ asset('/favicon.png') }}" rel="icon" sizes="192x192"> --}}
|
||||
|
||||
<!-- Font preloads (should be done for each font file) -->
|
||||
<link href="{{ asset('/fonts/Nunito/Nunito-Regular.woff2') }}" rel="preload" as="font" type="font/woff2" crossorigin="anonymous">
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="{{ mix('/css/app.css') }}" rel="stylesheet" media="screen">
|
||||
|
||||
<!-- JS that must be executed before the document is loaded -->
|
||||
<script>
|
||||
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
document.documentElement.classList.add("dark"); localStorage.theme = "dark";
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark"); localStorage.theme = "light";
|
||||
}
|
||||
</script>
|
||||
|
||||
@routes
|
||||
<script src="{{ mix('/js/app.js') }}" defer></script>
|
||||
</head>
|
||||
<body class="bg-gray-100 font-sans antialiased">
|
||||
@inertia
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user