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:
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AiChat;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
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()
|
||||
{
|
||||
$this->messages = $this->chatable->chats()->orderByRaw('created_at, id')->limit(25)->get(['role', 'content'])->toArray();
|
||||
}
|
||||
|
||||
public function startCompletion($suggestedPrompt = null)
|
||||
{
|
||||
// prevent spam
|
||||
if ($this->isRateLimited() || $this->streaming) {
|
||||
array_push($this->messages, [
|
||||
'role' => 'assistant',
|
||||
'content' => __('Hang on! You\'re doing that too much.'),
|
||||
]);
|
||||
$this->js('scrollChatWindow(250)');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($suggestedPrompt) {
|
||||
$this->prompt = $suggestedPrompt;
|
||||
}
|
||||
|
||||
if (empty(trim($this->prompt))) {
|
||||
$this->resetPrompt();
|
||||
|
||||
array_push($this->messages, ['role' => 'assistant', 'content' => __('Feel free to ask me a question!')]);
|
||||
$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]);
|
||||
$this->js('scrollChatWindow(250)');
|
||||
|
||||
$this->resetPrompt();
|
||||
|
||||
$this->streaming = true;
|
||||
$this->js('$wire.generateCompletion()');
|
||||
}
|
||||
|
||||
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),
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
$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: $response->choices[0]->delta->content, replace: false);
|
||||
$this->answer .= $response->choices[0]->delta->content;
|
||||
}
|
||||
$this->js('scrollChatWindow()');
|
||||
}
|
||||
|
||||
$this->chatable->chats()->save(new AiChat(['role' => 'assistant', 'content' => $this->answer]));
|
||||
array_push($this->messages, ['role' => 'assistant', 'content' => $this->answer]);
|
||||
$this->resetPrompt();
|
||||
$this->js('$wire.generateSuggestedPrompts()');
|
||||
}
|
||||
|
||||
public function generateSuggestedPrompts(): void
|
||||
{
|
||||
try {
|
||||
$client = $this->createOpenAiClient();
|
||||
|
||||
$suggested_prompts = $client->chat()->create([
|
||||
'model' => config('openai.model'),
|
||||
'response_format' => [
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => [
|
||||
'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)',
|
||||
],
|
||||
'value' => [
|
||||
'type' => 'string',
|
||||
'description' => 'The detailed version of the question',
|
||||
],
|
||||
],
|
||||
'required' => ['text', 'value'],
|
||||
'additionalProperties' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
'required' => ['suggested_prompts'],
|
||||
'additionalProperties' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
'messages' => [
|
||||
['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
|
||||
are based on the provided context. Be sure to keep the questions short!
|
||||
|
||||
The questions you recommend might be based on natural follow up from the given context, requests
|
||||
to further refine a previous response, clarify undefined terms, common decision frameworks,
|
||||
possible risks or benefits, or commonly understood investing concepts that may require additional
|
||||
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)),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->suggested_prompts = json_decode($suggested_prompts->choices[0]->message->content, true)['suggested_prompts'];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
$this->suggested_prompts = [];
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function resetPrompt(): void
|
||||
{
|
||||
$this->answer = null;
|
||||
$this->prompt = null;
|
||||
$this->streaming = false;
|
||||
}
|
||||
|
||||
public function isRateLimited(): bool
|
||||
{
|
||||
$rateLimitKey = auth()->id().'/'.$this->chatable->id;
|
||||
|
||||
if (RateLimiter::tooManyAttempts($rateLimitKey, 20)) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RateLimiter::hit($rateLimitKey, 60);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function createOpenAiClient()
|
||||
{
|
||||
$apiKey = config('openai.api_key');
|
||||
$organization = config('openai.organization');
|
||||
$baseUri = config('openai.base_uri');
|
||||
|
||||
return OpenAI::factory()
|
||||
->withApiKey($apiKey)
|
||||
->withOrganization($organization)
|
||||
->withHttpHeader('OpenAI-Beta', 'assistants=v2')
|
||||
->withHttpClient(new \GuzzleHttp\Client(['timeout' => config('openai.request_timeout', 30)]))
|
||||
->withBaseUri($baseUri)
|
||||
->make();
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
open: false,
|
||||
async scrollChatWindow(delay = 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
this.$refs.chatWindow.scrollBy({
|
||||
top: this.$refs.chatWindow.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}"
|
||||
class="fixed z-50 bottom-8 right-8"
|
||||
>
|
||||
{{-- toggle 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-ui.icon name="o-sparkles" class="w-6 h-6 md:w-8 md:h-8"></x-ui.icon>
|
||||
</x-slot:label>
|
||||
</x-ui.button>
|
||||
|
||||
{{-- popup --}}
|
||||
<div
|
||||
x-on:toggle-ai-chat.window="open = !open"
|
||||
x-show="open"
|
||||
x-trap="open"
|
||||
x-bind:inert="!open"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform translate-y-full"
|
||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 transform translate-y-0"
|
||||
x-transition:leave-end="opacity-0 transform translate-y-full"
|
||||
x-cloak
|
||||
key="ai-chat"
|
||||
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 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') }}"
|
||||
@click="open = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{-- chat window --}}
|
||||
<div class="grow overflow-hidden overflow-y-scroll ai-chat" x-ref="chatWindow">
|
||||
|
||||
<div class="flex gap-3 mb-5 flex-1">
|
||||
<span class="
|
||||
flex
|
||||
rounded-full
|
||||
w-10 h-10
|
||||
border border-gray-600
|
||||
dark:border-gray-400
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
bg-slate-200
|
||||
dark:bg-slate-800
|
||||
">
|
||||
<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?') }}
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@foreach($messages as $message)
|
||||
|
||||
@if ($message['role'] == 'user')
|
||||
<div class="flex gap-3 mb-5 flex-1">
|
||||
<span class="relative flex shrink-0 overflow-hidden rounded-full w-10 h-10">
|
||||
|
||||
<x-ui.avatar :image="auth()->user()->profile_photo_url" class="!w-10" />
|
||||
|
||||
</span>
|
||||
<p class="leading-relaxed">
|
||||
<span class="block font-bold ">{{ __('You') }} </span> {{ $message['content'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@else
|
||||
<div class="flex gap-3 mb-5 flex-1">
|
||||
<span class="
|
||||
flex
|
||||
rounded-full
|
||||
w-10 h-10
|
||||
border border-gray-600
|
||||
dark:border-gray-400
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
bg-slate-200
|
||||
dark:bg-slate-800
|
||||
">
|
||||
<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']) !!}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
|
||||
@if($streaming)
|
||||
<div class="flex gap-3 mb-10 flex-1">
|
||||
<span class="
|
||||
flex
|
||||
rounded-full
|
||||
w-10 h-10
|
||||
border border-gray-600
|
||||
dark:border-gray-400
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
bg-slate-200
|
||||
dark:bg-slate-800
|
||||
">
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- prompt input --}}
|
||||
<div class="mt-3 grow-0">
|
||||
<form submit="startCompletion">
|
||||
<div class="">
|
||||
@foreach($suggested_prompts as $prompt)
|
||||
<x-ui.button
|
||||
class="btn-xs btn-primary btn-outline mr-1 mb-2"
|
||||
wire:click="startCompletion('{{ addslashes($prompt['value']) }}')"
|
||||
>{{ $prompt['text'] }}</x-ui.button>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between align-bottom space-x-2 mt-1">
|
||||
|
||||
<div class="w-full" >
|
||||
|
||||
<x-ui.textarea
|
||||
wire:model="prompt"
|
||||
class="h-18 resize-none bg-base-200"
|
||||
placeholder="{{ __('Have a question? AI might be able to help...') }}"
|
||||
wire:keydown.enter.prevent="startCompletion"
|
||||
autofocus
|
||||
@toggle-ai-chat.window="setTimeout(() => $el.focus(), 250)"
|
||||
></x-ui.textarea>
|
||||
{{-- --}}
|
||||
</div>
|
||||
<x-ui.button
|
||||
spinner="generateCompletion"
|
||||
wire:click="startCompletion"
|
||||
class="btn btn-ghost h-32"
|
||||
icon="o-paper-airplane"
|
||||
></x-ui.button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</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,176 @@
|
||||
@props(['seriesData' => [], 'name' => 'apex-chart' ])
|
||||
|
||||
@php
|
||||
$seriesData = array_merge([
|
||||
'chart' => [
|
||||
'type' => "area",
|
||||
'stacked' => false,
|
||||
'height' => 300,
|
||||
'foreColor' => "#999",
|
||||
'dropShadow' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'toolbar' => [
|
||||
'show' => false,
|
||||
],
|
||||
'zoom' => [
|
||||
'enabled' => false
|
||||
]
|
||||
],
|
||||
'colors' => ['#3185FC', '#48435C', '#9792E3', '#00E396', '#B74F6F'],
|
||||
'stroke' => [
|
||||
'curve' => "smooth",
|
||||
'width' => 3
|
||||
],
|
||||
'dataLabels' => [
|
||||
'enabled' => false
|
||||
],
|
||||
'markers' => [
|
||||
'size' => 0,
|
||||
'strokeColor' => "#fff",
|
||||
'strokeWidth' => 3,
|
||||
'strokeOpacity' => 1,
|
||||
'fillOpacity' => 1,
|
||||
'hover' => [
|
||||
'size' => 6
|
||||
]
|
||||
],
|
||||
'xaxis' => [
|
||||
'type' => "datetime",
|
||||
'axisBorder' => [
|
||||
'show' => false
|
||||
],
|
||||
'axisTicks' => [
|
||||
'show' => false
|
||||
],
|
||||
'labels' => [
|
||||
'offsetX' => 15,
|
||||
'offsetY' => 0
|
||||
],
|
||||
'tooltip' => [
|
||||
'enabled' => false
|
||||
]
|
||||
],
|
||||
'yaxis' => [
|
||||
'labels' => [
|
||||
'offsetX' => -10,
|
||||
'offsetY' => -10
|
||||
],
|
||||
'tooltip' => [
|
||||
'enabled' => false
|
||||
]
|
||||
],
|
||||
'grid' => [
|
||||
'strokeColor' => "#000",
|
||||
'padding' => [
|
||||
'top' => 0,
|
||||
'left' => -30,
|
||||
'right' => 0,
|
||||
'bottom' => -5
|
||||
]
|
||||
],
|
||||
'legend' => [
|
||||
'show' => false,
|
||||
],
|
||||
|
||||
], $seriesData);
|
||||
$seriesData = json_encode($seriesData)
|
||||
@endphp
|
||||
|
||||
<div
|
||||
id="chart"
|
||||
wire:key="{{ rand() }}"
|
||||
x-data="{
|
||||
data: {{ $seriesData }},
|
||||
init(){
|
||||
|
||||
this.data.chart.events = {
|
||||
mounted: function (chartContext, config) {
|
||||
renderLegend(chartContext);
|
||||
},
|
||||
{{-- updated: function (chartContext, config) {
|
||||
renderLegend(chartContext);
|
||||
} --}}
|
||||
}
|
||||
|
||||
this.data.yaxis.labels.formatter = function (value) {
|
||||
return `{{ Number::currencySymbol(auth()->user()->getCurrency()) }}${value}`
|
||||
}
|
||||
|
||||
this.data.tooltip = {
|
||||
enabled: true,
|
||||
y: {
|
||||
formatter: (value, { series, seriesIndex, dataPointIndex, w }) => {
|
||||
const firstDataPoint = this.data.series[seriesIndex].data[0][1]
|
||||
const percentageChange = ((value - firstDataPoint) / firstDataPoint) * 100;
|
||||
return `${parseFloat(value.toFixed(2))} (${percentageChange.toFixed(2)}%)`;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var chart = new ApexCharts(document.querySelector('#chart-{{ $name }}'), this.data);
|
||||
|
||||
chart.render();
|
||||
|
||||
{{-- // reset custom zoom button
|
||||
var resetZoomButton = document.querySelector('#chart-reset-zoom-{{ $name }}');
|
||||
resetZoomButton.addEventListener('click', function () {
|
||||
chart.resetSeries()
|
||||
}); --}}
|
||||
|
||||
// generate custom legend view
|
||||
function renderLegend(chartContext) {
|
||||
|
||||
var legendContainer = document.querySelector('#chart-legend-{{ $name }}');
|
||||
|
||||
if (!legendContainer) return;
|
||||
|
||||
legendContainer.innerHTML = ''; // Clear any existing legend items
|
||||
|
||||
chartContext.w.globals.seriesNames.forEach(function (seriesName, i) {
|
||||
|
||||
var seriesColor = chartContext.w.config.colors[i];
|
||||
var legendItem = document.createElement('div');
|
||||
legendItem.classList.add('flex', 'items-center', 'my-2', 'mr-4', 'text-xs', 'md:text-sm', 'cursor-pointer');
|
||||
legendItem.setAttribute('data-series-index', i);
|
||||
|
||||
var colorBox = document.createElement('span');
|
||||
colorBox.id = seriesName
|
||||
colorBox.classList.add('w-4', 'h-4', 'inline-block', 'mr-2');
|
||||
colorBox.style.backgroundColor = seriesColor;
|
||||
|
||||
var labelText = document.createElement('span');
|
||||
labelText.textContent = seriesName;
|
||||
|
||||
legendItem.appendChild(colorBox);
|
||||
legendItem.appendChild(labelText);
|
||||
legendContainer.appendChild(legendItem);
|
||||
|
||||
// Initial visibility state
|
||||
var isCollapsed = chartContext.w.globals.collapsedSeriesIndices.includes(i);
|
||||
if (isCollapsed) {
|
||||
legendItem.classList.add('opacity-50');
|
||||
}
|
||||
|
||||
legendItem.addEventListener('click', function () {
|
||||
|
||||
var seriesIndex = parseInt(this.getAttribute('data-series-index'), 10);
|
||||
var isCurrentlyCollapsed = chartContext.w.globals.collapsedSeriesIndices.includes(seriesIndex);
|
||||
|
||||
chart.toggleSeries(chartContext.w.globals.seriesNames[seriesIndex]);
|
||||
|
||||
if (isCurrentlyCollapsed) {
|
||||
this.classList.remove('opacity-50');
|
||||
} else {
|
||||
this.classList.add('opacity-50');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}"
|
||||
>
|
||||
<div id="chart-{{ $name }}" class="apex-chart"></div>
|
||||
</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>
|
||||
@@ -0,0 +1,34 @@
|
||||
@props(['key' => 'confirmation'])
|
||||
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ms-4 sm:text-start">
|
||||
<h3 class="text-xl font-bold text-primary-content">
|
||||
{{ $title }}
|
||||
</h3>
|
||||
|
||||
<div class="mt-2 text-sm text-secondary-content">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center justify-center sm:justify-end mt-8 text-end">
|
||||
{{ $footer }}
|
||||
</div>
|
||||
</div>
|
||||
</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,24 @@
|
||||
<span
|
||||
class=""
|
||||
style="width:90em;overflow: hidden; white-space: nowrap;"
|
||||
title="{{ Number::currency($marketData->fifty_two_week_low ?? 0, $marketData->currency) }} - {{ Number::currency($marketData->fifty_two_week_high ?? 0, $marketData->currency) }}"
|
||||
>
|
||||
|
||||
@php
|
||||
// 52-week low must be a non-zero
|
||||
if (empty($marketData->fifty_two_week_low)) {
|
||||
$marketData->fifty_two_week_low = 1;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@for ($x = 0; $x < 10; $x++)
|
||||
@if ((($marketData->market_value - $marketData->fifty_two_week_low) * 100) / ($marketData->fifty_two_week_high - $marketData->fifty_two_week_low) > ($x * 10))
|
||||
|
||||
●
|
||||
|
||||
@else
|
||||
|
||||
○
|
||||
@endif
|
||||
@endfor
|
||||
</span>
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="grow"></div>
|
||||
@@ -0,0 +1,21 @@
|
||||
@props([
|
||||
'noSeparator' => false,
|
||||
])
|
||||
|
||||
<form
|
||||
{{ $attributes->whereDoesntStartWith('class') }}
|
||||
{{ $attributes->class(['grid grid-flow-row auto-rows-min gap-3']) }}
|
||||
>
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@if ($actions)
|
||||
@if(!$noSeparator)
|
||||
<x-ui.section-border class="my-3" />
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
{{ $actions}}
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
@@ -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 ? '▲' :'▼' !!}
|
||||
{{ 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>
|
||||
@@ -0,0 +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/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>
|
||||
@@ -0,0 +1,60 @@
|
||||
@props([
|
||||
'uuid' => md5(rand()),
|
||||
'label' => '',
|
||||
'hint' => '',
|
||||
'errorField' => '',
|
||||
'rows' => 4
|
||||
])
|
||||
|
||||
<div {{ $attributes->class([]) }}>
|
||||
{{-- STANDARD LABEL --}}
|
||||
@if($label)
|
||||
<label for="{{ $uuid }}" class="pt-0 label label-text font-semibold">
|
||||
<span>
|
||||
{{ $label }}
|
||||
|
||||
@if($attributes->get('required'))
|
||||
<span class="text-error">*</span>
|
||||
@endif
|
||||
</span>
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<textarea {{ $attributes
|
||||
->merge([
|
||||
'id' => $uuid
|
||||
])
|
||||
->class([
|
||||
'textarea textarea-primary w-full peer',
|
||||
'border border-dashed' => $attributes->has('readonly') && $attributes->get('readonly') == true,
|
||||
'textarea-error' => $errors->has($errorField),
|
||||
])
|
||||
}}
|
||||
x-data="{
|
||||
resize (rows) {
|
||||
$el.style.height = '0px';
|
||||
$el.style.height = ($el.scrollHeight >= rows * 32 ? $el.scrollHeight : rows * 32) + 'px';
|
||||
}
|
||||
}"
|
||||
x-init="resize({{$rows}})"
|
||||
@input="resize({{$rows}})"
|
||||
type="text"
|
||||
placeholder = "{{ $attributes->whereStartsWith('placeholder')->first() }}"
|
||||
>{{ $slot }}</textarea>
|
||||
|
||||
@if($errors->has($errorField))
|
||||
@foreach($errors->get($errorField) 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 x-classes="label-text-alt text-gray-400 py-1 pb-0">{{ $hint }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
@props(['title' => ''])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'flex items-center mb-6']) }} class="">
|
||||
<h1 class="text-2xl font-medium mr-3 truncate"> {{ $title }} </h1>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
Reference in New Issue
Block a user