Replace native confirm() dialogs with styled confirm modals.
Deploy Ladill Hosting / deploy (push) Successful in 30s

Add shared confirm-dialog component, Alpine ladillConfirm store, and
swap browser confirms for consistent bottom-sheet modals across user,
admin, and hosting flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-07 21:11:04 +00:00
co-authored by Cursor
parent 61999f3a21
commit bc08624fff
21 changed files with 499 additions and 85 deletions
+7
View File
@@ -1,5 +1,10 @@
import './bootstrap';
import { registerLadillConfirmStore, registerLadillModalHelpers } from './ladill-modals';
registerLadillModalHelpers();
import Alpine from 'alpinejs';
import collapse from '@alpinejs/collapse';
@@ -187,4 +192,6 @@ Alpine.data('topbarSearch', (config = {}) => ({
}));
window.Alpine = Alpine;
registerLadillConfirmStore(Alpine);
Alpine.start();
+75
View File
@@ -0,0 +1,75 @@
/**
* Lightweight modal dispatch helpers (optional — balanceGate in layout is primary).
*/
export function openLadillModal(name) {
if (!name) {
return false;
}
window.dispatchEvent(new CustomEvent('open-modal', { detail: name, bubbles: true }));
return true;
}
export function closeLadillModal(name) {
if (!name) {
return false;
}
window.dispatchEvent(new CustomEvent('close-modal', { detail: name, bubbles: true }));
return true;
}
export function ladillNeedsTopup(balance, price) {
const normalizedBalance = Number.parseFloat(balance);
const normalizedPrice = Number.parseFloat(price);
if (!Number.isFinite(normalizedBalance) || !Number.isFinite(normalizedPrice)) {
return false;
}
return normalizedBalance <= 0 || normalizedBalance < normalizedPrice;
}
export function registerLadillConfirmStore(Alpine) {
Alpine.store('ladillConfirm', {
open: false,
title: '',
message: '',
confirmLabel: 'Confirm',
cancelLabel: 'Cancel',
variant: 'danger',
_resolve: null,
ask(options = {}) {
return new Promise((resolve) => {
this.title = options.title || 'Are you sure?';
this.message = options.message || '';
this.confirmLabel = options.confirmLabel || 'Confirm';
this.cancelLabel = options.cancelLabel || 'Cancel';
this.variant = options.variant || 'danger';
this._resolve = resolve;
this.open = true;
});
},
answer(confirmed) {
if (this._resolve) {
this._resolve(confirmed);
}
this.open = false;
this._resolve = null;
},
});
window.ladillConfirm = (options = {}) => Alpine.store('ladillConfirm').ask(options);
}
export function registerLadillModalHelpers() {
window.openLadillModal = openLadillModal;
window.closeLadillModal = closeLadillModal;
window.ladillNeedsTopup = ladillNeedsTopup;
}
@@ -0,0 +1,69 @@
@props([
'name',
'title',
'message' => null,
'action',
'method' => 'POST',
'confirmLabel' => 'Confirm',
'cancelLabel' => 'Cancel',
'variant' => 'danger',
])
@php
$confirmBtnClass = $variant === 'danger'
? 'bg-red-600 hover:bg-red-700'
: 'bg-violet-600 hover:bg-violet-700';
$iconWrapClass = $variant === 'danger'
? 'bg-red-100 text-red-600'
: 'bg-violet-100 text-violet-600';
@endphp
@if(isset($trigger))
<span @click.stop="$dispatch('open-modal', {{ Js::from($name) }})" class="inline-flex">
{{ $trigger }}
</span>
@endif
<x-modal :name="$name" maxWidth="md">
<div class="px-5 pb-6 pt-2 sm:px-6 sm:pb-6 sm:pt-4">
<div class="flex flex-col items-center text-center sm:items-start sm:text-left">
<div @class(['flex h-12 w-12 items-center justify-center rounded-full', $iconWrapClass])>
@if($variant === 'danger')
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>
</svg>
@else
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"/>
</svg>
@endif
</div>
<h2 class="mt-4 text-lg font-semibold text-slate-900">{{ $title }}</h2>
@if($message)
<p class="mt-2 text-sm leading-relaxed text-slate-500">{{ $message }}</p>
@endif
@isset($details)
<div class="mt-3 w-full">{{ $details }}</div>
@endisset
</div>
<form method="post" action="{{ $action }}" class="mt-6 flex flex-col-reverse gap-2.5 sm:flex-row sm:justify-end">
@csrf
@if(strtoupper($method) !== 'POST')
@method($method)
@endif
@isset($fields)
{{ $fields }}
@endisset
<button type="button"
@click="$dispatch('close-modal', {{ Js::from($name) }})"
class="rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">
{{ $cancelLabel }}
</button>
<button type="submit"
@class(['rounded-xl px-4 py-2.5 text-sm font-semibold text-white transition', $confirmBtnClass])>
{{ $confirmLabel }}
</button>
</form>
</div>
</x-modal>
@@ -0,0 +1,88 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
setTimeout(() => { if (!show) document.body.classList.remove('overflow-y-hidden'); }, 200);
}
})"
x-on:open-modal.window="String($event.detail) === @js($name) ? show = true : null"
data-modal-name="{{ $name }}"
x-on:close-modal.window="String($event.detail) === @js($name) ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
x-cloak
x-transition:enter="transition-opacity ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-50 flex items-end sm:items-center sm:justify-center"
style="background: rgba(0,0,0,0.45); backdrop-filter: blur(3px);"
>
{{-- Backdrop click to close --}}
<div class="absolute inset-0" x-on:click="show = false" data-close-modal="{{ $name }}"></div>
{{-- Panel: bottom-sheet on mobile, centered card on sm+ --}}
<div
x-show="show"
class="relative z-10 w-full max-h-[90vh] overflow-y-auto rounded-t-3xl bg-white shadow-2xl sm:mb-6 sm:max-h-[85vh] sm:rounded-2xl {{ $maxWidth }} sm:border sm:border-gray-200"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
x-transition:enter-end="translate-y-0 sm:opacity-100 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="translate-y-0 sm:opacity-100 sm:scale-100"
x-transition:leave-end="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
x-on:click.stop
>
{{-- Drag handle (mobile only) --}}
<div class="flex justify-center pb-1 pt-3 sm:hidden">
<div class="h-1 w-10 rounded-full bg-slate-200"></div>
</div>
{{-- Close button (desktop only) --}}
<button @click="show = false" data-close-modal="{{ $name }}" type="button"
class="absolute right-3 top-3 z-10 hidden h-8 w-8 items-center justify-center rounded-full text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 sm:flex">
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
</svg>
</button>
{{ $slot }}
</div>
</div>
@@ -46,10 +46,18 @@
{{ $token->last_used_at ? 'last used '.$token->last_used_at->diffForHumans() : 'never used' }}
</p>
</div>
<form method="POST" action="{{ route('account.developers.destroy', $token->id) }}" onsubmit="return confirm('Revoke {{ $token->name }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
</form>
<x-confirm-dialog
:name="'revoke-token-'.$token->id"
title="Revoke API token?"
:message="'Revoke '.$token->name.'? Apps using this token will stop working.'"
:action="route('account.developers.destroy', $token->id)"
method="DELETE"
confirm-label="Revoke"
>
<x-slot:trigger>
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
@empty
<p class="px-5 py-8 text-center text-sm text-slate-400">No tokens yet.</p>
@@ -22,13 +22,22 @@
<div class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2.5 text-sm text-emerald-800">
Linked mailbox: <span class="font-semibold">{{ $linkStatus['linked_mailbox'] }}</span>
</div>
<form method="POST" action="{{ route('account.settings.mailbox-unlink') }}" class="mt-3"
onsubmit="return confirm('Unlink this mailbox from your Ladill account? Ladill Mail will no longer open automatically with Ladill sign-in.')">
@csrf @method('DELETE')
<button type="submit" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50">
Unlink mailbox
</button>
</form>
<x-confirm-dialog
name="unlink-mailbox"
title="Unlink mailbox?"
message="Unlink this mailbox from your Ladill account? Ladill Mail will no longer open automatically with Ladill sign-in."
:action="route('account.settings.mailbox-unlink')"
method="DELETE"
confirm-label="Unlink"
variant="primary"
class="mt-3"
>
<x-slot:trigger>
<button type="button" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50">
Unlink mailbox
</button>
</x-slot:trigger>
</x-confirm-dialog>
@elseif(($linkStatus['stage'] ?? '') === 'needs_domain')
<p class="mt-3 text-sm text-amber-800">Add and verify an email domain first.</p>
<a href="{{ route('email.domains.index') }}" class="mt-3 inline-flex items-center gap-1 text-sm font-semibold text-indigo-700 hover:text-indigo-800">
+12 -4
View File
@@ -64,10 +64,18 @@
<option value="admin" @selected($member->role === 'admin')>Admin</option>
</select>
</form>
<form method="POST" action="{{ route('account.team.destroy', $member) }}" onsubmit="return confirm('Remove {{ $member->email }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
</form>
<x-confirm-dialog
:name="'remove-member-'.$member->id"
title="Remove team member?"
:message="'Remove '.$member->email.' from this team?'"
:action="route('account.team.destroy', $member)"
method="DELETE"
confirm-label="Remove"
>
<x-slot:trigger>
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
</x-slot:trigger>
</x-confirm-dialog>
@else
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium capitalize text-slate-600">{{ $member->role }}</span>
@endif
+13 -4
View File
@@ -54,9 +54,18 @@
</div>
@endunless
<form method="POST" action="{{ route('email.domains.destroy', $domain['id']) }}" class="mt-4" onsubmit="return confirm('Remove {{ $domain['domain'] }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Remove domain</button>
</form>
<x-confirm-dialog
:name="'remove-domain-'.$domain['id']"
title="Remove domain?"
:message="'Remove '.$domain['domain'].'?'"
:action="route('email.domains.destroy', $domain['id'])"
method="DELETE"
confirm-label="Remove domain"
class="mt-4"
>
<x-slot:trigger>
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Remove domain</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
@endsection
+13 -4
View File
@@ -74,10 +74,19 @@
<div class="mt-4 rounded-2xl border border-rose-200 bg-white p-5">
<h2 class="text-sm font-semibold text-rose-700">Delete mailbox</h2>
<p class="mt-0.5 text-xs text-slate-400">This permanently removes the mailbox and its email.</p>
<form method="POST" action="{{ route('email.mailboxes.destroy', $mailbox['id']) }}" class="mt-3" onsubmit="return confirm('Delete {{ $mailbox['address'] }}? This cannot be undone.')">
@csrf @method('DELETE')
<button class="rounded-lg bg-rose-600 px-4 py-2 text-sm font-semibold text-white hover:bg-rose-700">Delete mailbox</button>
</form>
<x-confirm-dialog
:name="'delete-mailbox-'.$mailbox['id']"
title="Delete mailbox?"
:message="'Delete '.$mailbox['address'].'? This cannot be undone.'"
:action="route('email.mailboxes.destroy', $mailbox['id'])"
method="DELETE"
confirm-label="Delete mailbox"
class="mt-3"
>
<x-slot:trigger>
<button type="button" class="rounded-lg bg-rose-600 px-4 py-2 text-sm font-semibold text-white hover:bg-rose-700">Delete mailbox</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
</div>
@endsection
@@ -42,10 +42,18 @@
{{ $token->last_used_at ? 'last used '.$token->last_used_at->diffForHumans() : 'never used' }}
</p>
</div>
<form method="POST" action="{{ route('account.developers.destroy', $token->id) }}" onsubmit="return confirm('Revoke {{ $token->name }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
</form>
<x-confirm-dialog
:name="'revoke-token-'.$token->id"
title="Revoke API token?"
:message="'Revoke '.$token->name.'? Apps using this token will stop working.'"
:action="route('account.developers.destroy', $token->id)"
method="DELETE"
confirm-label="Revoke"
>
<x-slot:trigger>
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Revoke</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
@empty
<p class="px-5 py-8 text-center text-sm text-slate-400">No tokens yet.</p>
+12 -4
View File
@@ -62,10 +62,18 @@
<option value="admin" @selected($member->role === 'admin')>Admin</option>
</select>
</form>
<form method="POST" action="{{ route('account.team.destroy', $member) }}" onsubmit="return confirm('Remove {{ $member->email }}?')">
@csrf @method('DELETE')
<button class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
</form>
<x-confirm-dialog
:name="'remove-member-'.$member->id"
title="Remove team member?"
:message="'Remove '.$member->email.' from this team?'"
:action="route('account.team.destroy', $member)"
method="DELETE"
confirm-label="Remove"
>
<x-slot:trigger>
<button type="button" class="text-xs font-medium text-rose-600 hover:underline">Remove</button>
</x-slot:trigger>
</x-confirm-dialog>
@else
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium capitalize text-slate-600">{{ $member->role }}</span>
@endif
+15 -8
View File
@@ -359,14 +359,21 @@
<span x-text="showCredentials ? 'Hide' : 'Show'"></span> Credentials
</button>
@endif
<form action="{{ route('hosting.panel.apps.delete', [$account, $site]) }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this {{ ucfirst($site->installed_app) }} installation? This will remove all application files and cannot be undone.');">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
Delete
</button>
</form>
<x-confirm-dialog
:name="'delete-app-'.$site->id"
:title="'Delete '.ucfirst($site->installed_app).'?'"
message="This will remove all application files and cannot be undone."
:action="route('hosting.panel.apps.delete', [$account, $site])"
method="DELETE"
confirm-label="Delete"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
Delete
</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
</div>
+18 -9
View File
@@ -111,15 +111,24 @@
<code class="text-sm font-mono text-slate-600 break-all">{{ $job['command'] }}</code>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<form action="{{ route('hosting.panel.cron.delete', $account) }}" method="POST" onsubmit="return confirm('Delete this cron job?')">
@csrf
@method('DELETE')
<input type="hidden" name="line_number" value="{{ $job['line_number'] }}">
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</form>
<x-confirm-dialog
:name="'delete-cron-'.$job['line_number']"
title="Delete cron job?"
message="Delete this cron job?"
:action="route('hosting.panel.cron.delete', $account)"
method="DELETE"
confirm-label="Delete"
>
<x-slot:fields>
<input type="hidden" name="line_number" value="{{ $job['line_number'] }}">
</x-slot:fields>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</x-slot:trigger>
</x-confirm-dialog>
</td>
</tr>
@endforeach
@@ -71,14 +71,21 @@
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
Reset Password
</button>
<form action="{{ route('hosting.panel.databases.delete', [$account, $database]) }}" method="POST" onsubmit="return confirm('Are you sure? This will permanently delete the database and all its data.')">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</form>
<x-confirm-dialog
:name="'delete-db-'.$database->id"
title="Delete database?"
message="This will permanently delete the database and all its data."
:action="route('hosting.panel.databases.delete', [$account, $database])"
method="DELETE"
confirm-label="Delete"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Delete
</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
</td>
</tr>
@@ -126,8 +133,8 @@
@push('scripts')
<script>
async function resetPassword(databaseId) {
if (!confirm('Reset the database password? You will need to update your application configuration.')) return;
async async function resetPassword(databaseId) {
if (!await window.ladillConfirm({ title: 'Reset password?', message: 'Reset the database password? You will need to update your application configuration.', confirmLabel: 'Reset', variant: 'primary' })) return;
try {
const res = await fetch(`{{ url('hosting/panel/' . $account->id . '/databases') }}/${databaseId}/reset-password`, {
method: 'POST',
@@ -178,14 +178,21 @@
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
@if((int) $account->user_id === (int) auth()->id())
<form action="{{ route('hosting.panel.domains.remove', [$account, $site]) }}" method="POST" onsubmit="return confirm('Remove this domain? The files will not be deleted.')">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Remove
</button>
</form>
<x-confirm-dialog
:name="'remove-domain-'.$site->id"
title="Remove domain?"
message="Remove this domain? The files will not be deleted."
:action="route('hosting.panel.domains.remove', [$account, $site])"
method="DELETE"
confirm-label="Remove"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Remove
</button>
</x-slot:trigger>
</x-confirm-dialog>
@else
<span class="text-xs text-slate-400">Owner only</span>
@endif
@@ -418,9 +418,9 @@
this.selected = event.target.checked ? [...this.allPaths] : [];
},
bulkAction(action) {
async async async bulkAction(action) {
if (action === 'delete') {
if (!confirm('Delete ' + this.selected.length + ' item(s)? This cannot be undone.')) return;
if (!await this.$store.ladillConfirm.ask({ title: 'Delete items?', message: 'Delete ' + this.selected.length + ' item(s)? This cannot be undone.' })) return;
this.submitBulkDelete();
} else if (action === 'move' || action === 'copy') {
this.moveAction = action;
@@ -567,7 +567,7 @@
},
async deleteFile(path) {
if (!confirm('Are you sure you want to delete this?')) return;
if (!await this.$store.ladillConfirm.ask({ title: 'Delete item?', message: 'Are you sure you want to delete this?' })) return;
this.loading = true;
try {
const res = await fetch('{{ route("hosting.panel.files.delete", $account) }}', {
@@ -583,7 +583,7 @@
},
async extractFile(path, name) {
if (!confirm('Extract "' + name + '" in the current directory?\n\nThis may take a while for large archives.')) return;
if (!await this.$store.ladillConfirm.ask({ title: 'Extract archive?', message: 'Extract "' + name + '" in the current directory?\n\nThis may take a while for large archives.', confirmLabel: 'Extract', variant: 'primary' })) return;
this.loading = true;
this.extracting = true;
this.extractFileName = name;
+19 -8
View File
@@ -37,14 +37,25 @@
</select>
</form>
<form action="{{ route('hosting.panel.logs.clear', $account) }}" method="POST" onsubmit="return confirm('Clear this log file?')" class="ml-auto">
@csrf
<input type="hidden" name="type" value="{{ $logType }}">
<button type="submit" class="inline-flex items-center gap-2 rounded-lg border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Clear Log
</button>
</form>
<x-confirm-dialog
name="clear-log"
title="Clear log file?"
message="Clear this log file? This cannot be undone."
:action="route('hosting.panel.logs.clear', $account)"
method="POST"
confirm-label="Clear log"
class="ml-auto"
>
<x-slot:fields>
<input type="hidden" name="type" value="{{ $logType }}">
</x-slot:fields>
<x-slot:trigger>
<button type="button" class="inline-flex items-center gap-2 rounded-lg border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 transition">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/></svg>
Clear Log
</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden">
@@ -80,13 +80,21 @@
</form>
@if ($teamMembership->hasSshAccessKey())
<form method="POST" action="{{ route('hosting.panel.settings.ssh-key.destroy', $account) }}" class="mt-3" onsubmit="return confirm('Remove your SFTP key access for this hosting account?');">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center rounded-lg border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:border-slate-400 hover:text-slate-900">
Remove SFTP Key
</button>
</form>
<x-confirm-dialog
name="remove-sftp-key"
title="Remove SFTP key?"
message="Remove your SFTP key access for this hosting account?"
:action="route('hosting.panel.settings.ssh-key.destroy', $account)"
method="DELETE"
confirm-label="Remove key"
class="mt-3"
>
<x-slot:trigger>
<button type="button" class="inline-flex items-center rounded-lg border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:border-slate-400 hover:text-slate-900">
Remove SFTP Key
</button>
</x-slot:trigger>
</x-confirm-dialog>
@endif
@else
<div class="mt-5 rounded-xl bg-slate-50 px-4 py-4 text-sm text-slate-600">
+1
View File
@@ -57,5 +57,6 @@
@include('partials.afia')
@endauth
@include('partials.sso-keepalive')
@include('partials.confirm-prompt')
</body>
</html>
@@ -56,5 +56,6 @@
@include('partials.afia')
@endauth
@include('partials.sso-keepalive')
@include('partials.confirm-prompt')
</body>
</html>
@@ -0,0 +1,65 @@
<div
x-cloak
x-show="$store.ladillConfirm.open"
x-transition:enter="transition-opacity ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-[60] flex items-end sm:items-center sm:justify-center"
style="background: rgba(0,0,0,0.45); backdrop-filter: blur(3px);"
@keydown.escape.window="$store.ladillConfirm.answer(false)"
>
<div class="absolute inset-0" @click="$store.ladillConfirm.answer(false)"></div>
<div
x-show="$store.ladillConfirm.open"
class="relative z-10 w-full max-h-[90vh] overflow-y-auto rounded-t-3xl bg-white shadow-2xl sm:mb-6 sm:max-h-[85vh] sm:max-w-md sm:rounded-2xl sm:border sm:border-gray-200"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
x-transition:enter-end="translate-y-0 sm:opacity-100 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="translate-y-0 sm:opacity-100 sm:scale-100"
x-transition:leave-end="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
@click.stop
>
<div class="flex justify-center pb-1 pt-3 sm:hidden">
<div class="h-1 w-10 rounded-full bg-slate-200"></div>
</div>
<div class="px-5 pb-6 pt-2 sm:px-6 sm:pb-6 sm:pt-4">
<div class="flex flex-col items-center text-center sm:items-start sm:text-left">
<div class="flex h-12 w-12 items-center justify-center rounded-full"
:class="$store.ladillConfirm.variant === 'primary' ? 'bg-violet-100 text-violet-600' : 'bg-red-100 text-red-600'">
<template x-if="$store.ladillConfirm.variant === 'primary'">
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"/>
</svg>
</template>
<template x-if="$store.ladillConfirm.variant !== 'primary'">
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>
</svg>
</template>
</div>
<h2 class="mt-4 text-lg font-semibold text-slate-900" x-text="$store.ladillConfirm.title"></h2>
<p x-show="$store.ladillConfirm.message" class="mt-2 whitespace-pre-line text-sm leading-relaxed text-slate-500" x-text="$store.ladillConfirm.message"></p>
</div>
<div class="mt-6 flex flex-col-reverse gap-2.5 sm:flex-row sm:justify-end">
<button type="button"
@click="$store.ladillConfirm.answer(false)"
class="rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50"
x-text="$store.ladillConfirm.cancelLabel">
</button>
<button type="button"
@click="$store.ladillConfirm.answer(true)"
class="rounded-xl px-4 py-2.5 text-sm font-semibold text-white transition"
:class="$store.ladillConfirm.variant === 'primary' ? 'bg-violet-600 hover:bg-violet-700' : 'bg-red-600 hover:bg-red-700'"
x-text="$store.ladillConfirm.confirmLabel">
</button>
</div>
</div>
</div>
</div>