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:
hackerESQ
2025-09-26 17:41:28 -05:00
committed by GitHub
parent 910d426ad4
commit e6f38d9481
146 changed files with 5443 additions and 3909 deletions
+269 -56
View File
@@ -1,5 +1,195 @@
<div>
<!-- Generate API Token -->
<?php
use App\Traits\Toast;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Livewire\Volt\Component;
new class extends Component
{
use Toast;
/**
* The create API token form state.
*
* @var array
*/
public $createApiTokenForm = [
'name' => '',
'permissions' => [],
];
/**
* Indicates if the plain text token is being displayed to the user.
*
* @var bool
*/
public $displayingToken = false;
/**
* The plain text token value.
*
* @var string|null
*/
public $plainTextToken;
/**
* Indicates if the user is currently managing an API token's permissions.
*
* @var bool
*/
public $managingApiTokenPermissions = false;
/**
* The token that is currently having its permissions managed.
*
* @var \Laravel\Sanctum\PersonalAccessToken|null
*/
public $managingPermissionsFor;
/**
* The update API token form state.
*
* @var array
*/
public $updateApiTokenForm = [
'permissions' => [],
];
/**
* Indicates if the application is confirming if an API token should be deleted.
*
* @var bool
*/
public $confirmingApiTokenDeletion = false;
/**
* The ID of the API token being deleted.
*
* @var int
*/
public $apiTokenIdBeingDeleted;
/**
* Mount the component.
*
* @return void
*/
public function mount()
{
//
}
/**
* Create a new API token.
*
* @return void
*/
public function createApiToken()
{
$this->resetErrorBag();
Validator::make([
'name' => $this->createApiTokenForm['name'],
], [
'name' => ['required', 'string', 'max:255'],
])->validateWithBag('createApiToken');
$this->displayTokenValue($this->user->createToken(
$this->createApiTokenForm['name']
));
$this->createApiTokenForm['name'] = '';
$this->dispatch('created');
}
/**
* Display the token value to the user.
*
* @param \Laravel\Sanctum\NewAccessToken $token
* @return void
*/
protected function displayTokenValue($token)
{
$this->displayingToken = true;
$this->plainTextToken = explode('|', $token->plainTextToken, 2)[1];
}
/**
* Allow the given token's permissions to be managed.
*
* @param int $tokenId
* @return void
*/
public function manageApiTokenPermissions($tokenId)
{
$this->managingApiTokenPermissions = true;
$this->managingPermissionsFor = $this->user->tokens()->where(
'id', $tokenId
)->firstOrFail();
$this->updateApiTokenForm['permissions'] = $this->managingPermissionsFor->abilities;
}
/**
* Update the API token's permissions.
*
* @return void
*/
public function updateApiToken()
{
$this->managingPermissionsFor->forceFill([
'abilities' => [],
])->save();
$this->managingApiTokenPermissions = false;
}
/**
* Confirm that the given API token should be deleted.
*
* @param int $tokenId
* @return void
*/
public function confirmApiTokenDeletion($tokenId)
{
$this->confirmingApiTokenDeletion = true;
$this->apiTokenIdBeingDeleted = $tokenId;
}
/**
* Delete the API token.
*
* @return void
*/
public function deleteApiToken()
{
$this->user->tokens()->where('id', $this->apiTokenIdBeingDeleted)->first()->delete();
$this->user->load('tokens');
$this->confirmingApiTokenDeletion = false;
$this->managingPermissionsFor = null;
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
}; ?>
<div x-data>
{{-- Generate API Token --}}
<x-forms.form-section submit="createApiToken">
<x-slot name="title">
{{ __('Create API Token') }}
@@ -10,13 +200,13 @@
</x-slot>
<x-slot name="form">
<!-- Token Name -->
{{-- Token Name --}}
<div class="col-span-6 sm:col-span-4">
<x-input id="name" label="{{ __('Token Name') }}" type="text" class="mt-1 block w-full" wire:model="createApiTokenForm.name" autofocus />
<x-ui.input id="name" label="{{ __('Token Name') }}" type="text" class="mt-1 block w-full" wire:model="createApiTokenForm.name" autofocus />
</div>
<!-- Token Permissions -->
@if (Laravel\Jetstream\Jetstream::hasPermissions())
{{-- Token Permissions --}}
@if (false)
<div class="col-span-6">
<label class="pt-0 label label-text font-semibold">
<span>
@@ -25,15 +215,63 @@
</span>
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach (Laravel\Jetstream\Jetstream::$permissions as $label => $permission)
@foreach ([] as $label => $permission)
<label class="flex items-center">
<x-checkbox wire:model="createApiTokenForm.permissions" :value="$permission"/>
<x-ui.checkbox wire:model="createApiTokenForm.permissions" :value="$permission"/>
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ $label }}</span>
</label>
@endforeach
</div>
</div>
@endif
{{-- Token Value Modal --}}
<x-ui.modal
persistent
key="token-display-modal"
wire:model.live="displayingToken"
title="{{ __('API Token') }}"
>
<div class="mt-2 text-sm text-secondary-content">
<div class="mb-4">
{{ __('Please copy your new API token. For your security, it won\'t be shown again.') }}
</div>
<x-ui.input
x-ref="plaintextToken"
type="text"
readonly
:value="$plainTextToken"
class="font-mono break-all focus:outline-none focus:ring-0"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
>
<x-slot:suffix>
<x-ui.button
title="{{ __('Copy to clipboard') }}"
class="btn-circle btn-sm btn-ghost me-2"
icon="o-clipboard"
@click="
navigator.clipboard.writeText($wire.plainTextToken);
$wire.$set('displayingToken', false);
$wire.success('{{ __('Successfully copied!') }}')
"
/>
</x-slot:suffix>
</x-ui.input>
</div>
<div class="flex flex-row items-center justify-end mt-8 text-end">
<x-ui.button class="btn-outline" wire:click="$set('displayingToken', false)" wire:loading.attr="disabled">
{{ __('Close') }}
</x-ui.button>
</div>
</x-ui.modal>
</x-slot>
<x-slot name="actions">
@@ -41,16 +279,16 @@
{{ __('Created.') }}
</x-forms.action-message>
<x-button type="submit">
<x-ui.button type="submit">
{{ __('Create') }}
</x-button>
</x-ui.button>
</x-slot>
</x-forms.form-section>
@if ($this->user->tokens->isNotEmpty())
<x-section-border hide-on-mobile />
<x-ui.section-border hide-on-mobile />
<!-- Manage API Tokens -->
{{-- Manage API Tokens --}}
<div class="mt-10 sm:mt-0">
<x-forms.action-section>
<x-slot name="title">
@@ -61,12 +299,12 @@
{{ __('You may delete any of your existing tokens if they are no longer needed.') }}
</x-slot>
<!-- API Token List -->
{{-- API Token List --}}
<x-slot name="content">
<div class="space-y-6">
@foreach ($this->user->tokens->sortBy('name') as $token)
<div class="flex items-center justify-between">
<div class="break-all dark:text-white">
<div class="break-all">
{{ $token->name }}
</div>
@@ -77,7 +315,7 @@
</div>
@endif
@if (Laravel\Jetstream\Jetstream::hasPermissions())
@if (false)
<button class="cursor-pointer ms-6 text-sm text-gray-400 underline" wire:click="manageApiTokenPermissions({{ $token->id }})">
{{ __('Permissions') }}
</button>
@@ -95,42 +333,17 @@
</div>
@endif
<!-- Token Value Modal -->
<x-dialog-modal wire:model.live="displayingToken">
<x-slot name="title">
{{ __('API Token') }}
</x-slot>
<x-slot name="content">
<div>
{{ __('Please copy your new API token. For your security, it won\'t be shown again.') }}
</div>
<x-input x-ref="plaintextToken" type="text" readonly :value="$plainTextToken"
class="mt-4 px-4 py-2 rounded font-mono text-sm w-full break-all"
autofocus autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
@showing-token-modal.window="setTimeout(() => $refs.plaintextToken.select(), 250)"
/>
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$set('displayingToken', false)" wire:loading.attr="disabled">
{{ __('Close') }}
</x-button>
</x-slot>
</x-dialog-modal>
<!-- API Token Permissions Modal -->
<x-dialog-modal wire:model.live="managingApiTokenPermissions">
{{-- API Token Permissions Modal --}}
<x-ui.dialog-modal key="manage-permission-modal" wire:model.live="managingApiTokenPermissions">
<x-slot name="title">
{{ __('API Token Permissions') }}
</x-slot>
<x-slot name="content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach (Laravel\Jetstream\Jetstream::$permissions as $label => $permission)
@foreach ([] as $label => $permission)
<label class="flex items-center">
<x-checkbox wire:model="updateApiTokenForm.permissions" :value="$permission"/>
<x-ui.checkbox wire:model="updateApiTokenForm.permissions" :value="$permission"/>
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ $label }}</span>
</label>
@endforeach
@@ -138,18 +351,18 @@
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$set('managingApiTokenPermissions', false)" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:click="$set('managingApiTokenPermissions', false)" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-button>
</x-ui.button>
<x-button type="submit" class="ms-3" wire:click="updateApiToken" wire:loading.attr="disabled">
<x-ui.button type="submit" class="ms-3" wire:click="updateApiToken" wire:loading.attr="disabled">
{{ __('Save') }}
</x-button>
</x-ui.button>
</x-slot>
</x-dialog-modal>
</x-ui.dialog-modal>
<!-- Delete Token Confirmation Modal -->
<x-confirmation-modal wire:model.live="confirmingApiTokenDeletion">
{{-- Delete Token Confirmation Modal --}}
<x-ui.confirmation-modal key="confirm-deletion-modal" wire:model.live="confirmingApiTokenDeletion">
<x-slot name="title">
{{ __('Delete API Token') }}
</x-slot>
@@ -159,13 +372,13 @@
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$toggle('confirmingApiTokenDeletion')" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:click="$toggle('confirmingApiTokenDeletion')" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-button>
</x-ui.button>
<x-button class="ms-3 btn-error text-white" wire:click="deleteApiToken" wire:loading.attr="disabled">
<x-ui.button class="ms-3 btn-error text-white" wire:click="deleteApiToken" wire:loading.attr="disabled">
{{ __('Delete') }}
</x-button>
</x-ui.button>
</x-slot>
</x-confirmation-modal>
</x-ui.confirmation-modal>
</div>
+5 -11
View File
@@ -1,13 +1,7 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('API Tokens') }}
</h2>
</x-slot>
<x-layouts.app>
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
@livewire('api.api-token-manager')
</div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
@livewire('api-token-manager')
</div>
</x-app-layout>
</x-layouts.app>
@@ -1,8 +1,8 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot name="logo">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot>
@@ -10,20 +10,20 @@
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<x-errors class="mb-4" />
<x-ui.errors class="mb-4" />
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<div>
<x-input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="current-password" autofocus />
<x-ui.input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="current-password" autofocus />
</div>
<div class="flex justify-end mt-4">
<x-button type="submit" class="btn-primary ms-4">
<x-ui.button type="submit" class="btn-primary ms-4">
{{ __('Confirm') }}
</x-button>
</x-ui.button>
</div>
</form>
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
+11 -11
View File
@@ -1,8 +1,8 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot name="logo">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot>
@@ -11,26 +11,26 @@
</div>
@session('status')
<x-alert icon="o-envelope" class="alert-success mb-4">
<x-ui.alert icon="o-envelope" class="alert-success mb-4">
{{ $value }}
</x-alert>
</x-ui.alert>
@endsession
<x-errors class="mb-4" />
<x-ui.errors class="mb-4" />
<form method="POST" action="{{ route('password.email') }}">
@csrf
<div class="block">
<x-input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-ui.input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
</div>
<div class="flex items-center justify-end mt-4">
<x-button class="btn-primary" type="submit">
<x-ui.button class="btn-primary" type="submit">
{{ __('Email Password Reset Link') }}
</x-button>
</x-ui.button>
</div>
</form>
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
@@ -6,10 +6,11 @@ use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Rule;
use Livewire\Volt\Component;
new class extends Component {
new class extends Component
{
// props
public Portfolio $portfolio;
public User $user;
#[Rule('required|string')]
@@ -41,30 +42,29 @@ new class extends Component {
return redirect(route('portfolio.show', ['portfolio' => $this->portfolio->id]));
}
}; ?>
<x-form wire:submit="updateUserInformation" class="">
<div class="mt-2">
<x-input wire:model="name" label="{{ __('Name') }}" class="block mt-1 w-full" required autofocus />
<x-ui.input wire:model="name" label="{{ __('Name') }}" class="block mt-1 w-full" required autofocus />
</div>
<div class="mt-2">
<x-input wire:model="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" required autocomplete="new-password" />
<x-ui.input wire:model="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" required autocomplete="new-password" />
</div>
<div class="mt-2">
<x-input wire:model="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" required autocomplete="new-password" />
<x-ui.input wire:model="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" required autocomplete="new-password" />
</div>
<div class="flex items-center justify-end mt-2">
<x-button class="btn-primary" type="submit">
<x-ui.button class="btn-primary" type="submit">
{{ __('Get Started') }}
</x-button>
</x-ui.button>
</div>
</x-form>
@@ -1,8 +1,8 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot:logo>
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot:logo>
@@ -14,5 +14,5 @@
'user' => $user,
])
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
+17 -17
View File
@@ -1,17 +1,17 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot name="logo">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot>
<x-errors class="mb-4" />
<x-ui.errors class="mb-4" />
@session('status')
<x-alert icon="o-envelope" class="alert-success mb-4">
<x-ui.alert icon="o-envelope" class="alert-success mb-4">
{{ $value }}
</x-alert>
</x-ui.alert>
@endsession
<form method="POST" action="{{ route('login') }}">
@@ -19,16 +19,16 @@
<div>
<x-input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-ui.input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
</div>
<div class="mt-4">
<x-input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="current-password" />
<x-ui.input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="current-password" />
</div>
<div class="block mt-4">
<x-checkbox id="remember_me" name="remember" class="text-sm" label="{{ __('Remember me') }}" />
<x-ui.checkbox id="remember_me" name="remember" class="text-sm" label="{{ __('Remember me') }}" />
</div>
<div class="flex items-center justify-end mt-4">
@@ -38,26 +38,26 @@
</a>
@endif
<x-button type="submit" class="btn-primary ms-4" >
<x-ui.button type="submit" class="btn-primary ms-4" >
{{ __('Log in') }}
</x-button>
</x-ui.button>
</div>
@if (\Laravel\Fortify\Features::enabled('registration'))
<x-section-border />
<x-ui.section-border />
<x-connected-accounts-login />
<x-social.connected-accounts-login />
<x-button
<x-ui.button
link="{{ route('register') }}"
class="btn-sm btn-block btn-outline btn-secondary my-1"
>
{{ __('Sign up with email') }}
</x-button>
</x-ui.button>
@endif
</form>
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
+13 -13
View File
@@ -1,41 +1,41 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot name="logo">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot>
<x-errors class="mb-4" />
<x-ui.errors class="mb-4" />
<form method="POST" action="{{ route('register') }}">
@csrf
<div>
<x-input id="name" label="{{ __('Name') }}" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-ui.input id="name" label="{{ __('Name') }}" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
</div>
<div class="mt-4">
<x-input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-ui.input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
</div>
<div class="mt-4">
<x-input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-ui.input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
</div>
<div class="mt-4">
<x-input id="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" name="password_confirmation" required autocomplete="new-password" />
<x-ui.input id="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" name="password_confirmation" required autocomplete="new-password" />
</div>
@if (! config('investbrain.self_hosted'))
<div class="mt-4">
<label>
<div class="flex items-center">
<x-checkbox name="terms" id="terms" required />
<x-ui.checkbox name="terms" id="terms" required />
<div class="ms-2 text-sm">
{!! __('I agree to the :terms_of_service and :privacy_policy', [
@@ -53,10 +53,10 @@
{{ __('Already registered?') }}
</a>
<x-button type="submit" class="btn-primary ms-4">
<x-ui.button type="submit" class="btn-primary ms-4">
{{ __('Register') }}
</x-button>
</x-ui.button>
</div>
</form>
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
+11 -11
View File
@@ -1,12 +1,12 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot name="logo">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot>
<x-errors class="mb-4" />
<x-ui.errors class="mb-4" />
<form method="POST" action="{{ route('password.update') }}">
@csrf
@@ -15,24 +15,24 @@
<div class="block">
<x-input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-ui.input id="email" label="{{ __('Email') }}" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
</div>
<div class="mt-4">
<x-input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-ui.input id="password" label="{{ __('Password') }}" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
</div>
<div class="mt-4">
<x-input id="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" name="password_confirmation" required autocomplete="new-password" />
<x-ui.input id="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" name="password_confirmation" required autocomplete="new-password" />
</div>
<div class="flex items-center justify-end mt-4">
<x-button class="btn-primary" type="submit">
<x-ui.button class="btn-primary" type="submit">
{{ __('Reset Password') }}
</x-button>
</x-ui.button>
</div>
</form>
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
@@ -1,8 +1,8 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot name="logo">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot>
@@ -15,19 +15,19 @@
{{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }}
</div>
<x-errors class="mb-4" />
<x-ui.errors class="mb-4" />
<form method="POST" action="{{ route('two-factor.login') }}">
@csrf
<div class="mt-4" x-show="! recovery">
<x-input id="code" label="{{ __('Code') }}" class="block mt-1 w-full" type="text" inputmode="numeric" name="code" autofocus x-ref="code" autocomplete="one-time-code" />
<x-ui.input id="code" label="{{ __('Code') }}" class="block mt-1 w-full" type="text" inputmode="numeric" name="code" autofocus x-ref="code" autocomplete="one-time-code" />
</div>
<div class="mt-4" x-cloak x-show="recovery">
<x-input id="recovery_code" label="{{ __('Recovery Code') }}" class="block mt-1 w-full" type="text" name="recovery_code" x-ref="recovery_code" autocomplete="one-time-code" />
<x-ui.input id="recovery_code" label="{{ __('Recovery Code') }}" class="block mt-1 w-full" type="text" name="recovery_code" x-ref="recovery_code" autocomplete="one-time-code" />
</div>
<div class="flex items-center justify-end mt-4">
@@ -50,11 +50,11 @@
{{ __('Use an authentication code') }}
</button>
<x-button type="submit" class="btn-primary ms-4">
<x-ui.button type="submit" class="btn-primary ms-4">
{{ __('Log in') }}
</x-button>
</x-ui.button>
</div>
</form>
</div>
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
+10 -10
View File
@@ -1,8 +1,8 @@
<x-guest-layout>
<x-authentication-card>
<x-layouts.guest>
<x-ui.authentication-card>
<x-slot name="logo">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
</x-slot>
@@ -11,9 +11,9 @@
</div>
@if (session('status') == 'verification-link-sent')
<x-alert icon="o-envelope" class="alert-success mb-4">
<x-ui.alert icon="o-envelope" class="alert-success mb-4">
{{ __('A new verification link has been sent to the email address you provided in your profile settings.') }}
</x-alert>
</x-ui.alert>
@endif
<div class="mt-4 flex items-center justify-between">
@@ -21,9 +21,9 @@
@csrf
<div>
<x-button type="submit" type="submit" class="btn-primary">
{{ __('Resend Verification Email') }}
</x-button>
<x-ui.button label="{{ __('Resend Verification Email') }}" type="submit" class="bg-primary hover:bg-secondary focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 focus:shadow-outline focus:outline-none" />
</div>
</form>
@@ -43,5 +43,5 @@
</form>
</div>
</div>
</x-authentication-card>
</x-guest-layout>
</x-ui.authentication-card>
</x-layouts.guest>
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div>
{{ $logo }}
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
@@ -1,17 +0,0 @@
@props(['id' => null, 'maxWidth' => null])
<x-ib-livewire-modal :id="$id" :maxWidth="$maxWidth" {{ $attributes }} :showClose="false">
<div class="p-2">
<div class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ $title }}
</div>
<div class="mt-4 text-sm text-gray-600 dark:text-gray-400">
{{ $content }}
</div>
</div>
<div class="flex flex-row items-center justify-end mt-3 p-2 text-end">
{{ $footer }}
</div>
</x-ib-livewire-modal>
@@ -5,7 +5,7 @@
</x-forms.section-title>
<div class="mt-5 md:mt-0 md:col-span-2">
<div class="px-4 py-5 sm:p-6 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6 bg-base-100 shadow sm:rounded-lg">
{{ $content }}
</div>
</div>
@@ -15,7 +15,7 @@
</span>
@once
<x-dialog-modal wire:model.live="confirmingPassword">
<x-ui.dialog-modal wire:model.live="confirmingPassword">
<x-slot name="title">
{{ $title }}
</x-slot>
@@ -25,7 +25,7 @@
<div class="mt-4" x-data="{}" x-on:confirming-password.window="setTimeout(() => $refs.confirmable_password.focus(), 250)">
<x-input type="password" class="mt-1 block w-3/4" placeholder="{{ __('Password') }}" autocomplete="current-password"
<x-ui.input type="password" class="mt-1 block w-3/4" placeholder="{{ __('Password') }}" autocomplete="current-password"
x-ref="confirmable_password"
wire:model="confirmablePassword"
wire:keydown.enter="confirmPassword"
@@ -36,13 +36,13 @@
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="stopConfirmingPassword" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:click="stopConfirmingPassword" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-button>
</x-ui.button>
<x-button type="submit" class="ms-3" dusk="confirm-password-button" wire:click="confirmPassword" wire:loading.attr="disabled">
<x-ui.button type="submit" class="ms-3" dusk="confirm-password-button" wire:click="confirmPassword" wire:loading.attr="disabled">
{{ $button }}
</x-button>
</x-ui.button>
</x-slot>
</x-dialog-modal>
</x-ui.dialog-modal>
@endonce
@@ -8,14 +8,14 @@
<div class="mt-5 md:mt-0 md:col-span-2">
<form wire:submit="{{ $submit }}">
<div class="px-4 py-5 bg-white dark:bg-gray-800 sm:p-6 shadow {{ isset($actions) ? 'sm:rounded-tl-md sm:rounded-tr-md' : 'sm:rounded-md' }}">
<div class="px-4 py-5 bg-base-100 sm:p-6 shadow {{ isset($actions) ? 'sm:rounded-tl-md sm:rounded-tr-md' : 'sm:rounded-md' }}">
<div class="grid grid-cols-6 gap-6">
{{ $form }}
</div>
</div>
@if (isset($actions))
<div class="flex items-center justify-end px-4 py-3 bg-gray-50 dark:bg-gray-800 text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
<div class="flex items-center justify-end px-4 py-3 bg-base-100 text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
{{ $actions }}
</div>
@endif
@@ -1,27 +0,0 @@
@php
if (isset($percent)) {
$isUp = $percent > 0;
} else {
$isUp = $costBasis <= $marketValue;
$percent = $costBasis ? (($marketValue - $costBasis) / $costBasis) * 100 : 0;
}
@endphp
@if(!empty($percent))
<x-badge class="badge-sm {{ $isUp ? 'badge-success' : 'badge-error' }} badge-outline ml-2">
<x-slot:value>
{!! $isUp ? '&#9650;' :'&#9660;' !!}
{{ Number::percentage(
$percent,
$percent < 1 ? 2 : 0
) }}
</x-slot:value>
</x-badge>
@endif
@@ -1,52 +0,0 @@
<a href="/" title="Investbrain">
<svg width="100%" height="100%" id="Layer_1" class="fill-current" data-name="Layer 1" viewBox="0 0 1001 783" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" >
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M553.875,632.571L567.884,627.131C567.884,627.131 588.94,642.044 611.341,650.542C660.03,669.007 666.181,693.68 670.67,711.697C671.541,715.201 672.368,718.512 673.431,721.293C679.103,736.17 685.326,746.904 694.882,758.003L737.893,737.748C730.866,729.455 721.087,714.1 721.273,693.007C721.419,676.837 731.456,663.936 740.313,652.55C749.261,641.048 756.99,631.115 754.689,619.792C754.428,618.501 750.205,606.681 683.457,589.378C664.971,584.588 632.955,577.931 632.955,577.931C632.955,577.931 635.967,564.803 636.504,564.91C650.287,567.669 668.765,571.64 687.293,576.443C757.295,594.586 767.837,608.754 769.682,617.83C773.076,634.571 762.843,647.722 752.948,660.444C744.551,671.243 736.616,681.44 736.507,693.555C736.275,719.609 754.703,734.781 754.889,734.933L762.945,741.43L691.292,775.182L687.22,770.803C674.012,756.601 666.101,743.826 659.014,725.231C657.68,721.736 656.764,718.071 655.801,714.191C651.633,697.476 641.744,677.506 600.566,661.887C573.409,651.585 553.875,632.571 553.875,632.571Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M469.894,617.03C491.9,625.608 537.785,632.498 578.066,616.912C606.757,605.811 625.69,585.647 634.343,556.967L635.932,544.895L650.678,549.582L650.547,550.556L649.213,560.348C639.567,592.821 617.208,616.664 584.546,629.301C557.593,639.728 525.875,641.415 498.98,637.874C484.116,635.917 475.069,632.909 464.77,628.35L469.894,617.03Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M756.363,647.659C766.597,652.226 735.904,647.812 749.88,647.831C752.018,647.834 754.181,647.777 756.363,647.659ZM756.363,647.659L759.121,630.608C776.312,645.1 814.041,614.388 822.007,607.977C847.271,587.646 859.429,573.432 865.582,531.56L857.892,525.97L871.854,519.291L871.902,520.701L871.646,533.167C868.374,581.138 854.724,595.681 826.924,619.726C805.922,637.888 779.998,646.378 756.363,647.659Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M278.425,160.523C277.934,160.447 277.438,160.37 276.948,160.286C277.44,160.359 277.93,160.439 278.425,160.523Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M56.427,281.384L53.276,278.254C53.915,253.987 64.477,230.809 81.468,211.975C77.818,216.38 74.378,221.228 71.199,226.564C59.438,246.327 55.735,264.917 56.427,281.384ZM125.55,179.848C146.913,170.054 171.349,165.376 196.488,168.027C196.773,169.34 196.992,170.047 196.992,170.047L196.819,170.025C194.125,169.67 160.165,165.575 125.55,179.848ZM876.465,148.788C875.253,140.191 872.604,131.249 868.55,122.317C872.79,131.336 875.507,140.302 876.465,148.788ZM636.968,67.078C632.455,59.767 626.37,52.471 618.591,45.74C627.058,52.458 633.412,59.75 636.968,67.078ZM830.233,73.639C814.235,60.456 794.248,49.212 770.413,41.637C761.569,38.826 752.892,36.944 744.475,35.836C730.805,34.037 717.803,34.272 705.832,35.868C719.737,33.548 733.39,33.475 746.563,35.21C778.577,39.424 807.696,54.337 830.233,73.639Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M546.485,261.415C546.455,261.429 546.424,261.444 546.394,261.458C546.389,261.461 546.389,261.461 546.385,261.46L546.485,261.415ZM546.485,261.415C547.832,260.79 549.176,260.186 550.513,259.608L546.485,261.415Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M951.448,338.201C967.066,346.748 978.94,360.259 984.357,371.461C991.028,385.267 1009.88,464.746 938.38,509.182C874.197,549.074 824.465,524.364 805.506,511.387C784.609,543.131 735.375,571.199 677.333,565.042C620.575,558.985 591.016,530.312 576.286,507.76C561.52,528.554 541.685,537.599 526.615,541.516C511.159,545.534 494.383,545.742 479.342,542.424C490.897,565.908 498.729,604.571 467.806,641.145C445.634,667.37 414.398,675.189 392.099,677.128C370.311,679.025 349.702,675.811 337.482,671.143C324.069,682.794 288.704,704.331 243.88,698.43C233.085,697.009 221.742,694 210.023,688.874C158.439,666.305 147.126,623.344 154.77,594.547C111.783,593.541 77.23,581.082 51.934,557.44C20.86,528.397 4.976,481.26 9.445,431.35C14.204,378.198 55.606,354.896 74.754,346.889C60.508,328.89 30.467,280.339 64.437,223.27C98.829,165.496 163.458,161.805 188.094,162.668C186.255,144.263 189.011,101.243 245.926,69.359C313.249,31.644 373.035,54.386 397.781,70.781C414.353,45.661 452.387,10.329 510.461,7.925C585.849,4.8 622.364,35.827 637.949,55.854C660.603,36.887 713.288,16.387 772.778,35.297C839.209,56.412 875.616,104.13 883.278,143.714C947.161,162.729 985.435,206.49 988.544,264.393C990.859,307.537 971.373,327.501 951.448,338.201ZM929.36,498.056C991.957,459.153 975.932,387.913 970.382,376.422C965.636,366.6 951.481,350.075 931.943,344.8L911.518,339.288L930.807,332.122C953.179,323.814 975.727,309.291 973.33,264.596C971.652,233.374 956.575,177.653 874.439,155.23L869.539,153.891L868.907,149.406C863.81,113.255 830.649,67.872 768.052,47.976C704.505,27.778 653.597,58.332 643.204,71.132L636.222,79.733L630.312,70.164C620.597,54.426 589.582,18.173 511.669,21.398C440.094,24.362 408.415,81.914 407.103,84.363L402.557,92.834L394.883,85.976C379.908,72.608 321.954,42.965 254.369,80.828C189.472,117.183 204.179,167.969 204.337,168.478L207.359,178.33L196.014,176.705C192.739,176.258 115.819,166.267 77.965,229.863C39.999,293.638 91.82,344.913 92.351,345.421L100.424,353.227L89.31,356.328C86.907,357.008 29.915,373.837 24.63,432.853C20.503,478.949 34.782,522.122 62.831,548.336C87.022,570.947 121.342,581.965 164.831,581.071L176.905,580.826L172.295,590.588C161.421,613.62 167.914,655.605 216.409,676.818C274.503,702.221 322.493,666.638 329.535,658.577L333.699,653.813L339.597,657.183C352.785,664.72 419.879,674.98 455.506,632.841C490.702,591.211 468.011,546.744 456.297,533.124L432.423,505.365L466.09,523.416C481.09,531.455 502.99,533.478 521.89,528.564C536.577,524.747 556.747,514.997 569.395,490.245L576.355,476.635L583.311,490.646C593.267,510.709 618.898,545.364 678.656,551.649C737.765,557.918 782.891,523.943 796.055,497.816L800.455,489.081L808.318,496.037C816.887,503.614 862.973,539.314 929.36,498.056Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M164.45,581.093C168.821,580.604 195.946,573.542 211.278,553.997C221.78,540.606 224.635,523.998 219.76,504.639C219.76,504.639 247.605,532.462 221.626,564.658C202.884,586.185 171.934,594.399 165.717,594.554L164.791,581.073C164.703,581.077 164.591,581.078 164.45,581.093ZM164.45,581.093C164.448,581.093 164.447,581.094 164.445,581.094C164.447,581.094 164.448,581.093 164.45,581.093Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M213.575,553.555C222.582,551.317 231.34,551.047 239.325,552.226C255.233,554.572 268.058,562.655 273.619,572.298C273.619,572.298 245.37,554.286 218.136,566.552L213.575,553.555Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M310.41,312.188C362.408,280.533 430.339,286.541 475.605,326.802L464.987,336.16C425.111,300.696 365.213,295.443 319.316,323.38C275.107,350.295 256.595,417.442 280.48,464.26C307.052,516.356 357.867,539.991 394.332,536.42C394.332,536.42 358.776,552.43 324.184,531.648C299.807,517 279.907,495.504 266.63,469.474C239.543,416.376 259.999,342.877 310.41,312.188Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M330.494,520.536C338.379,526.429 342.731,534.458 344.7,543.023C348.021,570.08 326.463,580.957 326.463,580.957C333.323,569.603 338.064,541.076 322.267,529.275L330.494,520.536Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M170.498,451.168C205.064,437.661 243.653,440.477 276.359,458.882L268.492,470.196C240.229,454.292 206.893,451.865 177.027,463.541C153.301,472.81 139.19,488.122 137.509,498.456C137.509,498.456 134.627,468.393 170.498,451.168Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M122.051,418.399C124.266,425.291 153.678,451.572 184.056,447.87L186.251,459.326C180.771,459.993 175.279,459.883 169.892,459.174C162.413,458.189 155.13,456.047 148.357,453.205C119.151,440.422 122.051,418.399 122.051,418.399Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M83.89,343.578C93.152,340.443 119.954,335.902 149.393,342.543C189.622,353.407 200.664,388.305 200.664,388.305C157.717,337.275 90.386,355.973 89.732,356.197L83.89,343.578Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M403.057,462.599C412.109,479.736 430.231,504.115 465.183,522.924L466.14,523.442L458.465,534.863L457.586,534.391C419.225,513.745 399.276,486.879 389.295,467.98C380.508,451.336 378.406,434.828 379.826,419.97C384.383,385.186 414.727,373.954 414.727,373.954C402.573,387.065 383.727,425.985 403.057,462.599Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M322.397,408.686C322.397,408.686 360.323,391.713 392.009,422.14L382.306,429.591C369.382,416.512 339.485,407.613 322.397,408.686Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M690.853,274.028C735.995,311.348 753.013,373.417 740.587,407.967C734.08,426.056 723.599,439.419 712.073,449.263C685.57,471.905 649.16,462.799 649.16,462.799C651.515,462.516 707.46,455.197 726.054,403.494C736.835,373.524 720.838,316.972 680.654,283.744C656.633,263.884 614.846,244.873 552.154,267.086C489.758,289.191 475.391,326.522 474.322,353.951C472.725,395.045 499.768,434.915 514.438,442.054C514.438,442.054 481.579,436.735 467.588,400.693C461.928,385.953 458.458,369.521 459.1,353.048C460.315,321.737 476.396,279.231 546.1,254.536C616.21,229.702 663.52,251.425 690.853,274.028Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M574.041,405.452C577.929,399.868 582.591,394.925 588.017,390.626C622.566,366.135 666.154,382.688 666.154,382.688C627.495,382.845 600.885,392.975 587.077,412.802C566.225,442.734 580.371,485.117 583.223,490.479L569.482,495.902C565.551,488.513 549.669,440.434 574.041,405.452Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M586.976,325.57C586.976,325.57 612.868,328.978 618.672,355.583C621.052,372.019 615.594,386.416 603.3,396.116L594.44,387.377C606.107,378.168 607.013,365.603 605.724,356.686C603.595,342.023 594.518,329.351 586.976,325.57Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M708.183,440.658C738.696,458.976 721.687,492.93 721.687,492.93C725.772,471.083 707.178,453.368 701.816,450.565L708.183,440.658Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M793.227,434.541L805.895,427.418C827.426,456.566 813.55,497.025 809.929,503.784L796.126,497.678C798.539,493.179 810.551,457.997 793.227,434.541Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M843.106,432.778C872.723,432.517 895.844,420.562 906.531,399.983C906.531,399.983 907.929,428.423 880.172,440.043C869.358,444.009 857.092,446.148 843.754,446.269C837.448,446.323 831.171,445.926 825.002,445.114C790.398,440.559 759.118,422.884 743.733,398.136C712.965,348.644 746.651,293.716 782.802,273.093C802.223,262.012 823.22,255.668 842.44,254.262C876.032,253.772 883.4,276.414 883.4,276.414C864.944,262.029 824.51,265.578 791.345,284.501C760.84,301.905 731.064,350.173 757.023,391.933C772.169,416.297 807.583,433.1 843.106,432.778Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M901.023,427.592C919.082,437.647 914.375,459.502 914.375,459.502C914.382,455.074 911.352,449.929 905.845,445.02C898.256,438.251 889.946,435.226 888.088,435.21C888.281,435.213 882.328,424.936 882.328,424.936C886.457,422.572 892.743,423.469 901.023,427.592Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M841.501,320.927C869.016,346.791 923.886,334.774 930.727,332.154L937.161,344.56C930.833,346.984 903.788,353.308 876.093,349.661C844.437,342.756 841.501,320.927 841.501,320.927Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M856.8,381.78C856.8,381.78 854.677,356.846 873.976,346.331C881.356,342.487 889.96,339.571 899.7,338.12L902.276,349.522C873.542,353.801 859.967,372.495 856.8,381.78Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M609.849,376.237C641.704,348.782 649.339,320.22 649.339,320.22C651.29,328.519 647.387,350.181 634.449,367.575C629.624,374.059 623.544,379.948 616.045,384.331L609.849,376.237Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M834.92,234.947C840.865,227.575 848.327,221.775 857.035,217.633C881.755,204.76 908.313,222.518 908.313,222.518C888.901,219.963 862.264,221.245 845.624,241.87L834.92,234.947Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M831.928,192.93C831.928,192.93 855.516,211.969 848.036,239.116C840.815,259.861 823.515,271.361 810.178,275.615L804.608,262.904C806.453,262.317 849.598,247.868 831.928,192.93Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M730.919,99.579C730.919,99.579 763.085,113.201 772.811,151.075C776.7,170.409 776.278,191.455 771.605,214.455C761.626,263.558 702.305,281.445 679.251,280.632L679.359,267.156C693.228,267.644 747.798,255.006 756.611,211.647C765.902,165.932 757.739,130.323 730.919,99.579Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M762.546,160.453C800.164,134.33 864.452,138.777 878.343,142.312L874.583,155.268C864.309,152.659 804.878,148.468 772.188,171.172L762.546,160.453Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M630.258,70.076L643.679,64.083C644.1,64.79 673.076,114.431 662.623,161.021C654.783,197.124 613.774,203.603 613.774,203.603C683.262,160.803 630.799,70.977 630.258,70.076Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M659.485,165.881C663.907,186.681 682.077,207.853 695.622,213.847C695.622,213.847 651.903,202.29 646.659,167.659L659.485,165.881Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M539.576,186.699C541.122,210.599 554.444,240.07 576.078,247.622L570.893,260.15C542.825,250.352 526.242,215.953 524.367,187.035C523.494,173.554 526.287,151.23 536.041,131.967C553.129,100.656 581.582,110.001 581.582,110.001C548.902,118.029 538.098,163.918 539.576,186.699Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M493.292,103.391C493.292,103.391 525.2,108.011 535.526,135.513C538.424,143.235 540.253,152.053 540.204,161.981L527.145,161.553C527.336,122.786 493.632,103.579 493.292,103.391Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M276.948,160.286C277.399,160.303 311.64,161.895 336.502,187.86C344.788,196.516 352.036,207.878 356.639,222.792C374.898,281.945 349.413,306.144 319.144,323.482C309.497,329.013 295.604,330.674 280.619,328.701C269.215,327.2 257.176,323.595 245.876,317.989C231.133,310.674 218.952,300.706 209.701,288.689C189.323,255.458 204.276,225.968 204.276,225.968C204.379,274.859 234.842,297.268 252.991,306.273C276.57,317.97 300.259,318.002 310.581,312.087C335.503,297.81 358.45,279.395 341.951,225.95C326.56,176.089 276.948,160.286 276.948,160.286Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M351.881,137.831C351.881,137.831 334.475,159.829 340.948,195.549L328.057,197.016C320.399,154.765 351.881,137.831 351.881,137.831Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M153.949,295.541C153.949,295.541 171.351,271.232 201.075,276.773C207.935,278.053 215.116,280.367 222.406,284.126L216.118,294.07C184.328,277.698 154.249,295.363 153.949,295.541Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M197.109,163.312C203.351,163.568 225.129,169.866 245.94,182.735C292.963,211.559 273.952,244.742 273.952,244.742C272.306,198.284 202.893,177.207 196.827,176.78L197.109,163.312Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M405.259,76.415C409.176,79.606 443.528,109.023 443.781,158.317C444.017,204.496 406.987,221.615 406.987,221.615C407.205,221.492 428.805,207.829 428.547,157.937C428.324,114.494 398.54,88.97 395.144,86.2L405.259,76.415Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M442.447,165.707C445.65,183.47 472.051,205.798 482.678,207.829C482.678,207.829 438.374,205.199 429.554,167.162L442.447,165.707Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M528.269,208.508L543.276,206.633C543.59,208.225 550.613,245.979 521.528,275.478C495.28,302.101 458.033,314.248 418.869,309.091C417.654,308.932 416.445,308.757 415.226,308.561C378.701,302.768 354.675,288.29 347.06,276.512L360.223,270.105C364.706,277.043 384.206,290.018 417.431,295.288C452.627,300.856 486.324,290.348 509.913,266.423C534.272,241.715 528.331,208.838 528.269,208.508Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M346.454,612.287C346.454,612.287 364.459,630.137 350.49,655.895C346.095,662.684 342.024,666.615 341.627,666.989L330.334,657.619C330.525,657.439 349.385,639.196 346.454,612.287Z"/>
<path {{ $attributes->merge(['class' => 'text-primary']) }} d="M746.169,21.499C756.017,22.795 765.878,25.001 775.478,28.052C846.582,50.652 881.597,100.292 890.828,137.976C955.354,158.86 994.038,204.652 997.236,264.281C999.012,297.296 988.628,322.18 966.359,338.423C979.244,347.991 988.032,359.715 992.343,368.627C996.839,377.939 1002.72,402.821 999.332,429.929C996.187,455.119 983.953,490.419 943.534,515.538C921.02,529.53 898.079,537.216 875.214,538.444C874.524,557.481 866.669,577.821 852.635,596.528C833.067,622.623 803.096,642.513 769.991,651.49C766.899,656.306 763.357,660.861 760.17,664.955C752.191,675.212 745.299,684.072 745.21,693.867C745.015,715.994 760.064,728.832 760.639,729.311L768.69,735.812C770.809,737.518 771.894,740.002 771.599,742.476C771.308,744.951 769.679,747.105 767.227,748.26L695.574,782.012C693.919,782.79 692.07,783.039 690.288,782.805C688.153,782.524 686.117,781.546 684.646,779.964L680.573,775.585C666.603,760.562 658.245,747.077 650.775,727.481C649.287,723.582 648.322,719.717 647.301,715.615C643.246,699.342 639.415,683.975 602.531,669.984C582.529,662.395 566.583,652.959 555.043,641.893C536.815,644.647 517.346,644.794 498.397,642.299C492.15,641.477 486.064,640.365 480.225,638.984C478.528,641.351 476.724,643.657 474.834,645.894C450.844,674.272 417.216,682.715 393.226,684.799C381.934,685.782 370.071,685.527 358.921,684.059C351.917,683.137 345.364,681.766 339.649,680.041C319.088,696.008 283.075,711.345 242.877,706.053C230.564,704.432 218.283,700.97 206.372,695.761C181.07,684.694 162.338,667.791 152.203,646.89C145.306,632.663 142.634,616.673 144.507,601.785C139.806,601.472 135.2,601.017 130.725,600.428C96.286,595.894 67.686,583.183 45.707,562.646C12.903,531.987 -3.897,482.581 0.767,430.49C5.257,380.339 40.511,354.902 62.285,343.855C46.313,320.624 24.457,273.681 56.708,219.503C89.53,164.361 147.812,155.406 179.02,154.885C179.027,132.85 187.435,92.873 241.101,62.81C273.272,44.788 306.984,37.939 341.311,42.462C365.525,45.65 384.1,53.896 395.041,59.951C402.448,50.197 414.871,36.551 433.253,24.573C456.287,9.566 482.031,1.374 509.768,0.225C523.796,-0.356 537.288,0.189 549.873,1.846C595.648,7.874 623.192,27.2 639.177,44.655C661.749,29.219 700.506,15.487 746.169,21.499ZM745.165,29.122C697.38,22.831 657.024,39.885 637.949,55.854C625.287,39.581 598.795,16.044 548.865,9.469C537.348,7.953 524.59,7.339 510.461,7.925C452.387,10.329 414.353,45.661 397.781,70.781C385.798,62.844 365.594,53.414 340.307,50.085C313.364,46.538 280.651,49.905 245.926,69.359C189.011,101.247 186.254,144.267 188.094,162.668C163.458,161.805 98.829,165.496 64.437,223.27C30.466,280.339 60.508,328.89 74.754,346.889C55.606,354.896 14.204,378.198 9.445,431.35C4.976,481.26 20.86,528.397 51.934,557.44C72.538,576.699 99.287,588.534 131.729,592.805C139.117,593.778 146.795,594.358 154.77,594.547C147.126,623.344 158.439,666.305 210.023,688.874C221.738,693.999 233.085,697.009 243.88,698.43C288.704,704.331 324.069,682.794 337.482,671.143C343.311,673.373 351.058,675.269 359.925,676.436C369.639,677.715 380.709,678.122 392.095,677.127C414.398,675.189 445.634,667.37 467.806,641.145C470.947,637.429 473.669,633.693 476.048,629.954C483.187,631.956 491.044,633.576 499.401,634.676C517.749,637.092 538.341,637.065 558.132,633.497C566.971,643.025 581.569,653.745 605.736,662.914C646.914,678.533 651.633,697.476 655.797,714.19C656.764,718.071 657.68,721.736 659.01,725.23C666.101,743.826 674.012,756.601 687.22,770.803L691.292,775.182L762.945,741.43L754.889,734.933C754.703,734.781 736.275,719.609 736.507,693.555C736.616,681.44 744.547,671.243 752.948,660.444C756.865,655.411 760.818,650.309 763.97,644.946C800.832,635.856 828.982,613.927 845.338,592.12C860.499,571.901 867.745,550.109 866.404,530.89C873.884,531.027 881.855,530.414 890.261,528.779C905.055,525.902 921.196,519.861 938.38,509.182C1009.87,464.746 991.028,385.267 984.357,371.461C978.94,360.259 967.066,346.748 951.448,338.201C971.373,327.501 990.859,307.537 988.544,264.393C985.435,206.49 947.161,162.729 883.274,143.713C875.616,104.13 839.209,56.412 772.778,35.297C763.372,32.308 754.136,30.303 745.165,29.122Z"/>
</svg>
<span class="sr-only">Investbrain</span>
</a>
@@ -1,55 +0,0 @@
@props([
'key' => 'modal',
'showClose' => true,
'closeOnEscape' => true,
'title' => null,
'subtitle' => null,
'persistent' => false
])
<div>
@teleport('body')
<dialog
x-data="{ open: false }"
x-on:toggle-{{ $key }}.window="open = !open"
class="relative z-50 w-auto h-auto"
@if($closeOnEscape)
@keydown.window.escape="open = false"
@endif
>
<template x-teleport="body">
<div x-transition.opacity x-show="open" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-full h-full">
<div
@if(!$persistent)
@click="open=false"
@endif
class="absolute inset-0 w-full h-full bg-black bg-opacity-40"
x-show="open"
x-cloak
></div>
<x-card
x-trap.inert.noscroll="open"
:title="$title"
:subtitle="$subtitle"
{{ $attributes->merge(['class' => 'relative transform overflow-hidden rounded-md ext-left shadow-xl w-full sm:w-2/3 lg:w-1/3 m-2 sm:m-0']) }}
x-show="open"
x-cloak
>
@if ($showClose)
<x-button
icon="o-x-mark"
title="{{ __('Close') }}"
class="absolute top-4 right-4 btn-ghost btn-circle btn-sm"
@click="open = false"
/>
@endif
{{ $slot }}
</x-card>
</div>
</template>
</dialog>
@endteleport
</div>
@@ -1,10 +0,0 @@
@props(['title' => ''])
<x-card
{{ $attributes->merge(['class' => 'bg-slate-100 dark:bg-base-200 rounded-lg']) }}
>
<h2 class="text-xl mb-2 flex items-center truncate"> {{ $title }} </h2>
{{ $slot }}
</x-card>
@@ -1,47 +0,0 @@
@props([
'key' => 'drawer',
'showClose' => true,
'closeOnEscape' => true,
'title' => null,
'subtitle' => null
])
<div
x-data="{ open: false }"
x-on:toggle-{{ $key }}.window="open = !open"
x-show="open"
@if($closeOnEscape)
@keydown.window.escape="open = false"
@endif
x-trap="open"
x-bind:inert="!open"
class="fixed inset-0 flex justify-end z-50"
x-transition.opacity
x-cloak
>
<div @click="open = false" class="fixed inset-0 bg-black opacity-50"></div>
<x-card
{{ $attributes->merge(['class' => 'min-h-screen w-full md:w-3/4 xl:w-3/5 rounded-none px-8 transition overflow-y-scroll']) }}
>
@if($title)
<x-slot:title>
{!! strip_tags($title) !!}
</x-slot:title>
@endif
@if($subtitle)
<x-slot:subtitle>
{!! strip_tags($subtitle) !!}
</x-slot:subtitle>
@endif
@if ($showClose)
<x-button icon="o-x-mark" title="{{ __('Close') }}" class="btn-ghost btn-circle btn-sm absolute top-4 right-4 " @click="open = false" />
@endif
{{ $slot }}
</x-card>
</div>
@@ -1,51 +0,0 @@
@props([
'showClose' => true,
'closeOnEscape' => true,
'title' => null,
'subtitle' => null,
'persistent' => false
])
<div>
@teleport('body')
<dialog
{{ $attributes->except('wire:model')->class(["modal"]) }}
x-data="{open: @entangle($attributes->wire('model')).live }"
:class="{'modal-open !animate-none': open}"
:open="open"
@if($closeOnEscape)
@keydown.escape.window = "$wire.{{ $attributes->wire('model')->value() }} = false"
@endif
>
<x-card
:title="$title"
:subtitle="$subtitle"
{{ $attributes->merge(['class' => 'modal-box relative transform overflow-hidden rounded-md ext-left shadow-xl w-full sm:w-2/3 lg:w-1/3 m-2 sm:m-0']) }}
>
@if ($showClose)
<x-button
icon="o-x-mark"
title="{{ __('Close') }}"
class="absolute top-4 right-4 btn-ghost btn-circle btn-sm"
@click="$wire.{{ $attributes->wire('model')->value() }} = false"
/>
@endif
{{ $slot }}
</x-card>
<div class="modal-backdrop" method="dialog">
<a
@if(!$persistent)
@click="$wire.{{ $attributes->wire('model')->value() }} = false"
@endif
type="button"
title="{{ __('Close') }}"
>
{{ __('Close') }}
</a>
</div>
</dialog>
@endteleport
</div>
@@ -1,9 +0,0 @@
<div role="status" class="flex w-full animate-pulse" wire:loading.delay>
<div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[330px] mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[300px] mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
<span class="sr-only">Loading...</span>
</div>
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="bg-base-200">
<head>
@include('components.partials.head')
</head>
<body class="font-sans antialiased scroll-smooth" x-data="{ sideBarOpen: false }">
@livewire('partials.nav-bar')
@livewire('partials.side-bar')
<main class="py-5 px-6 md:px-8 md:py-0 md:ml-68 mb-14">
{{ $slot }}
</main>
@if(session('toast'))
<script lang="text/javascript">
window.addEventListener('DOMContentLoaded', function () {
window.toast(JSON.parse(@json(session('toast'))))
});
</script>
@endif
<x-ui.toast />
@livewireScripts
</body>
</html>
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@include('components.partials.head')
</head>
<body class="font-sans antialiased scroll-smooth min-h-screen" x-data="{}">
<main class="">
<x-ui.theme-selector hidden="true" />
{{ $slot }}
</main>
@livewireScripts
</body>
</html>
@@ -0,0 +1,10 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" href="{{ asset('favicon.svg') }}">
<title>{{ config('app.name', 'Investbrain') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
@@ -1,64 +0,0 @@
@props([
'sidebar' => null,
'content' => null,
'footer' => null,
'fullWidth' => false,
'withNav' => false,
'collapseText' => 'Collapse',
'collapseIcon' => 'o-bars-3-bottom-right',
'collapsible' => false,
'url' => route('mary.toogle-sidebar', absolute: false),
])
<main class="{{ !$fullWidth ? 'max-w-screen-2xl' : '' }} w-full mx-auto">
<div class="drawer {{ $sidebar?->attributes['right'] ? 'drawer-end' : '' }} lg:drawer-open">
<input id="{{ $sidebar?->attributes['drawer'] }}" type="checkbox" class="drawer-toggle" />
<div {{ $content->attributes->class(["drawer-content w-full mx-auto p-5 lg:px-10 lg:py-5"]) }}>
{{-- MAIN CONTENT --}}
{{ $content }}
</div>
{{-- SIDEBAR --}}
@if($sidebar)
<div
x-data="{
collapsed: {{ session('mary-sidebar-collapsed', 'false') }},
collapseText: '{{ $collapseText }}',
toggle() {
this.collapsed = !this.collapsed;
fetch('{{ $url }}?collapsed=' + this.collapsed);
this.$dispatch('sidebar-toggled', this.collapsed);
}
}"
@menu-sub-clicked="if(collapsed) { toggle() }"
@class(["drawer-side z-20 lg:z-auto", "top-0 lg:top-[73px] lg:h-[calc(100vh-73px)]" => $withNav])
>
<label for="{{ $sidebar?->attributes['drawer'] }}" aria-label="close sidebar" class="drawer-overlay"></label>
{{-- SIDEBAR CONTENT --}}
<div>
{{ $sidebar }}
{{-- SIDEBAR COLLAPSE --}}
@if($sidebar->attributes['collapsible'])
<x-mary-menu class="hidden !bg-inherit lg:block">
<x-mary-menu-item
@click="toggle"
icon="{{ $sidebar->attributes['collapse-icon'] ?? $collapseIcon }}"
title="{{ $sidebar->attributes['collapse-text'] ?? $collapseText }}" />
</x-mary-menu>
@endif
</div>
</div>
@endif
{{-- END SIDEBAR--}}
</div>
</main>
{{-- FOOTER --}}
@if($footer)
<footer {{ $footer?->attributes->class(["mx-auto w-full", "max-w-screen-2xl" => !$fullWidth ]) }}>
{{ $footer }}
</footer>
@endif
@@ -18,34 +18,31 @@ new class extends Component
// methods
}; ?>
<div class="bg-base-100 border-base-300 border-b sticky top-0 z-10">
<div class="flex justify-between items-center px-7 py-3 gap-4 mx-auto">
<div class="flex flex-0 items-center">
<label for="main-drawer" class="lg:hidden mr-3">
<x-icon name="o-bars-3" class="cursor-pointer" />
</label>
<div class="hidden md:block" style="height:2.5em">
<x-application-logo />
</div>
<nav class="z-10 p-5 ml-0 md:ml-68 md:border-0 border-b border-zinc-200 dark:border-zinc-800">
<div class="flex flex-wrap justify-between items-center">
</div>
<div class="flex flex-1 justify-center" x-data>
<x-spotlight
shortcut="slash"
search-text="{{ __('Search holdings, portfolios, or anything else...') }}"
no-results-text="{{ __('Darn! Nothing found for that search.') }}"
<div class="flex">
<x-ui.button
aria-controls="drawer-navigation"
title="{{ __('Toggle Sidebar') }}"
class="btn-circle btn-ghost btn-sm block md:hidden"
icon="o-bars-3"
@click="sideBarOpen = true"
/>
<x-button
@click.stop="$dispatch('mary-search-open')"
class="btn-sm flex-1 justify-start md:flex-none"
<div class="ml-3 w-8 hidden sm:block md:hidden"> <x-ui.logo /> </div>
</div>
<div>
<x-ui.button
@click.stop="$dispatch('toggle-spotlight')"
class="btn-sm btn-ghost bg-base-300 flex-1 justify-start md:flex-none border-none"
>
<x-slot:label>
<span class="flex items-center text-gray-400">
<x-icon name="o-magnifying-glass" class="mr-2" />
<x-ui.icon name="o-magnifying-glass" class="mr-2" />
<span class=" truncate hidden sm:block">
@lang('Click or press :key to search', ['key' => '<kbd class="kbd kbd-sm">/</kbd>'])
</span>
@@ -54,35 +51,41 @@ new class extends Component
</span>
</span>
</x-slot:label>
</x-button>
</x-ui.button>
<x-ui.spotlight
search-text="{{ __('Search holdings, portfolios, or anything else...') }}"
no-results-text="{{ __('Darn! Nothing found for that search.') }}"
/>
</div>
<div class="flex flex-0 items-center gap-4">
<x-button
<x-ui.button
title="{{ __('Documentation') }}"
icon="o-book-open"
class="btn-circle btn-ghost btn-sm"
link="https://github.com/investbrainapp/investbrain"
external
>
</x-button>
</x-ui.button>
<x-button
<x-ui.button
title="{{ __('We\'re open source!') }}"
class="btn-circle btn-ghost btn-sm"
link="https://github.com/investbrainapp/investbrain"
external
>
<x-github-icon />
</x-button>
<x-social.github-icon />
</x-ui.button>
<x-theme-toggle
<x-ui.theme-selector
id="theme-selector"
title="{{ __('Toggle Theme') }}"
class="btn-circle btn-ghost btn-sm"
darkTheme="business"
lightTheme="corporate"
/>
</div>
</div>
</div>
</nav>
@@ -19,76 +19,97 @@ new class extends Component
}; ?>
<div class="
flex
flex-col
!transition-all
!duration-100
ease-out
overflow-x-hidden
overflow-y-auto
h-screen
lg:h-[calc(100vh-73px)]
bg-base-100
lg:bg-inherit
{{ session('mary-sidebar-collapsed') == 'true' ? 'w-[70px] [&>*_summary::after]:hidden [&_.mary-hideable]:hidden [&_.display-when-collapsed]:block [&_.hidden-when-collapsed]:hidden' : null }}
{{ session('mary-sidebar-collapsed') != 'true' ? 'w-[270px] [&>*_summary::after]:block [&_.mary-hideable]:block [&_.hidden-when-collapsed]:block [&_.display-when-collapsed]:hidden' : null }}
">
<div class="flex-1">
<x-menu activate-by-route>
<div
aria-label="Sidebar"
style="background-image: url('{{ asset('images/noise.svg') }}')"
class="
h-full
bg-base-300
border-r
border-base-100
fixed
top-0
left-0
z-50
md:w-68
w-3/4
transition-transform
-translate-x-full
md:translate-x-0
"
:class="{'translate-x-0': sideBarOpen, '-translate-x-full': !sideBarOpen}"
x-data="{
responsiveSidebar() {
if (window.innerWidth >= 768) {
this.sideBarOpen = true
return;
}
this.sideBarOpen = false
}
}"
@resize.window="responsiveSidebar"
@keyup.escape.window="sideBarOpen = false"
>
<template x-teleport="body">
<div
aria-label="Overlay"
class="block md:hidden z-10 fixed w-screen h-screen inset-0 bg-black/20 backdrop-blur-sm"
x-on:click="sideBarOpen=false"
x-show="sideBarOpen"
x-cloak
></div>
</template>
<div class="h-full px-1 overflow-y-auto flex flex-col ">
<x-menu-item title="{{ __('Dashboard') }}" icon="o-home" link="{{ route('dashboard') }}" />
<x-menu-sub title="{{ __('Portfolios') }}" icon="o-document-duplicate">
@foreach (auth()->user()->portfolios as $portfolio)
<x-menu-item icon="o-document" link="{{ route('portfolio.show', ['portfolio' => $portfolio->id ]) }}" >
<x-slot:title>
{{ $portfolio->title }}
@if($portfolio->wishlist)
<x-badge value="{{ __('Wishlist') }}" class="badge-secondary badge-sm ml-2" />
@endif
</x-slot:title>
</x-menu-item>
@endforeach
<div class="w-10 m-5"> <x-ui.logo /> </div>
<x-menu-item title="{{ __('Create Portfolio') }}" icon="o-document-plus" link="{{ route('portfolio.create') }}" />
</x-menu-sub>
<x-menu-item title="{{ __('Transactions') }}" icon="o-banknotes" link="{{ route('transaction.index') }}" />
{{-- <x-menu-item title="{{ __('Reporting') }}" icon="o-chart-bar-square" link="####" /> --}}
<x-ui.menu class="space-y-2 text-wrap w-full overflow-x-hidden" activate-by-route="true">
<x-ui.menu-item icon="o-home" title="{{ __('Dashboard') }}" link="{{ route('dashboard') }}" class="font-medium text-md" />
</x-menu>
@foreach (auth()->user()->portfolios as $portfolio)
<x-ui.menu-item
:title="$portfolio->title"
icon="o-document"
:badge="$portfolio->wishlist ? __('Wishlist') : null"
badge-classes="badge-secondary badge-outline"
link="{{ route('portfolio.show', ['portfolio' => $portfolio->id ]) }}"
class="font-medium text-md"
/>
@endforeach
<x-ui.menu-item icon="o-document-plus" title="{{ __('Create Portfolio') }}" link="{{ route('portfolio.create') }}" class="font-medium text-md" />
</div>
<div class="px-3">
<x-section-border />
<x-ui.menu-item icon="o-banknotes" title="{{ __('Transactions') }}" link="{{ route('transaction.index') }}" class="font-medium text-md" />
</x-ui.menu>
<div class="flex-1"></div>
@php
$user = auth()->user();
@endphp
<x-list-item :item="$user" avatar="profile_photo_url" value="name" sub-value="email" no-separator no-hover class="mb-3 !-mt-3 rounded">
<x-ui.list-item :item="$user" avatar="profile_photo_url" value="name" sub-value="email" no-separator no-hover class="rounded mb-2">
<x-slot:actions>
<x-dropdown>
<x-ui.dropdown>
<x-slot:trigger>
<x-button icon="o-cog-6-tooth" class="btn-circle btn-ghost btn-xs" />
<x-ui.button icon="o-cog-6-tooth" class="btn-circle btn-ghost btn-sm relative transition-transform focus:rotate-90" />
</x-slot:trigger>
<x-menu-item title="{{ __('Manage Profile') }}" icon="o-user" link="{{ @route('profile.show') }}" />
<x-menu-item title="{{ __('API Tokens') }}" icon="o-command-line" link="{{ @route('api-tokens.index') }}" />
<x-menu-item title="{{ __('Import / Export Data') }}" icon="o-cloud-arrow-down" link="{{ @route('import-export') }}" />
<x-ui.menu-item title="{{ __('Manage Profile') }}" icon="o-user" link="{{ @route('profile.show') }}" />
<x-ui.menu-item title="{{ __('API Tokens') }}" icon="o-command-line" link="{{ @route('api-tokens.index') }}" />
<x-ui.menu-item title="{{ __('Import / Export Data') }}" icon="o-cloud-arrow-down" link="{{ @route('import-export') }}" />
<x-section-border class="py-1" />
<x-ui.section-border class="py-1" />
<x-menu-item title="{{ __('Log Out') }}" icon="o-power" onclick="event.preventDefault(); document.getElementById('logout').submit();" />
<x-ui.menu-item title="{{ __('Log Out') }}" icon="o-power" onclick="event.preventDefault(); document.getElementById('logout').submit();" />
<form id="logout" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
</x-dropdown>
</x-ui.dropdown>
</x-slot:actions>
</x-list-item>
</div>
</x-ui.list-item>
</div>
</div>
@@ -1,16 +1,16 @@
<div>
@if(!empty(config('services.enabled_login_providers')))
@foreach(explode(',', config('services.enabled_login_providers')) as $provider)
<x-button
<x-ui.button
link="{{ route('oauth.redirect', ['provider' => $provider]) }}"
class="btn-sm btn-block my-1"
class="btn-sm btn-block my-1 text-white"
style='background-color: {{ config("services.$provider.color") }}'
no-wire-navigate
>
@include("components.$provider-icon")
@include("components.social.$provider-icon")
{{ __('Login with') }} {{ config("services.$provider.name") }}
</x-button>
</x-ui.button>
@endforeach
@endif
</div>

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 472 B

