Chore: Upgrade to Laravel 12 + remove Mary and Jetstream dependencies (#141)
* docs: remove requirement for setting APP_KEY manually * optimize date picker * clean up modals * spot light working * reorganization * add lazy load * wip * remove filament * styling
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Jetstream;
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Jetstream\Contracts\DeletesUsers;
|
||||
|
||||
class DeleteUser implements DeletesUsers
|
||||
{
|
||||
/**
|
||||
* Delete the given user.
|
||||
*/
|
||||
public function delete(User $user): void
|
||||
{
|
||||
$user->deleteProfilePhoto();
|
||||
$user->tokens->each->delete();
|
||||
$user->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class ApiTokenController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user API token screen.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('api.index', [
|
||||
'request' => $request,
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ class ConnectedAccountController extends Controller
|
||||
'title' => __('Your :provider account has been connected.', ['provider' => config("services.{$connected_account->provider}.name")]),
|
||||
'description' => null,
|
||||
'css' => 'alert-success',
|
||||
'icon' => Blade::render("<x-mary-icon class='w-7 h-7' name='o-check-circle' />"),
|
||||
'icon' => Blade::render("<x-ui.icon class='w-7 h-7' name='o-check-circle' />"),
|
||||
'position' => 'toast-top toast-end',
|
||||
'timeout' => '5000',
|
||||
],
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Traits\HasLocalizedMarkdown;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PrivacyPolicyController extends Controller
|
||||
{
|
||||
use HasLocalizedMarkdown;
|
||||
|
||||
/**
|
||||
* Show the privacy policy for the application.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
$policyFile = $this->localizedMarkdownPath('policy.md');
|
||||
|
||||
return view('policy', [
|
||||
'policy' => Str::markdown(file_get_contents($policyFile)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Traits\HasLocalizedMarkdown;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TermsOfServiceController extends Controller
|
||||
{
|
||||
use HasLocalizedMarkdown;
|
||||
|
||||
/**
|
||||
* Show the terms of service for the application.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
$termsFile = $this->localizedMarkdownPath('terms.md');
|
||||
|
||||
return view('terms', [
|
||||
'terms' => Str::markdown(file_get_contents($termsFile)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user profile screen.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
return view('profile.show', [
|
||||
'request' => $request,
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatables;
|
||||
|
||||
use App\Models\Holding;
|
||||
use Illuminate\Support\Number;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
|
||||
class HoldingsTable extends DataTableComponent
|
||||
{
|
||||
public $portfolio;
|
||||
public array $hiddenColumns = [];
|
||||
|
||||
public function mount ($portfolio): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Holding::query()
|
||||
->portfolio($this->portfolio->id)
|
||||
->with(['market_data'])
|
||||
->withCount(['transactions as num_transactions' => function ($query) {
|
||||
return $query->whereRaw('transactions.symbol = holdings.symbol');
|
||||
}])
|
||||
->withPerformance();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->hiddenColumns = ['name', 'average_cost_basis', 'market_value', 'fifty_two_week_low', 'fifty_two_week_high'];
|
||||
|
||||
$this->setTableWrapperAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'overflow-scroll'
|
||||
]);
|
||||
$this->setTableAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'table',
|
||||
]);
|
||||
$this->setTheadAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setThAttributes(function(Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap'
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
$this->setThSortButtonAttributes(fn() => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer'
|
||||
]);
|
||||
$this->setTbodyAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setTrAttributes(fn() => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer hover:bg-neutral/25'
|
||||
]);
|
||||
$this->setTdAttributes(function(Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-nowrap'
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
|
||||
$this->setDefaultSort('symbol', 'asc');
|
||||
|
||||
$this->setToolsDisabled();
|
||||
$this->setFooterDisabled();
|
||||
$this->setPaginationDisabled();
|
||||
$this->setDisplayPaginationDetailsDisabled();
|
||||
|
||||
$this->setPrimaryKey('id');
|
||||
|
||||
$this->setTableRowUrl(function($row) {
|
||||
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
||||
|
||||
})->setTableRowUrlTarget(function($row) {
|
||||
|
||||
return 'navigate';
|
||||
});
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make(__('Symbol'), 'symbol')
|
||||
->sortable(),
|
||||
Column::make(__('Name'), 'market_data.name')
|
||||
->sortable(),
|
||||
Column::make(__('Quantity'), 'quantity')
|
||||
->sortable(),
|
||||
Column::make(__('Average Cost Basis'), 'average_cost_basis')
|
||||
->sortable()
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
||||
Column::make(__('Total Cost Basis'), 'total_cost_basis')
|
||||
->sortable()
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
||||
Column::make(__('Market Value'), 'market_data.market_value')
|
||||
->sortable()
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
||||
Column::make(__('Total Market Value'))
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('total_market_value', $direction))
|
||||
->label(fn ($row) => Number::currency($row->total_market_value ?? 0, $row->market_data->currency)),
|
||||
Column::make(__('Market Gain/Loss'))
|
||||
->html()
|
||||
->label(fn($row) => Number::currency($row->market_gain_dollars ?? 0, $row->market_data->currency) . view('components.ui.gain-loss-arrow-badge', [
|
||||
'costBasis' => $row->average_cost_basis,
|
||||
'marketValue' => $row->market_data->market_value,
|
||||
'small' => true,
|
||||
]))
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('market_gain_dollars', $direction)),
|
||||
Column::make(__('Realized Gain/Loss'), 'realized_gain_dollars')
|
||||
->sortable()
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) )
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
||||
Column::make(__('Dividends Earned'), 'dividends_earned')
|
||||
->sortable()
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
||||
Column::make(__('52 week low'), 'market_data.fifty_two_week_low')
|
||||
->sortable()
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
||||
Column::make(__('52 week high'), 'market_data.fifty_two_week_high')
|
||||
->sortable()
|
||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
||||
Column::make(__('Number of Transactions'))
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('num_transactions', $direction))
|
||||
->label(fn ($row) => $row->num_transactions),
|
||||
Column::make(__('Last Refreshed'), 'market_data.updated_at')
|
||||
->sortable()
|
||||
->format(fn($value) => \Carbon\Carbon::parse($value)->diffForHumans() )
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatables;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Number;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
|
||||
class TransactionsTable extends DataTableComponent
|
||||
{
|
||||
public array $hiddenColumns = [];
|
||||
|
||||
public function mount (): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Transaction::query()
|
||||
->with(['portfolio', 'market_data'])
|
||||
->myTransactions()
|
||||
->addSelect(['portfolio_id', 'transaction_type', 'split'])
|
||||
->selectRaw('
|
||||
CASE
|
||||
WHEN transaction_type = \'SELL\'
|
||||
THEN COALESCE(transactions.sale_price - transactions.cost_basis, 0)
|
||||
ELSE COALESCE(market_data.market_value - transactions.cost_basis, 0)
|
||||
END AS gain_dollars');
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->hiddenColumns = ['name', 'cost_basis', 'gain_dollars'];
|
||||
|
||||
$this->setTableWrapperAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'overflow-scroll'
|
||||
]);
|
||||
$this->setTableAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'table',
|
||||
]);
|
||||
$this->setTheadAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setThAttributes(function(Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap'
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
$this->setThSortButtonAttributes(fn() => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer'
|
||||
]);
|
||||
$this->setTbodyAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setTrAttributes(fn() => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer hover:bg-neutral/25'
|
||||
]);
|
||||
$this->setTdAttributes(function(Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-nowrap'
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
|
||||
$this->setDefaultSort('date', 'desc');
|
||||
|
||||
$this->setPerPageAccepted([10, 15, 20]);
|
||||
$this->setPerPage(15);
|
||||
$this->setSearchDisabled();
|
||||
$this->setColumnSelectDisabled();
|
||||
$this->setPerPageVisibilityDisabled();
|
||||
$this->setFooterDisabled();
|
||||
|
||||
$this->setPrimaryKey('id');
|
||||
|
||||
$this->setTableRowUrl(function($row) {
|
||||
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
||||
|
||||
})->setTableRowUrlTarget(function($row) {
|
||||
|
||||
return 'navigate';
|
||||
});
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Column::make(__('Date'), 'date')
|
||||
->sortable()
|
||||
->format(fn($value) => \Carbon\Carbon::parse($value)->format('M d, Y') ),
|
||||
Column::make(__('Portfolio'), 'portfolio.title')
|
||||
->sortable(),
|
||||
Column::make(__('Symbol'), 'symbol')
|
||||
->sortable(),
|
||||
Column::make(__('Name'), 'market_data.name')
|
||||
->sortable(),
|
||||
Column::make(__('Type'), 'transaction_type')
|
||||
->label(fn($row) => view('components.ui.badge', [
|
||||
'value' => $row->split ? 'SPLIT'
|
||||
: ($row->reinvested_dividend
|
||||
? 'REINVEST'
|
||||
: $row->transaction_type),
|
||||
'class' => ($row->transaction_type == 'BUY'
|
||||
? 'badge-success'
|
||||
: 'badge-error') . ' badge-sm mr-3',
|
||||
]))
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('transaction_type', $direction)),
|
||||
Column::make(__('Quantity'), 'quantity')
|
||||
->sortable(),
|
||||
Column::make(__('Cost Basis'), 'cost_basis')
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('cost_basis', $direction))
|
||||
->label(fn ($row) => Number::currency($row->cost_basis ?? 0, $row->market_data->currency)),
|
||||
Column::make(__('Gain/Loss'), 'gain_dollars')
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('gain_dollars', $direction))
|
||||
->label(fn ($row) => Number::currency($row->gain_dollars ?? 0, $row->market_data->currency)),
|
||||
];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasConnectedAccounts;
|
||||
use App\Traits\HasProfilePhoto;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -12,7 +13,6 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
|
||||
|
||||
@@ -30,6 +30,7 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Fortify::viewPrefix('auth.');
|
||||
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Jetstream\DeleteUser;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
class JetstreamServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
|
||||
$this->configurePermissions();
|
||||
|
||||
Jetstream::deleteUsersUsing(DeleteUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the permissions that are available within the application.
|
||||
*/
|
||||
protected function configurePermissions(): void
|
||||
{
|
||||
Jetstream::defaultApiTokenPermissions([
|
||||
// 'portfolio:read',
|
||||
// 'portfolio:write',
|
||||
// 'holding:read',
|
||||
// 'holding:write',
|
||||
// 'transaction:read',
|
||||
// 'transaction:write',
|
||||
]);
|
||||
|
||||
Jetstream::permissions([
|
||||
// 'Read Portfolios' => 'portfolio:read',
|
||||
// 'Create Portfolios' => 'portfolio:write',
|
||||
// 'Read Holdings' => 'holding:read',
|
||||
// 'Update Holdings' => 'holding:write',
|
||||
// 'Read Transactions' => 'transaction:read',
|
||||
// 'Create Transactions' => 'transaction:write',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Volt\Volt;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class VoltServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -21,14 +21,16 @@ class VoltServiceProvider extends ServiceProvider
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
{
|
||||
Volt::mount([
|
||||
// config('livewire.view_path', resource_path('views/livewire')),
|
||||
resource_path('views/components'),
|
||||
resource_path('views/profile'),
|
||||
resource_path('views/api'),
|
||||
resource_path('views/holding'),
|
||||
resource_path('views/transaction'),
|
||||
resource_path('views/portfolio'),
|
||||
resource_path('views/import-export'),
|
||||
resource_path('views/auth'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Actions\ConfirmPassword;
|
||||
|
||||
trait ConfirmsPasswords
|
||||
{
|
||||
/**
|
||||
* Indicates if the user's password is being confirmed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $confirmingPassword = false;
|
||||
|
||||
/**
|
||||
* The ID of the operation being confirmed.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $confirmableId = null;
|
||||
|
||||
/**
|
||||
* The user's password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $confirmablePassword = '';
|
||||
|
||||
/**
|
||||
* Start confirming the user's password.
|
||||
*
|
||||
* @param string $confirmableId
|
||||
* @return void
|
||||
*/
|
||||
public function startConfirmingPassword(string $confirmableId)
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
|
||||
if ($this->passwordIsConfirmed()) {
|
||||
return $this->dispatch('password-confirmed',
|
||||
id: $confirmableId,
|
||||
);
|
||||
}
|
||||
|
||||
$this->confirmingPassword = true;
|
||||
$this->confirmableId = $confirmableId;
|
||||
$this->confirmablePassword = '';
|
||||
|
||||
$this->dispatch('confirming-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop confirming the user's password.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function stopConfirmingPassword()
|
||||
{
|
||||
$this->confirmingPassword = false;
|
||||
$this->confirmableId = null;
|
||||
$this->confirmablePassword = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function confirmPassword()
|
||||
{
|
||||
if (! app(ConfirmPassword::class)(app(StatefulGuard::class), Auth::user(), $this->confirmablePassword)) {
|
||||
throw ValidationException::withMessages([
|
||||
'confirmable_password' => [__('This password does not match our records.')],
|
||||
]);
|
||||
}
|
||||
|
||||
session(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$this->dispatch('password-confirmed',
|
||||
id: $this->confirmableId,
|
||||
);
|
||||
|
||||
$this->stopConfirmingPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the user's password has been recently confirmed.
|
||||
*
|
||||
* @param int|null $maximumSecondsSinceConfirmation
|
||||
* @return void
|
||||
*/
|
||||
protected function ensurePasswordIsConfirmed($maximumSecondsSinceConfirmation = null)
|
||||
{
|
||||
$maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900);
|
||||
|
||||
$this->passwordIsConfirmed($maximumSecondsSinceConfirmation) ? null : abort(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user's password has been recently confirmed.
|
||||
*
|
||||
* @param int|null $maximumSecondsSinceConfirmation
|
||||
* @return bool
|
||||
*/
|
||||
protected function passwordIsConfirmed($maximumSecondsSinceConfirmation = null)
|
||||
{
|
||||
$maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900);
|
||||
|
||||
return (time() - session('auth.password_confirmed_at', 0)) < $maximumSecondsSinceConfirmation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
trait HasLocalizedMarkdown
|
||||
{
|
||||
public function localizedMarkdownPath($name)
|
||||
{
|
||||
$localName = preg_replace('#(\.md)$#i', '.'.app()->getLocale().'$1', $name);
|
||||
|
||||
return Arr::first([
|
||||
resource_path('markdown/'.$localName),
|
||||
resource_path('markdown/'.$name),
|
||||
], function ($path) {
|
||||
return file_exists($path);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
trait HasProfilePhoto
|
||||
{
|
||||
/**
|
||||
* Update the user's profile photo.
|
||||
*
|
||||
* @param string $storagePath
|
||||
* @return void
|
||||
*/
|
||||
public function updateProfilePhoto(UploadedFile $photo, $storagePath = 'profile-photos')
|
||||
{
|
||||
tap($this->profile_photo_path, function ($previous) use ($photo, $storagePath) {
|
||||
$this->forceFill([
|
||||
'profile_photo_path' => $photo->storePublicly(
|
||||
$storagePath, ['disk' => 'public']
|
||||
),
|
||||
])->save();
|
||||
|
||||
if ($previous) {
|
||||
Storage::disk('public')->delete($previous);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's profile photo.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteProfilePhoto()
|
||||
{
|
||||
if (is_null($this->profile_photo_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Storage::disk('public')->delete($this->profile_photo_path);
|
||||
|
||||
$this->forceFill([
|
||||
'profile_photo_path' => null,
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the user's profile photo.
|
||||
*/
|
||||
protected function profilePhotoUrl(): Attribute
|
||||
{
|
||||
return Attribute::get(function (): string {
|
||||
return $this->profile_photo_path
|
||||
? Storage::disk('public')->url($this->profile_photo_path)
|
||||
: $this->defaultProfilePhotoUrl();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default profile photo URL if no profile photo has been uploaded.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function defaultProfilePhotoUrl()
|
||||
{
|
||||
$name = trim(collect(explode(' ', $this->name))->map(function ($segment) {
|
||||
return mb_substr($segment, 0, 1);
|
||||
})->join(' '));
|
||||
|
||||
return 'https://ui-avatars.com/api/?name='.urlencode($name).'&color=7F9CF5&background=EBF4FF';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
|
||||
trait Toast
|
||||
{
|
||||
public function toast(
|
||||
string $type,
|
||||
string $title,
|
||||
?string $description = null,
|
||||
?string $position = null,
|
||||
string $icon = 'o-information-circle',
|
||||
string $css = 'alert-info',
|
||||
int $timeout = 3000,
|
||||
?string $redirectTo = null
|
||||
) {
|
||||
$toast = [
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'position' => $position,
|
||||
'icon' => Blade::render("<x-ui.icon class='w-7 h-7' name='".$icon."' />"),
|
||||
'css' => $css,
|
||||
'timeout' => $timeout,
|
||||
];
|
||||
|
||||
$this->js('toast('.json_encode(['toast' => $toast]).')');
|
||||
|
||||
// session()->flash('ib.toast.title', $title);
|
||||
// session()->flash('ib.toast.description', $description);
|
||||
|
||||
if ($redirectTo) {
|
||||
return $this->redirect($redirectTo, navigate: true);
|
||||
}
|
||||
}
|
||||
|
||||
public function success(
|
||||
string $title,
|
||||
?string $description = null,
|
||||
?string $position = null,
|
||||
string $icon = 'o-check-circle',
|
||||
string $css = 'alert-success',
|
||||
int $timeout = 3000,
|
||||
?string $redirectTo = null
|
||||
) {
|
||||
return $this->toast('success', $title, $description, $position, $icon, $css, $timeout, $redirectTo);
|
||||
}
|
||||
|
||||
public function warning(
|
||||
string $title,
|
||||
?string $description = null,
|
||||
?string $position = null,
|
||||
string $icon = 'o-exclamation-triangle',
|
||||
string $css = 'alert-warning',
|
||||
int $timeout = 3000,
|
||||
?string $redirectTo = null
|
||||
) {
|
||||
return $this->toast('warning', $title, $description, $position, $icon, $css, $timeout, $redirectTo);
|
||||
}
|
||||
|
||||
public function error(
|
||||
string $title,
|
||||
?string $description = null,
|
||||
?string $position = null,
|
||||
string $icon = 'o-x-circle',
|
||||
string $css = 'alert-error',
|
||||
int $timeout = 3000,
|
||||
?string $redirectTo = null
|
||||
) {
|
||||
return $this->toast('error', $title, $description, $position, $icon, $css, $timeout, $redirectTo);
|
||||
}
|
||||
|
||||
public function info(
|
||||
string $title,
|
||||
?string $description = null,
|
||||
?string $position = null,
|
||||
string $icon = 'o-information-circle',
|
||||
string $css = 'alert-info',
|
||||
int $timeout = 3000,
|
||||
?string $redirectTo = null
|
||||
) {
|
||||
return $this->toast('info', $title, $description, $position, $icon, $css, $timeout, $redirectTo);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<x-main-layout>
|
||||
<x-slot:body class="min-h-screen font-sans antialiased bg-base-200/50 dark:bg-base-200" x-data>
|
||||
|
||||
<div>
|
||||
|
||||
<x-partials.nav-bar />
|
||||
|
||||
<x-partials.main with-nav full-width>
|
||||
|
||||
<x-slot:sidebar drawer="main-drawer" class="bg-base-100 lg:bg-inherit">
|
||||
|
||||
@livewire('partials.side-bar')
|
||||
|
||||
</x-slot:sidebar>
|
||||
|
||||
<x-slot:content>
|
||||
|
||||
{{ $slot }}
|
||||
</x-slot:content>
|
||||
|
||||
</x-partials.main>
|
||||
|
||||
@if(session('toast'))
|
||||
<script lang="text/javascript">
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
window.toast(JSON.parse(@json(session('toast'))))
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
<x-toast />
|
||||
</div>
|
||||
|
||||
</x-slot:body>
|
||||
</x-main-layout>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class GuestLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<x-main-layout>
|
||||
<x-slot:body class="font-sans text-gray-900 dark:text-gray-100 antialiased">
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
<x-theme-toggle class="hidden" darkTheme="business" lightTheme="corporate"/>
|
||||
|
||||
</x-slot:body>
|
||||
</x-main-layout>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MainLayout extends Component
|
||||
{
|
||||
public function __construct(
|
||||
|
||||
// Slots
|
||||
public mixed $body = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.main-layout');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user