Add all files

This commit is contained in:
root
2025-11-30 11:57:22 +01:00
parent 2ee51cac4f
commit 044271db3d
1476 changed files with 142544 additions and 0 deletions

View 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 &copy;
{{ 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,26 @@
@if (config('settings.captcha') !== 'disabled')
<div class="flex flex-col items-center justify-center bg-background-secondary/50 mt-4 p-4 border border-primary/50 rounded-lg shadow-md">
<div wire:ignore class="w-full">
<p class="text-base text-sm text-center">
Are you a bot?
</p>
<p class="text-xs text-center text-base/50 mb-4">
Please complete the CAPTCHA below to continue.
</p>
<div class="flex justify-center">
@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>
</div>
@error('captcha')
<p class="text-red-500 text-xs mt-2 text-center">{{ $message }}</p>
@enderror
</div>
@endif

View File

@@ -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>

View File

@@ -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>

View 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

View 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

View 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>

View File

@@ -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>

View 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>

View 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

View File

@@ -0,0 +1,23 @@
@props([
'name',
'id' => null,
'label' => null,
'divClass' => '',
])
<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>

View File

@@ -0,0 +1,149 @@
<div class="space-y-4">
@switch($config->type)
@case('select')
<div class="group">
<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 ?? ''"
class="transition-all duration-300 focus:ring-2 focus:ring-primary focus:border-primary-500 rounded-xl"
>
{{ $slot }}
</x-form.select>
</div>
@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">
<div class="relative flex items-center" :style="`--progress:${progressOption};--segments-width:${segmentsWidthOption}`">
<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">
</div>
<!-- Options -->
<ul class="flex justify-between text-xs font-medium text-light px-2.5">
<template x-for="(plan, index) in options" :key="index">
<li class="relative @if($showPriceTag) pb-7 @else pb-2 @endif">
<button @click="setOptionValue(index)" class="fixed flex flex-col items-center -translate-x-1/2">
<span class="hidden lg:inline text-sm font-semibold" x-text="`${plan.option}`"></span>
<span class="hidden lg:inline text-sm font-semibold" x-text="`${plan.price}`"></span>
<span class="inline lg:hidden" x-text="`${plan.option}`"></span>
</button>
</li>
</template>
</ul>
</div>
@break
@case('text')
@case('password')
@case('email')
@case('number')
@case('color')
@case('file')
<div class="group">
<x-form.input
name="{{ $name }}"
:type="$config->type"
:label="__($config->label ?? $config->name)"
:placeholder="$config->placeholder ?? ($config->default ?? '')"
:required="$config->required ?? false"
wire:model.live="{{ $name }}"
class="transition-all duration-300 focus:ring-2 focus:ring-primary focus:border-primary rounded-xl border-2 border-gray hover:border-gray"
/>
</div>
@break
@case('checkbox')
<div class="group">
<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="{{ $name }}"
class="rounded-2xl border-2 border-gray-200 text-primary-600 focus:ring-primary focus:ring-2 transition-all duration-300"
/>
</div>
@break
@case('radio')
<div class="group">
<x-form.radio
name="{{ $name }}"
:label="__($config->label ?? $config->name)"
:selected="config('configs.' . $config->name)"
:required="$config->required ?? false"
wire:model="{{ $name }}"
class="border-2 border-gray-200 text-primary-600 focus:ring-primary focus:ring-2 transition-all duration-300"
>
{{ $slot }}
</x-form.radio>
</div>
@break
@default
<div class="p-4 bg-gray-50 rounded-xl border-2 border-dashed border-gray-300">
<p class="text-sm text-gray-500 text-center">Unsupported field type: {{ $config->type }}</p>
</div>
@endswitch
@if(isset($config->description))
<div class="mt-3">
@if(isset($config->link))
<a href="{{ $config->link }}"
class="inline-flex items-center text-sm text-primary-600 hover:text-primary-700 transition-colors duration-200 group">
<span class="mr-2">{{ $config->description }}</span>
<x-ri-arrow-right-long-line class="w-4 h-4 transform transition-transform duration-200 group-hover:translate-x-1" />
</a>
@else
<p class="text-sm text-gray-600 leading-relaxed">{{ $config->description }}</p>
</div>
</div>
@endif
@endif
</div>

View 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 w-full {{ $divClass ?? '' }}">
@if ($label)
<legend class="mb-1">
<label for="{{ $name }}"
class="text-sm font-bold text-base">
{{ $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/50 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>

View File

@@ -0,0 +1,258 @@
@props([
'name' => 'password',
'label' => null,
'required' => false,
'divClass' => null,
'class' => null,
'placeholder' => null,
'id' => null,
'hideRequiredIndicator' => false,
'dirty' => false,
'minStrengthLevel' => 3 // 1=red, 2=yellow, 3=blue, 4=green
])
<fieldset class="flex flex-col relative w-full mt-4 {{ $divClass ?? '' }}">
@if ($label)
<legend class="mb-1">
<label for="{{ $name }}"
class="text-sm font-bold text-base">
{{ $label }}
@if ($required && !$hideRequiredIndicator)
<span class="text-red-500">*</span>
@endif
</label>
</legend>
@endif
<div class="relative">
<input
type="password"
id="{{ $id ?? $name }}"
name="{{ $name }}"
placeholder="{{ $placeholder ?? ($label ?? '') }}"
class="block w-full text-sm text-base bg-background-secondary/50 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 px-2.5 py-2.5 pr-10 {{ $class ?? '' }}"
@if ($dirty && isset($attributes['wire:model'])) wire:dirty.class="!border-yellow-600" @endif
{{ $attributes->except(['placeholder', 'label', 'id', 'name', 'class', 'divClass', 'required', 'hideRequiredIndicator', 'dirty', 'minStrengthLevel']) }}
@required($required)
data-hs-strong-password='{
"target": "#{{ $id ?? $name }}-strength",
"hints": "#{{ $id ?? $name }}-hints",
"minStrengthLevel": {{ $minStrengthLevel }},
"stripClasses": "hs-strong-password:opacity-100 hs-strong-password-accepted:bg-teal-500 h-2 flex-auto rounded-full bg-blue-500 opacity-50 mx-1"
}'
>
</div>
<div id="{{ $id ?? $name }}-strength" class="flex mt-2 -mx-1" aria-live="polite"></div>
<div id="{{ $id ?? $name }}-hints" class="mt-3">
<div class="text-xs text-primary-100 mb-2 font-medium">Password requirements:</div>
<div class="flex flex-wrap gap-2 text-xs">
<div class="flex items-center gap-x-2" data-hs-strong-password-hints-rule-text="min-length">
<span class="hidden" data-check>
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</span>
<span data-uncheck>
<svg class="w-3 h-3 text-neutral-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</span>
<span class="text-base">Minimum 8 characters</span>
</div>
<div class="flex items-center gap-x-2" data-hs-strong-password-hints-rule-text="lowercase">
<span class="hidden" data-check>
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</span>
<span data-uncheck>
<svg class="w-3 h-3 text-neutral-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</span>
<span class="text-base">One lowercase letter</span>
</div>
<div class="flex items-center gap-x-2" data-hs-strong-password-hints-rule-text="uppercase">
<span class="hidden" data-check>
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</span>
<span data-uncheck>
<svg class="w-3 h-3 text-neutral-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</span>
<span class="text-base">One uppercase letter</span>
</div>
<div class="flex items-center gap-x-2" data-hs-strong-password-hints-rule-text="numbers">
<span class="hidden" data-check>
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</span>
<span data-uncheck>
<svg class="w-3 h-3 text-neutral-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</span>
<span class="text-base">One number</span>
</div>
<div class="flex items-center gap-x-2" data-hs-strong-password-hints-rule-text="special-characters">
<span class="hidden" data-check>
<svg class="w-3 h-3 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</span>
<span data-uncheck>
<svg class="w-3 h-3 text-neutral-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</span>
<span class="text-base">One special character (!@#$%^&*)</span>
</div>
</div>
</div>
@error($name)
<p class="text-red-500 text-xs">{{ $message }}</p>
@enderror
</fieldset>
<script>
// Enhanced password strength validation
document.addEventListener('DOMContentLoaded', function() {
const passwordInputs = document.querySelectorAll('[data-hs-strong-password]');
passwordInputs.forEach(function(input) {
const config = JSON.parse(input.getAttribute('data-hs-strong-password'));
const strengthBar = document.querySelector(config.target);
const hints = document.querySelector(config.hints);
const warningElement = document.querySelector(config.target.replace('-strength', '-warning'));
const minStrengthLevel = config.minStrengthLevel || 3;
// Initial setup - create empty bars
if (strengthBar) {
strengthBar.innerHTML = '';
for (let i = 0; i < 5; i++) {
const bar = document.createElement('div');
bar.className = 'h-2 flex-auto rounded-full mx-1 bg-neutral-300 transition-all duration-300';
strengthBar.appendChild(bar);
}
}
// Input event listener
input.addEventListener('input', function() {
const strengthLevel = checkPasswordStrength(input.value, strengthBar, hints, warningElement, minStrengthLevel);
// Set custom validity based on strength level
if (input.value && strengthLevel < minStrengthLevel) {
input.setCustomValidity('Password terlalu lemah! Minimal harus mencapai level orange.');
} else {
input.setCustomValidity('');
}
});
// Prevent form submission if password is too weak
const form = input.closest('form');
if (form) {
form.addEventListener('submit', function(e) {
const strengthLevel = getCurrentPasswordStrength(input.value);
if (input.value && strengthLevel < minStrengthLevel) {
e.preventDefault();
input.focus();
// Show warning
if (warningElement) {
warningElement.classList.remove('hidden');
}
// Show validation message
input.reportValidity();
}
});
}
});
});
function getCurrentPasswordStrength(password) {
const checks = {
'min-length': password.length >= 8,
'lowercase': /[a-z]/.test(password),
'uppercase': /[A-Z]/.test(password),
'numbers': /\d/.test(password),
'special-characters': /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
};
return Object.values(checks).filter(Boolean).length;
}
function checkPasswordStrength(password, strengthBar, hintsContainer, warningElement, minStrengthLevel) {
const checks = {
'min-length': password.length >= 8,
'lowercase': /[a-z]/.test(password),
'uppercase': /[A-Z]/.test(password),
'numbers': /\d/.test(password),
'special-characters': /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
};
const passed = Object.values(checks).filter(Boolean).length;
// Update strength bars
if (strengthBar) {
const bars = strengthBar.querySelectorAll('div');
bars.forEach((bar, index) => {
// Reset classes
bar.className = 'h-2 flex-auto rounded-full mx-1 transition-all duration-300';
if (index < passed) {
if (passed <= 2) {
bar.classList.add('bg-red-500');
} else if (passed === 3) {
bar.classList.add('bg-yellow-500'); // Orange/Yellow level
} else if (passed === 4) {
bar.classList.add('bg-blue-500');
} else if (passed === 5) {
bar.classList.add('bg-green-500');
}
} else {
bar.classList.add('bg-neutral-300');
}
});
}
// Show/hide warning based on strength
if (warningElement) {
if (password && passed < minStrengthLevel) {
warningElement.classList.remove('hidden');
} else {
warningElement.classList.add('hidden');
}
}
// Update requirement icons
if (hintsContainer) {
Object.keys(checks).forEach(function(rule) {
const ruleElement = hintsContainer.querySelector(`[data-hs-strong-password-hints-rule-text="${rule}"]`);
if (ruleElement) {
const checkIcon = ruleElement.querySelector('[data-check]');
const uncheckIcon = ruleElement.querySelector('[data-uncheck]');
if (checks[rule]) {
checkIcon.classList.remove('hidden');
uncheckIcon.classList.add('hidden');
} else {
checkIcon.classList.add('hidden');
uncheckIcon.classList.remove('hidden');
}
}
});
}
return passed;
}
</script>

View 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

View 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 w-full {{ $divClass ?? '' }}" name="{{ $name }}">
@if ($label)
<legend class="mb-1">
<label for="{{ $name }}"
class="text-sm font-bold text-base">
{{ $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 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>

View 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 w-full {{ $divClass ?? '' }}">
@if ($label)
<legend class="mb-1">
<label for="{{ $name }}"
class="text-sm font-bold text-base">
{{ $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>

View 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>

View 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>

View File

@@ -0,0 +1,19 @@
@if(session()->has('impersonating'))
<div class="fixed bottom-0 right-0 z-50 flex gap-4 justify-between items-center bg-background-secondary shadow-lg p-4 w-full border-t border-neutral animate-slide-up">
<div class="flex items-center gap-2">
<x-ri-user-line class="text-primary w-5 h-5" />
<p class="text-sm text-color-base">
{{ __('You are currently impersonating') }}:
<strong class="text-primary">{{ auth()->user()->name }}</strong>
</p>
</div>
<div class="flex items-center gap-4">
<a href="/admin/users/{{ auth()->user()->id }}/edit">
<x-button.primary class="bg-red-500 hover:bg-red-700 px-4 py-2 text-sm flex items-center gap-2">
<x-ri-logout-box-line class="w-4 h-4" />
{{ __('Leave') }}
</x-button.primary>
</a>
</div>
</div>
@endif

View File

@@ -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"
/>

View File

@@ -0,0 +1,9 @@
@props([
'target' => null,
])
<div role="status"
@if(!$target) wire:loading @else wire:loading wire:target="{{ $target }}" @endif
>
<svg wire:loading fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="w-4 h-4 mr-2"><g><path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/><animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform></g></svg>
<span class="sr-only">Loading...</span>
</div>

View 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

View 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-auto 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 {{ $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>

View File

@@ -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>

View File

@@ -0,0 +1,93 @@
<footer class="w-full rounded-2xl bg-background-secondary/80 backdrop-blur-md border border-neutral/50 mt-8">
<div class="container mx-auto px-6 py-8">
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
<!-- Left Section -->
<div class="flex flex-col space-y-4 max-w-md">
<div class="flex items-center gap-3">
<!-- Logo without background -->
<x-logo class="h-4 w-auto" />
<h3 class="text-lg font-bold text-base">{{ config('app.name') }}</h3>
</div>
<div class="prose prose-sm text-base/70 prose-headings:text-base prose-p:text-base/60 prose-a:text-primary hover:prose-a:text-primary/80 prose-a:no-underline hover:prose-a:underline">
{!! Str::markdown(theme('footer_text', 'Welcome to Paymenter'), ['allow_unsafe_links' => false]) !!}
</div>
<div class="text-xs text-base/50">
{{ __('© :year :app_name. | All rights reserved.', ['year' => date('Y'), 'app_name' => config('app.name')]) }}
</div>
<span style="display:none;">%%__USERNAME__%%</span>
</div>
<!-- Right Section -->
<div class="flex flex-col lg:items-end gap-4">
<!-- Policy & Utility Links (Alphabetically) -->
<div class="flex flex-wrap gap-4 text-sm">
<a href="https://billing.virtnet.bond/announcements" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">Announcements</a>
<a href="https://billing.virtnet.bond/commercial" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">Commercial</a>
<a href="/contact" class="text-base/60 hover:text-secondary transition-colors duration-200">Contact Us</a>
<a href="https://billing.virtnet.bond/documentation" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">Documentation</a>
@if (theme('privacy_policy_url'))
<a href="{{ theme('privacy_policy_url') }}" class="text-base/60 hover:text-secondary transition-colors duration-200">Privacy Policy</a>
@endif
<a href="/abuse" class="text-base/60 hover:text-secondary transition-colors duration-200">Report Abuse</a>
@if (theme('support_url'))
<a href="{{ theme('support_url') }}" class="text-base/60 hover:text-secondary transition-colors duration-200">Support</a>
@endif
@if (theme('terms_of_service_url'))
<a href="{{ theme('terms_of_service_url') }}" class="text-base/60 hover:text-secondary transition-colors duration-200">Terms of Service</a>
@endif
</div>
<div class="flex gap-3 mt-2">
<!-- Network logos with hover effect -->
<a href="https://manrs.org/participant/6536/" target="_blank" rel="noopener noreferrer"
class="p-2 bg-bg-secondary/50 hover:bg-bg-secondary rounded-lg border border-neutral/20 hover:border-neutral/30 transition-all duration-200 group flex items-center justify-center">
<img src="https://billing.virtnet.bond/assets/images/manrs.png" class="w-5 h-5 filter grayscale group-hover:grayscale-0 transition-all duration-200" alt="MANRS logo">
</a>
<a href="https://stat.ripe.net/resource/AS214022#tab=overview" target="_blank" rel="noopener noreferrer"
class="p-2 bg-bg-secondary/50 hover:bg-bg-secondary rounded-lg border border-neutral/20 hover:border-neutral/30 transition-all duration-200 group flex items-center justify-center">
<img src="https://billing.virtnet.bond/assets/images/ripencc.png" class="w-5 h-5 filter grayscale group-hover:grayscale-0 transition-all duration-200" alt="RIPE NCC logo">
</a>
<a href="https://www.peeringdb.com/net/37672" target="_blank" rel="noopener noreferrer"
class="p-2 bg-bg-secondary/50 hover:bg-bg-secondary rounded-lg border border-neutral/20 hover:border-neutral/30 transition-all duration-200 group flex items-center justify-center">
<img src="https://billing.virtnet.bond/assets/images/peerdb.png" class="w-5 h-5 filter grayscale group-hover:grayscale-0 transition-all duration-200" alt="PeeringDB logo">
</a>
<a href="https://bgp.tools/as/214022" target="_blank" rel="noopener noreferrer"
class="p-2 bg-bg-secondary/50 hover:bg-bg-secondary rounded-lg border border-neutral/20 hover:border-neutral/30 transition-all duration-200 group flex items-center justify-center">
<img src="https://billing.virtnet.bond/assets/images/bgptools.png" class="w-5 h-5 filter grayscale group-hover:grayscale-0 transition-all duration-200" alt="BGP Tools logo">
</a>
<!-- GitHub -->
@if (theme('github_url'))
<a href="{{ theme('github_url') }}" class="p-2 bg-bg-secondary/50 hover:bg-bg-secondary rounded-lg border border-neutral/20 hover:border-neutral/30 transition-all duration-200 group flex items-center justify-center">
<x-ri-github-fill class="w-5 h-5 text-base/60 group-hover:text-secondary" />
</a>
@endif
<!-- Gitea -->
<a href="https://gitea.virtnet.bond/" target="_blank" rel="noopener noreferrer"
class="p-2 bg-bg-secondary/50 hover:bg-bg-secondary rounded-lg border border-neutral/20 hover:border-neutral/30 transition-all duration-200 group flex items-center justify-center">
<x-ri-git-branch-line class="w-5 h-5 text-base/60 group-hover:text-secondary" />
</a>
</div>
<!-- Trusted Partners Title -->
<div class="mt-4 text-sm font-semibold text-base/70">Trusted Partners</div>
<!-- Trusted Partners Links -->
<div class="flex flex-wrap gap-4 text-sm mt-2">
<a href="https://www.avsisp.com/" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">AVS ISP</a>
<a href="https://cilix.co.uk/" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">Cilix Limited</a>
<a href="https://informatiq.org/" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">InformatiQ</a>
<a href="https://stocade.com/" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">Stocade</a>
<a href="https://tritan.gg/" target="_blank" rel="noopener noreferrer" class="text-base/60 hover:text-secondary transition-colors duration-200">Tritan Internet</a>
</div>
<!-- Social Icons Section -->
</div>
</div>
</div>
</footer>

View File

@@ -0,0 +1,181 @@
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('img:not([loading])')
.forEach(img => img.loading = 'lazy');
});
</script>
<style>
.text-flow-effect {
background: linear-gradient(75deg,
hsl(178, 46.40%, 11.00%) 0%,
hsl(211, 57.80%, 17.60%) 30%,
hsl(220, 98%, 61%) 45%,
hsl(207, 53.00%, 50.80%) 55%,
hsl(214, 82.50%, 31.40%) 70%,
hsl(222, 47%, 11%) 100%);
background-size: 400% auto;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: textFlowAnimation 8s linear infinite;
}
@keyframes textFlowAnimation {
to {
background-position: 400% center;
}
}
</style>
<nav class="w-full px-4 lg:px-8 bg-background-secondary border-b border-neutral flex flex-col lg:flex-row justify-between fixed top-0 z-20 h-auto">
<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">
<!-- Logo -->
<div class="flex flex-row items-center">
<a href="{{ route('home') }}" class="flex items-center h-10 gap-2" wire:navigate>
<x-logo class="h-10" />
<span class="text-xl font-bold leading-none flex items-center text-flow-effect">{{ config('app.name') }}</span>
</a>
<!-- Desktop menu: lg and above -->
<div class="hidden lg:flex 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>
<span class="flex flex-row items-center p-3 text-sm font-semibold whitespace-nowrap text-base hover:text-base/80">{{ $nav['name'] }}</span>
</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 px-4 py-2">
{{ $nav['name'] }}
</x-navigation.link>
@endif
@endforeach
</div>
</div>
<!-- Right-side controls -->
<div class="flex items-center gap-3">
<!-- Desktop-only: md/lg controls -->
<!--<div class="hidden lg:flex items-center gap-2">
<x-dropdown>
<x-slot:trigger>
<span class="text-sm font-semibold">{{ strtoupper(app()->getLocale()) }} <span class="text-base/50">|</span> {{ session('currency', config('settings.default_currency')) }}</span>
</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 />
<!-- User account -->
@if(auth()->check())
<div class="relative hidden lg:block" x-data="{ open: false }">
<button @click="open = !open" class="flex items-center gap-3 p-2 rounded-lg transition-all duration-200 group">
<div class="relative">
<img src="{{ auth()->user()->avatar }}" class="w-10 h-10 rounded-full border-2 border-background-secondary group-hover:border-blue-500 transition-all duration-300 shadow-lg" alt="avatar" />
<div class="absolute -bottom-0.5 -right-0.5 w-4 h-4 bg-green-500 rounded-full border-2 border-background shadow-lg" x-show="isOnline" x-transition:enter="transition-all duration-300" x-transition:enter-start="scale-0 opacity-0" x-transition:enter-end="scale-100 opacity-100">
<div class="w-full h-full bg-green-400 rounded-full animate-ping"></div>
</div>
</div>
<div class="hidden lg:block text-left">
<div class="text-sm font-bold">{{ Auth::user()->first_name }}</div>
<div class="text-xs">{{ auth()->user()->email }}</div>
</div>
<svg class="w-4 h-4 transform transition-transform duration-200" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div x-show="open" x-transition ... @click.away="open = false" class="absolute right-0 mt-2 w-72 bg-background-secondary border border-neutral rounded-2xl shadow-2xl z-50">
<div class="p-4 border-b border-neutral">
<div class="flex items-center gap-3">
<img src="{{ auth()->user()->avatar }}" class="rounded-full border-2 border-neutral" alt="avatar" />
<div>
<div class="font-medium text-base">{{ auth()->user()->name }}</div>
<div class="text-sm text-base">{{ auth()->user()->email }}</div>
<div class="text-xs text-green-400 font-medium">● Online</div>
</div>
</div>
</div>
<div class="py-2">
@foreach (\App\Classes\Navigation::getAccountDropdownLinks() as $nav)
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? null)" :spa="isset($nav['spa']) ? $nav['spa'] : true" class="flex items-center gap-3 px-4 py-3 text-sm text-base hover:bg-background transition-all duration-200">
{{ $nav['name'] }}
</x-navigation.link>
@endforeach
<div class="px-1">
<livewire:auth.logout />
</div>
</div>
</div>
</div>
@else
<div class="hidden lg:flex items-center gap-3">
<a href="{{ route('login') }}" wire:navigate>
<button class="px-4 py-2 text-sm font-medium text-base border border-neutral hover:text-white hover:bg-primary rounded-lg transition-all duration-200">
{{ __('navigation.login') }}
</button>
</a>
@if(!config('settings.registration_disabled', false))
<a href="{{ route('register') }}" wire:navigate>
<button class="px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/50 hover:to-secondary/50 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105">
{{ __('navigation.register') }}
</button>
</a>
@endif
</div>
@endif
<!-- Mobile menu toggle -->
<button @click="slideOverOpen = !slideOverOpen" class="relative w-12 h-12 flex lg:hidden items-center justify-center rounded-xl bg-background hover:bg-neutral/50 transition-all duration-300 group" aria-label="Toggle Menu">
<span x-show="!slideOverOpen" x-transition ... class="absolute inset-0 flex items-center justify-center">
<x-ri-menu-fill class="size-6 text-base/80 group-hover:text-foreground transition-colors duration-200" />
</span>
<span x-show="slideOverOpen" x-transition ... class="absolute inset-0 flex items-center justify-center">
<x-ri-close-fill class="size-6 text-base/80 group-hover:text-foreground transition-colors duration-200" />
</span>
</button>
</div>
</div>
<!-- Mobile slide-over menu (<1024px) -->
<template x-teleport="body">
<div x-show="slideOverOpen" @keydown.window.escape="slideOverOpen=false" x-cloak class="fixed left-0 right-0 top-20 w-full z-[99]" style="height:calc(100dvh - 5rem);" aria-modal="true">
<div x-show="slideOverOpen" x-transition ... @click.away="slideOverOpen = false" class="absolute inset-0 bg-background-secondary/80 backdrop-blur-md border-t border-neutral/50 shadow-2xl overflow-y-auto flex flex-col rounded-t-2xl">
<div class="flex flex-col h-full p-6">
<div class="flex-1 min-h-0 overflow-y-auto">
<x-navigation.sidebar-links />
</div>
</div>
</div>
</div>
</template>
</div>
</nav>

View File

@@ -0,0 +1,6 @@
@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>

View File

@@ -0,0 +1,185 @@
<div class="lg:px-4 lg:py-6 flex flex-col gap-3 h-full">
<div class="flex flex-col gap-2">
@foreach (\App\Classes\Navigation::getLinks() as $nav)
@if (!empty($nav['children']))
<div x-data="{ activeAccordion: {{ $nav['active'] ? 'true' : 'false' }} }"
class="relative w-full overflow-hidden text-sm font-medium">
<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/10 transition-colors duration-200 {{ $nav['active'] ? 'text-primary bg-primary/5' : 'text-color-base' }}">
<div class="flex items-center gap-3">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="size-5 {{ $nav['active'] ? 'text-primary' : 'text-color-muted' }}" />
@endisset
<span>{{ $nav['name'] }}</span>
</div>
<x-ri-arrow-down-s-line x-bind:class="{ 'rotate-180': activeAccordion }"
class="size-5 text-color-muted transition-transform duration-300" />
</button>
<div x-show="activeAccordion" x-collapse x-cloak>
<div class="py-2 pl-8 pr-4 border-l-2 border-primary/50 ml-4 opacity-90">
@foreach ($nav['children'] as $child)
<div class="flex items-center py-1">
<x-navigation.link :href="route($child['route'], $child['params'] ?? [])"
:spa="$child['spa'] ?? true"
class="block w-full p-2 rounded-md transition-colors duration-200
{{ $child['active'] ? 'text-primary font-bold bg-primary/10' : 'text-color-muted hover:bg-primary/5' }}">
{{ $child['name'] }}
</x-navigation.link>
</div>
@endforeach
</div>
</div>
</div>
</div>
@else
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? [])"
:spa="$nav['spa'] ?? true"
class="flex items-center gap-3 p-3 rounded-lg transition-colors duration-200 w-full
{{ $nav['active'] ? 'bg-primary/10 text-primary font-semibold' : 'hover:bg-primary/5 text-color-base' }}">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="size-5 {{ $nav['active'] ? 'text-primary' : 'text-color-muted' }}" />
@endisset
<span>{{ $nav['name'] }}</span>
</x-navigation.link>
@endif
@isset($nav['separator'])
<div class="h-px w-full bg-neutral/50 my-2"></div>
@endisset
@endforeach
</div>
{{-- Separator between main navigation and dashboard navigation --}}
@auth
<div class="h-px w-full bg-neutral/50 my-2"></div>
@endauth
{{-- Dashboard Navigation Section --}}
<div class="flex flex-col gap-2 flex-grow">
@foreach (\App\Classes\Navigation::getDashboardLinks() as $nav)
@if (!empty($nav['children']))
<div x-data="{ activeAccordion: {{ $nav['active'] ? 'true' : 'false' }} }"
class="relative w-full overflow-hidden text-sm font-medium">
<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/10 transition-colors duration-200 {{ $nav['active'] ? 'text-primary bg-primary/5' : 'text-color-base' }}">
<div class="flex items-center gap-3">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="size-5 {{ $nav['active'] ? 'text-primary' : 'text-color-muted' }}" />
@endisset
<span>{{ $nav['name'] }}</span>
</div>
<x-ri-arrow-down-s-line x-bind:class="{ 'rotate-180': activeAccordion }"
class="size-5 text-color-muted transition-transform duration-300" />
</button>
<div x-show="activeAccordion" x-collapse x-cloak>
<div class="py-2 pl-8 pr-4 border-l-2 border-primary/50 ml-4 opacity-90">
@foreach ($nav['children'] as $child)
@if ($child['condition'] ?? true)
<div class="flex items-center py-1">
<x-navigation.link :href="route($child['route'], $child['params'] ?? [])"
:spa="$child['spa'] ?? true"
class="block w-full p-2 rounded-md transition-colors duration-200
{{ $child['active'] ? 'text-primary font-bold bg-primary/10' : 'text-color-muted hover:bg-primary/5' }}">
{{ $child['name'] }}
</x-navigation.link>
</div>
@endif
@endforeach
</div>
</div>
</div>
</div>
@else
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? [])"
:spa="$nav['spa'] ?? true"
class="flex items-center gap-3 p-3 rounded-lg transition-colors duration-200 w-full
{{ $nav['active'] ? 'bg-primary/10 text-primary font-semibold' : 'hover:bg-primary/5 text-color-base' }}">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="size-5 {{ $nav['active'] ? 'text-primary' : 'text-color-muted' }}" />
@endisset
<span>{{ $nav['name'] }}</span>
</x-navigation.link>
@endif
@isset($nav['separator'])
<div class="h-px w-full bg-neutral/50 my-2"></div>
@endisset
@endforeach
<div class="flex flex-row items-center mt-6 justify-between md:hidden border-t border-neutral/50 pt-4">
<x-dropdown>
<x-slot:trigger>
<div class="flex flex-col items-start cursor-pointer hover:opacity-80 transition-opacity duration-200">
<div class="flex items-center gap-2">
<x-ri-global-line class="size-5 text-color-muted" />
<span class="text-sm text-color-base font-semibold">
{{ strtoupper(app()->getLocale()) }}
<span class="text-color-muted/50 font-semibold mx-1">|</span>
<span class="text-color-base">{{ session('currency', config('settings.default_currency')) }}</span>
</span>
</div>
<span class="text-xs text-color-muted mt-1">Language & Currency</span>
</div>
</x-slot:trigger>
<x-slot:content>
<strong class="block p-2 text-xs font-semibold uppercase text-color-muted">Language</strong>
<livewire:components.language-switch />
<strong class="block p-2 text-xs font-semibold uppercase text-color-muted mt-2">Currency</strong>
<livewire:components.currency-switch />
</x-slot:content>
</x-dropdown>
<x-theme-toggle class="p-2 rounded-full hover:bg-primary/5 transition-colors duration-200" />
</div>
</div>
@if(auth()->check())
<div class="border-t border-neutral/50 pt-4 mt-auto">
<div class="flex items-center gap-3 p-3 mb-3 bg-background-secondary/50 rounded-xl">
<img src="{{ auth()->user()->avatar }}" class="size-10 rounded-full border-2 border-neutral bg-background shadow-lg" alt="User Avatar" />
<div class="flex flex-col flex-1">
<span class="text-sm font-semibold text-color-base">{{ auth()->user()->name }}</span>
<span class="text-xs text-color-muted truncate">{{ auth()->user()->email }}</span>
</div>
</div>
<!-- Account dropdown links for mobile -->
<div class="flex flex-col gap-1 mb-3">
@foreach (\App\Classes\Navigation::getAccountDropdownLinks() as $nav)
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? null)" :spa="isset($nav['spa']) ? $nav['spa'] : true"
class="flex items-center gap-3 px-4 py-2.5 text-sm text-color-base hover:bg-background-secondary/50 rounded-lg transition-all duration-200">
{{ $nav['name'] }}
</x-navigation.link>
@endforeach
</div>
<livewire:auth.logout
class="flex items-center gap-3 p-3 rounded-lg transition-colors duration-200 w-full text-color-base hover:bg-error/10 hover:text-error border border-neutral/50" />
</div>
@else
<div class="border-t border-neutral/50 pt-4 mt-auto">
<div class="flex flex-col gap-3">
<a href="{{ route('login') }}" wire:navigate class="w-full">
<button class="flex items-center justify-center gap-2 w-full px-4 py-3 text-sm font-semibold text-color-base border-2 border-neutral hover:border-primary hover:text-primary hover:bg-primary/5 rounded-xl transition-all duration-200">
<x-ri-login-box-line class="size-5" />
<span>{{ __('navigation.login') }}</span>
</button>
</a>
@if(!config('settings.registration_disabled', false))
<a href="{{ route('register') }}" wire:navigate class="w-full">
<button class="flex items-center justify-center gap-2 w-full px-4 py-3 text-sm font-semibold text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-[1.02]">
<x-ri-user-add-line class="size-5" />
<span>{{ __('navigation.register') }}</span>
</button>
</a>
@endif
</div>
</div>
@endif
</div>

View File

@@ -0,0 +1,211 @@
<aside class="mt-24 ml-8 w-64 h-[calc(100vh-2rem)] md:flex hidden flex-col fixed top-0 left-0 rtl:right-0 z-10">
<div class="h-9/10 bg-background-secondary/80 backdrop-blur-md border border-neutral/50 rounded-2xl shadow-xl overflow-hidden">
<div class="flex flex-col h-full">
<div class="p-6 border-b border-neutral/50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-primary to-secondary rounded-xl flex items-center justify-center shadow-lg">
<x-ri-dashboard-line class="w-6 h-6 text-white" />
</div>
<div>
<h2 class="text-lg font-bold text-foreground">{{ __('navigation.home') }}</h2>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto p-4">
<div class="space-y-2">
<div class="mb-6 md:hidden">
<h3 class="text-xs font-semibold text-base/50 uppercase tracking-wider mb-3 px-3"></h3>
<div class="space-y-1">
@foreach (\App\Classes\Navigation::getLinks() as $nav)
@if (!empty($nav['children']))
<div x-data="{ activeAccordion: {{ $nav['active'] ? 'true' : 'false' }} }"
class="relative w-full overflow-hidden">
<button @click="activeAccordion = !activeAccordion"
class="flex items-center justify-between w-full px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 group
{{ $nav['active'] ? 'text-primary bg-primary/10 border border-primary/20 shadow-sm' : 'text-base/80 hover:text-foreground hover:bg-background' }}">
<div class="flex items-center gap-3">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="w-5 h-5 {{ $nav['active'] ? 'text-primary' : 'text-base/60 group-hover:text-primary' }} transition-colors" />
@endisset
<span>{{ $nav['name'] }}</span>
</div>
<x-ri-arrow-down-s-line x-bind:class="{ 'rotate-180': activeAccordion }"
class="w-5 h-5 text-base/60 transition-transform duration-300" />
</button>
<div x-show="activeAccordion" x-collapse x-cloak>
<div class="py-2 pl-8 pr-4 mt-2 border-l-2 border-primary/30 ml-4">
@foreach ($nav['children'] as $child)
<x-navigation.link :href="route($child['route'], $child['params'] ?? [])"
:spa="$child['spa'] ?? true"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium mb-1 transition-all duration-200 group
{{ $child['active'] ? 'text-primary bg-primary/10 border border-primary/20' : 'text-base/70 hover:text-foreground hover:bg-background/50' }}">
{{ $child['name'] }}
@if($child['active'])
<div class="ml-auto w-2 h-2 bg-primary rounded-full"></div>
@endif
</x-navigation.link>
@endforeach
</div>
</div>
</div>
@else
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? [])"
:spa="$nav['spa'] ?? true"
class="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 group
{{ $nav['active'] ? 'text-primary bg-primary/10 border border-primary/20 shadow-sm' : 'text-base/80 hover:text-foreground hover:bg-background' }}">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="w-5 h-5 {{ $nav['active'] ? 'text-primary' : 'text-base/60 group-hover:text-primary' }} transition-colors" />
@endisset
<span>{{ $nav['name'] }}</span>
@if($nav['active'])
<div class="ml-auto w-2 h-2 bg-primary rounded-full"></div>
@endif
</x-navigation.link>
@endif
@isset($nav['separator'])
<div class="h-px w-full bg-neutral/30 my-3 mx-3"></div>
@endisset
@endforeach
</div>
</div>
<div class="mb-6">
<h3 class="text-xs font-semibold text-base/50 uppercase tracking-wider mb-3 px-3">{{ __('navigation.dashboard') }}</h3>
<div class="space-y-1">
@foreach (\App\Classes\Navigation::getDashboardLinks() as $nav)
@if (!empty($nav['children']))
<div x-data="{ activeAccordion: {{ $nav['active'] ? 'true' : 'false' }} }"
class="relative w-full overflow-hidden">
<button @click="activeAccordion = !activeAccordion"
class="flex items-center justify-between w-full px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 group
{{ $nav['active'] ? 'text-primary bg-primary/10 border border-primary/20 shadow-sm' : 'text-base/80 hover:text-foreground hover:bg-background' }}">
<div class="flex items-center gap-3">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="w-5 h-5 {{ $nav['active'] ? 'text-primary' : 'text-base/60 group-hover:text-primary' }} transition-colors" />
@endisset
<span>{{ $nav['name'] }}</span>
</div>
<x-ri-arrow-down-s-line x-bind:class="{ 'rotate-180': activeAccordion }"
class="w-5 h-5 text-base/60 transition-transform duration-300" />
</button>
<div x-show="activeAccordion" x-collapse x-cloak>
<div class="py-2 pl-8 pr-4 mt-2 border-l-2 border-primary/30 ml-4">
@foreach ($nav['children'] as $child)
@if ($child['condition'] ?? true)
<x-navigation.link :href="route($child['route'], $child['params'] ?? [])"
:spa="$child['spa'] ?? true"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium mb-1 transition-all duration-200 group
{{ $child['active'] ? 'text-primary bg-primary/10 border border-primary/20' : 'text-base/70 hover:text-foreground hover:bg-background/50' }}">
{{ $child['name'] }}
@if($child['active'])
<div class="ml-auto w-2 h-2 bg-primary rounded-full"></div>
@endif
</x-navigation.link>
@endif
@endforeach
</div>
</div>
</div>
@else
<x-navigation.link :href="route($nav['route'], $nav['params'] ?? [])"
:spa="$nav['spa'] ?? true"
class="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 group
{{ $nav['active'] ? 'text-primary bg-primary/10 border border-primary/20 shadow-sm' : 'text-base/80 hover:text-foreground hover:bg-background' }}">
@isset($nav['icon'])
<x-dynamic-component :component="$nav['icon']"
class="w-5 h-5 {{ $nav['active'] ? 'text-primary' : 'text-base/60 group-hover:text-primary' }} transition-colors" />
@endisset
<span>{{ $nav['name'] }}</span>
@if($nav['active'])
<div class="ml-auto w-2 h-2 bg-primary rounded-full"></div>
@endif
</x-navigation.link>
@endif
@isset($nav['separator'])
<div class="h-px w-full bg-neutral/30 my-3 mx-3"></div>
@endisset
@endforeach
<div class="h-px w-full bg-neutral/30 my-3 mx-3"></div>
<a href="https://auth.virtnet.bond" target="_blank" rel="noopener" class="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 group hover:text-foreground hover:bg-background">
<svg height="20px" width="20px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 25.573 25.573" xml:space="preserve" class="w-5 h-5 text-base/60 group-hover:text-primary transition-colors">
<g>
<g>
<polygon style="currentColor" points="12.036,24.589 12.036,3.296 15.391,0.983 15.391,22.74 "/>
<path style="fill:currentColor;" d="M11.11,7.926v2.893c0,0-6.632,0.521-7.058,5.556c0,0-0.93,4.396,7.058,5.785v2.43
c0,0-11.226-1.155-11.109-8.331C0.001,16.258-0.115,8.968,11.11,7.926z"/>
<path style="fill:currentColor;" d="M16.2,7.926v2.702c0,0,2.142-0.029,3.934,1.463l-1.964,0.807l7.403,1.855V8.967l-2.527,1.43
C23.046,10.397,20.889,8.13,16.2,7.926z"/>
</g>
</g>
</svg><span>OpenID</span>
</a>
<a href="https://pve.virtnet.bond" target="_blank" rel="noopener" class="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 group hover:text-foreground hover:bg-background">
<svg fill="currentColor" width="20px" height="20px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-base/60 group-hover:text-primary transition-colors"><path d="M4.928 1.825c-1.09.553-1.09.64-.07 1.78 5.655 6.295 7.004 7.782 7.107 7.782.139.017 7.971-8.542 8.058-8.801.034-.07-.208-.312-.519-.536-.415-.312-.864-.433-1.712-.467-1.59-.104-2.144.242-4.115 2.455-.899 1.003-1.66 1.833-1.66 1.833-.017 0-.76-.813-1.642-1.798S8.473 2.1 8.127 1.91c-.796-.45-2.421-.484-3.2-.086zM1.297 4.367C.45 4.695 0 5.007 0 5.248c0 .121 1.331 1.678 2.94 3.459 1.625 1.78 2.939 3.268 2.939 3.302 0 .035-1.331 1.522-2.94 3.303C1.314 17.11.017 18.683.035 18.822c.086.467 1.504 1.055 2.541 1.055 1.678-.018 2.058-.312 5.603-4.202 1.78-1.954 3.233-3.614 3.233-3.666 0-.069-1.435-1.694-3.199-3.63-2.3-2.508-3.423-3.632-3.96-3.874-.812-.398-2.126-.467-2.956-.138zm18.467.12c-.502.26-1.764 1.505-3.943 3.891-1.763 1.937-3.199 3.562-3.199 3.631 0 .07 1.453 1.712 3.234 3.666 3.544 3.89 3.925 4.184 5.602 4.202 1.038 0 2.455-.588 2.542-1.055.017-.156-1.28-1.712-2.905-3.493-1.608-1.78-2.94-3.285-2.94-3.32 0-.034 1.332-1.539 2.94-3.32C22.72 6.91 24.017 5.352 24 5.214c-.087-.45-1.366-.968-2.473-1.038-.795-.034-1.21.035-1.763.312zM7.954 16.973c-2.144 2.369-3.908 4.374-3.943 4.46-.034.07.208.312.52.537.414.311.864.432 1.711.467 1.574.103 2.161-.26 4.15-2.508.864-.968 1.608-1.78 1.625-1.78s.761.812 1.643 1.798c2.023 2.248 2.559 2.576 4.132 2.49.848-.035 1.297-.156 1.712-.467.311-.225.553-.467.519-.536-.087-.26-7.92-8.819-8.058-8.801-.069 0-1.867 1.954-4.011 4.34z"/></svg><span>Proxmox</span>
</a>
<a
href="https://discord.gg/zFTJG2hmwN"
target="_blank"
rel="noopener"
class="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 group hover:text-foreground hover:bg-background">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-5 h-5 text-base/60 group-hover:text-primary transition-colors">
<path d="M8.52062 13.8458C7.48059 13.8458 6.63159 12.9014 6.63159 11.7446 6.63159 10.5879 7.45936 9.64331 8.52062 9.64331 9.57123 9.64331 10.4308 10.5879 10.4096 11.7446 10.4096 12.9014 9.57123 13.8458 8.52062 13.8458ZM15.4941 13.8458C14.454 13.8458 13.604 12.9014 13.604 11.7446 13.604 10.5879 14.4328 9.64331 15.4941 9.64331 16.5447 9.64331 17.4043 10.5879 17.3831 11.7446 17.3831 12.9014 16.5553 13.8458 15.4941 13.8458ZM10.1253 4.32296 9.81655 3.76001 9.18323 3.86556C7.71915 4.10958 6.32658 4.54677 5.02544 5.14604L4.79651 5.25148 4.65507 5.46009C2.0418 9.31441 1.3258 13.1087 1.68032 16.8362L1.71897 17.2425 2.04912 17.4824C3.78851 18.7465 5.47417 19.5189 7.12727 20.0257L7.91657 20.2676 9.03013 17.5506C10.9397 18.0226 13.0592 18.0228 14.969 17.5511L16.0757 20.2683 16.8668 20.0256C18.5173 19.5193 20.2137 18.7472 21.9466 17.4811L22.2726 17.243 22.3131 16.8414C22.7491 12.5213 21.616 8.75773 19.3547 5.45652L19.2128 5.24944 18.9846 5.14504C17.6767 4.54685 16.2852 4.10981 14.8309 3.86573L14.2132 3.76207 13.8987 4.30369C13.8112 4.45445 13.7215 4.62464 13.6364 4.79687 12.5441 4.6847 11.456 4.68446 10.3726 4.79652 10.2882 4.62736 10.2025 4.4638 10.1253 4.32296ZM6.71436 16.6102C6.91235 16.7243 7.11973 16.8358 7.32557 16.9381L6.8764 18.034C5.75585 17.6259 4.61837 17.0637 3.4476 16.2557 3.22313 13.1178 3.86092 9.951 6.01196 6.68626 6.90962 6.29123 7.8535 5.98279 8.83606 5.77295 8.89631 5.89831 8.95235 6.02066 8.99839 6.12917L9.27128 6.77238 9.96259 6.67098C11.3152 6.4726 12.6772 6.47234 14.0523 6.67124L14.7424 6.77106 15.0147 6.12917C15.0621 6.01743 15.1167 5.89547 15.1743 5.77322 16.1525 5.98326 17.098 6.29212 18.0029 6.68812 19.8781 9.50857 20.8241 12.6544 20.5486 16.2552 19.3837 17.0625 18.2422 17.6249 17.1193 18.0335L16.6735 16.939C16.8799 16.8365 17.0879 16.7246 17.2865 16.6102 17.7763 16.328 18.3039 15.976 18.6402 15.6397L17.3606 14.3602C17.1969 14.5239 16.837 14.7808 16.3831 15.0423 15.9388 15.2983 15.498 15.5052 15.2164 15.5983 13.2126 16.2608 10.7883 16.2608 8.78443 15.5983 8.50285 15.5052 8.06205 15.2983 7.61772 15.0423 7.16383 14.7808 6.80392 14.5239 6.64017 14.3602L5.36065 15.6397C5.6969 15.976 6.2245 16.328 6.71436 16.6102Z"></path>
</svg>
<span>Discord</span>
</a>
<a
href="https://t.me/virtnetbondvps"
target="_blank"
rel="noopener"
class="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 hover:scale-105 group hover:text-foreground hover:bg-background">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-5 h-5 text-base/60 group-hover:text-primary transition-colors">
<path d="M17.0943 7.14643C17.6874 6.93123 17.9818 6.85378 18.1449 6.82608C18.1461 6.87823 18.1449 6.92051 18.1422 6.94825C17.9096 9.39217 16.8906 15.4048 16.3672 18.2026C16.2447 18.8578 16.1507 19.1697 15.5179 18.798C15.1014 18.5532 14.7245 18.2452 14.3207 17.9805C12.9961 17.1121 11.1 15.8189 11.2557 15.8967C9.95162 15.0373 10.4975 14.5111 11.2255 13.8093C11.3434 13.6957 11.466 13.5775 11.5863 13.4525C11.64 13.3967 11.9027 13.1524 12.2731 12.8081C13.4612 11.7035 15.7571 9.56903 15.8151 9.32202C15.8246 9.2815 15.8334 9.13045 15.7436 9.05068C15.6539 8.97092 15.5215 8.9982 15.4259 9.01989C15.2904 9.05064 13.1326 10.4769 8.95243 13.2986C8.33994 13.7192 7.78517 13.9242 7.28811 13.9134L7.29256 13.9156C6.63781 13.6847 5.9849 13.4859 5.32855 13.286C4.89736 13.1546 4.46469 13.0228 4.02904 12.8812C3.92249 12.8466 3.81853 12.8137 3.72083 12.783C8.24781 10.8109 11.263 9.51243 12.7739 8.884C14.9684 7.97124 16.2701 7.44551 17.0943 7.14643ZM19.5169 5.21806C19.2635 5.01244 18.985 4.91807 18.7915 4.87185C18.5917 4.82412 18.4018 4.80876 18.2578 4.8113C17.7814 4.81969 17.2697 4.95518 16.4121 5.26637C15.5373 5.58382 14.193 6.12763 12.0058 7.03736C10.4638 7.67874 7.39388 9.00115 2.80365 11.001C2.40046 11.1622 2.03086 11.3451 1.73884 11.5619C1.46919 11.7622 1.09173 12.1205 1.02268 12.6714C0.970519 13.0874 1.09182 13.4714 1.33782 13.7738C1.55198 14.037 1.82635 14.1969 2.03529 14.2981C2.34545 14.4483 2.76276 14.5791 3.12952 14.6941C3.70264 14.8737 4.27444 15.0572 4.84879 15.233C6.62691 15.7773 8.09066 16.2253 9.7012 17.2866C10.8825 18.0651 12.041 18.8775 13.2243 19.6531C13.6559 19.936 14.0593 20.2607 14.5049 20.5224C14.9916 20.8084 15.6104 21.0692 16.3636 20.9998C17.5019 20.8951 18.0941 19.8479 18.3331 18.5703C18.8552 15.7796 19.8909 9.68351 20.1332 7.13774C20.1648 6.80544 20.1278 6.433 20.097 6.25318C20.0653 6.068 19.9684 5.58448 19.5169 5.21806Z"></path>
</svg>
<span>Telegram</span>
</a>
</div>
</div>
</div>
</div>
@if(auth()->check())
<div class="flex items-center gap-3 p-3 bg-background/80 backdrop-blur-sm rounded-xl border border-neutral/50">
<div class="relative">
<img src="{{ auth()->user()->avatar }}"
class="w-10 h-10 rounded-full border-2 border-neutral/50 bg-background"
alt="User Avatar" />
<div class="absolute -bottom-0.5 -right-0.5 w-4 h-4 bg-green-500 rounded-full border-2 border-background">
<div class="w-full h-full bg-green-400 rounded-full animate-ping"></div>
</div>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-foreground truncate">{{ auth()->user()->name }}</p>
<p class="text-xs text-base/60 truncate">{{ auth()->user()->email }}</p>
</div>
<div class="flex items-center gap-1">
<livewire:auth.logout class="w-8 h-8 hover:bg-neutral/50 rounded-lg flex items-center justify-center transition-all duration-200" />
</div>
</div>
@endif
</div>
</div>
</div>
</aside>

View 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>

View 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>

View File

@@ -0,0 +1,112 @@
<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.selectPositionUpdate());
},
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() {
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>

View 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>