Before

Width:  |  Height:  |  Size: 871 B

After

Width:  |  Height:  |  Size: 871 B

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 582 B

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 676 B

@@ -1,27 +1,26 @@
<?php
use Mary\Traits\Toast;
use App\Models\AiChat;
use App\Models\Holding;
use Illuminate\Database\Eloquent\Model;
use Livewire\Volt\Component;
use OpenAI\Factory;
use OpenAI\Responses\StreamResponse;
new class extends Component {
use Toast;
new class extends Component
{
// props
public Model $chatable;
public string $system_prompt = 'You are an investment portfolio assistant providing advice to an investor. Use the following information to provide relevant recommendations. Use the words \'likely\' or \'may\' instead of concrete statements (except for obvious statements of fact or common sense). Use github style markdown for any formatting.';
public array $suggested_prompts = [];
public array $messages = [];
public ?string $prompt = null;
public ?string $answer = null;
public bool $streaming = false;
// methods
public function mount()
{
@@ -33,10 +32,11 @@ new class extends Component {
// prevent spam
if ($this->isRateLimited() || $this->streaming) {
array_push($this->messages, [
'role' => 'assistant',
'content' => __('Hang on! You\'re doing that too much.')
'role' => 'assistant',
'content' => __('Hang on! You\'re doing that too much.'),
]);
$this->js('scrollChatWindow(250)');
return;
}
@@ -51,7 +51,7 @@ new class extends Component {
$this->js('scrollChatWindow(250)');
return;
}
}
$this->chatable->chats()->save(new AiChat(['role' => 'user', 'content' => $this->prompt]));
array_push($this->messages, ['role' => 'user', 'content' => $this->prompt]);
@@ -65,17 +65,17 @@ new class extends Component {
public function generateCompletion(): void
{
try {
$client = $this->createOpenAiClient();
$stream = $client->chat()->createStreamed([
'model' => config('openai.model'),
'messages' => [
['role' => 'system', 'content' => "Today's date is "
.now()->toDateString()
.".\n\n".$this->system_prompt],
...array_slice($this->messages, -10)
...array_slice($this->messages, -10),
],
]);
} catch (\Exception $e) {
@@ -83,14 +83,15 @@ new class extends Component {
$this->chatable->chats()->save(new AiChat(['role' => 'assistant', 'content' => $e->getMessage()]));
array_push($this->messages, ['role' => 'assistant', 'content' => $e->getMessage()]);
$this->resetPrompt();
return;
}
$this->stream(to: "answer", content: '', replace: true);
foreach($stream as $response){
if(!empty($response->choices[0]->delta->content)) {
$this->stream(to: 'answer', content: '', replace: true);
foreach ($stream as $response) {
if (! empty($response->choices[0]->delta->content)) {
$this->stream(to: 'answer', content: $response->choices[0]->delta->content, replace: false);
$this->answer .= $response->choices[0]->delta->content;
}
@@ -116,34 +117,34 @@ new class extends Component {
'name' => 'suggested_prompts_schema',
'strict' => true,
'schema' => [
"type" => "object",
"properties" => [
"suggested_prompts" => [
"type" => "array",
"items" => [
"type" => "object",
"properties" => [
"text" => [
"type" => "string",
"description" => "The suggested prompt question (no more than 5 words)"
'type' => 'object',
'properties' => [
'suggested_prompts' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'text' => [
'type' => 'string',
'description' => 'The suggested prompt question (no more than 5 words)',
],
'value' => [
'type' => 'string',
'description' => 'The detailed version of the question',
],
"value" => [
"type" => "string",
"description" => "The detailed version of the question"
]
],
"required" => ["text", "value"],
"additionalProperties" => false
]
]
'required' => ['text', 'value'],
'additionalProperties' => false,
],
],
],
"required" => ["suggested_prompts"],
"additionalProperties" => false
]
]
'required' => ['suggested_prompts'],
'additionalProperties' => false,
],
],
],
'messages' => [
['role' => 'system', 'content' => "
['role' => 'system', 'content' => '
Your role is to assist investors in asking thoughtful questions of their investment advisors.
When you help investors ask good questions, you should ensure the you questions you recommend
@@ -155,12 +156,12 @@ new class extends Component {
explanation.
Your response should only include valid JSON.
"],
'],
['role' => 'user', 'content' => "
Generate between 1 and 5 (no more than 5) follow up questions a savvy investor might ask their
advisor based on the following conversation:
\n\n
".json_encode(array_slice($this->messages, -4))
".json_encode(array_slice($this->messages, -4)),
],
],
]);
@@ -171,6 +172,7 @@ new class extends Component {
$this->suggested_prompts = [];
$this->error($e->getMessage());
return;
}
}
@@ -184,10 +186,10 @@ new class extends Component {
public function isRateLimited(): bool
{
$rateLimitKey = auth()->id() . '/' . $this->chatable->id;
$rateLimitKey = auth()->id().'/'.$this->chatable->id;
if (RateLimiter::tooManyAttempts($rateLimitKey, 20)) {
return true;
}
@@ -210,7 +212,6 @@ new class extends Component {
->withBaseUri($baseUri)
->make();
}
}; ?>
<div
@@ -227,15 +228,16 @@ new class extends Component {
class="fixed z-50 bottom-8 right-8"
>
{{-- toggle button --}}
<x-button
<x-ui.button
x-show="!open"
@click="$dispatch('toggle-ai-chat')"
@keyup.escape.window="open = false"
class="flex btn btn-circle md:btn-lg btn-primary"
>
<x-slot:label>
<x-icon name="o-sparkles" class="w-6 h-6 md:w-8 md:h-8"></x-icon>
<x-ui.icon name="o-sparkles" class="w-6 h-6 md:w-8 md:h-8"></x-ui.icon>
</x-slot:label>
</x-button>
</x-ui.button>
{{-- popup --}}
<div
@@ -251,17 +253,17 @@ new class extends Component {
x-transition:leave-end="opacity-0 transform translate-y-full"
x-cloak
key="ai-chat"
class="fixed bg-base-100 shadow-2xl rounded-none md:rounded-lg
inset-0 h-screen w-full
md:inset-auto md:right-6 md:bottom-6 md:w-[32rem] md:h-[46rem]"
class="fixed bg-base-300 shadow-2xl rounded-none md:rounded-lg
inset-0 h-screen w-full md:inset-auto md:right-6
md:bottom-6 md:w-[32rem] md:h-[46rem]"
>
<div
class="absolute inset-0 flex flex-col overflow-hidden p-4"
x-intersect="scrollChatWindow()"
>
<div class="flex grow-0 justify-between items-center pb-4 ">
<h2 class="text-lg text-bold">{{ __('AI Chat') }}</h2>
<x-button
<h2 class="text-lg text-bold select-none">{{ __('AI Chat') }}</h2>
<x-ui.button
icon="o-x-mark"
class="absolute top-5 right-4 btn-ghost btn-circle btn-sm"
title="{{ __('Close') }}"
@@ -284,7 +286,7 @@ new class extends Component {
bg-slate-200
dark:bg-slate-800
">
<x-icon name="o-sparkles" class="h-auto p-1 w-10" />
<x-ui.icon name="o-sparkles" class="h-auto p-1 w-10" />
</span>
<p class="leading-relaxed w-full">
<span class="block font-bold">AI</span> {{ __('Hi, how can I help?') }}
@@ -298,7 +300,7 @@ new class extends Component {
<div class="flex gap-3 mb-5 flex-1">
<span class="relative flex shrink-0 overflow-hidden rounded-full w-10 h-10">
<x-avatar :image="auth()->user()->profile_photo_url" class="!w-10" />
<x-ui.avatar :image="auth()->user()->profile_photo_url" class="!w-10" />
</span>
<p class="leading-relaxed">
@@ -319,7 +321,7 @@ new class extends Component {
bg-slate-200
dark:bg-slate-800
">
<x-icon name="o-sparkles" class="h-auto p-1 w-10" />
<x-ui.icon name="o-sparkles" class="h-auto p-1 w-10" />
</span>
<div class="leading-relaxed" >
<span class="block font-bold ">AI </span> {!! Str::markdown($message['content']) !!}
@@ -342,7 +344,7 @@ new class extends Component {
bg-slate-200
dark:bg-slate-800
">
<x-icon name="o-sparkles" class="h-auto p-1 w-10" />
<x-ui.icon name="o-sparkles" class="h-auto p-1 w-10" />
</span>
<p class="leading-relaxed" >
<span class="block font-bold ">AI </span> <span wire:stream="answer">{{ $answer }}</span>
@@ -353,40 +355,42 @@ new class extends Component {
{{-- prompt input --}}
<div class="mt-3 grow-0">
<form submit="startCompletion" >
<form submit="startCompletion">
<div class="">
@foreach($suggested_prompts as $prompt)
<x-button
<x-ui.button
class="btn-xs btn-primary btn-outline mr-1 mb-2"
wire:click="startCompletion('{{ addslashes($prompt['value']) }}')"
>{{ $prompt['text'] }}</x-button>
>{{ $prompt['text'] }}</x-ui.button>
@endforeach
</div>
<div class="flex justify-between align-bottom space-x-2 mt-1">
<div class="w-full">
<div class="w-full" >
<x-textarea
<x-ui.textarea
wire:model="prompt"
class="h-24 resize-none "
class="h-18 resize-none bg-base-200"
placeholder="{{ __('Have a question? AI might be able to help...') }}"
wire:keydown.enter.prevent="startCompletion"
autofocus
></x-textarea>
@toggle-ai-chat.window="setTimeout(() => $el.focus(), 250)"
></x-ui.textarea>
{{-- --}}
</div>
<x-button
<x-ui.button
spinner="generateCompletion"
wire:click="startCompletion"
class="btn btn-ghost h-24"
class="btn btn-ghost h-32"
icon="o-paper-airplane"
></x-button>
></x-ui.button>
</div>
<div class="w-full mt-2">
<p class="text-xs text-secondary leading-tight">{{ __('Advice generated by AI may contain errors. Use at your own risk. Always consult a licensed investment advisor.') }} </p>
<p class="text-xs text-secondary leading-tight select-none">{{ __('Advice generated by AI may contain errors. Use at your own risk. Always consult a licensed investment advisor.') }} </p>
</div>
</form>
</div>
@@ -0,0 +1,36 @@
@props([
'id' => null,
'title' => null
'icon' => null,
'description' => null,
'shadow' => false,
'dismissable' => false
])
<div
wire:key="{{ $id }}"
{{ $attributes->whereDoesntStartWith('class') }}
{{ $attributes->class(['alert rounded-md', 'shadow-md' => $shadow])}}
x-data="{ show: true }" x-show="show"
>
@if($icon)
<x-icon :name="$icon" class="self-center" />
@endif
@if($title)
<div>
<div @class(["font-bold" => $description])>{{ $title }}</div>
<div class="text-xs">{{ $description }}</div>
</div>
@else
<span>{{ $slot }}</span>
@endif
<div class="flex items-center gap-3">
{{ $actions }}
</div>
@if($dismissible)
<x-button icon="o-x-mark" @click="show = false" class="btn-xs btn-circle btn-ghost static self-start end-0" />
@endif
</div>
@@ -0,0 +1,9 @@
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0">
<div>
{{ $logo }}
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-base-200 shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
@@ -0,0 +1,36 @@
@props([
'id' => null,
'image' => '',
'alt' => '',
'placeholder' => '',
'fallbackImage' => null,
'title' => null,
'subtitle' => null,
])
<div class="flex items-center gap-3">
<div class="avatar @if(empty($image)) avatar-placeholder @endif">
<div {{ $attributes->class(["w-7 rounded-full", "bg-neutral text-neutral-content" => empty($image)]) }}>
@if(empty($image))
<span class="text-xs" alt="{{ $alt }}">{{ $placeholder }}</span>
@else
<img src="{{ $image }}" alt="{{ $alt }}" @if($fallbackImage) onerror="this.src='{{ $fallbackImage }}'" @endif />
@endif
</div>
</div>
@if($title || $subtitle)
<div>
@if($title)
<div @class(["font-semibold font-lg", is_string($title) ? '' : $title?->attributes->get('class') ]) >
{{ $title }}
</div>
@endif
@if($subtitle)
<div @class(["text-sm text-base-content/50", is_string($subtitle) ? '' : $subtitle?->attributes->get('class') ]) >
{{ $subtitle }}
</div>
@endif
</div>
@endif
</div>
@@ -0,0 +1,13 @@
@props([
'value' => null,
])
@php
if (isset($class)) {
$attributes->setAttributes(['class' => $class]);
}
@endphp
<div {{ $attributes->class(["badge select-none"]) }}>
{{ $value ?? $slot ?? '' }}
</div>
@@ -0,0 +1,75 @@
@props([
'type' => 'button',
'external' => false,
'link' => null,
'label' => null,
'icon' => null,
'spinner' => null,
'tooltip' => null,
'tooltipLeft' => null,
'tooltipRight' => null,
'tooltipBottom' => null,
'badge' => null,
'badgeClasses' => null,
])
@php
$tooltip = $tooltip ?? $tooltipLeft ?? $tooltipRight ?? $tooltipBottom;
$tooltipPosition = $tooltipLeft ? 'lg:tooltip-left' : ($tooltipRight ? 'lg:tooltip-right' : ($tooltipBottom ? 'lg:tooltip-bottom' : 'lg:tooltip-top'));
$spinnerTarget = $spinner ?? $attributes->whereStartsWith('wire:click')->first();
@endphp
@if($link)
<a href="{!! $link !!}"
@else
<button
@endif
{{ $attributes->whereDoesntStartWith('class')->merge(['type' => $type]) }}
type="button"
{{ $attributes->class(['btn', "!inline-flex lg:tooltip $tooltipPosition" => $tooltip]) }}
@if($link && $external)
target="_blank"
@endif
@if($link && !$external)
wire:navigate
@endif
data-tip="{{ $tooltip }}"
@if($spinner)
wire:target="{{ $spinnerTarget }}"
wire:loading.attr="disabled"
@endif
>
{{-- spinner --}}
@if($spinner)
<span wire:loading wire:target="{{ $spinnerTarget }}" class="loading loading-spinner w-5 h-5">Loading</span>
@endif
{{-- icon --}}
@if($icon)
<span class="block" @if($spinner) wire:loading.class="hidden" wire:target="{{ $spinnerTarget }}" @endif>
<x-ui.icon :name="$icon" />
</span>
@endif
{{-- label / slot --}}
@if($label)
<span>
{{ $label }}
</span>
@if(strlen($badge ?? '') > 0)
<span class="badge badge-sm {{ $badgeClasses }}">{{ $badge }}</span>
@endif
@else
{{ $slot }}
@endif
@if($link)
</a>
@else
</button>
@endif
@@ -0,0 +1,22 @@
@props([
'title' => '',
'subTitle' => '',
'dense' => false,
'expanded' => false
])
<div
{{ $attributes->merge()->class(['p-5', 'shadow-sm', 'rounded-lg', 'bg-base-100']) }}
>
@if($title)
<h3 @class(['pb-2' => !$subTitle && !$dense, 'text-xl font-bold leading-none tracking-tight flex items-center truncate'])> {{ $title }} </h3>
@endif
@if($subTitle)
<h5 @class(['pb-2' => !$dense, 'text-sm text-gray-400 flex items-center truncate'])> {{ $subTitle }} </h5>
@endif
<div @class(['mt-2' => !$dense && !$expanded, 'mt-0' => $dense, 'mt-5' => $expanded])>
{{ $slot }}
</div>
</div>
@@ -0,0 +1,62 @@
@props([
'id' => null,
'label' => null,
'right' => false,
'tight' => false,
'hint' => null,
'hintClass' => 'label-text-alt text-gray-400 py-1 pb-0',
'errorField' => null,
'errorClass' => 'text-red-500 label-text-alt p-1',
'omitError' => false,
'firstErrorOnly' => false,
])
@php
$modelName = $attributes->whereStartsWith('wire:model')->first();
$errorFieldName = $errorField ?? $modelName;
$id = $id == $modelName ? $modelName : "{$id}{$modelName}";
@endphp
<div>
<label for="{{ $id }}" class="flex gap-3 items-center cursor-pointer">
@if($right)
<span @class(["flex-1" => !$tight])>
{{ $label }}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
</span>
@endif
<input
id="{{ $id }}"
type="checkbox"
{{ $attributes->whereDoesntStartWith('id')->merge(['class' => 'checkbox checkbox-primary']) }} />
@if(!$right)
{{ $label }}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
@endif
</label>
{{-- ERROR --}}
@if(!$omitError && $errors->has($errorFieldName))
@foreach($errors->get($errorFieldName) as $message)
@foreach(Arr::wrap($message) as $line)
<div class="{{ $errorClass }}" x-classes="text-red-500 label-text-alt p-1">{{ $line }}</div>
@break($firstErrorOnly)
@endforeach
@break($firstErrorOnly)
@endforeach
@endif
{{-- HINT --}}
@if($hint)
<div class="{{ $hintClass }}" x-classes="label-text-alt text-gray-400 py-1 pb-0">{{ $hint }}</div>
@endif
</div>
@@ -1,7 +1,14 @@
@props(['id' => null, 'maxWidth' => null])
@props(['key' => 'confirmation'])
<x-ib-livewire-modal :id="$id" :maxWidth="$maxWidth" {{ $attributes }} :showClose="false">
<div class="p-2">
<x-ui.modal
:key="$key"
box-class="max-w-xl"
persistent="true"
no-card="true"
{{ $attributes }}
>
<div class="p-5">
<div class="sm:flex sm:items-start">
<div class="mx-auto shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600 dark:text-red-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
@@ -10,18 +17,18 @@
</div>
<div class="mt-3 text-center sm:mt-0 sm:ms-4 sm:text-start">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
<h3 class="text-xl font-bold text-primary-content">
{{ $title }}
</h3>
<div class="mt-4 text-sm text-gray-600 dark:text-gray-400">
<div class="mt-2 text-sm text-secondary-content">
{{ $content }}
</div>
</div>
</div>
</div>
<div class="flex flex-row items-center justify-end mt-3 p-2 text-end">
{{ $footer }}
<div class="flex flex-row items-center justify-center sm:justify-end mt-8 text-end">
{{ $footer }}
</div>
</div>
</x-ib-livewire-modal>
</x-ui.modal>
@@ -0,0 +1,261 @@
@props([
'id' => null,
'label' => null,
'icon' => null,
'hint' => null,
'hintClass' => 'label-text-alt text-gray-400 py-1 pb-0',
'errorField' => null,
'errorClass' => 'text-red-500 label-text-alt p-1',
'omitError' => false,
'firstErrorOnly' => false,
])
@php
$modelName = $attributes->whereStartsWith('wire:model')->first();
$errorFieldName = $errorField ?? $modelName;
$id = $id == $modelName ? $modelName : "{$id}{$modelName}";
@endphp
<style>
input[type="date"]::-webkit-calendar-picker-indicator {
color: transparent;
background: transparent;
}
</style>
<div
x-cloak
x-data="{
datePickerOpen: false,
datePickerValue: $wire.entangle(@js($modelName)),
datePickerMonth: '',
datePickerYear: '',
datePickerDay: '',
datePickerDaysInMonth: [],
datePickerBlankDaysInMonth: [],
datePickerMonthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
datePickerDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
datePickerDayClicked(day) {
let selectedDate = new Date(this.datePickerYear, this.datePickerMonth, day);
this.datePickerDay = day;
this.datePickerValue = this.dateToValue(selectedDate);
this.datePickerIsSelectedDate(day);
this.datePickerOpen = false;
},
datePickerPreviousMonth(){
if (this.datePickerMonth == 0) {
this.datePickerYear--;
this.datePickerMonth = 12;
}
this.datePickerMonth--;
this.datePickerCalculateDays();
},
datePickerNextMonth(){
if (this.datePickerMonth == 11) {
this.datePickerMonth = 0;
this.datePickerYear++;
} else {
this.datePickerMonth++;
}
this.datePickerCalculateDays();
},
datePickerIsSelectedDate(day) {
const d = new Date(this.datePickerYear, this.datePickerMonth, day);
return this.datePickerValue === this.dateToValue(d) ? true : false;
},
datePickerIsToday(day) {
const today = new Date();
const d = new Date(this.datePickerYear, this.datePickerMonth, day);
return today.toDateString() === d.toDateString() ? true : false;
},
datePickerCalculateDays() {
let daysInMonth = new Date(this.datePickerYear, this.datePickerMonth + 1, 0).getDate();
// find where to start calendar day of week
let dayOfWeek = new Date(this.datePickerYear, this.datePickerMonth).getDay();
let blankdaysArray = [];
for (var i = 1; i <= dayOfWeek; i++) {
blankdaysArray.push(i);
}
let daysArray = [];
for (var i = 1; i <= daysInMonth; i++) {
daysArray.push(i);
}
this.datePickerBlankDaysInMonth = blankdaysArray;
this.datePickerDaysInMonth = daysArray;
},
dateToValue(d) {
d = this.parseDate(d)
let formattedDate = ('0' + d.getDate()).slice(-2);
let formattedMonthInNumber = ('0' + (parseInt(d.getMonth()) + 1)).slice(-2);
let formattedYear = d.getFullYear();
return `${formattedYear}-${formattedMonthInNumber}-${formattedDate}`;
},
parseDate(d) {
date = new Date();
let userTimezoneOffset = date.getTimezoneOffset() * 60000;
return new Date(Date.parse(d) + userTimezoneOffset);
}
}"
x-init="
currentDate = new Date();
if (datePickerValue) {
currentDate = parseDate(datePickerValue)
}
datePickerMonth = currentDate.getMonth();
datePickerYear = currentDate.getFullYear();
datePickerDay = currentDate.getDay();
datePickerValue = currentDate.toISOString().slice(0, 10);
datePickerCalculateDays();
"
>
{{-- STANDARD LABEL --}}
@if($label)
<label for="{{ $id }}" class="pt-0 label label-text font-semibold">
<span>
{{ $label }}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
</span>
</label>
@endif
<div class="flex-1 relative">
{{-- DESKTOP --}}
<div
x-ref="desktopDatePickerInput"
x-html="parseDate(datePickerValue).toLocaleDateString()"
x-on:keydown.escape="datePickerOpen=false"
@click="datePickerOpen=true"
{{ $attributes->class([
"hidden md:block py-2 input px-4 input-primary w-full peer appearance-none",
'ps-10' => ($icon),
'border border-dashed' => $attributes->has('readonly') && $attributes->get('readonly') == true,
'input-error' => $errors->has($errorFieldName)
]) }}
></div>
<div
x-show="datePickerOpen"
x-transition:enter="ease-out duration-200"
x-transition:enter-start="-translate-x-2"
x-transition:enter-end="translate-x-0"
@click.away="datePickerOpen = false"
class="
p-4
mt-12
top-0
left-0
max-w-lg
w-[17rem]
absolute
z-100
bg-base-100
dark:bg-base-300
rounded-box
shadow-md
select-none
"
>
<div class="flex justify-between items-center mb-2">
<div>
<span x-text="datePickerMonthNames[datePickerMonth]" class="text-lg font-bold"></span>
<span x-text="datePickerYear" class="ml-1 text-lg font-normal text-gray-600"></span>
</div>
<div>
<button @click="datePickerPreviousMonth()" type="button" class="inline-flex p-1 rounded-full transition duration-100 ease-in-out cursor-pointer focus:outline-none focus:shadow-outline hover:bg-accent/50">
<x-ui.icon name="o-chevron-left" />
</button>
<button @click="datePickerNextMonth()" type="button" class="inline-flex p-1 rounded-full transition duration-100 ease-in-out cursor-pointer focus:outline-none focus:shadow-outline hover:bg-accent/50">
<x-ui.icon name="o-chevron-right" />
</button>
</div>
</div>
<div class="grid grid-cols-7 mb-3">
<template x-for="(day, index) in datePickerDays" :key="index">
<div class="px-0.5">
<div x-text="day" class="text-xs font-medium text-center"></div>
</div>
</template>
</div>
<div class="grid grid-cols-7">
<template x-for="blankDay in datePickerBlankDaysInMonth">
<div class="p-1 text-sm text-center border border-transparent"></div>
</template>
<template x-for="(day, dayIndex) in datePickerDaysInMonth" :key="dayIndex">
<div class="px-0.5 mb-1 aspect-square">
<div
x-text="day"
@click="datePickerDayClicked(day)"
:class="{
'border border-accent/50': datePickerIsToday(day) == true,
'hover:bg-neutral-800/70': datePickerIsToday(day) == false && datePickerIsSelectedDate(day) == false,
'text-primary-content bg-primary hover:bg-primary/50': datePickerIsSelectedDate(day) == true
}"
class="flex justify-center items-center w-7 h-7 text-sm leading-none text-center rounded-full cursor-pointer"
></div>
</div>
</template>
</div>
</div>
{{-- MOBILE/NATIVE --}}
<input
type="date"
x-model="datePickerValue"
placeholder="Select date"
id="{{ $id }}"
onfocus="this.showPicker?.()"
x-ref="mobileDatePickerInput"
{{ $attributes->class([
"block md:hidden input input-primary w-full peer appearance-none",
'ps-10' => ($icon),
'border border-dashed' => $attributes->has('readonly') && $attributes->get('readonly') == true,
'input-error' => $errors->has($errorFieldName)
]) }}
/>
{{-- ICON --}}
<div @click="
if ($refs.mobileDatePickerInput?.checkVisibility()) {
$refs.mobileDatePickerInput?.showPicker()
return;
}
if(datePickerOpen) {
$refs.desktopDatePickerInput.focus();
return;
}
datePickerOpen=!datePickerOpen;
"
class="z-60 absolute top-1/2 -translate-y-1/2 end-0 p-3 cursor-pointer text-neutral-400 hover:text-neutral-500"
>
<x-ui.icon name="o-calendar" />
</div>
</div>
{{-- ERROR --}}
@if(!$omitError && $errors->has($errorFieldName))
@foreach($errors->get($errorFieldName) as $message)
@foreach(Arr::wrap($message) as $line)
<div class="{{ $errorClass }}" x-classes="text-red-500 label-text-alt p-1">{{ $line }}</div>
@break($firstErrorOnly)
@endforeach
@break($firstErrorOnly)
@endforeach
@endif
{{-- HINT --}}
@if($hint)
<div class="{{ $hintClass }}" x-classes="label-text-alt text-gray-400 py-1 pb-0">{{ $hint }}</div>
@endif
</div>
@@ -0,0 +1,25 @@
@props(['key' => 'dialog'])
<x-ui.modal
:key="$key"
box-class="max-w-xl"
persistent="true"
no-card="true"
{{ $attributes }}
>
<div class="p-5">
<div class="text-xl font-bold text-primary-content">
{{ $title }}
</div>
<div class="mt-2 text-sm text-secondary-content">
{{ $content }}
</div>
<div class="flex flex-row items-center justify-end mt-8 text-end">
{{ $footer }}
</div>
</div>
</x-ui.modal>
@@ -0,0 +1,52 @@
@props([
'key' => 'drawer',
'showClose' => true,
'closeOnEscape' => true,
'title' => null,
'subtitle' => null
])
<div
x-data="{ open: false }"
x-on:toggle-{{ $key }}.window="open = !open"
@if($closeOnEscape)
@keydown.window.escape="open = false"
@endif
x-trap="open"
x-bind:inert="!open"
class="fixed inset-0 flex justify-end z-50"
x-cloak
>
{{-- overlay --}}
<div @click="open = false" x-show="open" class="z-40 fixed inset-0 bg-black opacity-50"></div>
{{-- content --}}
<div
class="transition duration-200 ease-out transition-transform translate-x-full transform z-50 md:w-3/4 xl:w-3/5"
:class="{'translate-x-0': open, 'translate-x-full': !open}"
>
<x-ui.card
{{ $attributes->merge(['class' => 'w-full min-h-screen rounded-none px-8 overflow-y-scroll']) }}
>
@if($title)
<x-slot:title>
{!! strip_tags($title) !!}
</x-slot:title>
@endif
@if($subtitle)
<x-slot:subtitle>
{!! strip_tags($subtitle) !!}
</x-slot:subtitle>
@endif
@if ($showClose)
<x-ui.button icon="o-x-mark" title="{{ __('Close') }}" class="btn-ghost btn-circle btn-sm absolute top-4 right-4 " @click="open = false" />
@endif
{{ $slot }}
</x-ui.card>
</div>
</div>
@@ -0,0 +1,62 @@
@props([
'id' => null,
'label' => null,
'icon' => 'o-chevron-down',
'trigger' => null,
])
<details
x-data="{
dropdownOpen: false
}"
:open="dropdownOpen"
@click.outside="dropdownOpen = false"
@class(['dropdown'])
>
{{-- CUSTOM TRIGGER --}}
@if($trigger)
<summary x-ref="button" @click.prevent="dropdownOpen = !dropdownOpen" {{ $trigger->attributes->class(['list-none']) }}>
{{ $trigger }}
</summary>
@else
{{-- DEFAULT TRIGGER --}}
<summary
x-ref="button"
@click.prevent="dropdownOpen = !dropdownOpen"
{{ $attributes->class(["btn btn-ghost normal-case disabled:opacity-50 disabled:pointer-events-none"]) }}
>
{{ $label }}
<span class="transition-transform" :class="{'rotate-180': dropdownOpen }">
<x-ui.icon :name="$icon" />
</span>
</summary>
@endif
{{-- CONTENT --}}
<ul
@class([
'menu',
'absolute',
'top-0',
'p-2',
'shadow-lg',
'z-50',
'bg-base-100',
'rounded-box',
'w-auto',
'min-w-max',
])
x-anchor.bottom-start="$refs.button"
@click="dropdownOpen = false"
x-transition:enter="ease-out duration-200"
x-transition:enter-start="-translate-y-2"
x-transition:enter-end="translate-y-0"
x-cloak
>
<div wire:key="dropdown-slot-{{ $id }}">
{{ $slot }}
</div>
</ul>
</details>
@@ -0,0 +1,37 @@
@props([
'id' => null,
'title' => null,
'description' => null,
'icon' => 'o-x-circle',
'only' => []
])
<div>
@if ($errors->any())
<div {{ $attributes->class(["flex justify-start alert alert-error rounded rounded-md"]) }} >
<div class="grid gap-3">
<div class="flex gap-2">
@if($title)
<x-icon :name="$icon" class="w-6 h-6 mt-0.5" />
@endif
<div>
@if($title)
<div class="font-bold text-lg">{{ $title }}</div>
@endif
@if($description)
<div class="font-semibold">{{ $description }}</div>
@endif
</div>
</div>
<div>
<ul class="list-disc ms-3 space-y-2 sm:ms-6 pb-3">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
@endif
</div>
@@ -0,0 +1,122 @@
@props([
'id' => null,
'label' => null,
'hint' => null,
'hintClass' => 'label-text-alt text-gray-400 py-1 pb-0',
'multiple' => false,
'clearable' => true,
'hideProgress' => false,
'errorField' => null,
'errorClass' => 'text-red-500 label-text-alt p-1',
'omitError' => false,
'firstErrorOnly' => false,
])
@php
$modelName = $attributes->whereStartsWith('wire:model')->first();
$errorFieldName = $errorField ?? $modelName;
$id = $id == $modelName ? $modelName : "{$id}{$modelName}";
@endphp
<div
class="container"
x-data="{
files: @entangle($modelName),
progress: 0,
selectFiles(e) {
this.files = e.target.files[0].name
$wire.upload('{{ $modelName }}', e.target.files[0], (uploadedFilename) => {
// Success callback...
this.progress = 0;
}, () => {
// Error callback...
}, (event) => {
this.progress = event.detail.progress
}, () => {
// Cancelled callback...
})
},
reset(){
this.files = null
this.$refs.fileInput.value = null
}
}">
{{-- STANDARD LABEL --}}
@if($label)
<label for="{{ $id }}" class="pt-0 label label-text font-semibold">
<span>
{{ $label }}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
</span>
</label>
@endif
<div {{ $attributes->class(['relative']) }}>
{{-- PROGRESS BAR --}}
@if(!$hideProgress)
<progress
x-cloak
max="100"
:value="progress"
:class="{'hidden': !progress}"
class="progress h-1 absolute -mt-2 w-56">
</progress>
@endif
<input
type="file"
x-ref="fileInput"
id="{{ $id }}"
{{ $multiple ? 'multiple="true"' : '' }}
@change="selectFiles"
{{
$attributes->whereDoesntStartWith(['wire:model', 'class'])->class([
"file-input w-full",
"!file-input-error" => $errorFieldName && $errors->has($errorFieldName) && !$omitError
])
}}
>
@if($clearable)
<span :class="{'hidden': !files}">
<x-ui.button
type="reset"
@click="reset"
class="absolute top-2 right-2 btn btn-sm btn-ghost btn-circle"
icon="o-x-mark"
></x-ui.button>
</span>
@endif
</div>
{{-- ERROR --}}
@if(!$omitError && $errors->has($errorFieldName))
@foreach($errors->get($errorFieldName) as $message)
@foreach(Arr::wrap($message) as $line)
<div class="{{ $errorClass }}" x-classes="text-error">{{ $line }}</div>
@break($firstErrorOnly)
@endforeach
@break($firstErrorOnly)
@endforeach
@endif
{{-- MULTIPLE --}}
@error($modelName.'.*')
<div class="text-error" x-classes="text-error">{{ $message }}</div>
@enderror
{{-- HINT --}}
@if($hint)
<div class="{{ $hintClass }}" x-classes="fieldset-label">{{ $hint }}</div>
@endif
</div>
@@ -11,7 +11,7 @@
@if ($actions)
@if(!$noSeparator)
<x-section-border class="my-3" />
<x-ui.section-border class="my-3" />
@endif
<div class="flex justify-end gap-3">
@@ -0,0 +1,38 @@
@props([
'small' => false,
'percent' => null,
'costBasis' => null,
'marketValue' => null
])
@php
if (!is_null($percent)) {
$isUp = $percent > 0;
} else {
$isUp = $costBasis <= $marketValue;
$percent = $costBasis ? (($marketValue - $costBasis) / $costBasis) * 100 : 0;
}
@endphp
@if(!empty($percent))
<x-ui.badge
class="{{ $small ? 'badge-xs' : 'badge-sm' }} {{ $isUp ? 'badge-success' : 'badge-error' }} badge-outline ml-2"
title="{{ Number::percentage(
$percent,
$percent < 1 ? 2 : 0
) }}"
>
<x-slot:value>
{!! $isUp ? '&#9650;' :'&#9660;' !!}
{{ Number::percentage(
abs($percent),
($percent && $small) < 1 ? 2 : 0
) }}
</x-slot:value>
</x-ui.badge>
@endif
@@ -0,0 +1,33 @@
@props([
'id' => null,
'name' => null,
'label' => null,
])
@php
$name = Str::of($name);
$icon = $name->contains('.') ? $name->replace('.', '-') : "heroicon-{$name}";
// Remove `w-*` and `h-*` classes, because it applies only for icon
$labelClasses = Str::replaceMatches('/(w-\w*)|(h-\w*)/', '', $attributes->get('class') ?? '');
@endphp
@if(strlen($label ?? '') > 0)
<div class="inline-flex items-center gap-1">
@endif
<x-icon :name="$icon"
{{
$attributes->class([
'inline',
'w-5 h-5' => !Str::contains($attributes->get('class') ?? '', ['w-', 'h-'])
])
}}
/>
@if(strlen($label ?? '') > 0)
<div class="{{ $labelClasses }}">
{{ $label }}
</div>
</div>
@endif
@@ -0,0 +1,128 @@
@props([
'id' => null,
'label' => null,
'icon' => null,
'hint' => null,
'hintClass' => 'label-text-alt text-gray-400 py-1 pb-0',
'prefix' => null,
'suffix' => null,
'prepend' => null,
'append' => null,
'errorField' => null,
'errorClass' => 'text-red-500 label-text-alt p-1',
'omitError' => false,
'firstErrorOnly' => false,
])
@php
$modelName = $attributes->whereStartsWith('wire:model')->first();
$errorFieldName = $errorField ?? $modelName;
$id = $id == $modelName ? $modelName : "{$id}{$modelName}";
@endphp
<div>
{{-- STANDARD LABEL --}}
@if($label)
<label for="{{ $id }}" class="pt-0 label label-text font-semibold">
<span>
{{ $label }}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
</span>
</label>
@endif
{{-- PREFIX/SUFFIX/PREPEND/APPEND CONTAINER --}}
@if($prefix || $suffix || $prepend || $append)
<div class="flex">
@endif
{{-- PREFIX / PREPEND --}}
@if($prefix || $prepend)
<div
@class([
"rounded-s-lg flex items-center",
"border border-primary border-e-0 px-4" => $prefix,
"border-0" => $attributes->has('disabled') && $attributes->get('disabled') == true,
"border-dashed" => $attributes->has('readonly') && $attributes->get('readonly') == true,
"!border-error" => $errorFieldName && $errors->has($errorFieldName) && !$omitError
])
>
{{ $prepend ?? $prefix }}
</div>
@endif
<div class="flex-1 relative">
{{-- INPUT --}}
<input
id="{{ $id }}"
placeholder = "{{ $attributes->whereStartsWith('placeholder')->first() }} "
@if($attributes->has('autofocus') && $attributes->get('autofocus') == true)
autofocus
@endif
{{
$attributes
->merge(['type' => 'text'])
->class([
'input input-primary w-full peer',
'ps-10' => ($icon),
'rounded-s-none' => $prefix || $prepend,
'rounded-e-none' => $suffix || $append,
'border-e-0' => $suffix,
'border border-dashed' => $attributes->has('readonly') && $attributes->get('readonly') == true,
'input-error' => $errorFieldName && $errors->has($errorFieldName) && !$omitError
])
}}
/>
{{-- ICON --}}
@if($icon)
<x-ui.icon :name="$icon" class="z-60 absolute top-1/2 -translate-y-1/2 start-3 text-gray-400 pointer-events-none" />
@endif
</div>
{{-- SUFFIX/APPEND --}}
@if($suffix || $append)
<div
@class([
"rounded-e-lg flex items-center",
"border border-primary border-s-0" => $suffix,
"border-0" => $attributes->has('disabled') && $attributes->get('disabled') == true,
"border-dashed" => $attributes->has('readonly') && $attributes->get('readonly') == true,
"!border-error" => $errorFieldName && $errors->has($errorFieldName) && !$omitError
])
>
{{ $append ?? $suffix }}
</div>
@endif
{{-- END: PREFIX/SUFFIX/APPEND/PREPEND CONTAINER --}}
@if($prefix || $suffix || $prepend || $append)
</div>
@endif
{{-- ERROR --}}
@if(!$omitError && $errors->has($errorFieldName))
@foreach($errors->get($errorFieldName) as $message)
@foreach(Arr::wrap($message) as $line)
<div class="{{ $errorClass }}" x-classes="text-red-500 label-text-alt p-1">{{ $line }}</div>
@break($firstErrorOnly)
@endforeach
@break($firstErrorOnly)
@endforeach
@endif
{{-- HINT --}}
@if($hint)
<div class="{{ $hintClass }}" x-classes="label-text-alt text-gray-400 py-1 pb-0">{{ $hint }}</div>
@endif
</div>
@@ -0,0 +1,91 @@
@props([
'id' => null,
'item' => array(),
'avatar' => 'avatar',
'value' => 'name',
'subValue' => '',
'noSeparator' => false,
'noHover' => false,
'link' => null,
'actions' => null,
])
<div wire:key="{{ $id }}">
<div
{{ $attributes->class([
"flex justify-start items-center gap-4 px-3",
"hover:bg-base-200/50" => !$noHover,
"cursor-pointer" => $link
])
}}
>
@if($link && (data_get($item, $avatar) || !is_string($avatar)))
<div>
<a href="{{ $link }}" wire:navigate>
@endif
{{-- AVATAR --}}
@if(data_get($item, $avatar))
<div class="py-3">
<div class="avatar">
<div class="w-11 rounded-full">
<img src="{{ data_get($item, $avatar) }}" />
</div>
</div>
</div>
@endif
@if(!is_string($avatar))
<div {{ $avatar->attributes->class(["py-3"]) }}>
{{ $avatar }}
</div>
@endif
@if($link && (data_get($item, $avatar) || !is_string($avatar)))
</a>
</div>
@endif
{{-- CONTENT --}}
<div class="flex-1 overflow-hidden whitespace-nowrap text-ellipsis truncate w-0">
@if($link)
<a href="{{ $link }}" wire:navigate>
@endif
<div class="py-3">
<div @if(!is_string($value)) {{ $value->attributes->class(["font-semibold truncate"]) }} @else class="font-semibold truncate" @endif>
{{ is_string($value) ? data_get($item, $value) : $value }}
</div>
<div @if(!is_string($subValue)) {{ $subValue->attributes->class(["text-gray-400 text-sm truncate"]) }} @else class="text-gray-400 text-sm truncate" @endif>
{{ is_string($subValue) ? data_get($item, $subValue) : $subValue }}
</div>
</div>
@if($link)
</a>
@endif
</div>
{{-- ACTION --}}
@if($actions)
@if($link && !Str::of($actions)->contains([':click', '@click' , 'href']))
<a href="{{ $link }}" wire:navigate>
@endif
<div {{ $actions->attributes->class(["py-3 flex items-center gap-3"]) }}>
{{ $actions }}
</div>
@if($link && !Str::of($actions)->contains([':click', '@click' , 'href']))
</a>
@endif
@endif
</div>
@if(!$noSeparator)
<hr class="border-base-300"/>
@endif
</div>
@@ -0,0 +1 @@
<span {{ $attributes->class("loading loading-spinner") }}></span>
@@ -0,0 +1,48 @@
<a href="{{ route('dashboard') }}" title="Investbrain" alt="Investbrain Logo">
<svg width="100%" height="100%" id="Layer_1" class="fill-current" data-name="Layer 1" viewBox="0 0 1001 783" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" >
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M553.875,632.571L567.884,627.131C567.884,627.131 588.94,642.044 611.341,650.542C660.03,669.007 666.181,693.68 670.67,711.697C671.541,715.201 672.368,718.512 673.431,721.293C679.103,736.17 685.326,746.904 694.882,758.003L737.893,737.748C730.866,729.455 721.087,714.1 721.273,693.007C721.419,676.837 731.456,663.936 740.313,652.55C749.261,641.048 756.99,631.115 754.689,619.792C754.428,618.501 750.205,606.681 683.457,589.378C664.971,584.588 632.955,577.931 632.955,577.931C632.955,577.931 635.967,564.803 636.504,564.91C650.287,567.669 668.765,571.64 687.293,576.443C757.295,594.586 767.837,608.754 769.682,617.83C773.076,634.571 762.843,647.722 752.948,660.444C744.551,671.243 736.616,681.44 736.507,693.555C736.275,719.609 754.703,734.781 754.889,734.933L762.945,741.43L691.292,775.182L687.22,770.803C674.012,756.601 666.101,743.826 659.014,725.231C657.68,721.736 656.764,718.071 655.801,714.191C651.633,697.476 641.744,677.506 600.566,661.887C573.409,651.585 553.875,632.571 553.875,632.571Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M469.894,617.03C491.9,625.608 537.785,632.498 578.066,616.912C606.757,605.811 625.69,585.647 634.343,556.967L635.932,544.895L650.678,549.582L650.547,550.556L649.213,560.348C639.567,592.821 617.208,616.664 584.546,629.301C557.593,639.728 525.875,641.415 498.98,637.874C484.116,635.917 475.069,632.909 464.77,628.35L469.894,617.03Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M756.363,647.659C766.597,652.226 735.904,647.812 749.88,647.831C752.018,647.834 754.181,647.777 756.363,647.659ZM756.363,647.659L759.121,630.608C776.312,645.1 814.041,614.388 822.007,607.977C847.271,587.646 859.429,573.432 865.582,531.56L857.892,525.97L871.854,519.291L871.902,520.701L871.646,533.167C868.374,581.138 854.724,595.681 826.924,619.726C805.922,637.888 779.998,646.378 756.363,647.659Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M278.425,160.523C277.934,160.447 277.438,160.37 276.948,160.286C277.44,160.359 277.93,160.439 278.425,160.523Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M56.427,281.384L53.276,278.254C53.915,253.987 64.477,230.809 81.468,211.975C77.818,216.38 74.378,221.228 71.199,226.564C59.438,246.327 55.735,264.917 56.427,281.384ZM125.55,179.848C146.913,170.054 171.349,165.376 196.488,168.027C196.773,169.34 196.992,170.047 196.992,170.047L196.819,170.025C194.125,169.67 160.165,165.575 125.55,179.848ZM876.465,148.788C875.253,140.191 872.604,131.249 868.55,122.317C872.79,131.336 875.507,140.302 876.465,148.788ZM636.968,67.078C632.455,59.767 626.37,52.471 618.591,45.74C627.058,52.458 633.412,59.75 636.968,67.078ZM830.233,73.639C814.235,60.456 794.248,49.212 770.413,41.637C761.569,38.826 752.892,36.944 744.475,35.836C730.805,34.037 717.803,34.272 705.832,35.868C719.737,33.548 733.39,33.475 746.563,35.21C778.577,39.424 807.696,54.337 830.233,73.639Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M546.485,261.415C546.455,261.429 546.424,261.444 546.394,261.458C546.389,261.461 546.389,261.461 546.385,261.46L546.485,261.415ZM546.485,261.415C547.832,260.79 549.176,260.186 550.513,259.608L546.485,261.415Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M951.448,338.201C967.066,346.748 978.94,360.259 984.357,371.461C991.028,385.267 1009.88,464.746 938.38,509.182C874.197,549.074 824.465,524.364 805.506,511.387C784.609,543.131 735.375,571.199 677.333,565.042C620.575,558.985 591.016,530.312 576.286,507.76C561.52,528.554 541.685,537.599 526.615,541.516C511.159,545.534 494.383,545.742 479.342,542.424C490.897,565.908 498.729,604.571 467.806,641.145C445.634,667.37 414.398,675.189 392.099,677.128C370.311,679.025 349.702,675.811 337.482,671.143C324.069,682.794 288.704,704.331 243.88,698.43C233.085,697.009 221.742,694 210.023,688.874C158.439,666.305 147.126,623.344 154.77,594.547C111.783,593.541 77.23,581.082 51.934,557.44C20.86,528.397 4.976,481.26 9.445,431.35C14.204,378.198 55.606,354.896 74.754,346.889C60.508,328.89 30.467,280.339 64.437,223.27C98.829,165.496 163.458,161.805 188.094,162.668C186.255,144.263 189.011,101.243 245.926,69.359C313.249,31.644 373.035,54.386 397.781,70.781C414.353,45.661 452.387,10.329 510.461,7.925C585.849,4.8 622.364,35.827 637.949,55.854C660.603,36.887 713.288,16.387 772.778,35.297C839.209,56.412 875.616,104.13 883.278,143.714C947.161,162.729 985.435,206.49 988.544,264.393C990.859,307.537 971.373,327.501 951.448,338.201ZM929.36,498.056C991.957,459.153 975.932,387.913 970.382,376.422C965.636,366.6 951.481,350.075 931.943,344.8L911.518,339.288L930.807,332.122C953.179,323.814 975.727,309.291 973.33,264.596C971.652,233.374 956.575,177.653 874.439,155.23L869.539,153.891L868.907,149.406C863.81,113.255 830.649,67.872 768.052,47.976C704.505,27.778 653.597,58.332 643.204,71.132L636.222,79.733L630.312,70.164C620.597,54.426 589.582,18.173 511.669,21.398C440.094,24.362 408.415,81.914 407.103,84.363L402.557,92.834L394.883,85.976C379.908,72.608 321.954,42.965 254.369,80.828C189.472,117.183 204.179,167.969 204.337,168.478L207.359,178.33L196.014,176.705C192.739,176.258 115.819,166.267 77.965,229.863C39.999,293.638 91.82,344.913 92.351,345.421L100.424,353.227L89.31,356.328C86.907,357.008 29.915,373.837 24.63,432.853C20.503,478.949 34.782,522.122 62.831,548.336C87.022,570.947 121.342,581.965 164.831,581.071L176.905,580.826L172.295,590.588C161.421,613.62 167.914,655.605 216.409,676.818C274.503,702.221 322.493,666.638 329.535,658.577L333.699,653.813L339.597,657.183C352.785,664.72 419.879,674.98 455.506,632.841C490.702,591.211 468.011,546.744 456.297,533.124L432.423,505.365L466.09,523.416C481.09,531.455 502.99,533.478 521.89,528.564C536.577,524.747 556.747,514.997 569.395,490.245L576.355,476.635L583.311,490.646C593.267,510.709 618.898,545.364 678.656,551.649C737.765,557.918 782.891,523.943 796.055,497.816L800.455,489.081L808.318,496.037C816.887,503.614 862.973,539.314 929.36,498.056Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M164.45,581.093C168.821,580.604 195.946,573.542 211.278,553.997C221.78,540.606 224.635,523.998 219.76,504.639C219.76,504.639 247.605,532.462 221.626,564.658C202.884,586.185 171.934,594.399 165.717,594.554L164.791,581.073C164.703,581.077 164.591,581.078 164.45,581.093ZM164.45,581.093C164.448,581.093 164.447,581.094 164.445,581.094C164.447,581.094 164.448,581.093 164.45,581.093Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M213.575,553.555C222.582,551.317 231.34,551.047 239.325,552.226C255.233,554.572 268.058,562.655 273.619,572.298C273.619,572.298 245.37,554.286 218.136,566.552L213.575,553.555Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M310.41,312.188C362.408,280.533 430.339,286.541 475.605,326.802L464.987,336.16C425.111,300.696 365.213,295.443 319.316,323.38C275.107,350.295 256.595,417.442 280.48,464.26C307.052,516.356 357.867,539.991 394.332,536.42C394.332,536.42 358.776,552.43 324.184,531.648C299.807,517 279.907,495.504 266.63,469.474C239.543,416.376 259.999,342.877 310.41,312.188Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M330.494,520.536C338.379,526.429 342.731,534.458 344.7,543.023C348.021,570.08 326.463,580.957 326.463,580.957C333.323,569.603 338.064,541.076 322.267,529.275L330.494,520.536Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M170.498,451.168C205.064,437.661 243.653,440.477 276.359,458.882L268.492,470.196C240.229,454.292 206.893,451.865 177.027,463.541C153.301,472.81 139.19,488.122 137.509,498.456C137.509,498.456 134.627,468.393 170.498,451.168Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M122.051,418.399C124.266,425.291 153.678,451.572 184.056,447.87L186.251,459.326C180.771,459.993 175.279,459.883 169.892,459.174C162.413,458.189 155.13,456.047 148.357,453.205C119.151,440.422 122.051,418.399 122.051,418.399Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M83.89,343.578C93.152,340.443 119.954,335.902 149.393,342.543C189.622,353.407 200.664,388.305 200.664,388.305C157.717,337.275 90.386,355.973 89.732,356.197L83.89,343.578Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M403.057,462.599C412.109,479.736 430.231,504.115 465.183,522.924L466.14,523.442L458.465,534.863L457.586,534.391C419.225,513.745 399.276,486.879 389.295,467.98C380.508,451.336 378.406,434.828 379.826,419.97C384.383,385.186 414.727,373.954 414.727,373.954C402.573,387.065 383.727,425.985 403.057,462.599Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M322.397,408.686C322.397,408.686 360.323,391.713 392.009,422.14L382.306,429.591C369.382,416.512 339.485,407.613 322.397,408.686Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M690.853,274.028C735.995,311.348 753.013,373.417 740.587,407.967C734.08,426.056 723.599,439.419 712.073,449.263C685.57,471.905 649.16,462.799 649.16,462.799C651.515,462.516 707.46,455.197 726.054,403.494C736.835,373.524 720.838,316.972 680.654,283.744C656.633,263.884 614.846,244.873 552.154,267.086C489.758,289.191 475.391,326.522 474.322,353.951C472.725,395.045 499.768,434.915 514.438,442.054C514.438,442.054 481.579,436.735 467.588,400.693C461.928,385.953 458.458,369.521 459.1,353.048C460.315,321.737 476.396,279.231 546.1,254.536C616.21,229.702 663.52,251.425 690.853,274.028Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M574.041,405.452C577.929,399.868 582.591,394.925 588.017,390.626C622.566,366.135 666.154,382.688 666.154,382.688C627.495,382.845 600.885,392.975 587.077,412.802C566.225,442.734 580.371,485.117 583.223,490.479L569.482,495.902C565.551,488.513 549.669,440.434 574.041,405.452Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M586.976,325.57C586.976,325.57 612.868,328.978 618.672,355.583C621.052,372.019 615.594,386.416 603.3,396.116L594.44,387.377C606.107,378.168 607.013,365.603 605.724,356.686C603.595,342.023 594.518,329.351 586.976,325.57Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M708.183,440.658C738.696,458.976 721.687,492.93 721.687,492.93C725.772,471.083 707.178,453.368 701.816,450.565L708.183,440.658Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M793.227,434.541L805.895,427.418C827.426,456.566 813.55,497.025 809.929,503.784L796.126,497.678C798.539,493.179 810.551,457.997 793.227,434.541Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M843.106,432.778C872.723,432.517 895.844,420.562 906.531,399.983C906.531,399.983 907.929,428.423 880.172,440.043C869.358,444.009 857.092,446.148 843.754,446.269C837.448,446.323 831.171,445.926 825.002,445.114C790.398,440.559 759.118,422.884 743.733,398.136C712.965,348.644 746.651,293.716 782.802,273.093C802.223,262.012 823.22,255.668 842.44,254.262C876.032,253.772 883.4,276.414 883.4,276.414C864.944,262.029 824.51,265.578 791.345,284.501C760.84,301.905 731.064,350.173 757.023,391.933C772.169,416.297 807.583,433.1 843.106,432.778Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M901.023,427.592C919.082,437.647 914.375,459.502 914.375,459.502C914.382,455.074 911.352,449.929 905.845,445.02C898.256,438.251 889.946,435.226 888.088,435.21C888.281,435.213 882.328,424.936 882.328,424.936C886.457,422.572 892.743,423.469 901.023,427.592Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M841.501,320.927C869.016,346.791 923.886,334.774 930.727,332.154L937.161,344.56C930.833,346.984 903.788,353.308 876.093,349.661C844.437,342.756 841.501,320.927 841.501,320.927Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M856.8,381.78C856.8,381.78 854.677,356.846 873.976,346.331C881.356,342.487 889.96,339.571 899.7,338.12L902.276,349.522C873.542,353.801 859.967,372.495 856.8,381.78Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M609.849,376.237C641.704,348.782 649.339,320.22 649.339,320.22C651.29,328.519 647.387,350.181 634.449,367.575C629.624,374.059 623.544,379.948 616.045,384.331L609.849,376.237Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M834.92,234.947C840.865,227.575 848.327,221.775 857.035,217.633C881.755,204.76 908.313,222.518 908.313,222.518C888.901,219.963 862.264,221.245 845.624,241.87L834.92,234.947Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M831.928,192.93C831.928,192.93 855.516,211.969 848.036,239.116C840.815,259.861 823.515,271.361 810.178,275.615L804.608,262.904C806.453,262.317 849.598,247.868 831.928,192.93Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M730.919,99.579C730.919,99.579 763.085,113.201 772.811,151.075C776.7,170.409 776.278,191.455 771.605,214.455C761.626,263.558 702.305,281.445 679.251,280.632L679.359,267.156C693.228,267.644 747.798,255.006 756.611,211.647C765.902,165.932 757.739,130.323 730.919,99.579Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M762.546,160.453C800.164,134.33 864.452,138.777 878.343,142.312L874.583,155.268C864.309,152.659 804.878,148.468 772.188,171.172L762.546,160.453Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M630.258,70.076L643.679,64.083C644.1,64.79 673.076,114.431 662.623,161.021C654.783,197.124 613.774,203.603 613.774,203.603C683.262,160.803 630.799,70.977 630.258,70.076Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M659.485,165.881C663.907,186.681 682.077,207.853 695.622,213.847C695.622,213.847 651.903,202.29 646.659,167.659L659.485,165.881Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M539.576,186.699C541.122,210.599 554.444,240.07 576.078,247.622L570.893,260.15C542.825,250.352 526.242,215.953 524.367,187.035C523.494,173.554 526.287,151.23 536.041,131.967C553.129,100.656 581.582,110.001 581.582,110.001C548.902,118.029 538.098,163.918 539.576,186.699Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M493.292,103.391C493.292,103.391 525.2,108.011 535.526,135.513C538.424,143.235 540.253,152.053 540.204,161.981L527.145,161.553C527.336,122.786 493.632,103.579 493.292,103.391Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M276.948,160.286C277.399,160.303 311.64,161.895 336.502,187.86C344.788,196.516 352.036,207.878 356.639,222.792C374.898,281.945 349.413,306.144 319.144,323.482C309.497,329.013 295.604,330.674 280.619,328.701C269.215,327.2 257.176,323.595 245.876,317.989C231.133,310.674 218.952,300.706 209.701,288.689C189.323,255.458 204.276,225.968 204.276,225.968C204.379,274.859 234.842,297.268 252.991,306.273C276.57,317.97 300.259,318.002 310.581,312.087C335.503,297.81 358.45,279.395 341.951,225.95C326.56,176.089 276.948,160.286 276.948,160.286Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M351.881,137.831C351.881,137.831 334.475,159.829 340.948,195.549L328.057,197.016C320.399,154.765 351.881,137.831 351.881,137.831Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M153.949,295.541C153.949,295.541 171.351,271.232 201.075,276.773C207.935,278.053 215.116,280.367 222.406,284.126L216.118,294.07C184.328,277.698 154.249,295.363 153.949,295.541Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M197.109,163.312C203.351,163.568 225.129,169.866 245.94,182.735C292.963,211.559 273.952,244.742 273.952,244.742C272.306,198.284 202.893,177.207 196.827,176.78L197.109,163.312Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M405.259,76.415C409.176,79.606 443.528,109.023 443.781,158.317C444.017,204.496 406.987,221.615 406.987,221.615C407.205,221.492 428.805,207.829 428.547,157.937C428.324,114.494 398.54,88.97 395.144,86.2L405.259,76.415Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M442.447,165.707C445.65,183.47 472.051,205.798 482.678,207.829C482.678,207.829 438.374,205.199 429.554,167.162L442.447,165.707Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M528.269,208.508L543.276,206.633C543.59,208.225 550.613,245.979 521.528,275.478C495.28,302.101 458.033,314.248 418.869,309.091C417.654,308.932 416.445,308.757 415.226,308.561C378.701,302.768 354.675,288.29 347.06,276.512L360.223,270.105C364.706,277.043 384.206,290.018 417.431,295.288C452.627,300.856 486.324,290.348 509.913,266.423C534.272,241.715 528.331,208.838 528.269,208.508Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M346.454,612.287C346.454,612.287 364.459,630.137 350.49,655.895C346.095,662.684 342.024,666.615 341.627,666.989L330.334,657.619C330.525,657.439 349.385,639.196 346.454,612.287Z"/>
<path {{ $attributes->merge(['class' => 'text-accent']) }} d="M746.169,21.499C756.017,22.795 765.878,25.001 775.478,28.052C846.582,50.652 881.597,100.292 890.828,137.976C955.354,158.86 994.038,204.652 997.236,264.281C999.012,297.296 988.628,322.18 966.359,338.423C979.244,347.991 988.032,359.715 992.343,368.627C996.839,377.939 1002.72,402.821 999.332,429.929C996.187,455.119 983.953,490.419 943.534,515.538C921.02,529.53 898.079,537.216 875.214,538.444C874.524,557.481 866.669,577.821 852.635,596.528C833.067,622.623 803.096,642.513 769.991,651.49C766.899,656.306 763.357,660.861 760.17,664.955C752.191,675.212 745.299,684.072 745.21,693.867C745.015,715.994 760.064,728.832 760.639,729.311L768.69,735.812C770.809,737.518 771.894,740.002 771.599,742.476C771.308,744.951 769.679,747.105 767.227,748.26L695.574,782.012C693.919,782.79 692.07,783.039 690.288,782.805C688.153,782.524 686.117,781.546 684.646,779.964L680.573,775.585C666.603,760.562 658.245,747.077 650.775,727.481C649.287,723.582 648.322,719.717 647.301,715.615C643.246,699.342 639.415,683.975 602.531,669.984C582.529,662.395 566.583,652.959 555.043,641.893C536.815,644.647 517.346,644.794 498.397,642.299C492.15,641.477 486.064,640.365 480.225,638.984C478.528,641.351 476.724,643.657 474.834,645.894C450.844,674.272 417.216,682.715 393.226,684.799C381.934,685.782 370.071,685.527 358.921,684.059C351.917,683.137 345.364,681.766 339.649,680.041C319.088,696.008 283.075,711.345 242.877,706.053C230.564,704.432 218.283,700.97 206.372,695.761C181.07,684.694 162.338,667.791 152.203,646.89C145.306,632.663 142.634,616.673 144.507,601.785C139.806,601.472 135.2,601.017 130.725,600.428C96.286,595.894 67.686,583.183 45.707,562.646C12.903,531.987 -3.897,482.581 0.767,430.49C5.257,380.339 40.511,354.902 62.285,343.855C46.313,320.624 24.457,273.681 56.708,219.503C89.53,164.361 147.812,155.406 179.02,154.885C179.027,132.85 187.435,92.873 241.101,62.81C273.272,44.788 306.984,37.939 341.311,42.462C365.525,45.65 384.1,53.896 395.041,59.951C402.448,50.197 414.871,36.551 433.253,24.573C456.287,9.566 482.031,1.374 509.768,0.225C523.796,-0.356 537.288,0.189 549.873,1.846C595.648,7.874 623.192,27.2 639.177,44.655C661.749,29.219 700.506,15.487 746.169,21.499ZM745.165,29.122C697.38,22.831 657.024,39.885 637.949,55.854C625.287,39.581 598.795,16.044 548.865,9.469C537.348,7.953 524.59,7.339 510.461,7.925C452.387,10.329 414.353,45.661 397.781,70.781C385.798,62.844 365.594,53.414 340.307,50.085C313.364,46.538 280.651,49.905 245.926,69.359C189.011,101.247 186.254,144.267 188.094,162.668C163.458,161.805 98.829,165.496 64.437,223.27C30.466,280.339 60.508,328.89 74.754,346.889C55.606,354.896 14.204,378.198 9.445,431.35C4.976,481.26 20.86,528.397 51.934,557.44C72.538,576.699 99.287,588.534 131.729,592.805C139.117,593.778 146.795,594.358 154.77,594.547C147.126,623.344 158.439,666.305 210.023,688.874C221.738,693.999 233.085,697.009 243.88,698.43C288.704,704.331 324.069,682.794 337.482,671.143C343.311,673.373 351.058,675.269 359.925,676.436C369.639,677.715 380.709,678.122 392.095,677.127C414.398,675.189 445.634,667.37 467.806,641.145C470.947,637.429 473.669,633.693 476.048,629.954C483.187,631.956 491.044,633.576 499.401,634.676C517.749,637.092 538.341,637.065 558.132,633.497C566.971,643.025 581.569,653.745 605.736,662.914C646.914,678.533 651.633,697.476 655.797,714.19C656.764,718.071 657.68,721.736 659.01,725.23C666.101,743.826 674.012,756.601 687.22,770.803L691.292,775.182L762.945,741.43L754.889,734.933C754.703,734.781 736.275,719.609 736.507,693.555C736.616,681.44 744.547,671.243 752.948,660.444C756.865,655.411 760.818,650.309 763.97,644.946C800.832,635.856 828.982,613.927 845.338,592.12C860.499,571.901 867.745,550.109 866.404,530.89C873.884,531.027 881.855,530.414 890.261,528.779C905.055,525.902 921.196,519.861 938.38,509.182C1009.87,464.746 991.028,385.267 984.357,371.461C978.94,360.259 967.066,346.748 951.448,338.201C971.373,327.501 990.859,307.537 988.544,264.393C985.435,206.49 947.161,162.729 883.274,143.713C875.616,104.13 839.209,56.412 772.778,35.297C763.372,32.308 754.136,30.303 745.165,29.122Z"/>
</svg>
<span class="sr-only">Investbrain</span>
</a>
@@ -0,0 +1,79 @@
@props([
'id' => null,
'title' => null,
'icon' => null,
'spinner' => null,
'link' => null,
'route' => null,
'external' => false,
'noWireNavigate' => false,
'badge' => null,
'badgeClasses' => null,
'badge' => false,
'separator' => false,
'enabled' => true,
])
@aware(['activateByRoute' => false, 'activeBgColor' => 'bg-neutral text-neutral-content'])
@php
$spinnerTarget = $spinner == true ? $attributes->whereStartsWith('wire:click')->first() : $spinner;
@endphp
@if (!$enabled)
{{-- DISABLED --}}
@else
{{-- ENABLED --}}
<li
title="{{ $title }}"
{{ $attributes->class(["my-0.5 hover:text-inherit rounded-md"]) }}
>
<a
@if($link)
href="{{ $link }}"
@if($activateByRoute)
wire:current="{{ $activeBgColor }}"
@endif
@if($external)
target="_blank"
@endif
@if(!$external && !$noWireNavigate)
{{ $attributes->wire('navigate')->value() ? $attributes->wire('navigate') : 'wire:navigate' }}
@endif
@endif
@if($spinner)
wire:target="{{ $spinnerTarget }}"
wire:loading.attr="disabled"
@endif
>
{{-- SPINNER --}}
@if($spinner)
<span wire:loading wire:target="{{ $spinnerTarget }}" class="loading loading-spinner w-5 h-5"></span>
@endif
@if($icon)
<span class="block -mt-0.5" @if($spinner) wire:loading.class="hidden" wire:target="{{ $spinnerTarget }}" @endif>
<x-ui.icon :name="$icon" />
</span>
@endif
@if($title || $slot->isNotEmpty())
<span class="whitespace-nowrap">
@if($title)
{{ $title }}
@if($badge)
<span class="badge badge-sm ml-2 {{ $badgeClasses }}">{{ $badge }}</span>
@endif
@else
{{ $slot }}
@endif
</span>
@endif
</a>
</li>
@endif
@@ -0,0 +1,28 @@
@props([
'title' => null,
'icon' => null,
'separator' => false,
'activateByRoute' => false,
'activeBgColor' => 'bg-base-100'
])
<ul {{ $attributes->class(["menu rounded-md"]) }} >
@if($title)
<li class="menu-title text-inherit uppercase">
<div class="flex items-center gap-2">
@if($icon)
<x-ui.icon :name="$icon" class="w-4 h-4 inline-flex" />
@endif
{{ $title }}
</div>
</li>
@endif
@if($separator)
<hr class="mb-3"/>
@endif
{{ $slot }}
</ul>
@@ -0,0 +1,116 @@
@props([
'key' => 'modal',
'title' => null,
'subtitle' => null,
'persistent' => false,
'withoutTrapFocus' => false,
'boxClass' => '',
'noCard' => false,
'shortcut' => null,
'noTeleport' => false,
])
@if(!$noTeleport)
<template x-teleport="body">
@endif
<dialog
x-data="{
@if (!empty($attributes->whereStartsWith('wire:model')->first()))
init(){
this.$watch('wireModelValue', value => value ? this.show() : this.close())
},
wireModelValue: $wire.entangle('{{ $attributes->whereStartsWith('wire:model')->first() }}').live,
@endif
open: false,
close() {
this.open = false;
this.$el.close()
},
cancel() {
@if($persistent)
this.$refs.modalContent.classList.add('wiggle')
this.$refs.modalContent.addEventListener('animationend', (e) => {
this.$refs.modalContent.classList.remove('wiggle')
})
@else
this.close()
@endif
},
show() {
this.open = true;
@if($persistent)
this.$el.showModal();
@else
this.$el.show();
@endif
}
}"
@close="close()"
:open="open"
{{
$attributes->filter(
fn ($value, $key) => !Str::startsWith($key, 'wire:model')
)->class(["modal duration-50 z-50"])
}}
id="{{ $key }}"
x-on:toggle-{{ $key }}.window="open ? close() : show();"
@if($shortcut)
@keydown.window.prevent.{{ $shortcut }}="show();"
@endif
@keydown.escape.prevent.stop="cancel()"
@if(!$withoutTrapFocus)
x-trap="open"
x-bind:inert="!open"
@endif
>
{{-- BACKDROP --}}
<div
@click.prevent.stop="cancel()"
class="absolute inset-0 w-full h-full bg-base-300/50"
x-show="open"
></div>
{{-- MODAL CONTENT --}}
<div x-ref="modalContent" class="modal-box overflow-y-visible p-0 {{ $boxClass }}">
@if(!$noCard)
<x-ui.card
:title="$title"
:subtitle="$subtitle"
expanded="true"
>
@if (!$persistent && !$noCard)
<x-ui.button
icon="o-x-mark"
title="{{ __('Close') }}"
class="absolute top-4 right-4 btn-ghost btn-circle btn-sm z-10"
@click="close()"
tabindex="-999"
/>
@endif
{{ $slot }}
</x-ui.card>
@else
{{ $slot }}
@endif
</div>
</dialog>
@if(!$noTeleport)
</template>
@endif
@@ -0,0 +1,14 @@
@props([
'value' => 0,
'max' => 100,
'indeterminate' => null,
])
<progress
{{ $attributes->class("progress") }}
@if(!$indeterminate)
value="{{ $value }}"
max="{{ $max }}"
@endif
></progress>
@@ -1,5 +1,5 @@
<div {{ $attributes->class(['my-6' => !$attributes->has('class'), 'h-4 sm:h-auto' => $attributes->has('hide-on-mobile')]) }}>
<hr class="{{ $attributes->has('hide-on-mobile') ? 'hidden sm:block' : '' }} border-t border-gray-200 dark:border-gray-700" />
<hr class="{{ $attributes->has('hide-on-mobile') ? 'hidden sm:block' : '' }} border-t border-gray-200/50 dark:border-gray-700/50" />
</div>
@@ -0,0 +1,112 @@
@props([
'id' => null,
'label' => null,
'icon' => null,
'hint' => null,
'hintClass' => 'label-text-alt text-gray-400 ps-1 mt-2',
'placeholder' => null,
'optionValue' => 'id',
'optionLabel' => 'name',
'options' => array(),
'prepend' => null,
'append' => null,
'errorField' => null,
'errorClass' => 'text-red-500 label-text-alt p-1',
'omitError' => false,
'firstErrorOnly' => false,
])
@php
$modelName = $attributes->whereStartsWith('wire:model')->first();
$errorFieldName = $errorField ?? $modelName;
$id = $id == $modelName ? $modelName : "{$id}{$modelName}";
@endphp
<div>
{{-- STANDARD LABEL --}}
@if($label)
<label for="{{ $id }}" class="pt-0 label label-text font-semibold">
<span>
{{ $label }}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
</span>
</label>
@endif
{{-- PREPEND/APPEND CONTAINER --}}
@if($prepend || $append)
<div class="flex">
@endif
{{-- PREPEND --}}
@if($prepend)
<div class="rounded-s-lg flex items-center bg-base-200">
{{ $prepend }}
</div>
@endif
<div class="relative flex-1">
<select
id="{{ $id }}"
{{ $attributes->whereDoesntStartWith('class') }}
{{ $attributes->class([
'select select-primary w-full font-normal',
'ps-10' => ($icon),
'rounded-s-none' => $prepend,
'rounded-e-none' => $append,
'border border-dashed' => $attributes->has('readonly') && $attributes->get('readonly') == true,
'select-error' => $errors->has($errorFieldName)
])
}}
>
@if($placeholder)
<option value="">{{ $placeholder }}</option>
@endif
@foreach ($options as $option)
<option value="{{ data_get($option, $optionValue) }}" @if(data_get($option, 'disabled')) disabled @endif>{{ data_get($option, $optionLabel) }}</option>
@endforeach
</select>
{{-- ICON --}}
@if($icon)
<x-ui.icon :name="$icon" class="z-60 absolute pointer-events-none top-1/2 -translate-y-1/2 start-3 text-gray-400" />
@endif
</div>
{{-- APPEND --}}
@if($append)
<div class="rounded-e-lg flex items-center bg-base-200">
{{ $append }}
</div>
@endif
{{-- END: APPEND/PREPEND CONTAINER --}}
@if($prepend || $append)
</div>
@endif
{{-- ERROR --}}
@if(!$omitError && $errors->has($errorFieldName))
@foreach($errors->get($errorFieldName) as $message)
@foreach(Arr::wrap($message) as $line)
<div class="text-red-500 label-text-alt p-1" x-classes="text-red-500 label-text-alt p-1">{{ $line }}</div>
@break($firstErrorOnly)
@endforeach
@break($firstErrorOnly)
@endforeach
@endif
{{-- HINT --}}
@if($hint)
<div class="{{ $hintClass }}" x-classes="label-text-alt text-gray-400 ps-1 mt-2">{{ $hint }}</div>
@endif
</div>
@@ -0,0 +1,147 @@
@props([
'id' => null,
'shortcut' => "meta.g",
'searchText' => "Search ...",
'noResultsText' => "Nothing found.",
'url' => null,
'fallbackAvatar' => null,
])
@php
$url = $url ?? route('spotlight', absolute: false);
@endphp
<div x-data="{
loading: false,
value: '',
results: [],
maxDebounce: 250,
debounceTimer: null,
controller: new AbortController(),
query: '',
searchedWithNoResults: false,
init(){
this.$watch('value', value => {
this.loading = true
this.debounce(() => this.search(), this.maxDebounce)
})
},
debounce(fn, waitTime) {
clearTimeout(this.debounceTimer)
this.debounceTimer = setTimeout(() => fn(), waitTime)
},
async search() {
if (this.value == '') {
this.results = [];
this.loading = false
return
}
try {
this.controller?.abort()
this.controller = new AbortController();
let response = await fetch(`{{$url}}?search=${this.value}&${this.query}`, { signal: this.controller.signal })
this.results = await response.json()
} catch(e) {
console.log(e)
return
}
this.loading = false
Object.keys(this.results).length
? this.searchedWithNoResults = false
: this.searchedWithNoResults = true
}
}">
<x-ui.modal
key="spotlight"
class="backdrop-blur-sm shadow-xl"
box-class="absolute top-10 lg:top-24 w-full lg:max-w-3xl "
no-card="true"
shortcut="slash"
@keydown.up="$focus.wrap().previous()"
@keydown.down="$focus.wrap().next()"
>
<div class="relative">
{{-- CLOSE --}}
<x-ui.button
title="{{ __('Close') }} (esc)"
class="absolute top-1/2 -translate-y-1/2 right-4 btn btn-ghost hover:bg-transparent border-none shadow-none btn-xs select-none z-50"
@click="close()"
@focus="$focus.lastFocused().focus()"
>
<kbd class="kbd kbd-sm">ESC</kbd>
</x-ui.button>
{{-- INPUT --}}
<x-ui.input
id="{{ $id }}"
icon="o-magnifying-glass"
x-model="value"
placeholder=" {{ $searchText }}"
class="text-xl flex w-full input my-2 py-6 border-none outline-none shadow-none border-transparent focus:shadow-none focus:outline-none focus:border-transparent"
@focus="$el.focus()"
autofocus
tabindex="1"
/>
{{-- PROGRESS --}}
<x-ui.progress
x-show="loading"
class="z-60 absolute left-0 bottom-0 w-full progress progress-secondary h-[2px]"
indeterminate="true"
/>
</div>
{{-- NO RESULTS --}}
<template x-if="searchedWithNoResults && value != ''">
<div class="bg-base-100 text-base-content/50 p-4 spotlight-element">{{ $noResultsText }}</div>
</template>
{{-- RESULTS --}}
<div
@click="close()"
@keydown.enter="close()"
>
<template x-for="(item, index) in results" :key="index">
{{-- ITEM --}}
<a x-bind:href="item.link" class="spotlight-element" wire:navigate tabindex="0">
<div class="p-4 bg-base-100 hover:bg-base-200 rounded-md">
<div class="flex gap-3 items-center">
{{-- ICON --}}
<template x-if="item.icon">
<div x-html="item.icon"></div>
</template>
{{-- AVATAR --}}
<template x-if="item.avatar && !item.icon">
<div>
<img :src="item.avatar" class="rounded-full w-11 h-11" @if($fallbackAvatar) onerror="this.src='{{ $fallbackAvatar }}'" @endif />
</div>
</template>
<div class="flex-1 overflow-hidden whitespace-nowrap text-ellipsis truncate w-0">
{{-- NAME --}}
<div x-text="item.name" class="font-semibold truncate"></div>
{{-- DESCRIPTION --}}
<template x-if="item.description">
<div x-text="item.description" class="text-base-content/50 text-sm truncate"></div>
</template>
</div>
</div>
</div>
</a>
</template>
<div x-show="results.length" class="mb-3"></div>
</div>
</x-ui.modal>
</div>
@@ -7,7 +7,7 @@
])
<div {{ $attributes->class([]) }}>
<!-- STANDARD LABEL -->
{{-- STANDARD LABEL --}}
@if($label)
<label for="{{ $uuid }}" class="pt-0 label label-text font-semibold">
<span>
@@ -52,7 +52,7 @@
@endforeach
@endif
<!-- HINT -->
{{-- HINT --}}
@if($hint)
<div x-classes="label-text-alt text-gray-400 py-1 pb-0">{{ $hint }}</div>
@endif
@@ -0,0 +1,42 @@
@props([
'id' => null,
'darkTheme' => 'dark',
'lightTheme' => 'light',
'hidden' => false,
])
<div class="{{ $hidden ? 'hidden' : '' }}">
<label
for="{{ $id }}"
x-data="{
theme: $persist(window.matchMedia('(prefers-color-scheme: dark)').matches ? '{{ $darkTheme }}' : '{{ $lightTheme }}').as('theme'),
init() {
if (this.theme == '{{ $darkTheme }}') {
this.$refs.sun.classList.add('swap-off');
this.$refs.moon.classList.add('swap-on');
} else {
this.$refs.sun.classList.add('swap-on');
this.$refs.moon.classList.add('swap-off');
}
this.setToggle()
},
setToggle() {
document.documentElement.setAttribute('data-theme', this.theme)
this.$dispatch('theme-changed', this.theme)
},
toggle() {
this.theme = this.theme == '{{ $lightTheme }}' ? '{{ $darkTheme }}' : '{{ $lightTheme }}'
this.setToggle()
}
}"
{{ $attributes->class(["swap swap-rotate"]) }}
>
<input id="{{ $id }}" type="checkbox" class="theme-controller opacity-0" @click="toggle()" :value="theme" />
<x-ui.icon x-ref="sun" name="o-sun" x-cloak />
<x-ui.icon x-ref="moon" name="o-moon" x-cloak />
</label>
</div>
<script>
document.documentElement.setAttribute("data-theme", localStorage.getItem("theme")?.replaceAll("\"", ""))
</script>
@@ -0,0 +1,59 @@
@props([
'position' => 'toast-top toast-end'
])
<div>
@persist('toast')
<div
x-cloak
x-data="{ show: false, timer: '', toast: ''}"
@toast.window="
clearTimeout(timer);
toast = $event.detail.toast
setTimeout(() => show = true, 100);
timer = setTimeout(() => show = false, $event.detail.toast.timeout);
">
<div
class="toast rounded-md fixed cursor-pointer z-[999]"
:class="toast.position || '{{ $position }}'"
x-show="show"
x-classes="alert alert-success alert-warning alert-error alert-info top-10 end-10 toast toast-top toast-bottom toast-center toast-end toast-middle toast-start"
@click="show = false"
>
<div class="alert gap-2" :class="toast.css">
<div x-html="toast.icon"></div>
<div class="grid">
<div x-html="toast.title" class="font-bold"></div>
<div x-html="toast.description" class="text-xs"></div>
</div>
</div>
</div>
</div>
<script>
window.toast = function(payload){
window.dispatchEvent(new CustomEvent('toast', {detail: payload}))
}
document.addEventListener('livewire:init', () => {
Livewire.hook('request', ({fail}) => {
fail(({status, content, preventDefault}) => {
try {
let result = JSON.parse(content);
if (result?.toast && typeof window.toast === "function") {
window.toast(result);
}
if ((result?.prevent_default ?? false) === true) {
preventDefault();
}
} catch (e) {
console.log(e)
}
})
})
})
</script>
@endpersist
</div>
@@ -0,0 +1,60 @@
@props([
'id' => null,
'label' => null,
'right' => false,
'hint' => null,
'hintClass' => 'label-text-alt text-gray-400 py-1 pb-0',
'tight' => false,
'errorField' => null,
'errorClass' => 'text-red-500 label-text-alt p-1',
'omitError' => false,
'firstErrorOnly' => false,
])
@php
$modelName = $attributes->whereStartsWith('wire:model')->first();
$errorFieldName = $errorField ?? $modelName;
$id = $id == $modelName ? $modelName : "{$id}{$modelName}";
@endphp
<div>
<label for="{{ $id }}" class="flex items-center gap-3 cursor-pointer font-semibold">
@if($right)
<span @class(["flex-1" => !$tight])>
{{ $label}}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
</span>
@endif
<input id="{{ $id }}" type="checkbox" {{ $attributes->whereDoesntStartWith('class') }} {{ $attributes->class(['toggle toggle-primary']) }} />
@if(!$right)
{{ $label}}
@if($attributes->get('required'))
<span class="text-error">*</span>
@endif
@endif
</label>
{{-- ERROR --}}
@if(!$omitError && $errors->has($errorFieldName))
@foreach($errors->get($errorFieldName) as $message)
@foreach(Arr::wrap($message) as $line)
<div class="{{ $errorClass }}" x-classes="text-red-500 label-text-alt p-1">{{ $line }}</div>
@break($firstErrorOnly)
@endforeach
@break($firstErrorOnly)
@endforeach
@endif
{{-- HINT --}}
@if($hint)
<div class="{{ $hintClass }}" x-classes="label-text-alt text-gray-400 py-1 pb-0">{{ $hint }}</div>
@endif
</div>
+69 -87
View File
@@ -1,102 +1,84 @@
@use('App\Models\Currency')
<x-layouts.app>
<x-app-layout>
<x-ui.toolbar title="{{ __('Dashboard') }}"></x-ui.toolbar>
@livewire('portfolio-performance-chart', [
'name' => 'dashboard'
])
@livewire('portfolio-performance-chart', [
'name' => 'dashboard'
])
<div class="grid sm:grid-cols-5 gap-5">
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Market Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_gain_dollars', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Cost Basis') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_cost_basis', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Market Value') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_value', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Realized Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('realized_gain_dollars', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Dividends Earned') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_dividends_earned', 0)) }} </div>
</x-card>
<div class="grid sm:grid-cols-5 gap-5">
<x-ui.card dense="true" sub-title="{{ __('Market Gain/Loss') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_gain_dollars', 0)) }} </div>
</x-ui.card>
</div>
<div class="mt-6 grid md:grid-cols-7 gap-5">
<x-ib-card title="{{ __('My portfolios') }}" class="md:col-span-4">
@if ($user->portfolios->isEmpty())
<div class="flex justify-center items-center h-[100px] mb-8">
<x-button label="{{ __('Import / Export Data') }}" class="btn-primary btn-outline mr-6" link="{{ route('import-export') }}" />
<span>{{ __('or') }}</span>
<x-button label="{{ __('Create your first portfolio!') }}" class="btn-primary ml-6" link="{{ route('portfolio.create') }}" />
</div>
@endif
<x-ui.card dense="true" sub-title="{{ __('Total Cost Basis') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_cost_basis', 0)) }} </div>
</x-ui.card>
@foreach($user->portfolios as $portfolio)
<x-list-item :item="$portfolio" link="{{ route('portfolio.show', ['portfolio' => $portfolio->id]) }}">
<x-slot:value>
{{ $portfolio->title }}
@if($portfolio->wishlist)
<x-badge value="{{ __('Wishlist') }}" class="badge-secondary badge-sm ml-2" />
@endif
</x-slot:value>
</x-list-item>
@endforeach
<x-ui.card dense="true" sub-title="{{ __('Total Market Value') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_value', 0)) }} </div>
</x-ui.card>
<x-ui.card dense="true" sub-title="{{ __('Realized Gain/Loss') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('realized_gain_dollars', 0)) }} </div>
</x-ui.card>
</x-ib-card>
@if (!$user->transactions->isEmpty())
<x-ib-card title="{{ __('Recent activity') }}" class="md:col-span-3">
<x-ui.card dense="true" sub-title="{{ __('Dividends Earned') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_dividends_earned', 0)) }} </div>
</x-ui.card>
@livewire('transactions-list', [
'transactions' => $user->transactions,
'showPortfolio' => true,
'paginate' => false
])
</div>
</x-ib-card>
@endif
<div class="mt-6 grid md:grid-cols-7 gap-5">
@if (!$user->portfolios->isEmpty())
<x-ib-card title="{{ __('Top performers') }}" class="md:col-span-3">
<x-ui.card title="{{ __('My portfolios') }}" class="md:col-span-4">
@livewire('top-performers-list', [
'holdings' => $user->holdings
])
@if ($user->portfolios->isEmpty())
<div class="flex justify-center items-center h-[100px] mb-8">
<x-ui.button label="{{ __('Import / Export Data') }}" class="btn-primary btn-outline mr-6" link="{{ route('import-export') }}" />
<span>{{ __('or') }}</span>
<x-ui.button label="{{ __('Create your first portfolio!') }}" class="btn-primary ml-6" link="{{ route('portfolio.create') }}" />
</div>
@endif
@foreach($user->portfolios as $portfolio)
<x-ui.list-item no-separator :item="$portfolio" link="{{ route('portfolio.show', ['portfolio' => $portfolio->id]) }}">
<x-slot:value>
{{ $portfolio->title }}
@if($portfolio->wishlist)
<x-ui.badge value="{{ __('Wishlist') }}" class="badge-secondary badge-outline badge-sm ml-2" />
@endif
</x-slot:value>
</x-ui.list-item>
@endforeach
</x-ib-card>
@endif
</x-ui.card>
{{-- @if (!$user->portfolios->isEmpty())
<x-ib-card title="{{ __('Top headlines') }}" class="md:col-span-3">
@php
$users = App\Models\User::take(3)->get();
@endphp
@foreach($users as $user)
<x-list-item no-separator :item="$user" avatar="profile_photo_url" link="/docs/installation" />
@endforeach
@if (!$user->transactions->isEmpty())
<x-ui.card title="{{ __('Recent activity') }}" class="md:col-span-3">
@livewire('transactions-list', [
'transactions' => $user->transactions,
'showPortfolio' => true,
'paginate' => false
])
</x-ib-card>
@endif --}}
</x-ui.card>
@endif
@if (!$user->portfolios->isEmpty())
<x-ui.card title="{{ __('Top performers') }}" class="md:col-span-3">
@livewire('top-performers-list', [
'holdings' => $user->holdings
])
</x-ui.card>
@endif
</div>
</div>
</x-app-layout>
</x-layouts.app>
@@ -20,7 +20,7 @@ new class extends Component
<div>
@foreach ($holding->dividends->take(5) as $dividend)
<x-list-item :item="$dividend">
<x-ui.list-item :item="$dividend" no-separator>
<x-slot:value>
@php
@@ -35,7 +35,7 @@ new class extends Component
<x-slot:sub-value>
<span title="{{ __('Ex Dividend Date') }}">{{ $dividend->date->format('F d, Y') }}</span>
</x-slot:sub-value>
</x-list-item>
</x-ui.list-item>
@endforeach
</div>
@@ -21,7 +21,7 @@ new class extends Component
<div class="font-bold text-2xl py-1 flex items-center">
{{ Number::currency($holding->market_data->market_value ?? 0, $holding->market_data->currency) }}
<x-gain-loss-arrow-badge
<x-ui.gain-loss-arrow-badge
:cost-basis="$holding->average_cost_basis"
:market-value="$holding->market_data->market_value_base"
/>
@@ -1,19 +1,17 @@
<?php
use App\Models\Holding;
use Illuminate\Support\Collection;
use Livewire\Attributes\{Computed};
use App\Traits\Toast;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
use Illuminate\Validation\Rule;
new class extends Component {
new class extends Component
{
use Toast;
// props
public Holding $holding;
public Bool $reinvest_dividends = false;
public bool $reinvest_dividends = false;
// methods
public function rules()
@@ -24,9 +22,9 @@ new class extends Component {
];
}
public function mount()
public function mount()
{
$this->reinvest_dividends = $this->holding?->reinvest_dividends ?? false;
}
@@ -41,9 +39,9 @@ new class extends Component {
}; ?>
<div class="" x-data="{ }"> {{-- grid lg:grid-cols-4 gap-10 --}}
<x-ib-form wire:submit="save" class=""> {{-- col-span-3 --}}
<x-ui.form wire:submit="save" class=""> {{-- col-span-3 --}}
<x-toggle
<x-ui.toggle
label="{{ __('Reinvest Dividends') }}"
wire:model="reinvest_dividends"
right
@@ -52,7 +50,7 @@ new class extends Component {
<x-slot:actions>
<x-button
<x-ui.button
label="{{ __('Save') }}"
type="submit"
icon="o-paper-airplane"
@@ -60,6 +58,6 @@ new class extends Component {
spinner="save"
/>
</x-slot:actions>
</x-ib-form>
</x-ui.form>
</div>
@@ -1,103 +0,0 @@
<?php
use App\Models\Portfolio;
use Illuminate\Support\Collection;
use Livewire\Volt\Component;
use App\Models\Currency;
new class extends Component
{
// props
public Portfolio $portfolio;
public array $sortBy = ['column' => 'symbol', 'direction' => 'asc'];
public array $headers;
public function mount()
{
$this->headers = [
['key' => 'symbol', 'label' => __('Symbol')],
['key' => 'market_data_name', 'label' => __('Name'), 'sortable' => true, 'class' => 'hidden md:table-cell'],
['key' => 'quantity', 'label' => __('Quantity')],
['key' => 'average_cost_basis', 'label' => __('Average Cost Basis')],
['key' => 'total_cost_basis', 'label' => __('Total Cost Basis'), 'class' => 'hidden md:table-cell'],
['key' => 'market_data_market_value', 'label' => __('Market Value')],
['key' => 'total_market_value', 'label' => __('Total Market Value'), 'class' => 'hidden md:table-cell'],
['key' => 'market_gain_dollars', 'label' => __('Market Gain/Loss')],
['key' => 'market_gain_percent', 'label' => __('Market Gain/Loss'), 'class' => 'hidden md:table-cell'],
['key' => 'realized_gain_dollars', 'label' => __('Realized Gain/Loss')],
['key' => 'dividends_earned', 'label' => __('Dividends Earned')],
['key' => 'market_data_fifty_two_week_low', 'label' => __('52 week low'), 'class' => 'hidden md:table-cell'],
['key' => 'market_data_fifty_two_week_high', 'label' => __('52 week high'), 'class' => 'hidden md:table-cell'],
['key' => 'num_transactions', 'label' => __('Number of Transactions')],
['key' => 'market_data_updated_at', 'label' => __('Last Refreshed')],
];
}
public function holdings(): Collection
{
$holdings = $this->portfolio
->holdings()
->withCount(['transactions as num_transactions' => function ($query) {
return $query->whereRaw('transactions.symbol = holdings.symbol');
}])
->orderBy(...array_values($this->sortBy))
// ->where('holdings.quantity', '>', 0)
->get();
return $holdings;
}
public function goToHolding($holding)
{
return $this->redirect(route('holding.show', ['portfolio' => $holding['portfolio_id'], 'symbol' => $holding['symbol']]));
}
}; ?>
<x-table
:headers="$headers"
:rows="$this->holdings()"
:sort-by="$sortBy"
@row-click="$wire.goToHolding($event.detail)"
>
@scope('cell_average_cost_basis', $row)
{{ Number::currency($row->average_cost_basis ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_total_cost_basis', $row)
{{ Number::currency($row->total_cost_basis ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_realized_gain_dollars', $row)
{{ Number::currency($row->realized_gain_dollars ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_gain_dollars', $row)
{{ Number::currency($row->market_gain_dollars ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_gain_percent', $row)
<x-gain-loss-arrow-badge
:cost-basis="$row->average_cost_basis"
:market-value="$row->market_data->market_value"
/>
@endscope
@scope('cell_market_data_market_value', $row)
{{ Number::currency($row->market_data_market_value ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_data_fifty_two_week_low', $row)
{{ Number::currency($row->market_data_fifty_two_week_low ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_data_fifty_two_week_high', $row)
{{ Number::currency($row->market_data_fifty_two_week_high ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_total_market_value', $row)
{{ Number::currency($row->total_market_value ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_dividends_earned', $row)
{{ Number::currency($row->dividends_earned ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_data_updated_at', $row)
{{ \Carbon\Carbon::parse($row->market_data_updated_at)->diffForHumans() }}
@endscope
</x-table>
+26 -26
View File
@@ -1,9 +1,9 @@
@use('App\Models\Currency')
<x-app-layout>
<x-layouts.app>
<div x-data>
<x-ib-alpine-modal
<x-ui.modal
key="create-transaction"
title="{{ __('Create Transaction') }}"
>
@@ -12,9 +12,9 @@
'symbol' => $holding->market_data->symbol,
])
</x-ib-alpine-modal>
</x-ui.modal>
<x-ib-alpine-modal
<x-ui.modal
key="holding-options"
title="{{ __('Holding Options') }}"
>
@@ -22,9 +22,9 @@
'holding' => $holding
])
</x-ib-alpine-modal>
</x-ui.modal>
<x-ib-toolbar>
<x-ui.toolbar>
<x-slot:title>
<a href="{{ route('portfolio.show', ['portfolio' => $portfolio->id]) }}" title="{{ __('Portfolio') }}">
{{ $portfolio->title }}
@@ -33,30 +33,30 @@
</x-slot:title>
@can('fullAccess', $portfolio)
<x-button
<x-ui.button
title="{{ __('Holding options') }}"
icon="o-pencil"
class="btn-circle btn-ghost btn-sm text-secondary"
@click="$dispatch('toggle-holding-options')"
/>
@else
<x-icon name="o-eye" class="text-secondary w-4" title="{{ __('Read only') }}" />
<x-ui.icon name="o-eye" class="text-secondary w-4" title="{{ __('Read only') }}" />
@endcan
<x-ib-flex-spacer />
<x-ui.flex-spacer />
@can('fullAccess', $portfolio)
<x-button
<x-ui.button
label="{{ __('Create transaction') }}"
class="btn-sm btn-primary whitespace-nowrap"
@click="$dispatch('toggle-create-transaction')"
/>
@endcan
</x-ib-toolbar>
</x-ui.toolbar>
<div class="mt-6 grid md:grid-cols-9 gap-5">
<x-ib-card class="md:col-span-5">
<x-ui.card class="md:col-span-5">
<x-slot:title>
{{ $holding->market_data->symbol }}
@@ -65,9 +65,9 @@
@livewire('holding-market-data', ['holding' => $holding])
</x-ib-card>
</x-ui.card>
<x-ib-card title="{{ __('Fundamentals') }}" class="md:col-span-4">
<x-ui.card title="{{ __('Fundamentals') }}" class="md:col-span-4">
@if(!empty($holding->market_data->market_cap))
<p>
@@ -100,7 +100,7 @@
<p>
<span class="font-bold">{{ __('52 week') }}: </span>
<x-fifty-two-week-range :market-data="$holding->market_data" />
<x-ui.fifty-two-week-range :market-data="$holding->market_data" />
</p>
@if(!empty($holding->market_data->dividend_yield))
@@ -120,9 +120,9 @@
</p>
@endif
</x-ib-card>
</x-ui.card>
<x-ib-card title="{{ __('Recent activity') }}" class="md:col-span-3">
<x-ui.card title="{{ __('Recent activity') }}" class="md:col-span-3">
@livewire('transactions-list', [
'portfolio' => $holding->portfolio,
@@ -130,9 +130,9 @@
'shouldGoToHolding' => false
])
</x-ib-card>
</x-ui.card>
<x-ib-card title="{{ __('Dividends') }}" class="md:col-span-3">
<x-ui.card title="{{ __('Dividends') }}" class="md:col-span-3">
@if($holding->dividends->isEmpty())
<div class="flex justify-center items-center h-full pb-10 text-secondary">
@@ -144,9 +144,9 @@
@livewire('holding-dividends-list', ['holding' => $holding])
</x-ib-card>
</x-ui.card>
<x-ib-card title="{{ __('Splits') }}" class="md:col-span-3">
<x-ui.card title="{{ __('Splits') }}" class="md:col-span-3">
@if($holding->splits->isEmpty())
<div class="flex justify-center items-center h-full pb-10 text-secondary">
@@ -158,7 +158,7 @@
@foreach ($holding->splits->take(5) as $split)
<x-list-item :item="$split">
<x-ui.list-item :item="$split" no-separator>
<x-slot:value>
1:{{ $split->split_amount }}
@@ -167,17 +167,17 @@
<x-slot:sub-value>
<span title="{{ __('Distribution Date') }}">{{ $split->date->format('F d, Y') }}</span>
</x-slot:sub-value>
</x-list-item>
</x-ui.list-item>
@endforeach
</x-ib-card>
</x-ui.card>
@if(config('services.ai_chat_enabled'))
{{-- // todo: add to system prompt:
// Additionally, here is some recent news about {$this->holding->symbol}:
// And their latest SEC filings: --}}
@livewire('ai-chat-window', [
@livewire('ui.ai-chat-window', [
'chatable' => $holding,
'suggested_prompts' => [
[
@@ -228,4 +228,4 @@
</div>
</div>
</x-app-layout>
</x-layouts.app>
-34
View File
@@ -1,34 +0,0 @@
<x-app-layout>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
@livewire('import-portfolios-field')
<x-section-border hide-on-mobile />
</div>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<x-forms.action-section>
<x-slot name="title">
{{ __('Export') }}
</x-slot>
<x-slot name="description">
{{ __('Download all of your portfolios and transactions.') }}
</x-slot>
<x-slot name="content">
<div class="col-span-6 sm:col-span-4">
@livewire('export-portfolios-button')
</div>
</x-slot>
</x-forms.form-section>
</div>
</x-app-layout>
@@ -1,32 +1,33 @@
<?php
use Livewire\Volt\Component;
use Mary\Traits\Toast;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\BackupExport;
use App\Traits\Toast;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Volt\Component;
use Maatwebsite\Excel\Facades\Excel;
new class extends Component {
new class extends Component
{
use Toast;
// props
// methods
public function export()
{
if (!RateLimiter::attempt('export:'.auth()->user()->id, $perMinute = 3, fn()=>null)) {
public function export()
{
if (! RateLimiter::attempt('export:'.auth()->user()->id, $perMinute = 3, fn () => null)) {
$this->error(__('Hang on! You\'re doing that too much.'));
return;
}
return Excel::download(new BackupExport, now()->format('Y_m_d') . '_investbrain_backup.xlsx');
return Excel::download(new BackupExport, now()->format('Y_m_d').'_investbrain_backup.xlsx');
}
}; ?>
<div>
<x-button type="submit" @click="$wire.export" spinner="export">
<x-ui.button type="submit" @click="$wire.export" spinner="export">
{{ __('Download Export') }}
</x-button>
</x-ui.button>
</div>
@@ -2,11 +2,11 @@
use App\Exports\BackupExport;
use App\Models\BackupImport as BackupImportModel;
use App\Traits\Toast;
use Livewire\Attributes\Rule;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use Maatwebsite\Excel\Facades\Excel;
use Mary\Traits\Toast;
new class extends Component
{
@@ -95,10 +95,10 @@ new class extends Component
<x-slot:form>
<div class="col-span-6 sm:col-span-4">
<x-file wire:model="file" label="{{ __('Select a file') }}" hint="" accept=".xlsx" required />
<x-ui.file wire:model="file" label="{{ __('Select a file') }}" hint="" accept=".xlsx" required />
</div>
<x-dialog-modal wire:model.live="importStatusDialog" persistent>
<x-ui.dialog-modal wire:model.live="importStatusDialog" persistent>
<x-slot name="title">
@if($backupImport?->status)
@@ -111,7 +111,7 @@ new class extends Component
</x-slot>
<x-slot name="content">
@if($backupImport?->status != 'failed')
<x-progress
<x-ui.progress
:indeterminate="$backupImport?->status == 'pending'"
class="progress-primary h-3"
value="{{ $percent }}"
@@ -119,18 +119,18 @@ new class extends Component
/>
@endif
</x-slot>
<x-slot name="footer">
@if($backupImport?->status == 'failed')
<x-button wire:click="$toggle('importStatusDialog')"> {{ __('Try again') }} </x-button>
<x-ui.button wire:click="$toggle('importStatusDialog')"> {{ __('Try again') }} </x-ui.button>
@else
<div wire:poll="checkImportStatus" class="text-gray-400 text-sm">{{ __('Your import will continue in the background') }}</div>
<x-ib-flex-spacer />
<x-button wire:click="$toggle('importStatusDialog')"> {{ __('Close') }} </x-button>
<x-ui.flex-spacer />
<x-ui.button wire:click="$toggle('importStatusDialog')"> {{ __('Dismiss') }} </x-ui.button>
@endif
</x-slot>
</x-dialog-modal>
</x-ui.dialog-modal>
</x-slot:form>
@@ -140,9 +140,9 @@ new class extends Component
{{ __('Saved.') }}
</x-forms.action-message>
<x-button type="submit" wire:loading.attr="disabled" spinner="import">
<x-ui.button type="submit" wire:loading.attr="disabled" spinner="import">
{{ __('Import') }}
</x-button>
</x-ui.button>
</x-slot>
</x-forms.form-section>
@@ -0,0 +1,32 @@
<x-layouts.app>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
@livewire('import-portfolios-field')
<x-ui.section-border hide-on-mobile />
<x-forms.action-section>
<x-slot name="title">
{{ __('Export') }}
</x-slot>
<x-slot name="description">
{{ __('Download all of your portfolios and transactions.') }}
</x-slot>
<x-slot name="content">
<div class="col-span-6 sm:col-span-4">
@livewire('export-portfolios-button')
</div>
</x-slot>
</x-forms.form-section>
</div>
</x-layouts.app>
@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" href="{{ asset('favicon.svg') }}">
<title>{{ config('app.name', 'Investbrain') }}</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body {{ $attributes?->merge(['class']) }}>
@yield('body', $body ?? '')
@livewireScripts
</body>
</html>
+5 -5
View File
@@ -1,13 +1,13 @@
<x-guest-layout>
<div class="pt-4 bg-gray-100 dark:bg-gray-900">
<x-layouts.guest>
<div class="my-22">
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg prose dark:prose-invert">
<div class="w-full sm:max-w-2xl mt-6 p-6 overflow-hidden sm:rounded-lg prose dark:prose-invert">
{!! $policy !!}
</div>
</div>
</div>
</x-guest-layout>
</x-layouts.guest>
+3 -3
View File
@@ -1,8 +1,8 @@
<x-app-layout>
<x-layouts.app>
<div>
<x-ib-toolbar title="{{ __('Create Portfolio') }}" />
<x-ui.toolbar title="{{ __('Create Portfolio') }}" />
@livewire('manage-portfolio-form')
</div>
</x-app-layout>
</x-layouts.app>
@@ -1,33 +1,34 @@
<?php
use App\Models\Portfolio;
use Illuminate\Support\Collection;
use App\Traits\Toast;
use App\Traits\WithTrimStrings;
use Livewire\Attributes\Rule;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
use App\Traits\WithTrimStrings;
new class extends Component {
new class extends Component
{
use Toast;
use WithTrimStrings;
// props
public ?Portfolio $portfolio;
public Bool $hideCancel = false;
public bool $hideCancel = false;
#[Rule('required|min:5')]
public String $title;
public string $title;
#[Rule('sometimes|nullable')]
public ?String $notes;
public ?string $notes;
#[Rule('sometimes|nullable|boolean')]
public Bool $wishlist = false;
public bool $wishlist = false;
public Bool $confirmingPortfolioDeletion = false;
public bool $confirmingPortfolioDeletion = false;
// methods
public function mount()
public function mount()
{
if (isset($this->portfolio)) {
@@ -49,7 +50,7 @@ new class extends Component {
public function save()
{
$portfolio = (new Portfolio())->fill($this->validate());
$portfolio = (new Portfolio)->fill($this->validate());
$portfolio->save();
@@ -68,24 +69,24 @@ new class extends Component {
<div class="w-full md:w-3/4">
<x-ib-form wire:submit="{{ $portfolio ? 'update' : 'save' }}" >
<x-input label="{{ __('Title') }}" wire:model="title" required />
<x-ui.form wire:submit="{{ $portfolio ? 'update' : 'save' }}" >
<x-ui.input label="{{ __('Title') }}" wire:model="title" required />
<x-ib-textarea class="mt-1" label="{{ __('Notes') }}" wire:model="notes" rows="4" />
<x-ui.textarea class="mt-1" label="{{ __('Notes') }}" wire:model="notes" rows="4" />
@if (isset($this->portfolio))
@livewire('share-portfolio-form', ['portfolio' => $portfolio])
@endif
<x-toggle label="{{ __('Wishlist') }}" wire:model="wishlist" >
<x-ui.toggle label="{{ __('Wishlist') }}" wire:model="wishlist" >
<x-slot:hint>
{{ __('Treat this portfolio as a "wishlist" (holdings will be excluded from realized gains, unrealized gains, and dividends)') }}
</x-slot:hint>
</x-toggle>
</x-ui.toggle>
<x-slot:actions>
@if ($portfolio)
<x-button
<x-ui.button
wire:click="$toggle('confirmingPortfolioDeletion')"
wire:loading.attr="disabled"
class="btn text-error"
@@ -95,13 +96,13 @@ new class extends Component {
@endif
@if (!$hideCancel)
<x-button label="{{ __('Cancel') }}" link="/dashboard" />
<x-ui.button label="{{ __('Cancel') }}" link="/dashboard" />
@endif
<x-button label="{{ $portfolio ? __('Update') : __('Create') }}" type="submit" icon="o-paper-airplane" class="btn-primary" spinner="save" />
<x-ui.button label="{{ $portfolio ? __('Update') : __('Create') }}" type="submit" icon="o-paper-airplane" class="btn-primary" spinner="save" />
</x-slot:actions>
</x-ib-form>
</x-ui.form>
<x-confirmation-modal wire:model.live="confirmingPortfolioDeletion">
<x-ui.confirmation-modal wire:model.live="confirmingPortfolioDeletion">
<x-slot name="title">
{{ __('Delete Portfolio') }}
</x-slot>
@@ -111,13 +112,13 @@ new class extends Component {
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$toggle('confirmingPortfolioDeletion')" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:click="$toggle('confirmingPortfolioDeletion')" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-secondary-button>
<x-button class="ms-3 btn-error text-white" wire:click="delete" wire:loading.attr="disabled">
<x-ui.button class="ms-3 btn-error text-white" wire:click="delete" wire:loading.attr="disabled">
{{ __('Delete Portfolio') }}
</x-button>
</x-ui.button>
</x-slot>
</x-confirmation-modal>
</x-ui.confirmation-modal>
</div>
@@ -2,9 +2,10 @@
use App\Models\DailyChange;
use App\Models\Portfolio;
use Livewire\Attributes\Lazy;
use Livewire\Volt\Component;
new class extends Component
new #[Lazy] class extends Component
{
// props
public ?Portfolio $portfolio = null;
@@ -31,6 +32,13 @@ new class extends Component
$this->chartSeries = $this->generatePerformanceData();
}
public function placeholder()
{
return <<<'HTML'
<div class="skeleton h-[395px] mb-5"></div>
HTML;
}
public function generatePerformanceData()
{
$filterMethod = collect($this->scopeOptions)->where('id', $this->scope)->first();
@@ -116,7 +124,7 @@ new class extends Component
}
}; ?>
<x-card class="bg-slate-100 dark:bg-base-200 rounded-lg mb-6">
<x-ui.card class="mb-6">
<div class="flex flex-col md:flex-row md:justify-between mb-2">
<div class="flex flex-col md:flex-row items-start md:items-center">
@@ -128,15 +136,15 @@ new class extends Component
</div>
<div class="flex items-center" x-data="{ loading: false }">
{{-- <x-button title="{{ __('Reset chart') }}" icon="o-arrow-path" class="btn-ghost btn-sm btn-circle mr-2" id="chart-reset-zoom-{{ $name }}" /> --}}
{{-- <x-ui.button title="{{ __('Reset chart') }}" icon="o-arrow-path" class="btn-ghost btn-sm btn-circle mr-2" id="chart-reset-zoom-{{ $name }}" /> --}}
<x-loading x-show="loading" x-cloak class="text-gray-400 ml-2" />
<x-ui.loading x-show="loading" x-cloak class="text-gray-400 ml-2" />
<x-dropdown title="{{ __('Choose time period') }}" label="{{ $scope }}" class="btn-xs md:btn-sm btn-outline" x-bind:disabled="loading">
<x-ui.dropdown title="{{ __('Choose time period') }}" label="{{ $scope }}" class="btn-xs md:btn-sm btn-outline" x-bind:disabled="loading">
@foreach($scopeOptions as $option)
<x-menu-item
<x-ui.menu-item
title="{{ $option['name'] }}"
@click="
timeout = setTimeout(() => { loading = true }, 200);
@@ -156,7 +164,7 @@ new class extends Component
<div
class="h-[280px] mb-5"
>
<x-ib-apex-chart :series-data="$chartSeries" :name="$name" />
<x-ui.apex-chart :series-data="$chartSeries" :name="$name" />
</div>
</x-card>
</x-ui.card>
@@ -1,15 +1,13 @@
<?php
use App\Models\Portfolio;
use App\Models\User;
use App\Traits\Toast;
use App\Traits\WithTrimStrings;
use Livewire\Attributes\Rule;
use Livewire\Volt\Component;
use Illuminate\Support\Collection;
use Mary\Traits\Toast;
new class extends Component {
new class extends Component
{
use Toast;
use WithTrimStrings;
@@ -23,18 +21,20 @@ new class extends Component {
public int $fullAccess = 0;
public array $permissions;
public bool $confirmingAccessDeletion = false;
public ?string $deletingAccessFor = null;
// methods
public function mount()
{
if (!$this->portfolio) {
if (! $this->portfolio) {
$this->permissions = [
auth()->user()->id => [
'owner' => true,
'full_access' => false
]
'full_access' => false,
],
];
} else {
@@ -43,8 +43,8 @@ new class extends Component {
return [
$user->id => [
'owner' => $user->pivot->owner ?? 0,
'full_access' => $user->pivot->full_access ?? 0
]
'full_access' => $user->pivot->full_access ?? 0,
],
];
})->toArray();
}
@@ -65,13 +65,13 @@ new class extends Component {
{
$this->authorize('fullAccess', $this->portfolio);
if (!$confirmed) {
if (! $confirmed) {
$this->deletingAccessFor = $userId;
$this->confirmingAccessDeletion = true;
return;
}
unset($this->permissions[$userId]);
$this->portfolio->unShare($userId);
@@ -101,7 +101,6 @@ new class extends Component {
$this->emailAddress = '';
$this->fullAccess = false;
}
}; ?>
<div class="">
@@ -109,9 +108,9 @@ new class extends Component {
<span>{{ __('People with access') }}</span>
</label>
<div class="border-primary border rounded-sm px-2 py-5 mb-2 max-h-[20rem] overflow-y-scroll">
<div class="border-primary border rounded-md px-2 py-5 mb-2 max-h-[20rem] overflow-y-scroll">
@if ($portfolio?->owner)
<x-list-item
<x-ui.list-item
:item="$portfolio->owner"
avatar="profile_photo_url"
no-separator
@@ -129,11 +128,11 @@ new class extends Component {
<x-slot:sub-value>
{{ __('Owner') }}
</x-slot:sub-value>
</x-list-item>
</x-ui.list-item>
@endif
@foreach (collect($portfolio?->users)->where('pivot.owner', '!=', 1) as $user)
<x-list-item
<x-ui.list-item
:item="$user"
avatar="profile_photo_url"
no-separator
@@ -154,26 +153,26 @@ new class extends Component {
</x-slot:sub-value>
<x-slot:actions>
@if (auth()->user()->id != $user->id)
<x-select
<x-ui.select
class="select select-ghost border-none focus:outline-none focus:ring-0"
:options="[['id' => 0, 'name' => __('Read only')], ['id' => 1, 'name' => __('Full access')]]"
wire:model.live.number="permissions.{{ $user->id }}.full_access"
/>
<x-button
<x-ui.button
class="btn-sm btn-ghost btn-circle"
wire:click="deleteUser('{{ $user->id }}')"
spinner="deleteUser('{{ $user->id }}')"
title="{{ __('Remove Access') }}"
>
<x-icon name="o-x-mark" class="w-4" />
</x-button>
<x-ui.icon name="o-x-mark" class="w-4" />
</x-ui.button>
@endif
</x-slot:actions>
</x-list-item>
</x-ui.list-item>
@endforeach
<x-confirmation-modal wire:model.live="confirmingAccessDeletion">
<x-ui.confirmation-modal wire:model.live="confirmingAccessDeletion">
<x-slot:title>
{{ __('Remove Access') }}
</x-slot:title>
@@ -183,24 +182,24 @@ new class extends Component {
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$toggle('confirmingAccessDeletion')" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:click="$toggle('confirmingAccessDeletion')" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-secondary-button>
<x-button class="ms-3 btn-error text-white" wire:click="deleteUser('{{ $this->deletingAccessFor }}', true)" spinner="deleteUser" wire:loading.attr="disabled">
<x-ui.button class="ms-3 btn-error text-white" wire:click="deleteUser('{{ $this->deletingAccessFor }}', true)" spinner="deleteUser" wire:loading.attr="disabled">
{{ __('Remove Access') }}
</x-button>
</x-ui.button>
</x-slot>
</x-confirmation-modal>
</x-ui.confirmation-modal>
<x-ib-alpine-modal
<x-ui.modal
key="add-user-modal"
title="{{ __('Share Portfolio') }}"
>
<div class="" x-data="{ }">
<x-ib-form wire:submit="addUser" class="">
<x-ui.form wire:submit="addUser" class="">
<x-input
<x-ui.input
label="Email"
icon="o-envelope"
placeholder="{{ __('Type an email address to share portfolio') }}"
@@ -209,7 +208,7 @@ new class extends Component {
required
/>
<x-toggle
<x-ui.toggle
class="mt-2"
label="{{ __('Grant full access') }}"
wire:model="fullAccess"
@@ -219,7 +218,7 @@ new class extends Component {
<x-slot:actions>
<x-button
<x-ui.button
label="{{ __('Share') }}"
title="{{ __('Share Portfolio') }}"
type="submit"
@@ -228,14 +227,14 @@ new class extends Component {
spinner="addUser"
/>
</x-slot:actions>
</x-ib-form>
</x-ui.form>
</div>
</x-ib-alpine-modal>
</x-ui.modal>
<x-button class="btn-sm block mt-4" @click="$dispatch('toggle-add-user-modal')">
<x-ui.button class="btn-sm block mt-4" @click="$dispatch('toggle-add-user-modal')">
{{ __('Add People') }}
</x-button>
</x-ui.button>
</div>
</div>
+37 -42
View File
@@ -1,9 +1,9 @@
@use('App\Models\Currency')
<x-app-layout>
<x-layouts.app>
<div x-data>
<x-ib-alpine-modal
<x-ui.modal
key="create-transaction"
title="{{ __('Create Transaction') }}"
>
@@ -11,9 +11,9 @@
'portfolio' => $portfolio,
])
</x-ib-alpine-modal>
</x-ui.modal>
<x-ib-drawer
<x-ui.drawer
key="manage-portfolio"
title="{{ __('Manage Portfolio') }}"
>
@@ -22,41 +22,41 @@
'hideCancel' => true
])
</x-ib-drawer>
</x-ui.drawer>
<x-ib-toolbar :title="$portfolio->title">
<x-ui.toolbar :title="$portfolio->title">
@if($portfolio->wishlist)
<x-badge value="{{ __('Wishlist') }}" title="{{ __('Wishlist') }}" class="badge-secondary mr-3" />
<x-ui.badge value="{{ __('Wishlist') }}" title="{{ __('Wishlist') }}" class="badge-secondary badge-outline mr-3" />
@endif
@if(auth()->user()->id !== $portfolio->owner_id)
<x-badge value="{{ $portfolio->owner->name }}" title="{{ __('Owner').': '.$portfolio->owner->name }}" class="badge-secondary badge-outline mr-3" />
<x-ui.badge value="{{ $portfolio->owner->name }}" title="{{ __('Owner').': '.$portfolio->owner->name }}" class="badge-secondary badge-outline mr-3" />
@endif
@can('fullAccess', $portfolio)
<x-button
<x-ui.button
title="{{ __('Manage Portfolio') }}"
icon="o-pencil"
class="btn-circle btn-ghost btn-sm text-secondary"
@click="$dispatch('toggle-manage-portfolio')"
/>
@else
<x-icon name="o-eye" class="text-secondary w-4" title="{{ __('Read only') }}" />
<x-ui.icon name="o-eye" class="text-secondary w-4" title="{{ __('Read only') }}" />
@endcan
<x-ib-flex-spacer />
<x-ui.flex-spacer />
@can('fullAccess', $portfolio)
<div>
<x-button
<x-ui.button
label="{{ __('Create Transaction') }}"
class="btn-sm btn-primary whitespace-nowrap"
@click="$dispatch('toggle-create-transaction')"
/>
</div>
@endcan
</x-ib-toolbar>
</x-ui.toolbar>
@livewire('portfolio-performance-chart', [
'name' => 'portfolio-'.$portfolio->id,
@@ -65,36 +65,31 @@
<div class="grid sm:grid-cols-5 gap-5">
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Market Gain/Loss') }}</div>
<x-ui.card dense="true" sub-title="{{ __('Market Gain/Loss') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_gain_dollars', 0)) }} </div>
</x-card>
</x-ui.card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Cost Basis') }}</div>
<x-ui.card dense="true" sub-title="{{ __('Total Cost Basis') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_cost_basis', 0)) }} </div>
</x-card>
</x-ui.card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Market Value') }}</div>
<x-ui.card dense="true" sub-title="{{ __('Total Market Value') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_value', 0)) }} </div>
</x-card>
</x-ui.card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Realized Gain/Loss') }}</div>
<x-ui.card dense="true" sub-title="{{ __('Realized Gain/Loss') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('realized_gain_dollars', 0)) }} </div>
</x-card>
</x-ui.card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Dividends Earned') }}</div>
<x-ui.card dense="true" sub-title="{{ __('Dividends Earned') }}" class="col-span-5 sm:col-span-1">
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_dividends_earned', 0)) }} </div>
</x-card>
</x-ui.card>
</div>
<div class="mt-6 grid md:grid-cols-7 gap-5">
<x-ib-card title="{{ __('Holdings') }}" class="md:col-span-4 overflow-scroll">
<x-ui.card title="{{ __('Holdings') }}" class="md:col-span-4">
@if($portfolio->holdings->isEmpty())
<div class="flex justify-center items-center h-full pb-10 text-secondary">
@@ -104,14 +99,14 @@
@else
@livewire('holdings-table', [
'portfolio' => $portfolio
])
@livewire('datatables.holdings-table', [
'portfolio' => $portfolio
])
@endif
</x-ib-card>
</x-ui.card>
<x-ib-card title="{{ __('Recent activity') }}" class="md:col-span-3">
<x-ui.card title="{{ __('Recent activity') }}" class="md:col-span-3">
@if($portfolio->transactions->isEmpty())
<div class="flex justify-center items-center h-full pb-10 text-secondary">
@@ -126,9 +121,9 @@
'transactions' => $portfolio->transactions
])
</x-ib-card>
</x-ui.card>
<x-ib-card title="{{ __('Top performers') }}" class="md:col-span-3">
<x-ui.card title="{{ __('Top performers') }}" class="md:col-span-3">
@if($portfolio->holdings->isEmpty())
<div class="flex justify-center items-center h-full pb-10 text-secondary">
@@ -142,22 +137,22 @@
'holdings' => $portfolio->holdings
])
</x-ib-card>
</x-ui.card>
{{-- <x-ib-card title="{{ __('Top headlines') }}" class="md:col-span-3">
{{-- <x-ui.card title="{{ __('Top headlines') }}" class="md:col-span-3">
@php
$users = App\Models\User::take(3)->get();
@endphp
@foreach($users as $user)
<x-list-item no-separator :item="$user" avatar="profile_photo_url" link="/docs/installation" />
<x-ui.list-item no-separator :item="$user" avatar="profile_photo_url" link="/docs/installation" />
@endforeach
</x-ib-card> --}}
</x-ui.card> --}}
@if(config('services.ai_chat_enabled'))
@livewire('ai-chat-window', [
@livewire('ui.ai-chat-window', [
'chatable' => $portfolio,
'suggested_prompts' => [
[
@@ -184,4 +179,4 @@
</div>
</div>
</x-app-layout>
</x-layouts.app>
@@ -1,17 +1,15 @@
<?php
use App\Models\Portfolio;
use App\Models\Transaction;
use Illuminate\Support\Collection;
use Livewire\Volt\Component;
new class extends Component {
new class extends Component
{
// props
public Collection $holdings;
// methods
}; ?>
<div class="">
@@ -23,7 +21,7 @@ new class extends Component {
->take(5)
as $holding
)
<x-list-item
<x-ui.list-item
no-separator
:item="$holding"
link="{{ route('holding.show', [
@@ -36,7 +34,7 @@ new class extends Component {
{{ $holding->market_data?->name }} ({{ $holding->symbol }})
<x-gain-loss-arrow-badge
<x-ui.gain-loss-arrow-badge
:cost-basis="$holding->average_cost_basis"
:market-value="$holding->market_data->market_value"
/>
@@ -45,6 +43,6 @@ new class extends Component {
<x-slot:sub-value>
{{ $holding->portfolio->title }}
</x-slot:sub-value>
</x-list-item>
</x-ui.list-item>
@endforeach
</div>
@@ -1,3 +1,76 @@
<?php
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Livewire\Volt\Component;
new class extends Component
{
/**
* Indicates if user deletion is being confirmed.
*
* @var bool
*/
public $confirmingUserDeletion = false;
/**
* The user's current password.
*
* @var string
*/
public $password = '';
/**
* Confirm that the user would like to delete their account.
*
* @return void
*/
public function confirmUserDeletion()
{
$this->resetErrorBag();
$this->password = '';
$this->dispatch('confirming-delete-user');
$this->confirmingUserDeletion = true;
}
/**
* Delete the current user.
*
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
public function deleteUser(Request $request, StatefulGuard $auth)
{
$this->resetErrorBag();
if (! Hash::check($this->password, Auth::user()->password)) {
throw ValidationException::withMessages([
'password' => [__('This password does not match our records.')],
]);
}
$user = Auth::user()->fresh();
$user->deleteProfilePhoto();
$user->tokens->each->delete();
$user->delete();
$auth->logout();
if ($request->hasSession()) {
$request->session()->invalidate();
$request->session()->regenerateToken();
}
return redirect('/');
}
}; ?>
<x-forms.action-section>
<x-slot name="title">
{{ __('Delete Account') }}
@@ -13,13 +86,13 @@
</div>
<div class="mt-5">
<x-button class="btn-error text-white" wire:click="confirmUserDeletion" wire:loading.attr="disabled">
<x-ui.button class="btn-error text-white" wire:click="confirmUserDeletion" wire:loading.attr="disabled">
{{ __('Delete Account') }}
</x-button>
</x-ui.button>
</div>
<!-- Delete User Confirmation Modal -->
<x-dialog-modal wire:model.live="confirmingUserDeletion">
{{-- Delete User Confirmation Modal --}}
<x-ui.dialog-modal wire:model.live="confirmingUserDeletion">
<x-slot name="title">
{{ __('Delete Account') }}
</x-slot>
@@ -28,7 +101,7 @@
{{ __('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" x-data="{}" x-on:confirming-delete-user.window="setTimeout(() => $refs.password.focus(), 250)">
<x-input type="password" class="mt-1 block w-3/4"
<x-ui.input type="password" class="mt-1 block w-3/4"
autocomplete="current-password"
placeholder="{{ __('Password') }}"
x-ref="password"
@@ -39,14 +112,14 @@
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$toggle('confirmingUserDeletion')" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:click="$toggle('confirmingUserDeletion')" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-button>
</x-ui.button>
<x-button class="ms-3 btn-error text-white" wire:click="deleteUser" wire:loading.attr="disabled">
<x-ui.button class="ms-3 btn-error text-white" wire:click="deleteUser" wire:loading.attr="disabled">
{{ __('Delete Account') }}
</x-button>
</x-ui.button>
</x-slot>
</x-dialog-modal>
</x-ui.dialog-modal>
</x-slot>
</x-forms.action-section>
@@ -54,6 +54,7 @@ new class extends Component
// $this->js('window.location.reload();');
}
}; ?>
<x-forms.form-section submit="updateProfileInformation">
<x-slot name="title">
{{ __('Locale Options') }}
@@ -66,7 +67,7 @@ new class extends Component
<x-slot name="form">
<div class="col-span-6 sm:col-span-4">
<x-select
<x-ui.select
label="{{ __('Locale') }}"
class="select block mt-1 w-full"
:options="config('app.available_locales')"
@@ -75,12 +76,13 @@ new class extends Component
placeholder="Choose a locale"
wire:model="locale"
id="locale"
required
/>
</div>
<div class="col-span-6 sm:col-span-4">
<x-select
<x-ui.select
label="{{ __('Display Currency') }}"
class="select block mt-1 w-full"
:options="$currencies"
@@ -89,6 +91,7 @@ new class extends Component
placeholder="Choose a display currency"
wire:model="display_currency"
id="display_currency"
required
/>
</div>
@@ -100,8 +103,8 @@ new class extends Component
{{ __('Saved.') }}
</x-forms.action-message>
<x-button type="submit">
<x-ui.button type="submit">
{{ __('Save') }}
</x-button>
</x-ui.button>
</x-slot>
</x-forms.form-section>
@@ -1,97 +0,0 @@
<x-forms.action-section>
<x-slot name="title">
{{ __('Browser Sessions') }}
</x-slot>
<x-slot name="description">
{{ __('Manage and log out your active sessions on other browsers and devices.') }}
</x-slot>
<x-slot name="content">
<div class="max-w-xl text-sm text-gray-600 dark:text-gray-400">
{{ __('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>
@if (count($this->sessions) > 0)
<div class="mt-5 space-y-6">
<!-- Other Browser Sessions -->
@foreach ($this->sessions as $session)
<div class="flex items-center">
<div>
@if ($session->agent->isDesktop())
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 text-gray-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
</svg>
@else
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 text-gray-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
@endif
</div>
<div class="ms-3">
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ $session->agent->platform() ? $session->agent->platform() : __('Unknown') }} - {{ $session->agent->browser() ? $session->agent->browser() : __('Unknown') }}
</div>
<div>
<div class="text-xs text-gray-500">
{{ $session->ip_address }},
@if ($session->is_current_device)
<span class="text-green-500 font-semibold">{{ __('This device') }}</span>
@else
{{ __('Last active') }} {{ $session->last_active }}
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
@endif
<div class="flex items-center mt-5">
<x-button type="submit" wire:click="confirmLogout" wire:loading.attr="disabled">
{{ __('Log Out Other Browser Sessions') }}
</x-button>
<x-forms.action-message class="ms-3" on="loggedOut">
{{ __('Done.') }}
</x-forms.action-message>
</div>
<!-- Log Out Other Devices Confirmation Modal -->
<x-dialog-modal wire:model.live="confirmingLogout">
<x-slot name="title">
{{ __('Log Out Other Browser Sessions') }}
</x-slot>
<x-slot name="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" x-data="{}" x-on:confirming-logout-other-browser-sessions.window="setTimeout(() => $refs.password.focus(), 250)">
<x-input type="password" class="mt-1 block w-3/4"
autocomplete="current-password"
placeholder="{{ __('Password') }}"
x-ref="password"
wire:model="password"
wire:keydown.enter="logoutOtherBrowserSessions" />
</div>
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$toggle('confirmingLogout')" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-button>
<x-button type="submit" class="ms-3"
wire:click="logoutOtherBrowserSessions"
wire:loading.attr="disabled">
{{ __('Log Out Other Browser Sessions') }}
</x-button>
</x-slot>
</x-dialog-modal>
</x-slot>
</x-forms.action-section>
+34 -41
View File
@@ -1,46 +1,39 @@
<x-app-layout>
<x-layouts.app>
<div>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
@livewire('profile.update-profile-information-form')
<x-section-border hide-on-mobile />
@endif
<div class="mt-10 sm:mt-0">
@livewire('localization-form')
</div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
@livewire('update-profile-information-form')
<x-section-border hide-on-mobile />
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords()))
<div class="mt-10 sm:mt-0">
@livewire('profile.update-password-form')
</div>
<x-section-border hide-on-mobile />
@endif
@if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())
<div class="mt-10 sm:mt-0">
@livewire('profile.two-factor-authentication-form')
</div>
<x-section-border hide-on-mobile />
@endif
<div class="mt-10 sm:mt-0">
@livewire('profile.logout-other-browser-sessions-form')
</div>
@if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures())
<x-section-border hide-on-mobile />
<div class="mt-10 sm:mt-0">
@livewire('profile.delete-user-form')
</div>
@endif
<x-ui.section-border hide-on-mobile />
@endif
<div class="mt-10 sm:mt-0">
@livewire('localization-form')
</div>
<x-ui.section-border hide-on-mobile />
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords()))
<div class="mt-10 sm:mt-0">
@livewire('update-password-form')
</div>
<x-ui.section-border hide-on-mobile />
@endif
@if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())
<div class="mt-10 sm:mt-0">
@livewire('two-factor-authentication-form')
</div>
<x-ui.section-border hide-on-mobile />
@endif
<div class="mt-10 sm:mt-0">
@livewire('delete-user-form')
</div>
</div>
</x-app-layout>
</x-layouts.app>
@@ -1,3 +1,168 @@
<?php
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Livewire\Volt\Component;
use Laravel\Fortify\Features;
use App\Traits\ConfirmsPasswords;
new class extends Component
{
use ConfirmsPasswords;
/**
* Indicates if two factor authentication QR code is being displayed.
*
* @var bool
*/
public $showingQrCode = false;
/**
* Indicates if the two factor authentication confirmation input and button are being displayed.
*
* @var bool
*/
public $showingConfirmation = false;
/**
* Indicates if two factor authentication recovery codes are being displayed.
*
* @var bool
*/
public $showingRecoveryCodes = false;
/**
* The OTP code for confirming two factor authentication.
*
* @var string|null
*/
public $code;
/**
* Mount the component.
*
* @return void
*/
public function mount()
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm') &&
is_null(Auth::user()->two_factor_confirmed_at)) {
app(DisableTwoFactorAuthentication::class)(Auth::user());
}
}
/**
* Enable two factor authentication for the user.
*
* @return void
*/
public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$enable(Auth::user());
$this->showingQrCode = true;
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
$this->showingConfirmation = true;
} else {
$this->showingRecoveryCodes = true;
}
}
/**
* Confirm two factor authentication for the user.
*
* @return void
*/
public function confirmTwoFactorAuthentication(ConfirmTwoFactorAuthentication $confirm)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$confirm(Auth::user(), $this->code);
$this->showingQrCode = false;
$this->showingConfirmation = false;
$this->showingRecoveryCodes = true;
}
/**
* Display the user's recovery codes.
*
* @return void
*/
public function showRecoveryCodes()
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$this->showingRecoveryCodes = true;
}
/**
* Generate new recovery codes for the user.
*
* @return void
*/
public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$generate(Auth::user());
$this->showingRecoveryCodes = true;
}
/**
* Disable two factor authentication for the user.
*
* @return void
*/
public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $disable)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$disable(Auth::user());
$this->showingQrCode = false;
$this->showingConfirmation = false;
$this->showingRecoveryCodes = false;
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Determine if two factor authentication is enabled.
*
* @return bool
*/
public function getEnabledProperty()
{
return ! empty($this->user->two_factor_secret);
}
}; ?>
<x-forms.action-section>
<x-slot name="title">
{{ __('Two Factor Authentication') }}
@@ -8,7 +173,7 @@
</x-slot>
<x-slot name="content">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
<h3 class="text-lg font-medium">
@if ($this->enabled)
@if ($showingConfirmation)
{{ __('Finish enabling two factor authentication.') }}
@@ -20,7 +185,7 @@
@endif
</h3>
<div class="mt-3 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<div class="mt-3 max-w-xl text-sm">
<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>
@@ -52,7 +217,7 @@
<div class="mt-4">
<x-input id="code" label="{{ __('Code') }}" type="text" name="code" class="block mt-1 w-1/2" inputmode="numeric" autofocus autocomplete="one-time-code"
<x-ui.input id="code" label="{{ __('Code') }}" type="text" name="code" class="block mt-1 w-1/2" inputmode="numeric" autofocus autocomplete="one-time-code"
wire:model="code"
wire:keydown.enter="confirmTwoFactorAuthentication" />
@@ -67,7 +232,7 @@
</p>
</div>
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 dark:bg-gray-900 dark:text-gray-100 rounded-lg">
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-base-100 dark:text-gray-100 rounded-lg">
@foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
<div>{{ $code }}</div>
@endforeach
@@ -78,42 +243,42 @@
<div class="mt-5">
@if (! $this->enabled)
<x-forms.confirms-password wire:then="enableTwoFactorAuthentication">
<x-button type="button" wire:loading.attr="disabled">
<x-ui.button type="button" wire:loading.attr="disabled">
{{ __('Enable') }}
</x-button>
</x-ui.button>
</x-forms.confirms-password>
@else
@if ($showingRecoveryCodes)
<x-forms.confirms-password wire:then="regenerateRecoveryCodes">
<x-button class="btn-outline" class="me-3">
<x-ui.button class="btn-outline" class="me-3">
{{ __('Regenerate Recovery Codes') }}
</x-button>
</x-ui.button>
</x-forms.confirms-password>
@elseif ($showingConfirmation)
<x-forms.confirms-password wire:then="confirmTwoFactorAuthentication">
<x-button type="button" class="me-3" wire:loading.attr="disabled">
<x-ui.button type="button" class="me-3" wire:loading.attr="disabled">
{{ __('Confirm') }}
</x-button>
</x-ui.button>
</x-forms.confirms-password>
@else
<x-forms.confirms-password wire:then="showRecoveryCodes">
<x-button class="btn-outline" class="me-3">
<x-ui.button class="btn-outline" class="me-3">
{{ __('Show Recovery Codes') }}
</x-button>
</x-ui.button>
</x-forms.confirms-password>
@endif
@if ($showingConfirmation)
<x-forms.confirms-password wire:then="disableTwoFactorAuthentication">
<x-button class="btn-outline" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-button>
</x-ui.button>
</x-forms.confirms-password>
@else
<x-forms.confirms-password wire:then="disableTwoFactorAuthentication">
<x-button class="btn-error text-white" wire:loading.attr="disabled">
<x-ui.button class="btn-error text-white" wire:loading.attr="disabled">
{{ __('Disable') }}
</x-button>
</x-ui.button>
</x-forms.confirms-password>
@endif
@@ -1,3 +1,59 @@
<?php
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
use Livewire\Volt\Component;
new class extends Component
{
/**
* The component's state.
*
* @var array
*/
public $state = [
'current_password' => '',
'password' => '',
'password_confirmation' => '',
];
/**
* Update the user's password.
*
* @return void
*/
public function updatePassword(UpdatesUserPasswords $updater)
{
$this->resetErrorBag();
$updater->update(Auth::user(), $this->state);
if (request()->hasSession()) {
request()->session()->put([
'password_hash_'.Auth::getDefaultDriver() => Auth::user()->getAuthPassword(),
]);
}
$this->state = [
'current_password' => '',
'password' => '',
'password_confirmation' => '',
];
$this->dispatch('saved');
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
}; ?>
<x-forms.form-section submit="updatePassword">
<x-slot name="title">
{{ __('Update Password') }}
@@ -11,19 +67,19 @@
<div class="col-span-6 sm:col-span-4">
<x-input id="current_password" label="{{ __('Current Password') }}" type="password" class="mt-1 block w-full" wire:model="state.current_password" error-field="current_password" autocomplete="current-password" />
<x-ui.input id="current_password" label="{{ __('Current Password') }}" type="password" class="mt-1 block w-full" wire:model="state.current_password" error-field="current_password" autocomplete="current-password" />
</div>
<div class="col-span-6 sm:col-span-4">
<x-input id="password" label="{{ __('New Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password" error-field="password" autocomplete="new-password" />
<x-ui.input id="password" label="{{ __('New Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password" error-field="password" autocomplete="new-password" />
</div>
<div class="col-span-6 sm:col-span-4">
<x-input id="password_confirmation" label="{{ __('Confirm Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password_confirmation" error-field="password_confirmation" autocomplete="new-password" />
<x-ui.input id="password_confirmation" label="{{ __('Confirm Password') }}" type="password" class="mt-1 block w-full" wire:model="state.password_confirmation" error-field="password_confirmation" autocomplete="new-password" />
</div>
</x-slot>
@@ -33,8 +89,8 @@
{{ __('Saved.') }}
</x-forms.action-message>
<x-button type="submit">
<x-ui.button type="submit">
{{ __('Save') }}
</x-button>
</x-ui.button>
</x-slot>
</x-forms.form-section>
@@ -1,3 +1,109 @@
<?php
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
new class extends Component
{
use WithFileUploads;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* The new avatar for the user.
*
* @var mixed
*/
public $photo;
/**
* Determine if the verification email was sent.
*
* @var bool
*/
public $verificationLinkSent = false;
/**
* Prepare the component.
*
* @return void
*/
public function mount()
{
$user = Auth::user();
$this->state = array_merge([
'email' => $user->email,
], $user->withoutRelations()->toArray());
}
/**
* Update the user's profile information.
*
* @return \Illuminate\Http\RedirectResponse|null
*/
public function updateProfileInformation(UpdatesUserProfileInformation $updater)
{
$this->resetErrorBag();
$updater->update(
Auth::user(),
$this->photo
? array_merge($this->state, ['photo' => $this->photo])
: $this->state
);
if (isset($this->photo)) {
return redirect()->route('profile.show');
}
$this->dispatch('saved');
$this->dispatch('refresh-navigation-menu');
}
/**
* Delete user's profile photo.
*
* @return void
*/
public function deleteProfilePhoto()
{
Auth::user()->deleteProfilePhoto();
$this->dispatch('refresh-navigation-menu');
}
/**
* Sent the email verification.
*
* @return void
*/
public function sendEmailVerification()
{
Auth::user()->sendEmailVerificationNotification();
$this->verificationLinkSent = true;
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
}; ?>
<x-forms.form-section submit="updateProfileInformation">
<x-slot name="title">
{{ __('Profile Information') }}
@@ -9,63 +115,61 @@
<x-slot name="form">
<!-- Profile Photo -->
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
{{-- Profile Photo --}}
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
<!-- Profile Photo File Input -->
<input type="file" id="photo" class="hidden"
wire:model.live="photo"
x-ref="photo"
x-on:change="
photoName = $refs.photo.files[0].name;
const reader = new FileReader();
reader.onload = (e) => {
photoPreview = e.target.result;
};
reader.readAsDataURL($refs.photo.files[0]);
" />
{{-- Profile Photo File Input --}}
<input type="file" id="photo" class="hidden"
wire:model.live="photo"
x-ref="photo"
x-on:change="
photoName = $refs.photo.files[0].name;
const reader = new FileReader();
reader.onload = (e) => {
photoPreview = e.target.result;
};
reader.readAsDataURL($refs.photo.files[0]);
" />
<label for="photo" class="pt-0 label label-text font-semibold">
<span>{{ __('Photo') }} </span>
</label>
<label for="photo" class="pt-0 label label-text font-semibold">
<span>{{ __('Photo') }} </span>
</label>
<!-- Current Profile Photo -->
<div class="mt-2" x-show="! photoPreview">
<img src="{{ $this->user->profile_photo_url }}" alt="{{ $this->user->name }}" class="rounded-full h-20 w-20 object-cover">
</div>
<!-- New Profile Photo Preview -->
<div class="mt-2" x-show="photoPreview" style="display: none;">
<span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
x-bind:style="'background-image: url(\'' + photoPreview + '\');'">
</span>
</div>
<x-button class="btn-outline" class="mt-2 me-2" type="button" x-on:click.prevent="$refs.photo.click()">
{{ __('Select A New Photo') }}
</x-button>
@if ($this->user->profile_photo_path)
<x-button class="btn-outline" type="button" class="mt-2" wire:click="deleteProfilePhoto">
{{ __('Remove Photo') }}
</x-button>
@endif
@if ($errors->has('photo') && is_array($errors->get('photo')))
<p class="text-sm text-red-600 dark:text-red-400">{{ $errors->get('photo')[0] }}</p>
@endif
{{-- Current Profile Photo --}}
<div class="mt-2" x-show="! photoPreview">
<img src="{{ $this->user->profile_photo_url }}" alt="{{ $this->user->name }}" class="rounded-full h-20 w-20 object-cover">
</div>
@endif
<!-- Name -->
<div class="col-span-6 sm:col-span-4">
<x-input id="name" label="{{ __('Name') }}" type="text" class="mt-1 block w-full" wire:model="state.name" error-field="name" required autocomplete="name" />
{{-- New Profile Photo Preview --}}
<div class="mt-2" x-show="photoPreview" style="display: none;">
<span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
x-bind:style="'background-image: url(\'' + photoPreview + '\');'">
</span>
</div>
<x-ui.button class="btn-outline" class="mt-2 me-2" type="button" x-on:click.prevent="$refs.photo.click()">
{{ __('Select A New Photo') }}
</x-ui.button>
@if ($this->user->profile_photo_path)
<x-ui.button class="btn-outline" type="button" class="mt-2" wire:click="deleteProfilePhoto">
{{ __('Remove Photo') }}
</x-ui.button>
@endif
@if ($errors->has('photo') && is_array($errors->get('photo')))
<p class="text-sm text-red-600 dark:text-red-400">{{ $errors->get('photo')[0] }}</p>
@endif
</div>
<!-- Email -->
{{-- Name --}}
<div class="col-span-6 sm:col-span-4">
<x-input id="email" label="{{ __('Email') }}" type="email" class="mt-1 block w-full" wire:model="state.email" error-field="email" required autocomplete="username" />
<x-ui.input id="name" label="{{ __('Name') }}" type="text" class="mt-1 block w-full" wire:model="state.name" error-field="name" required autocomplete="name" />
</div>
{{-- Email --}}
<div class="col-span-6 sm:col-span-4">
<x-ui.input id="email" label="{{ __('Email') }}" type="email" class="mt-1 block w-full" wire:model="state.email" error-field="email" required autocomplete="username" />
@if (
! config('investbrain.self_hosted')
@@ -94,8 +198,8 @@
{{ __('Saved.') }}
</x-forms.action-message>
<x-button type="submit" spinner="photo" wire:loading.attr="disabled" wire:target="photo">
<x-ui.button type="submit" spinner="photo" wire:loading.attr="disabled" wire:target="photo">
{{ __('Save') }}
</x-button>
</x-ui.button>
</x-slot>
</x-forms.form-section>
+5 -5
View File
@@ -1,13 +1,13 @@
<x-guest-layout>
<div class="pt-4 bg-gray-100 dark:bg-gray-900">
<x-layouts.guest>
<div class="my-22">
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
<div class="w-24 mb-10">
<x-glyph-only-logo />
<x-ui.logo />
</div>
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg prose dark:prose-invert">
<div class="w-full sm:max-w-2xl mt-6 p-6 overflow-hidden sm:rounded-lg prose dark:prose-invert">
{!! $terms !!}
</div>
</div>
</div>
</x-guest-layout>
</x-layouts.guest>
+9 -10
View File
@@ -1,28 +1,27 @@
<x-app-layout>
<x-layouts.app>
<div x-data>
<x-ib-alpine-modal
<x-ui.modal
key="create-transaction"
title="{{ __('Create Transaction') }}"
>
@livewire('manage-transaction-form')
</x-ib-alpine-modal>
</x-ui.modal>
<x-ib-toolbar title="{{ __('All Transactions') }}">
<x-ui.toolbar title="{{ __('All Transactions') }}">
<x-ib-flex-spacer />
<x-ui.flex-spacer />
<div>
<x-button
<x-ui.button
label="{{ __('Create Transaction') }}"
class="btn-sm btn-primary whitespace-nowrap "
@click="$dispatch('toggle-create-transaction')"
/>
</div>
</x-ib-toolbar>
@livewire('transactions-table')
</x-ui.toolbar>
@livewire('datatables.transactions-table')
</div>
</x-app-layout>
</x-layouts.app>
@@ -6,10 +6,10 @@ use App\Models\Portfolio;
use App\Models\Transaction;
use App\Rules\QuantityValidationRule;
use App\Rules\SymbolValidationRule;
use App\Traits\Toast;
use App\Traits\WithTrimStrings;
use Illuminate\Support\Collection;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
new class extends Component
{
@@ -133,11 +133,11 @@ new class extends Component
}; ?>
<div class="" x-data="{ transaction_type: @entangle('transaction_type') }">
<x-ib-form wire:submit="{{ $transaction ? 'update' : 'save' }}" class="">
<x-ui.form wire:submit="{{ $transaction ? 'update' : 'save' }}" class="">
@if(empty($portfolio))
<x-select
<x-ui.select
label="{{ __('Portfolio') }}"
wire:model="portfolio_id"
required
@@ -147,19 +147,19 @@ new class extends Component
/>
@endif
<x-input label="{{ __('Symbol') }}" wire:model="symbol" required />
<x-ui.input label="{{ __('Symbol') }}" wire:model="symbol" required />
<x-select label="{{ __('Transaction Type') }}" :options="[
<x-ui.select label="{{ __('Transaction Type') }}" :options="[
['id' => 'BUY', 'name' => 'Buy'],
['id' => 'SELL', 'name' => 'Sell']
]" wire:model.live="transaction_type" />
<x-datetime label="{{ __('Transaction Date') }}" wire:model="date" required />
<x-ui.datetime label="{{ __('Transaction Date') }}" wire:model="date" required />
<x-input label="{{ __('Quantity') }}" type="number" step="any" wire:model="quantity" required />
<x-ui.input label="{{ __('Quantity') }}" type="number" step="any" wire:model="quantity" required />
@if($transaction_type == 'SELL')
<x-input
<x-ui.input
label="{{ __('Sale Price') }}"
wire:model.number="sale_price"
required
@@ -168,7 +168,7 @@ new class extends Component
>
<x-slot:prepend>
<x-select
<x-ui.select
class="rounded-e-none border-e-0 bg-base-200"
icon="o-banknotes"
:options="$currencies"
@@ -178,9 +178,9 @@ new class extends Component
id="currency"
/>
</x-slot:prepend>
</x-input>
</x-ui.input>
@else
<x-input
<x-ui.input
label="{{ __('Cost Basis') }}"
wire:model.number="cost_basis"
required
@@ -189,7 +189,7 @@ new class extends Component
>
<x-slot:prepend>
<x-select
<x-ui.select
class="rounded-e-none border-e-0 bg-base-200"
icon="o-banknotes"
:options="$currencies"
@@ -200,12 +200,12 @@ new class extends Component
/>
</x-slot:prepend>
</x-input>
</x-ui.input>
@endif
<x-slot:actions>
@if ($transaction)
<x-button
<x-ui.button
wire:click="$toggle('confirmingTransactionDeletion')"
wire:loading.attr="disabled"
class="btn text-error"
@@ -214,7 +214,7 @@ new class extends Component
/>
@endif
<x-button
<x-ui.button
label="{{ $transaction ? __('Update') : __('Create') }}"
type="submit"
icon="o-paper-airplane"
@@ -222,9 +222,9 @@ new class extends Component
spinner="{{ $transaction ? 'update' : 'save' }}"
/>
</x-slot:actions>
</x-ib-form>
</x-ui.form>
<x-confirmation-modal wire:model.live="confirmingTransactionDeletion">
<x-ui.confirmation-modal wire:model.live="confirmingTransactionDeletion">
<x-slot name="title">
{{ __('Delete Transaction') }}
</x-slot>
@@ -234,13 +234,13 @@ new class extends Component
</x-slot>
<x-slot name="footer">
<x-button class="btn-outline" wire:click="$toggle('confirmingTransactionDeletion')" wire:loading.attr="disabled">
<x-ui.button class="btn-outline" wire:click="$toggle('confirmingTransactionDeletion')" wire:loading.attr="disabled">
{{ __('Cancel') }}
</x-secondary-button>
</x-ui.button>
<x-button class="ms-3 btn-error text-white" wire:click="delete" wire:loading.attr="disabled">
<x-ui.button class="ms-3 btn-error text-white" wire:click="delete" wire:loading.attr="disabled">
{{ __('Delete Transaction') }}
</x-button>
</x-ui.button>
</x-slot>
</x-confirmation-modal>
</x-ui.confirmation-modal>
</div>

Some files were not shown because too many files have changed in this diff Show More