Add all files
This commit is contained in:
62
themes/default/views/components/admin-footer.blade.php
Normal file
62
themes/default/views/components/admin-footer.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<div class="ring-1 ring-gray-950/5 dark:ring-white/10">
|
||||
</div>
|
||||
|
||||
<span class="text-xs text-gray-400 mt-auto">
|
||||
Powered by <a href="https://paymenter.org" target="_blank" class="text-gray-400">Paymenter ©
|
||||
{{ date('Y') }}</a>
|
||||
</span>
|
||||
|
||||
<ul class="fi-sidebar-nav-groups -mx-2 flex flex-col gap-y-7">
|
||||
<li class="fi-sidebar-group flex flex-col gap-y-1">
|
||||
<ul class="fi-sidebar-group-items flex flex-col gap-y-1">
|
||||
<li class="fi-sidebar-item">
|
||||
<a href="https://github.com/sponsors/Paymenter" target="_blank" class="fi-sidebar-item-button relative flex items-center justify-center gap-x-3 rounded-lg px-2 py-2 outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5">
|
||||
<x-ri-service-fill class="fi-sidebar-item-icon h-6 w-6 sponsor" />
|
||||
<span class="fi-sidebar-item-label flex-1 truncate text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
Sponsor
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="fi-sidebar-item">
|
||||
<a href="https://github.com/Paymenter/Paymenter" target="_blank" class="fi-sidebar-item-button relative flex items-center justify-center gap-x-3 rounded-lg px-2 py-2 outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5">
|
||||
<x-ri-star-fill class="fi-sidebar-item-icon h-6 w-6 star-git" />
|
||||
<span class="fi-sidebar-item-label flex-1 truncate text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
Star us on GitHub
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="fi-sidebar-item">
|
||||
<a href="https://paymenter.org/docs/getting-started/introduction" target="_blank" class="fi-sidebar-item-button relative flex items-center justify-center gap-x-3 rounded-lg px-2 py-2 outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5">
|
||||
<x-ri-book-2-line class="fi-sidebar-item-icon h-6 w-6 text-gray-400 dark:text-gray-500" />
|
||||
<span class="fi-sidebar-item-label flex-1 truncate text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
Documentation
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.sponsor {
|
||||
fill: #DB61A2;
|
||||
}
|
||||
|
||||
.star-git {
|
||||
fill: #E3B341;
|
||||
}
|
||||
</style>
|
||||
3
themes/default/views/components/button/danger.blade.php
Normal file
3
themes/default/views/components/button/danger.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<x-button.primary {{ $attributes->merge(['class' => 'bg-red-700 bg-none text-white py-2 px-4 rounded hover:bg-red-600'])}}>
|
||||
{{ $slot }}
|
||||
</x-button.primary>
|
||||
14
themes/default/views/components/button/primary.blade.php
Normal file
14
themes/default/views/components/button/primary.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<button
|
||||
{{ $attributes->merge(['class' => 'flex items-center gap-2 justify-center bg-primary text-white text-sm font-semibold hover:bg-primary/80 py-2.5 lg:py-2 px-4.5 rounded-md w-full duration-300 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50']) }}>
|
||||
@if (isset($type) && $type === 'submit')
|
||||
<div role="status" wire:loading>
|
||||
<x-ri-loader-5-fill aria-hidden="true" class="size-6 me-2 fill-background animate-spin" />
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div wire:loading.remove>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@else
|
||||
{{ $slot }}
|
||||
@endif
|
||||
</button>
|
||||
28
themes/default/views/components/button/save.blade.php
Normal file
28
themes/default/views/components/button/save.blade.php
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
<!-- Show saved! when the form is saved -->
|
||||
<div class="flex flex-row items-center">
|
||||
<x-button.primary {{ $attributes }}>
|
||||
{{ $slot }}
|
||||
</x-button.primary>
|
||||
|
||||
<div class="flex items-center justify-center ml-2 text-green-500 hidden opacity-0 transition-opacity ease-in-out delay-150 duration-300 mt-4" id="saved">
|
||||
{{ __('Saved!') }}
|
||||
</div>
|
||||
@script
|
||||
<script>
|
||||
$wire.on('saved', function () {
|
||||
setTimeout(() => {
|
||||
document.getElementById('saved').classList.remove('hidden');
|
||||
}, 50);
|
||||
setTimeout(() => {
|
||||
document.getElementById('saved').classList.remove('opacity-0');
|
||||
document.getElementById('saved').classList.add('opacity-100');
|
||||
}, 70);
|
||||
setTimeout(() => {
|
||||
document.getElementById('saved').classList.remove('opacity-100');
|
||||
document.getElementById('saved').classList.add('opacity-0');
|
||||
}, 2500);
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
||||
14
themes/default/views/components/button/secondary.blade.php
Normal file
14
themes/default/views/components/button/secondary.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<button
|
||||
{{ $attributes->merge(['class' => 'flex items-center gap-2 justify-center bg-background-secondary text-sm font-semibold border border-neutral hover:bg-background-secondary/80 py-2.5 lg:py-2 px-4.5 rounded-md w-full duration-300 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50']) }}>
|
||||
@if (isset($type) && $type === 'submit')
|
||||
<div role="status" wire:loading>
|
||||
<x-ri-loader-5-fill aria-hidden="true" class="size-6 me-2 fill-background animate-spin" />
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div wire:loading.remove>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@else
|
||||
{{ $slot }}
|
||||
@endif
|
||||
</button>
|
||||
23
themes/default/views/components/captcha/hcaptcha.blade.php
Normal file
23
themes/default/views/components/captcha/hcaptcha.blade.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<script src="https://js.hcaptcha.com/1/api.js?render=explicit&onload=captchaOnload" async defer></script>
|
||||
<div id="h-captcha" data-sitekey="{{ config('settings.captcha_site_key') }}"></div>
|
||||
|
||||
<script>
|
||||
function captchaOnload() {
|
||||
// On livewire validation error reset captcha
|
||||
Livewire.hook('request', ({
|
||||
succeed,
|
||||
fail
|
||||
}) => {
|
||||
succeed(() => {
|
||||
hcaptcha.reset();
|
||||
});
|
||||
})
|
||||
|
||||
hcaptcha.render('h-captcha', {
|
||||
sitekey: '{{ config('settings.captcha_site_key') }}',
|
||||
callback: function(token) {
|
||||
@this.set('captcha', token, false)
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
18
themes/default/views/components/captcha/index.blade.php
Normal file
18
themes/default/views/components/captcha/index.blade.php
Normal file
@@ -0,0 +1,18 @@
|
||||
@if (config('settings.captcha') !== 'disabled')
|
||||
<div class="flex flex-col justify-center mt-2">
|
||||
<div wire:ignore>
|
||||
@if (config('settings.captcha') == 'recaptcha-v2')
|
||||
<x-captcha.recaptcha-v2 :$form />
|
||||
@elseif(config('settings.captcha') == 'recaptcha-v3')
|
||||
<x-captcha.recaptcha-v3 :$form />
|
||||
@elseif(config('settings.captcha') == 'turnstile')
|
||||
<x-captcha.turnstile :$form />
|
||||
@elseif(config('settings.captcha') == 'hcaptcha')
|
||||
<x-captcha.hcaptcha :$form />
|
||||
@endif
|
||||
</div>
|
||||
@error('captcha')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,21 @@
|
||||
<script src="https://www.google.com/recaptcha/api.js?render=explicit&onload=captchaOnload" async defer></script>
|
||||
<div id="g-recaptcha" data-sitekey="{{ config('settings.captcha_site_key') }}"></div>
|
||||
|
||||
<script>
|
||||
function captchaOnload() {
|
||||
// On livewire validation error reset captcha
|
||||
Livewire.hook('request', ({
|
||||
succeed,
|
||||
fail
|
||||
}) => {
|
||||
succeed(() => grecaptcha.reset());
|
||||
})
|
||||
|
||||
grecaptcha.render('g-recaptcha', {
|
||||
sitekey: '{{ config('settings.captcha_site_key') }}',
|
||||
callback: function(token) {
|
||||
@this.set('captcha', token, false)
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script src="https://www.google.com/recaptcha/api.js?render={{ config('settings.captcha_site_key') }}" async defer>
|
||||
</script>
|
||||
<div id="g-recaptcha" data-sitekey="{{ config('settings.captcha_site_key') }}"></div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('livewire:initialized', () => {
|
||||
const submitBtn = document.querySelector('#{{ $form }} button[type="submit"]')
|
||||
|
||||
submitBtn.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
grecaptcha.ready(function() {
|
||||
grecaptcha.execute('{{ config('settings.captcha_site_key') }}', {
|
||||
action: 'submit'
|
||||
}).then(function(token) {
|
||||
@this.set('captcha', token, false)
|
||||
@this.submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
34
themes/default/views/components/captcha/turnstile.blade.php
Normal file
34
themes/default/views/components/captcha/turnstile.blade.php
Normal file
@@ -0,0 +1,34 @@
|
||||
@assets
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
|
||||
@endassets
|
||||
|
||||
<div id="cf-turnstile"></div>
|
||||
|
||||
<script>
|
||||
function renderTurnstile() {
|
||||
const isDarkMode = localStorage.getItem('_x_darkMode') === 'true';
|
||||
const theme = isDarkMode ? 'dark' : 'light';
|
||||
|
||||
turnstile.render('#cf-turnstile', {
|
||||
sitekey: '{{ config('settings.captcha_site_key') }}',
|
||||
size: 'flexible',
|
||||
theme: theme,
|
||||
callback: token => @this.set('captcha', token, false),
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('livewire:initialized', () => {
|
||||
// On livewire validation error reset captcha
|
||||
Livewire.hook('request', ({ succeed }) => {
|
||||
succeed(() => {
|
||||
turnstile.reset();
|
||||
});
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
@script
|
||||
<script>
|
||||
renderTurnstile();
|
||||
</script>
|
||||
@endscript
|
||||
7
themes/default/views/components/cart.blade.php
Normal file
7
themes/default/views/components/cart.blade.php
Normal file
@@ -0,0 +1,7 @@
|
||||
@if($cartCount > 0)
|
||||
<div class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-neutral transition">
|
||||
<x-navigation.link :href="route('cart')">
|
||||
<x-ri-shopping-bag-4-fill class="size-4" />
|
||||
</x-navigation.link>
|
||||
</div>
|
||||
@endif
|
||||
10
themes/default/views/components/center.blade.php
Normal file
10
themes/default/views/components/center.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="flex min-h-[100vh]">
|
||||
<div class="justify-center flex flex-1 min-h-full flex-col mx-4">
|
||||
<div class="mx-auto container">
|
||||
<div
|
||||
class="mx-auto flex flex-col gap-2 mt-4 shadow-sm px-6 sm:px-14 pb-10 bg-primary-800 rounded-md xl:max-w-[700px] w-full">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<strong class="block p-2 text-xs font-semibold uppercase text-base/50"> Currency </strong>
|
||||
<x-select wire:model.live="currentCurrency" :options="$this->currencies" placeholder="Select currency" />
|
||||
</div>
|
||||
33
themes/default/views/components/dropdown.blade.php
Normal file
33
themes/default/views/components/dropdown.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<div class="relative" x-data="{ open: false, adjustWidth: 0 }" x-init="$watch('open', value => {
|
||||
if (value) {
|
||||
adjustWidth = 0; // Reset adjustWidth when opening
|
||||
$nextTick(() => {
|
||||
let dropdown = $refs.dropdown;
|
||||
let rect = dropdown.getBoundingClientRect();
|
||||
let windowWidth = window.innerWidth;
|
||||
adjustWidth = rect.right > windowWidth ? rect.width - 40 : 0;
|
||||
});
|
||||
}
|
||||
})">
|
||||
|
||||
<button
|
||||
class="flex flex-row items-center px-2 py-1 text-sm font-semibold whitespace-nowrap text-base hover:text-base/80"
|
||||
x-on:click="open = !open">
|
||||
{{ $trigger }}
|
||||
<x-ri-arrow-down-s-line x-bind:class="{ '-rotate-180' : open }"
|
||||
class="md:block hidden size-4 text-base ease-out duration-300" />
|
||||
</button>
|
||||
|
||||
<div x-ref="dropdown"
|
||||
class="absolute mt-2 w-48 px-2 py-1 bg-background-secondary rounded-md shadow-lg z-10 border border-neutral"
|
||||
x-bind:style="{
|
||||
left: `-${adjustWidth}px`,
|
||||
}"
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-90"
|
||||
x-on:click.outside="open = false" x-cloak>
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
69
themes/default/views/components/easymde-editor.blade.php
Normal file
69
themes/default/views/components/easymde-editor.blade.php
Normal file
@@ -0,0 +1,69 @@
|
||||
@assets
|
||||
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
|
||||
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
|
||||
@endassets
|
||||
@script
|
||||
<script>
|
||||
const editor = new EasyMDE({
|
||||
element: document.getElementById('editor'),
|
||||
spellChecker: false,
|
||||
previewImagesInEditor: true,
|
||||
uploadImage: false,
|
||||
autoDownloadFontAwesome: false,
|
||||
status: [{
|
||||
className: 'upload-image',
|
||||
defaultValue: '',
|
||||
}],
|
||||
toolbar: [{
|
||||
name: 'bold',
|
||||
action: EasyMDE.toggleBold,
|
||||
}, {
|
||||
name: 'italic',
|
||||
action: EasyMDE.toggleItalic,
|
||||
}, {
|
||||
name: 'strikethrough',
|
||||
action: EasyMDE.toggleStrikethrough,
|
||||
}, {
|
||||
name: 'link',
|
||||
action: EasyMDE.drawLink,
|
||||
}, '|',
|
||||
{
|
||||
name: 'heading',
|
||||
action: EasyMDE.toggleHeadingSmaller,
|
||||
}, '|',
|
||||
{
|
||||
name: 'quote',
|
||||
action: EasyMDE.toggleBlockquote,
|
||||
}, {
|
||||
name: 'code',
|
||||
action: EasyMDE.toggleCodeBlock,
|
||||
|
||||
}, {
|
||||
name: 'unordered-list',
|
||||
action: EasyMDE.toggleUnorderedList,
|
||||
}, {
|
||||
name: 'ordered-list',
|
||||
action: EasyMDE.toggleOrderedList,
|
||||
}, '|',
|
||||
{
|
||||
name: 'undo',
|
||||
action: EasyMDE.undo,
|
||||
}, {
|
||||
name: 'redo',
|
||||
action: EasyMDE.redo,
|
||||
},
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
editor.codemirror.on('change', function() {
|
||||
@this.set('message', editor.value(), false);
|
||||
});
|
||||
|
||||
// Listen for event called saved
|
||||
$wire.on('saved', () => {
|
||||
editor.clearAutosavedValue();
|
||||
editor.value('');
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
17
themes/default/views/components/form/checkbox.blade.php
Normal file
17
themes/default/views/components/form/checkbox.blade.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<div class="flex items-center {{ $divClass ?? '' }}">
|
||||
{{-- <input type="hidden" name="{{ $name }}" value="0" /> --}}
|
||||
<input type="checkbox" name="{{ $name }}" id="{{ $id ?? $name }}"
|
||||
{{ $attributes->except(['label', 'name', 'id', 'class', 'divClass', 'required']) }}
|
||||
class="form-checkbox size-4 text-primary rounded focus:ring-secondary hover:bg-secondary ring-offset-primary-800 focus:ring-2 bg-background-secondary border-neutral" />
|
||||
<label class="ml-2 text-sm text-primary-100" for="{{ $id ?? $name }}">
|
||||
@if(isset($label))
|
||||
{{ $label }}
|
||||
@else
|
||||
{{ $slot }}
|
||||
@endif
|
||||
</label>
|
||||
|
||||
@error($name)
|
||||
<p class="text-red-500 text-xs">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
115
themes/default/views/components/form/configoption.blade.php
Normal file
115
themes/default/views/components/form/configoption.blade.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
@switch($config->type)
|
||||
@case('select')
|
||||
<x-form.select name="{{ $name }}" :label="__($config->label ?? $config->name)" :required="$config->required ?? false"
|
||||
:selected="config('configs.' . $config->name)" :multiple="$config->multiple ?? false"
|
||||
wire:model.live="{{ $name }}" :placeholder="$config->placeholder ?? ''">
|
||||
{{ $slot }}
|
||||
</x-form.select>
|
||||
@break
|
||||
|
||||
@case('slider')
|
||||
<div x-data="{
|
||||
options: @js($config->children->map(fn($child) => ['option' => $child->name, 'value' => $child->id, 'price' => ($showPriceTag && $child->price(billing_period: $plan->billing_period, billing_unit: $plan->billing_unit)->available) ? (string)$child->price(billing_period: $plan->billing_period, billing_unit: $plan->billing_unit) : ''])),
|
||||
showPriceTag: @js($showPriceTag),
|
||||
selectedOption: 0,
|
||||
backendOption: $wire.entangle('{{ $name }}').live,
|
||||
progressOption: '0%',
|
||||
segmentsWidthOption: '0%',
|
||||
|
||||
init() {
|
||||
const initialValue = this.$wire.get('{{ $name }}');
|
||||
const foundIndex = this.options.findIndex(plan => plan.value == initialValue);
|
||||
if (foundIndex !== -1) {
|
||||
this.selectedOption = foundIndex;
|
||||
}
|
||||
this.updateSliderVisuals();
|
||||
$watch('selectedOption', Alpine.debounce(() => this.backendOption = this.options[this.selectedOption].value, 300))
|
||||
},
|
||||
|
||||
updateSliderVisuals() {
|
||||
this.progressOption = `${(this.selectedOption / (this.options.length - 1)) * 100}%`;
|
||||
this.segmentsWidthOption = `${100 / (this.options.length - 1)}%`;
|
||||
},
|
||||
|
||||
setOptionValue(index) {
|
||||
this.selectedOption = parseInt(index);
|
||||
this.updateSliderVisuals();
|
||||
}
|
||||
}" class="flex flex-col gap-1 relative">
|
||||
<label for="{{ $name }}"
|
||||
class="text-sm text-primary-100 w-fit start-1 ml-2 bg-background-secondary rounded-md px-2">
|
||||
{{ $config->label ?? $config->name }}
|
||||
</label>
|
||||
<div class="relative flex items-center" :style="`--progress:${progressOption};--segments-width:${segmentsWidthOption}`" wire:ignore>
|
||||
<div class="
|
||||
absolute left-2.5 right-2.5 h-1.5 bg-background-secondary rounded-full overflow-hidden transition-all duration-500 ease-in-out
|
||||
before:absolute before:inset-0 before:bg-primary
|
||||
before:[mask-image:_linear-gradient(to_right,theme(colors.white),theme(colors.white)_var(--progress),transparent_var(--progress))]
|
||||
[&[x-cloak]]:hidden" aria-hidden="true" x-cloak></div>
|
||||
<input class="
|
||||
relative appearance-none cursor-pointer w-full bg-transparent focus:outline-none transition-all duration-500 ease-in-out
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5
|
||||
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-none
|
||||
[&::-webkit-slider-thumb]:focus:ring-0 [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5
|
||||
[&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-base [&::-moz-range-thumb]:border-none
|
||||
[&::-moz-range-thumb]:shadow-none [&::-moz-range-thumb]:focus:ring-0
|
||||
" type="range" min="0" :max="options.length - 1" x-model="selectedOption" @input="setOptionValue(selectedOption)" aria-label="Option Slider" name="{{ $name }}" id="{{ $name }}" />
|
||||
</div>
|
||||
<!-- Options -->
|
||||
<ul class="flex justify-between text-xs font-medium text-light px-2.5">
|
||||
@foreach($config->children as $child)
|
||||
<li class="relative @if($showPriceTag) pb-7 @else pb-2 @endif">
|
||||
<button @click="setOptionValue({{ $loop->index }})" class="absolute flex flex-col items-center -translate-x-1/2">
|
||||
<span class="text-sm font-semibold">
|
||||
{{ $child->name }}
|
||||
</span>
|
||||
@if($showPriceTag)
|
||||
<span class="text-sm font-semibold">
|
||||
{{ ($showPriceTag && $child->price(billing_period: $plan->billing_period, billing_unit: $plan->billing_unit)->available) ? $child->price(billing_period: $plan->billing_period, billing_unit: $plan->billing_unit) : '' }}
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@break
|
||||
|
||||
@case('text')
|
||||
@case('password')
|
||||
|
||||
@case('email')
|
||||
@case('number')
|
||||
|
||||
@case('color')
|
||||
@case('file')
|
||||
<x-form.input name="{{ $name }}" :type="$config->type" :label="__($config->label ?? $config->name)"
|
||||
:placeholder="$config->default ?? ''" :required="$config->required ?? false" wire:model.live="{{ $name }}" :placeholder="$config->placeholder ?? ''" />
|
||||
@break
|
||||
|
||||
@case('checkbox')
|
||||
<x-form.checkbox name="{{ $name }}" type="checkbox" :label="__($config->label ?? $config->name)"
|
||||
:required="$config->required ?? false" :checked="config('configs.' . $config->name) ? true : false" wire:model.live="{{ $name }}" />
|
||||
@break
|
||||
|
||||
@case('radio')
|
||||
<x-form.radio name="{{ $name }}" :label="__($config->label ?? $config->name)"
|
||||
:selected="config('configs.' . $config->name)" :required="$config->required ?? false" wire:model.live="{{ $name }}">
|
||||
{{ $slot }}
|
||||
</x-form.radio>
|
||||
@break
|
||||
|
||||
@default
|
||||
@endswitch
|
||||
@isset($config->description)
|
||||
@isset($config->link)
|
||||
<a href="{{ $config->link }}" class="text-xs text-primary-500 hover:underline hover:text-secondary group">
|
||||
{{ $config->description }}
|
||||
<x-ri-arrow-right-long-line class="ml-1 size-3 inline-block -rotate-45 group-hover:rotate-0 transition" />
|
||||
</a>
|
||||
@else
|
||||
<p class="text-xs text-primary-500">{{ $config->description }}</p>
|
||||
@endisset
|
||||
@endisset
|
||||
</div>
|
||||
22
themes/default/views/components/form/input.blade.php
Normal file
22
themes/default/views/components/form/input.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@props(['name', 'label' => null, 'required' => false, 'divClass' => null, 'class' => null,'placeholder' => null, 'id' => null, 'type' => null, 'hideRequiredIndicator' => false, 'dirty' => false])
|
||||
<fieldset class="flex flex-col relative mt-3 w-full {{ $divClass ?? '' }}">
|
||||
@if ($label)
|
||||
<legend>
|
||||
<label for="{{ $name }}"
|
||||
class="text-sm text-primary-100 absolute -translate-y-1/2 start-1 ml-1 bg-background-secondary px-2 rounded-md">
|
||||
{{ $label }}
|
||||
@if ($required && !$hideRequiredIndicator)
|
||||
<span class="text-red-500">*</span>
|
||||
@endif
|
||||
</label>
|
||||
</legend>
|
||||
@endif
|
||||
<input type="{{ $type ?? 'text' }}" id="{{ $id ?? $name }}" name="{{ $name }}"
|
||||
class="block w-full text-sm text-base bg-background-secondary border border-neutral rounded-md shadow-sm focus:outline-none transition-all duration-300 ease-in-out disabled:bg-background-secondary/50 disabled:cursor-not-allowed {{ $class ?? '' }} @if ($type !== 'color') px-2.5 py-2.5 @endif"
|
||||
placeholder="{{ $placeholder ?? ($label ?? '') }}"
|
||||
@if ($dirty && isset($attributes['wire:model'])) wire:dirty.class="!border-yellow-600" @endif
|
||||
{{ $attributes->except(['placeholder', 'label', 'id', 'name', 'type', 'class', 'divClass', 'required', 'hideRequiredIndicator', 'dirty']) }} @required($required) />
|
||||
@error($name)
|
||||
<p class="text-red-500 text-xs">{{ $message }}</p>
|
||||
@enderror
|
||||
</fieldset>
|
||||
43
themes/default/views/components/form/properties.blade.php
Normal file
43
themes/default/views/components/form/properties.blade.php
Normal file
@@ -0,0 +1,43 @@
|
||||
@props(['properties', 'custom_properties' => []])
|
||||
|
||||
@foreach ($custom_properties as $property)
|
||||
@switch($property->type)
|
||||
@case('date')
|
||||
@case('string')
|
||||
|
||||
@case('number')
|
||||
<x-form.input :type="$property->type" name="properties.{{ $property->key }}" :label="$property->name" :required="$property->required"
|
||||
wire:model="properties.{{ $property->key }}" :value="$properties[$property->key] ?? ''" />
|
||||
@break
|
||||
|
||||
@case('checkbox')
|
||||
<x-form.checkbox name="properties.{{ $property->key }}" :label="$property->name" :required="$property->required"
|
||||
wire:model="properties.{{ $property->key }}" :checked="$properties[$property->key] ?? false" />
|
||||
@break
|
||||
|
||||
@case('radio')
|
||||
@foreach ($property->allowed_values as $value)
|
||||
<div class="flex items-center">
|
||||
<input type="radio" value="{{ $value }}" name="properties.{{ $property->key }}" type="radio"
|
||||
@checked($properties[$property->key] === $value ?? false) label="{{ $value }}" @required($property->required)
|
||||
wire:model="properties.{{ $property->key }}"
|
||||
class="form-radio size-4 text-primary rounded-full focus:ring-secondary hover:bg-secondary ring-offset-primary-800 focus:ring-2 bg-background-secondary border-neutral" />
|
||||
<label class="ml-2 text-sm text-primary-100"
|
||||
for="properties.{{ $property->key }}">{{ $value }}</label>
|
||||
</div>
|
||||
@endforeach
|
||||
@break
|
||||
|
||||
@case('select')
|
||||
<x-form.select name="properties.{{ $property->key }}" :label="$property->name"
|
||||
wire:model="properties.{{ $property->key }}" :required="$property->required" :options="$property->allowed_values" :selected="$properties[$property->key] ?? ''" />
|
||||
@break
|
||||
|
||||
@case('text')
|
||||
<x-form.textarea :type="$property->type" name="properties.{{ $property->key }}" :label="$property->name" :required="$property->required"
|
||||
wire:model="properties.{{ $property->key }}">{{ $properties[$property->key] ?? '' }}</x-form.textarea>
|
||||
@break
|
||||
|
||||
@default
|
||||
@endswitch
|
||||
@endforeach
|
||||
45
themes/default/views/components/form/radio.blade.php
Normal file
45
themes/default/views/components/form/radio.blade.php
Normal file
@@ -0,0 +1,45 @@
|
||||
@props([
|
||||
'name',
|
||||
'label' => null,
|
||||
'options' => [],
|
||||
'selected' => null,
|
||||
'multiple' => false,
|
||||
'required' => false,
|
||||
'divClass' => null,
|
||||
'hideRequiredIndicator' => false,
|
||||
])
|
||||
<fieldset class="flex flex-col relative mt-3 w-full {{ $divClass ?? '' }}" name="{{ $name }}">
|
||||
@if ($label)
|
||||
<legend>
|
||||
<label for="{{ $name }}"
|
||||
class="text-sm text-primary-100 absolute -translate-y-1/2 start-1 ml-1 bg-background-secondary rounded-md px-2">
|
||||
{{ $label }}
|
||||
@if ($required && !$hideRequiredIndicator)
|
||||
<span class="text-red-500">*</span>
|
||||
@endif
|
||||
</label>
|
||||
</legend>
|
||||
@endif
|
||||
|
||||
<div
|
||||
class="block px-2.5 py-2.5 w-full text-sm text-primary-100 bg-background-secondary border-2 border-neutral rounded-md outline-none focus:outline-none focus:border-secondary transition-all duration-300 ease-in-out">
|
||||
@if (count($options) == 0 && $slot)
|
||||
{{ $slot }}
|
||||
@else
|
||||
@foreach ($options as $key => $option)
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="radio" id="{{ $name }}_{{ $key }}" name="{{ $name }}"
|
||||
value="{{ gettype($options) == 'array' ? $option : $key }}" {{ ($multiple && $selected ? in_array($key,
|
||||
$selected) : $selected==$option) ? 'checked' : '' }} />
|
||||
<label for="{{ $name }}_{{ $key }}">
|
||||
{{ $option }}
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@error($name)
|
||||
<p class="text-red-500 text-xs">{{ $message }}</p>
|
||||
@enderror
|
||||
</fieldset>
|
||||
46
themes/default/views/components/form/select.blade.php
Normal file
46
themes/default/views/components/form/select.blade.php
Normal file
@@ -0,0 +1,46 @@
|
||||
@props([
|
||||
'name',
|
||||
'label' => null,
|
||||
'options' => [],
|
||||
'selected' => null,
|
||||
'multiple' => false,
|
||||
'required' => false,
|
||||
'divClass' => null,
|
||||
'hideRequiredIndicator' => false,
|
||||
])
|
||||
<fieldset class="flex flex-col relative mt-3 w-full {{ $divClass ?? '' }}">
|
||||
@if ($label)
|
||||
<legend>
|
||||
<label for="{{ $name }}"
|
||||
class="text-sm text-primary-100 absolute -translate-y-1/2 start-1 ml-1 bg-background-secondary rounded-md px-2">
|
||||
{{ $label }}
|
||||
@if ($required && !$hideRequiredIndicator)
|
||||
<span class="text-red-500">*</span>
|
||||
@endif
|
||||
</label>
|
||||
</legend>
|
||||
@endif
|
||||
|
||||
<select id="{{ $id ?? $name }}" {{ $multiple ? 'multiple' : '' }} {{ $attributes->only(['required', 'wire:model',
|
||||
'wire:dirty.class', 'wire:model.live']) }}
|
||||
class="block px-2.5 py-2.5 w-full text-sm text-primary-100 bg-background-secondary border-2 border-neutral
|
||||
rounded-md outline-none focus:outline-none focus:border-secondary transition-all duration-300 ease-in-out form-select">
|
||||
@if (count($options) == 0 && $slot)
|
||||
{{ $slot }}
|
||||
@else
|
||||
@foreach ($options as $key => $option)
|
||||
<option value="{{ gettype($options) == 'array' ? $option : $key }}" {{ ($multiple && $selected ? in_array($key,
|
||||
$selected) : $selected==$option) ? 'selected' : '' }}>
|
||||
{{ $option }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
</select>
|
||||
@if ($multiple)
|
||||
<p class="text-xs text-base">
|
||||
{{ __('Pro tip: Hold down the Ctrl (Windows) / Command (Mac) button to select multiple options.') }}</p>
|
||||
@endif
|
||||
|
||||
@error($name)
|
||||
<p class="text-red-500 text-xs">{{ $message }}</p>
|
||||
@enderror
|
||||
</fieldset>
|
||||
38
themes/default/views/components/form/setting.blade.php
Normal file
38
themes/default/views/components/form/setting.blade.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
@switch($setting->type)
|
||||
@case('select')
|
||||
<x-form.select name="fields.{{ $key }}.{{ $setting->name }}" :label="__($setting->label ?? $setting->name)" :required="$setting->required ?? false"
|
||||
:options="$setting->options" :selected="config('settings.' . $setting->name)" :multiple="$setting->multiple ?? false"
|
||||
wire:model="fields.{{ $key }}.{{ $setting->name }}" />
|
||||
@break
|
||||
|
||||
@case('text')
|
||||
@case('password')
|
||||
|
||||
@case('email')
|
||||
@case('number')
|
||||
|
||||
@case('color')
|
||||
@case('file')
|
||||
<x-form.input name="fields.{{ $key }}.{{ $setting->name }}" :type="$setting->type" :label="__($setting->label ?? $setting->name)"
|
||||
:placeholder="$setting->default ?? ''" :required="$setting->required ?? false" wire:model="fields.{{ $key }}.{{ $setting->name }}" />
|
||||
@break
|
||||
|
||||
@case('checkbox')
|
||||
<x-form.checkbox name="fields.{{ $key }}.{{ $setting->name }}" type="checkbox" :label="__($setting->label ?? $setting->name)"
|
||||
:required="$setting->required ?? false" :checked="config('settings.' . $setting->name) ? true : false" wire:model="fields.{{ $key }}.{{ $setting->name }}" />
|
||||
@break
|
||||
|
||||
@default
|
||||
@endswitch
|
||||
@isset($setting->description)
|
||||
@isset($setting->link)
|
||||
<a href="{{ $setting->link }}" class="text-xs text-primary-500 hover:underline hover:text-secondary group">
|
||||
{{ $setting->description }}
|
||||
<x-ri-arrow-right-long-line class="ml-1 size-3 inline-block -rotate-45 group-hover:rotate-0 transition" />
|
||||
</a>
|
||||
@else
|
||||
<p class="text-xs text-primary-500">{{ $setting->description }}</p>
|
||||
@endisset
|
||||
@endisset
|
||||
</div>
|
||||
33
themes/default/views/components/form/textarea.blade.php
Normal file
33
themes/default/views/components/form/textarea.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
@props([
|
||||
'name',
|
||||
'label' => null,
|
||||
'required' => false,
|
||||
'divClass' => null,
|
||||
'class' => null,
|
||||
'placeholder' => null,
|
||||
'id' => null,
|
||||
'type' => null,
|
||||
'hideRequiredIndicator' => false,
|
||||
'dirty' => false,
|
||||
])
|
||||
<fieldset class="flex flex-col relative mt-3 w-full {{ $divClass ?? '' }}">
|
||||
@if ($label)
|
||||
<legend>
|
||||
<label for="{{ $name }}"
|
||||
class="text-sm text-primary-100 absolute -translate-y-1/2 start-1 ml-1 bg-primary-800 px-2 rounded-md">
|
||||
{{ $label }}
|
||||
@if ($required && !$hideRequiredIndicator)
|
||||
<span class="text-red-500">*</span>
|
||||
@endif
|
||||
</label>
|
||||
</legend>
|
||||
@endif
|
||||
<textarea type="{{ $type ?? 'text' }}" id="{{ $id ?? $name }}" name="{{ $name }}"
|
||||
class="block w-full text-sm text-primary-100 bg-primary-800 border-2 border-neutral rounded-md outline-none focus:outline-none focus:border-secondary transition-all duration-300 ease-in-out disabled:bg-primary-700 disabled:cursor-not-allowed {{ $class ?? '' }} @if ($type !== 'color') px-2.5 py-2.5 @endif"
|
||||
placeholder="{{ $placeholder ?? ($label ?? '') }}" @if ($dirty && isset($attributes['wire:model']))
|
||||
wire:dirty.class="!border-yellow-600" @endif {{ $attributes->except(['placeholder', 'label', 'id', 'name', 'type', 'class', 'divClass', 'required', 'hideRequiredIndicator', 'dirty']) }}
|
||||
@required($required)>{{ $slot }}</textarea>
|
||||
@error($name)
|
||||
<p class="text-red-500 text-xs">{{ $message }}</p>
|
||||
@enderror
|
||||
</fieldset>
|
||||
13
themes/default/views/components/impersonating.blade.php
Normal file
13
themes/default/views/components/impersonating.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@if(session()->has('impersonating'))
|
||||
<div class="fixed bottom-0 right-0 z-50 flex gap-2 justify-center items-center bg-background-secondary shadow-lg p-4 w-full border-t border-neutral">
|
||||
<p>
|
||||
{{ __('You are currently impersonating') }}:
|
||||
<strong>{{ auth()->user()->name }}</strong>
|
||||
</p>
|
||||
<a href="/admin/users/{{ auth()->user()->id }}/edit">
|
||||
<x-button.primary>
|
||||
{{ __('Leave') }}
|
||||
</x-button.primary>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-select
|
||||
wire:model.live="currentLocale"
|
||||
:options="collect($locales)->map(fn($locale, $code) => [
|
||||
'value' => $code,
|
||||
'label' => $locale
|
||||
])->values()->toArray()"
|
||||
placeholder="Select language"
|
||||
/>
|
||||
9
themes/default/views/components/loading.blade.php
Normal file
9
themes/default/views/components/loading.blade.php
Normal file
@@ -0,0 +1,9 @@
|
||||
@props([
|
||||
'target' => null,
|
||||
])
|
||||
<div role="status"
|
||||
@if(!$target) wire:loading @else wire:loading wire:target="{{ $target }}" @endif
|
||||
>
|
||||
<x-ri-loader-5-fill aria-hidden="true" {{ $attributes->merge(['class' => 'size-6 me-2 fill-black animate-spin dark:fill-white']) }} />
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
3
themes/default/views/components/logo.blade.php
Normal file
3
themes/default/views/components/logo.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
@if (config('settings.logo'))
|
||||
<img src="{{ Storage::url(config('settings.logo')) }}" alt="{{ config('app.name') }}" {{ $attributes->merge(['class' => 'h-12 w-auto inline-block']) }}>
|
||||
@endif
|
||||
34
themes/default/views/components/modal.blade.php
Normal file
34
themes/default/views/components/modal.blade.php
Normal file
@@ -0,0 +1,34 @@
|
||||
@props([
|
||||
'title' => '',
|
||||
'closable' => true,
|
||||
'closeTrigger' => '',
|
||||
'open' => false,
|
||||
'width' => 'max-w-3xl'
|
||||
])
|
||||
<div x-data="{ open: {{ $open ? 'true' : 'false' }} }">
|
||||
<template x-teleport="body">
|
||||
<div class="fixed inset-0 z-30 flex items-center justify-center overflow-hidden bg-black/50"
|
||||
x-show="open">
|
||||
<!-- Modal inner -->
|
||||
<div class="px-6 py-4 w-full mx-2 md:mx-auto text-left bg-background-secondary rounded shadow-lg max-h-screen overflow-y-auto mb-8 mt-8 {{ $width }}" x-cloak
|
||||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-90">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-semibold text-primary-100">{{ $title }}</h2>
|
||||
@if ($closable && !$closeTrigger)
|
||||
<button @click="open = false" class="text-primary-100">
|
||||
<x-ri-close-fill class="size-6" />
|
||||
</button>
|
||||
@elseif ($closable && $closeTrigger)
|
||||
{{ $closeTrigger }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
@php
|
||||
$currentRoute = request()->route()->getName();
|
||||
|
||||
$navigation = [
|
||||
\App\Classes\Navigation::getLinks(),
|
||||
\App\Classes\Navigation::getAccountDropdownLinks(),
|
||||
\App\Classes\Navigation::getDashboardLinks(),
|
||||
];
|
||||
|
||||
function findBreadcrumb($items, $currentRoute) {
|
||||
foreach ($items as $item) {
|
||||
if (isset($item['route']) && $item['route'] === $currentRoute) {
|
||||
return [$item];
|
||||
}
|
||||
|
||||
if (!empty($item['children'])) {
|
||||
$childTrail = findBreadcrumb($item['children'], $currentRoute);
|
||||
if (!empty($childTrail)) {
|
||||
return array_merge([$item], $childTrail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$breadcrumbs = [];
|
||||
foreach ($navigation as $group) {
|
||||
$breadcrumbs = findBreadcrumb($group, $currentRoute);
|
||||
if (!empty($breadcrumbs)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-row items-center pb-4">
|
||||
@if (!empty($breadcrumbs))
|
||||
@foreach ($breadcrumbs as $index => $breadcrumb)
|
||||
@if ($index > 0)
|
||||
<x-ri-arrow-right-s-line class="size-4 text-base mx-2" />
|
||||
@endif
|
||||
|
||||
@if (count($breadcrumbs) === 1)
|
||||
<span class="text-2xl font-bold">
|
||||
{{ $breadcrumb['name'] ?? '' }}
|
||||
</span>
|
||||
@elseif ($index === count($breadcrumbs) - 1)
|
||||
<span class="text-base/80 font-semibold">
|
||||
{{ $breadcrumb['name'] ?? '' }}
|
||||
</span>
|
||||
@else
|
||||
<a href="{{ isset($breadcrumb['route']) ? route($breadcrumb['route'], $breadcrumb['params'] ?? []) : '#' }}"
|
||||
class="text-lg font-bold hover:text-primary">
|
||||
{{ $breadcrumb['name'] ?? '' }}
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
@else
|
||||
<span class="text-lg font-bold">{{ __('navigation.home') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
25
themes/default/views/components/navigation/footer.blade.php
Normal file
25
themes/default/views/components/navigation/footer.blade.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<footer class="w-full px-4 py-4 lg:mt-72 mt-44">
|
||||
<div class="container mx-auto grid gap-4">
|
||||
<div class="flex flex-col gap-2 items-center">
|
||||
<x-logo />
|
||||
<div class="text-sm text-base/80">
|
||||
{{ __('© :year :app_name. | All rights reserved.', ['year' => date('Y'), 'app_name' => config('app.name')]) }}
|
||||
</div>
|
||||
{{-- Paymenter is free and opensource, removing this link is not cool --}}
|
||||
<a href="https://paymenter.org" target="_blank"
|
||||
class="group mt-4 mb-6 flex items-center gap-2 text-base/50 hover:text-base">
|
||||
<svg class="size-4 text-current group-hover:text-[#4667FF]" width="150" height="205" viewBox="0 0 150 205" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_17)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 107V205H42.8571V139.638H100C133.333 139.638 150 123 150 89.7246V69.5L75 107V69.5L148.227 32.8863C143.133 10.9621 127.057 0 100 0H0V107ZM0 107V69.5L75 32V69.5L0 107Z"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_17">
|
||||
<rect width="150" height="205"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<p class="text-sm">{{ __('Powered by') }} Paymenter</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
247
themes/default/views/components/navigation/index.blade.php
Normal file
247
themes/default/views/components/navigation/index.blade.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<nav class="w-full px-4 lg:px-8 bg-background-secondary border-b border-neutral md:h-16 flex md:flex-row flex-col justify-between fixed top-0 z-20">
|
||||
<div x-data="{
|
||||
slideOverOpen: false
|
||||
}"
|
||||
x-init="$watch('slideOverOpen', value => { document.documentElement.style.overflow = value ? 'hidden' : '' })"
|
||||
class="relative z-50 w-full h-auto">
|
||||
|
||||
<div class="flex flex-row items-center justify-between w-full h-16">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<a href="{{ route('home') }}" class="flex flex-row items-center h-10" wire:navigate>
|
||||
<x-logo class="h-10 mr-2 rtl:ml-2" />
|
||||
<span class="text-xl font-bold leading-none flex items-center">{{ config('app.name') }}</span>
|
||||
</a>
|
||||
<div class="md:flex hidden flex-row ml-7">
|
||||
@foreach (\App\Classes\Navigation::getLinks() as $nav)
|
||||
@if (isset($nav['children']) && count($nav['children']) > 0)
|
||||
<div class="relative">
|
||||
<x-dropdown>
|
||||
<x-slot:trigger>
|
||||
<div class="flex flex-col">
|
||||
<span class="flex flex-row items-center p-3 text-sm font-semibold whitespace-nowrap text-base hover:text-base/80">
|
||||
{{ $nav['name'] }}
|
||||
</span>
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
<x-slot:content>
|
||||
@foreach ($nav['children'] as $child)
|
||||
<x-navigation.link
|
||||
:href="route($child['route'], $child['params'] ?? null)"
|
||||
:spa="isset($child['spa']) ? $nav['spa'] : true">
|
||||
{{ $child['name'] }}
|
||||
</x-navigation.link>
|
||||
@endforeach
|
||||
</x-slot:content>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
@else
|
||||
<x-navigation.link
|
||||
:href="route($nav['route'], $nav['params'] ?? null)"
|
||||
:spa="isset($nav['spa']) ? $nav['spa'] : true"
|
||||
class="flex items-center p-3">
|
||||
{{ $nav['name'] }}
|
||||
</x-navigation.link>
|
||||
@endif
|
||||
{{-- @if($nav['separator'])
|
||||
<div class="h-px w-full bg-neutral"></div>
|
||||
@endif --}}
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="items-center hidden md:flex mr-3">
|
||||
<x-dropdown>
|
||||
<x-slot:trigger>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-base font-semibold text-nowrap">{{ strtoupper(app()->getLocale()) }} <span class="text-base/50 font-semibold">|</span> {{ session('currency', config('settings.default_currency')) }}</span>
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
<x-slot:content>
|
||||
<strong class="block p-2 text-xs font-semibold uppercase text-base/50"> Language </strong>
|
||||
<livewire:components.language-switch />
|
||||
<livewire:components.currency-switch />
|
||||
</x-slot:content>
|
||||
</x-dropdown>
|
||||
<x-theme-toggle />
|
||||
</div>
|
||||
|
||||
<livewire:components.cart />
|
||||
|
||||
@if(auth()->check())
|
||||
<div class="hidden lg:flex">
|
||||
<x-dropdown>
|
||||
<x-slot:trigger>
|
||||
<img src="{{ auth()->user()->avatar }}" class="size-8 rounded-full border border-neutral bg-background" alt="avatar" />
|
||||
</x-slot:trigger>
|
||||
<x-slot:content>
|
||||
<div class="flex flex-col p-2">
|
||||
<span class="text-sm text-base break-words">{{ auth()->user()->name }}</span>
|
||||
<span class="text-sm text-base break-words">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
@foreach (\App\Classes\Navigation::getAccountDropdownLinks() as $nav)
|
||||
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? null)" :spa="isset($nav['spa']) ? $nav['spa'] : true">
|
||||
{{ $nav['name'] }}
|
||||
</x-navigation.link>
|
||||
@endforeach
|
||||
<livewire:auth.logout />
|
||||
</x-slot:content>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
@else
|
||||
<div class="hidden lg:flex flex-row gap-3">
|
||||
<a href="{{ route('login') }}" wire:navigate>
|
||||
<x-button.secondary>
|
||||
{{ __('navigation.login') }}
|
||||
</x-button.secondary>
|
||||
</a>
|
||||
@if(!config('settings.registration_disabled', false))
|
||||
<a href="{{ route('register') }}" wire:navigate>
|
||||
<x-button.primary>
|
||||
{{ __('navigation.register') }}
|
||||
</x-button.primary>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<button
|
||||
@click="slideOverOpen = !slideOverOpen"
|
||||
class="relative w-10 h-10 flex lg:hidden items-center justify-center rounded-lg hover:bg-neutral transition"
|
||||
aria-label="Toggle Menu">
|
||||
|
||||
<span
|
||||
x-show="!slideOverOpen"
|
||||
x-transition:enter="transition duration-300"
|
||||
x-transition:enter-start="opacity-0 -rotate-90 scale-75"
|
||||
x-transition:enter-end="opacity-100 rotate-0 scale-100"
|
||||
x-transition:leave="transition duration-150"
|
||||
x-transition:leave-start="opacity-100 rotate-0 scale-100"
|
||||
x-transition:leave-end="opacity-0 rotate-90 scale-75"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
aria-hidden="true">
|
||||
<x-ri-menu-fill class="size-5" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
x-show="slideOverOpen"
|
||||
x-transition:enter="transition duration-300"
|
||||
x-transition:enter-start="opacity-0 rotate-90 scale-75"
|
||||
x-transition:enter-end="opacity-100 rotate-0 scale-100"
|
||||
x-transition:leave="transition duration-150"
|
||||
x-transition:leave-start="opacity-100 rotate-0 scale-100"
|
||||
x-transition:leave-end="opacity-0 -rotate-90 scale-75"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
aria-hidden="true">
|
||||
<x-ri-close-fill class="size-5" />
|
||||
</span>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template x-teleport="body">
|
||||
<div
|
||||
x-show="slideOverOpen"
|
||||
@keydown.window.escape="slideOverOpen=false"
|
||||
x-cloak
|
||||
class="fixed left-0 right-0 top-16 w-full z-[99]"
|
||||
style="height:calc(100dvh - 4rem);"
|
||||
aria-modal="true"
|
||||
tabindex="-1">
|
||||
<div
|
||||
x-show="slideOverOpen"
|
||||
@click.away="slideOverOpen = false"
|
||||
x-transition.opacity.duration.300ms
|
||||
class="absolute inset-0 bg-background-secondary border-t border-neutral shadow-lg overflow-y-auto flex flex-col">
|
||||
|
||||
<div class="flex flex-col h-full p-4">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto">
|
||||
<x-navigation.sidebar-links />
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
@if(auth()->check())
|
||||
|
||||
<div
|
||||
x-data="{ userPanelOpen: false }"
|
||||
@keydown.escape.window="userPanelOpen = false"
|
||||
x-cloak
|
||||
class="relative">
|
||||
|
||||
<button @click="userPanelOpen = true" aria-label="Open user menu" class="flex gap-4 items-center justify-start">
|
||||
<img src="{{ auth()->user()->avatar }}" class="size-10 rounded-full border border-neutral bg-background" alt="avatar" />
|
||||
<div class="flex flex-col items-start gap-0.5">
|
||||
<span class="font-bold text-md">{{ auth()->user()->name }}</span>
|
||||
<span class="text-sm text-base/70">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-show="userPanelOpen"
|
||||
x-transition:enter="transition-opacity ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-60"
|
||||
x-transition:leave="transition-opacity ease-in duration-200"
|
||||
x-transition:leave-start="opacity-60"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click="userPanelOpen=false"
|
||||
class="fixed inset-0 bg-primary/5 backdrop-blur-xs z-40"
|
||||
style="pointer-events: auto"></div>
|
||||
|
||||
<div
|
||||
x-show="userPanelOpen"
|
||||
x-transition:enter="transition transform ease-out duration-300"
|
||||
x-transition:enter-start="translate-y-full opacity-0"
|
||||
x-transition:enter-end="translate-y-0 opacity-100"
|
||||
x-transition:leave="transition transform ease-in duration-200"
|
||||
x-transition:leave-start="translate-y-0 opacity-100"
|
||||
x-transition:leave-end="translate-y-full opacity-0"
|
||||
class="fixed bottom-0 left-0 right-0 z-50 mx-auto w-full"
|
||||
style="pointer-events: auto"
|
||||
@click.away="userPanelOpen = false"
|
||||
tabindex="-1"
|
||||
aria-modal="true">
|
||||
<div class="bg-background-secondary shadow-lg rounded-t-2xl border border-neutral p-6">
|
||||
<div class="flex gap-4 items-center justify-start">
|
||||
<img src="{{ auth()->user()->avatar }}" class="size-12 rounded-full border border-neutral bg-background" alt="avatar" />
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-bold text-lg">{{ auth()->user()->name }}</span>
|
||||
<span class="text-sm text-base/70">{{ auth()->user()->email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-px w-full bg-neutral my-6"></div>
|
||||
<div class="mt-4 flex flex-col gap-2 w-full">
|
||||
@foreach (\App\Classes\Navigation::getAccountDropdownLinks() as $nav)
|
||||
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? null)" :spa="isset($nav['spa']) ? $nav['spa'] : true">
|
||||
{{ $nav['name'] }}
|
||||
</x-navigation.link>
|
||||
@endforeach
|
||||
<livewire:auth.logout />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
<div class="flex flex-col gap-3 mb-3">
|
||||
@if(!config('settings.registration_disabled', false))
|
||||
<a href="{{ route('register') }}" wire:navigate>
|
||||
<x-button.primary>
|
||||
{{ __('navigation.register') }}
|
||||
</x-button.primary>
|
||||
</a>
|
||||
@endif
|
||||
<a href="{{ route('login') }}" wire:navigate>
|
||||
<x-button.secondary>
|
||||
{{ __('navigation.login') }}
|
||||
</x-button.secondary>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -0,0 +1,4 @@
|
||||
@props(['href', 'spa' => true])
|
||||
<a href="{{ $href }}" {{ $attributes->merge(['class' => 'flex flex-row items-center p-3 gap-2 text-sm font-semibold text-wrap ' . ($href === request()->url() ? 'text-primary' : 'text-base hover:text-base/80')]) }} @if($spa) wire:navigate @endif>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
@@ -0,0 +1,123 @@
|
||||
<div class="lg:px-4 lg:py-6 flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2 md:hidden">
|
||||
@foreach (\App\Classes\Navigation::getLinks() as $nav)
|
||||
@if (!empty($nav['children']))
|
||||
<div x-data="{ activeAccordion: {{ $nav['active'] ? 'true' : 'false' }} }"
|
||||
class="relative w-full mx-auto overflow-hidden text-sm font-normal divide-y divide-gray-200">
|
||||
<div class="cursor-pointer">
|
||||
<button @click="activeAccordion = !activeAccordion"
|
||||
class="flex items-center justify-between w-full p-3 text-sm font-semibold whitespace-nowrap rounded-lg hover:bg-primary/5">
|
||||
<div class="flex flex-row gap-2">
|
||||
@isset($nav['icon'])
|
||||
<x-dynamic-component :component="$nav['icon']"
|
||||
class="size-5 {{ $nav['active'] ? 'text-primary' : 'fill-base/50' }}" />
|
||||
@endisset
|
||||
<span>{{ $nav['name'] }}</span>
|
||||
</div>
|
||||
<x-ri-arrow-down-s-line x-bind:class="{ 'rotate-180': activeAccordion }"
|
||||
class="size-4 text-base ease-out duration-300" />
|
||||
</button>
|
||||
<div x-show="activeAccordion" x-collapse x-cloak>
|
||||
<div class="p-4 pt-0 opacity-70">
|
||||
@foreach ($nav['children'] as $child)
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-navigation.link :href="route($child['route'], $child['params'] ?? [])"
|
||||
:spa="$child['spa'] ?? true"
|
||||
class="{{ $child['active'] ? 'text-primary font-bold' : '' }}">
|
||||
{{ $child['name'] }}
|
||||
</x-navigation.link>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center rounded-lg {{ $nav['active'] ? 'bg-primary/5' : 'hover:bg-primary/5' }}">
|
||||
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? [])"
|
||||
:spa="$nav['spa'] ?? true" class="w-full">
|
||||
@isset($nav['icon'])
|
||||
<x-dynamic-component :component="$nav['icon']"
|
||||
class="size-5 {{ $nav['active'] ? 'text-primary' : 'fill-base/50' }}" />
|
||||
@endisset
|
||||
{{ $nav['name'] }}
|
||||
</x-navigation.link>
|
||||
</div>
|
||||
@endif
|
||||
@isset($nav['separator'])
|
||||
<div class="h-px w-full bg-neutral"></div>
|
||||
@endisset
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@foreach (\App\Classes\Navigation::getDashboardLinks() as $nav)
|
||||
@if (!empty($nav['children']))
|
||||
<div x-data="{ activeAccordion: {{ $nav['active'] ? 'true' : 'false' }} }"
|
||||
class="relative w-full mx-auto overflow-hidden text-sm font-normal divide-y divide-gray-200">
|
||||
<div class="cursor-pointer">
|
||||
<button @click="activeAccordion = !activeAccordion"
|
||||
class="flex items-center justify-between w-full p-3 text-sm font-semibold whitespace-nowrap rounded-lg hover:bg-primary/5">
|
||||
<div class="flex flex-row gap-2">
|
||||
@isset($nav['icon'])
|
||||
<x-dynamic-component :component="$nav['icon']"
|
||||
class="size-5 {{ $nav['active'] ? 'text-primary' : 'fill-base/50' }}" />
|
||||
@endisset
|
||||
<span>{{ $nav['name'] }}</span>
|
||||
</div>
|
||||
<x-ri-arrow-down-s-line x-bind:class="{ 'rotate-180': activeAccordion }"
|
||||
class="size-4 text-base ease-out duration-300" />
|
||||
</button>
|
||||
<div x-show="activeAccordion" x-collapse x-cloak>
|
||||
<div class="p-4 pt-0 opacity-70">
|
||||
@foreach ($nav['children'] as $child)
|
||||
@if ($child['condition'] ?? true)
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-navigation.link :href="route($child['route'], $child['params'] ?? [])"
|
||||
:spa="$child['spa'] ?? true"
|
||||
class="{{ $child['active'] ? 'text-primary font-bold' : '' }}">
|
||||
{{ $child['name'] }}
|
||||
</x-navigation.link>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center rounded-lg {{ $nav['active'] ? 'bg-primary/5' : 'hover:bg-primary/5' }}">
|
||||
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? [])"
|
||||
:spa="$nav['spa'] ?? true"
|
||||
class="w-full">
|
||||
@isset($nav['icon'])
|
||||
<x-dynamic-component :component="$nav['icon']"
|
||||
class="size-5 {{ $nav['active'] ? 'text-primary' : 'fill-base/50' }}" />
|
||||
@endisset
|
||||
{{ $nav['name'] }}
|
||||
</x-navigation.link>
|
||||
</div>
|
||||
@endif
|
||||
@isset($nav['separator'])
|
||||
<div class="h-px w-full bg-neutral"></div>
|
||||
@endisset
|
||||
@endforeach
|
||||
<div class="flex flex-row items-center mt-4 justify-between md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:trigger>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-base font-semibold text-nowrap">{{ strtoupper(app()->getLocale()) }} <span class="text-base/50 font-semibold">|</span> {{ session('currency', config('settings.default_currency')) }}</span>
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
<x-slot:content>
|
||||
<strong class="block p-2 text-xs font-semibold uppercase text-base/50"> Language </strong>
|
||||
<livewire:components.language-switch />
|
||||
<livewire:components.currency-switch />
|
||||
</x-slot:content>
|
||||
</x-dropdown>
|
||||
|
||||
<x-theme-toggle />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<aside class="mt-14 w-64 h-screen md:flex hidden flex-col justify-between border-e border-neutral bg-background-secondary fixed top-0 left-0 rtl:right-0 z-10">
|
||||
<x-navigation.sidebar-links />
|
||||
</aside>
|
||||
13
themes/default/views/components/notification.blade.php
Normal file
13
themes/default/views/components/notification.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<div x-data>
|
||||
<template x-for="(notification, index) in $store.notifications.notifications" :key="notification.id">
|
||||
<div x-show="notification.show" x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 scale-90" x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90" @click="$store.notifications.removeNotification(notification.id)"
|
||||
:class="notification.type === 'success' ? 'bg-secondary' : 'bg-red-500'"
|
||||
class="fixed text-white px-4 py-2 rounded shadow-md mb-4 z-50"
|
||||
:style="'top: ' + (20 + index * 60) + 'px;left: 50%; transform: translateX(-50%);'">
|
||||
<p x-text="notification.message"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
48
themes/default/views/components/pagination.blade.php
Normal file
48
themes/default/views/components/pagination.blade.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="flex justify-center">
|
||||
@if ($paginator->hasPages())
|
||||
<nav role="navigation" aria-label="Pagination Navigation" class="flex gap-2 items-center">
|
||||
<span>
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="bg-background-secondary text-white px-4 py-2 rounded-lg cursor-not-allowed">Previous</span>
|
||||
@else
|
||||
<button wire:click="previousPage" wire:loading.attr="disabled" rel="prev" class="bg-background-secondary text-white px-4 py-2 rounded-lg">Previous</button>
|
||||
@endif
|
||||
</span>
|
||||
|
||||
@foreach ($elements as $element)
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
@if (
|
||||
$page == $paginator->currentPage() ||
|
||||
$page <= 2 ||
|
||||
$page > $paginator->lastPage() - 2 ||
|
||||
abs($paginator->currentPage() - $page) <= 1)
|
||||
<span>
|
||||
<button wire:click="gotoPage({{ $page }})" wire:loading.attr="disabled"
|
||||
class="{{ $page === $paginator->currentPage() ? 'bg-background-secondary/30 text-white' : 'bg-background-secondary text-white' }} px-4 py-2 rounded-lg cursor-pointer">{{ $page }}</button>
|
||||
</span>
|
||||
@elseif($page == 3 || $page == $paginator->lastPage() - 3)
|
||||
<span class="bg-background-secondary text-white px-4 py-2 rounded-lg">
|
||||
<span>...</span>
|
||||
</span>
|
||||
@endif
|
||||
@endforeach
|
||||
@else
|
||||
<span class="bg-background-secondary text-white px-4 py-2 rounded-lg">
|
||||
<span>...</span>
|
||||
</span>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
|
||||
<span>
|
||||
@if ($paginator->onLastPage())
|
||||
<span class="bg-background-secondary text-white px-4 py-2 rounded-lg cursor-not-allowed">Next</span>
|
||||
@else
|
||||
<button wire:click="nextPage" wire:loading.attr="disabled" rel="next"
|
||||
class="bg-background-secondary text-white px-4 py-2 rounded-lg">Next</button>
|
||||
@endif
|
||||
</span>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
115
themes/default/views/components/select.blade.php
Normal file
115
themes/default/views/components/select.blade.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<div
|
||||
x-data="{
|
||||
selectOpen: false,
|
||||
selectedValue: @entangle($attributes->wire('model')),
|
||||
selectableItems: {{ json_encode($options) }},
|
||||
selectableItemActive: null,
|
||||
selectId: $id('select'),
|
||||
selectDropdownPosition: 'bottom',
|
||||
|
||||
init() {
|
||||
this.$watch('selectOpen', () => {
|
||||
if (this.selectOpen) {
|
||||
this.selectableItemActive = this.selectableItems.find(item => item.value === this.selectedValue)
|
||||
|| this.selectableItems[0];
|
||||
this.$nextTick(() => this.selectScrollToActiveItem());
|
||||
}
|
||||
this.selectPositionUpdate();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', this.resizeHandler = () => this.selectPositionUpdate());
|
||||
this.$el.addEventListener('alpine:destroyed', () => window.removeEventListener('resize', this.resizeHandler));
|
||||
},
|
||||
|
||||
selectableItemIsActive(item) {
|
||||
return this.selectableItemActive && this.selectableItemActive.value === item.value;
|
||||
},
|
||||
|
||||
selectableItemActiveNext() {
|
||||
let index = this.selectableItems.indexOf(this.selectableItemActive);
|
||||
if (index < this.selectableItems.length - 1) {
|
||||
this.selectableItemActive = this.selectableItems[index + 1];
|
||||
this.selectScrollToActiveItem();
|
||||
}
|
||||
},
|
||||
|
||||
selectableItemActivePrevious() {
|
||||
let index = this.selectableItems.indexOf(this.selectableItemActive);
|
||||
if (index > 0) {
|
||||
this.selectableItemActive = this.selectableItems[index - 1];
|
||||
this.selectScrollToActiveItem();
|
||||
}
|
||||
},
|
||||
|
||||
selectScrollToActiveItem() {
|
||||
if (this.selectableItemActive) {
|
||||
const activeElement = document.getElementById(this.selectableItemActive.value + '-' + this.selectId);
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
selectPositionUpdate() {
|
||||
if (!this.$refs.selectButton || !this.$refs.selectableItemsList) return;
|
||||
|
||||
const selectDropdownBottomPos = this.$refs.selectButton.getBoundingClientRect().top +
|
||||
this.$refs.selectButton.offsetHeight +
|
||||
this.$refs.selectableItemsList.offsetHeight;
|
||||
|
||||
this.selectDropdownPosition = window.innerHeight < selectDropdownBottomPos ? 'top' : 'bottom';
|
||||
}
|
||||
}"
|
||||
@keydown.escape="selectOpen = false"
|
||||
@keydown.down.prevent="if(selectOpen) { selectableItemActiveNext() } else { selectOpen = true }"
|
||||
@keydown.up.prevent="if(selectOpen) { selectableItemActivePrevious() } else { selectOpen = true }"
|
||||
@keydown.enter.prevent="selectedValue = selectableItemActive.value; selectOpen = false"
|
||||
class="relative w-full"
|
||||
>
|
||||
<button
|
||||
x-ref="selectButton"
|
||||
@click="selectOpen = !selectOpen"
|
||||
:class="{ 'ring-2 ring-offset-2 ring-neutral-800': selectOpen }"
|
||||
class="relative w-full min-h-[38px] py-2 pl-3 pr-10 text-left bg-background-secondary border border-neutral rounded-md shadow-sm cursor-pointer text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-800"
|
||||
type="button"
|
||||
>
|
||||
<span x-text="selectableItems.find(item => item.value === selectedValue)?.label ?? '{{ $placeholder ?? 'Select option' }}'" class="block truncate"></span>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<x-ri-expand-up-down-line class="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
x-show="selectOpen"
|
||||
x-ref="selectableItemsList"
|
||||
@click.away="selectOpen = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
:class="{ 'bottom-full mb-1': selectDropdownPosition === 'top', 'top-full mt-1': selectDropdownPosition === 'bottom' }"
|
||||
class="absolute z-50 w-full py-1 overflow-auto bg-background-secondary border border-neutral rounded-lg shadow-lg max-h-60 focus:outline-none text-sm"
|
||||
x-cloak
|
||||
>
|
||||
<template x-for="item in selectableItems" :key="item.value">
|
||||
<li
|
||||
:id="item.value + '-' + selectId"
|
||||
@click="selectedValue = item.value; selectOpen = false"
|
||||
@mousemove="selectableItemActive = item"
|
||||
:class="{ 'bg-neutral-700': selectableItemIsActive(item) }"
|
||||
class="relative py-2 pl-8 pr-4 cursor-pointer select-none hover:bg-neutral-700"
|
||||
>
|
||||
<span
|
||||
class="block truncate"
|
||||
:class="{ 'font-semibold': item.value === selectedValue }"
|
||||
x-text="item.label"
|
||||
></span>
|
||||
<span
|
||||
x-show="item.value === selectedValue"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-2 text-neutral-400"
|
||||
>
|
||||
<x-ri-check-fill class="size-4" />
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
8
themes/default/views/components/theme-toggle.blade.php
Normal file
8
themes/default/views/components/theme-toggle.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<button @click="darkMode = !darkMode" type="button" class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-neutral transition">
|
||||
<template x-if="!darkMode">
|
||||
<x-ri-sun-fill class="size-4 text-base" />
|
||||
</template>
|
||||
<template x-if="darkMode">
|
||||
<x-ri-moon-fill class="size-4 text-base" />
|
||||
</template>
|
||||
</button>
|
||||
Reference in New Issue
Block a user