Enhance layout and components with improved accessibility and performance
- Refactor JavaScript functions to prevent duplicate initializations on navigation - Add skeleton loaders for invoices and services during loading states - Introduce a new progress bar component with customizable properties - Update dropdown and checkbox components for better styling and accessibility - Improve modal and navigation components with enhanced animations and structure
This commit is contained in:
@@ -5,11 +5,19 @@
|
||||
'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 }}">
|
||||
<div class="relative">
|
||||
<input type="checkbox" name="{{ $name }}" id="{{ $id ?? $name }}"
|
||||
{{ $attributes->except(['label', 'name', 'id', 'class', 'divClass', 'required']) }}
|
||||
class="peer form-checkbox size-5 text-primary rounded border-2 border-neutral bg-background-secondary/50
|
||||
focus:ring-2 focus:ring-primary/30 focus:ring-offset-0 focus:outline-none
|
||||
checked:bg-primary checked:border-primary
|
||||
hover:border-primary/50 transition-all duration-200 cursor-pointer
|
||||
appearance-none" />
|
||||
<svg class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-3 text-white pointer-events-none opacity-0 scale-50 peer-checked:opacity-100 peer-checked:scale-100 transition-all duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<label class="ml-2 text-sm text-color-base cursor-pointer select-none" for="{{ $id ?? $name }}">
|
||||
@if(isset($label))
|
||||
{{ $label }}
|
||||
@else
|
||||
@@ -18,6 +26,6 @@
|
||||
</label>
|
||||
|
||||
@error($name)
|
||||
<p class="text-red-500 text-xs">{{ $message }}</p>
|
||||
<p class="text-red-500 text-xs ml-2">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@@ -123,61 +123,77 @@
|
||||
@enderror
|
||||
</fieldset>
|
||||
|
||||
<script>
|
||||
<script data-navigate-track>
|
||||
// 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;
|
||||
(function() {
|
||||
// Avoid duplicate initialization
|
||||
const initPasswordStrength = function() {
|
||||
const passwordInputs = document.querySelectorAll('[data-hs-strong-password]:not([data-password-strength-initialized])');
|
||||
|
||||
// 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);
|
||||
passwordInputs.forEach(function(input) {
|
||||
// Mark as initialized to prevent duplicate event listeners
|
||||
input.setAttribute('data-password-strength-initialized', 'true');
|
||||
|
||||
// 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('');
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// 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) {
|
||||
e.preventDefault();
|
||||
input.focus();
|
||||
|
||||
// Show warning
|
||||
if (warningElement) {
|
||||
warningElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Show validation message
|
||||
input.reportValidity();
|
||||
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.hasAttribute('data-password-form-initialized')) {
|
||||
form.setAttribute('data-password-form-initialized', 'true');
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Run on DOMContentLoaded and Livewire navigation
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPasswordStrength);
|
||||
} else {
|
||||
initPasswordStrength();
|
||||
}
|
||||
|
||||
document.addEventListener('livewire:navigated', initPasswordStrength);
|
||||
})();
|
||||
|
||||
function getCurrentPasswordStrength(password) {
|
||||
const checks = {
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
@props([
|
||||
'name',
|
||||
'label' => null,
|
||||
'options' => [],
|
||||
'selected' => null,
|
||||
'multiple' => false,
|
||||
'required' => false,
|
||||
'divClass' => null,
|
||||
'hideRequiredIndicator' => false,
|
||||
'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>
|
||||
<legend class="mb-2">
|
||||
<label for="{{ $name }}" class="text-sm font-bold text-color-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">
|
||||
<div class="block w-full text-sm text-color-base space-y-2">
|
||||
@if (count($options) == 0 && $slot)
|
||||
{{ $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
|
||||
@foreach ($options as $key => $option)
|
||||
<div class="flex items-center gap-3 group">
|
||||
<div class="relative">
|
||||
<input type="radio" id="{{ $name }}_{{ $key }}" name="{{ $name }}"
|
||||
value="{{ gettype($options) == 'array' ? $option : $key }}"
|
||||
{{ ($multiple && $selected ? in_array($key, $selected) : $selected==$option) ? 'checked' : '' }}
|
||||
class="peer form-radio size-5 text-primary border-2 border-neutral bg-background-secondary/50
|
||||
focus:ring-2 focus:ring-primary/30 focus:ring-offset-0 focus:outline-none
|
||||
checked:bg-primary checked:border-primary
|
||||
hover:border-primary/50 transition-all duration-200 cursor-pointer
|
||||
appearance-none rounded-full" />
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-2 bg-white rounded-full pointer-events-none opacity-0 scale-0 peer-checked:opacity-100 peer-checked:scale-100 transition-all duration-200"></div>
|
||||
</div>
|
||||
<label for="{{ $name }}_{{ $key }}" class="text-color-base cursor-pointer select-none group-hover:text-primary transition-colors duration-200">
|
||||
{{ $option }}
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@error($name)
|
||||
<p class="text-red-500 text-xs">{{ $message }}</p>
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</fieldset>
|
||||
Reference in New Issue
Block a user