style(arrInstance): transition to card styling

This commit is contained in:
Sam Chau
2025-10-26 10:30:48 +10:30
parent 77cff2de8f
commit 51eaa2cdca

View File

@@ -92,251 +92,280 @@
$: errorMessage = mode === 'create' ? 'Failed to save instance' : 'Failed to update instance';
</script>
<div class="p-8">
<div class="mb-8">
<div class="space-y-8 p-8">
<div class="space-y-3">
<h1 class="text-3xl font-bold text-neutral-900 dark:text-neutral-50">{title}</h1>
<p class="mt-3 text-lg text-neutral-600 dark:text-neutral-400">
<p class="text-lg text-neutral-600 dark:text-neutral-400">
{description}
</p>
</div>
<div class="max-w-2xl">
<form
method="POST"
class="space-y-6"
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'failure' && result.data) {
// Show error toast
toastStore.add('error', (result.data as { error?: string }).error || errorMessage);
} else if (result.type === 'redirect') {
// Show success toast before redirect
toastStore.add('success', successMessage);
}
await update();
};
}}
<form
method="POST"
class="space-y-6"
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'failure' && result.data) {
toastStore.add('error', (result.data as { error?: string }).error || errorMessage);
} else if (result.type === 'redirect') {
toastStore.add('success', successMessage);
}
await update();
};
}}
>
<!-- Instance Details -->
<div
class="rounded-lg border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900"
>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-neutral-900 dark:text-neutral-50">
Name <span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
bind:value={name}
required
placeholder="e.g., Main Radarr, 4K Sonarr"
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500"
/>
</div>
<h2 class="mb-4 text-lg font-semibold text-neutral-900 dark:text-neutral-50">
Instance Details
</h2>
<!-- Type -->
<div>
<label for="type" class="block text-sm font-medium text-neutral-900 dark:text-neutral-50">
Type <span class="text-red-500">*</span>
</label>
{#if mode === 'edit'}
<!-- Disabled type field for edit mode -->
<div class="space-y-4">
<!-- Name -->
<div>
<label
for="name"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Name <span class="text-red-500">*</span>
</label>
<input
type="text"
id="type"
name="type"
value={type.charAt(0).toUpperCase() + type.slice(1)}
disabled
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-neutral-100 px-3 py-2 text-neutral-500 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-400"
/>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Type cannot be changed after creation
</p>
<input type="hidden" name="type" value={type} />
{:else}
<!-- Editable type dropdown for create mode -->
<select
id="type"
name="type"
bind:value={type}
on:change={resetConnectionStatus}
id="name"
name="name"
bind:value={name}
required
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-900 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50"
>
<option value="">Select type...</option>
<option value="radarr">Radarr</option>
<option value="sonarr">Sonarr</option>
<option value="lidarr">Lidarr</option>
<option value="chaptarr">Chaptarr</option>
</select>
{/if}
</div>
<!-- URL -->
<div>
<label for="url" class="block text-sm font-medium text-neutral-900 dark:text-neutral-50">
URL <span class="text-red-500">*</span>
</label>
<input
type="url"
id="url"
name="url"
bind:value={url}
on:input={resetConnectionStatus}
required
placeholder="http://localhost:7878"
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500"
/>
</div>
<!-- API Key -->
<div>
<label
for="api_key"
class="block text-sm font-medium text-neutral-900 dark:text-neutral-50"
>
API Key <span class="text-red-500">*</span>
</label>
<input
type="text"
id="api_key"
name="api_key"
bind:value={apiKey}
on:input={resetConnectionStatus}
required
placeholder={mode === 'edit' ? 'Enter API key to test connection' : 'Enter API key'}
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500"
/>
{#if mode === 'edit'}
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Re-enter API key to update or test connection
</p>
{/if}
</div>
<!-- Tags (optional) -->
<div>
<label
for="tags-input"
class="block text-sm font-medium text-neutral-900 dark:text-neutral-50"
>
Tags
</label>
<div class="mt-1">
<TagInput bind:tags />
placeholder="e.g., Main Radarr, 4K Sonarr"
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500"
/>
</div>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Optional. Press Enter to add a tag, Backspace to remove.
</p>
<!-- Hidden input to submit tags as JSON -->
<input type="hidden" name="tags" value={tags.length > 0 ? JSON.stringify(tags) : ''} />
</div>
<!-- Buttons -->
<div class="space-y-3">
<div class="flex justify-between gap-3">
<div class="flex gap-3">
<button
type="button"
on:click={testConnection}
disabled={connectionStatus === 'testing'}
class="flex items-center gap-2 rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
<!-- Type -->
<div>
<label
for="type"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Type <span class="text-red-500">*</span>
</label>
{#if mode === 'edit'}
<input
type="text"
id="type"
name="type"
value={type.charAt(0).toUpperCase() + type.slice(1)}
disabled
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-neutral-100 px-3 py-2 text-sm text-neutral-500 disabled:cursor-not-allowed dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-400"
/>
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
Type cannot be changed after creation
</p>
<input type="hidden" name="type" value={type} />
{:else}
<select
id="type"
name="type"
bind:value={type}
on:change={resetConnectionStatus}
required
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100"
>
{#if connectionStatus === 'testing'}
<Loader2 size={14} class="animate-spin" />
Testing...
{:else if connectionStatus === 'success'}
<Check size={14} class="text-green-600 dark:text-green-400" />
Connected
{:else if connectionStatus === 'error'}
<X size={14} class="text-red-600 dark:text-red-400" />
Test
{:else}
<Wifi size={14} />
Test
{/if}
</button>
<option value="">Select type...</option>
<option value="radarr">Radarr</option>
<option value="sonarr">Sonarr</option>
<option value="lidarr">Lidarr</option>
<option value="chaptarr">Chaptarr</option>
</select>
{/if}
</div>
</div>
</div>
{#if mode === 'edit'}
<a
href="/arr/{type}/{instance?.id}"
data-sveltekit-reload
class="flex items-center gap-2 rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
>
Cancel
</a>
{/if}
</div>
<!-- Connection -->
<div
class="rounded-lg border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900"
>
<h2 class="mb-4 text-lg font-semibold text-neutral-900 dark:text-neutral-50">
Connection Settings
</h2>
<button
type="submit"
disabled={!canSubmit}
class="flex items-center gap-2 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
<div class="space-y-4">
<!-- URL -->
<div>
<label
for="url"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
<Save size={14} />
{submitButtonText}
</button>
URL <span class="text-red-500">*</span>
</label>
<input
type="url"
id="url"
name="url"
bind:value={url}
on:input={resetConnectionStatus}
required
placeholder="http://localhost:7878"
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500"
/>
</div>
<!-- Connection Status Messages -->
{#if connectionStatus === 'success'}
<p class="text-sm text-green-600 dark:text-green-400">
Connection test passed! You can now save this instance.
</p>
{/if}
{#if connectionStatus === 'error'}
<p class="text-sm text-red-600 dark:text-red-400">
{connectionError}
</p>
{/if}
{#if !canSubmit && connectionStatus !== 'success'}
<p class="text-sm text-neutral-500 dark:text-neutral-400">
Please test the connection before saving
</p>
{/if}
<!-- API Key -->
<div>
<label
for="api_key"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
API Key <span class="text-red-500">*</span>
</label>
<input
type="text"
id="api_key"
name="api_key"
bind:value={apiKey}
on:input={resetConnectionStatus}
required
placeholder={mode === 'edit' ? 'Enter API key to test connection' : 'Enter API key'}
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm font-mono text-neutral-900 placeholder-neutral-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500"
/>
{#if mode === 'edit'}
<p class="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
Re-enter API key to update or test connection
</p>
{/if}
</div>
</div>
</form>
<!-- Delete Section (Edit Mode Only) -->
{#if mode === 'edit'}
<div class="mt-8 border-t border-neutral-200 pt-8 dark:border-neutral-800">
<h2 class="text-lg font-semibold text-neutral-900 dark:text-neutral-50">Danger Zone</h2>
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
Once you delete this instance, there is no going back. Please be certain.
</p>
<div class="mt-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="space-y-1 text-sm">
{#if connectionStatus === 'success'}
<p class="text-green-600 dark:text-green-400">
Connection test passed! You can now save this instance.
</p>
{/if}
{#if connectionStatus === 'error'}
<p class="text-red-600 dark:text-red-400">
{connectionError}
</p>
{/if}
{#if !canSubmit && connectionStatus !== 'success'}
<p class="text-neutral-600 dark:text-neutral-400">
Please test the connection before saving
</p>
{/if}
</div>
<button
type="button"
on:click={() => (showDeleteModal = true)}
class="mt-4 flex items-center gap-2 rounded-lg border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-700 transition-colors hover:bg-red-50 dark:border-red-700 dark:bg-neutral-900 dark:text-red-400 dark:hover:bg-red-950"
on:click={testConnection}
disabled={connectionStatus === 'testing'}
class="flex items-center gap-2 self-start rounded-lg border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
>
<Trash2 size={14} />
Delete Instance
{#if connectionStatus === 'testing'}
<Loader2 size={14} class="animate-spin" />
Testing...
{:else if connectionStatus === 'success'}
<Check size={14} class="text-green-600 dark:text-green-400" />
Connected
{:else if connectionStatus === 'error'}
<X size={14} class="text-red-600 dark:text-red-400" />
Test Again
{:else}
<Wifi size={14} />
Test Connection
{/if}
</button>
<!-- Hidden delete form -->
<form
bind:this={deleteFormElement}
method="POST"
action="?/delete"
class="hidden"
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'failure' && result.data) {
toastStore.add(
'error',
(result.data as { error?: string }).error || 'Failed to delete instance'
);
} else if (result.type === 'redirect') {
toastStore.add('success', 'Instance deleted successfully');
}
await update();
};
}}
>
<!-- Empty form, just for submission -->
</form>
</div>
{/if}
</div>
</div>
<!-- Tags -->
<div
class="rounded-lg border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900"
>
<h2 class="mb-4 text-lg font-semibold text-neutral-900 dark:text-neutral-50">
Tags
</h2>
<label
for="tags-input"
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Optional Tags
</label>
<div class="mt-1">
<TagInput bind:tags />
</div>
<p class="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Press Enter to add a tag, Backspace to remove.
</p>
<input type="hidden" name="tags" value={tags.length > 0 ? JSON.stringify(tags) : ''} />
</div>
<!-- Actions -->
<div class="flex flex-wrap items-center justify-end gap-3">
{#if mode === 'edit'}
<a
href="/arr/{type}/{instance?.id}"
data-sveltekit-reload
class="flex items-center gap-2 rounded-lg border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
>
Cancel
</a>
{/if}
<button
type="submit"
disabled={!canSubmit}
class="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
>
<Save size={14} />
{submitButtonText}
</button>
</div>
</form>
<!-- Delete Section (Edit Mode Only) -->
{#if mode === 'edit'}
<div
class="rounded-lg border border-red-200 bg-red-50 p-6 dark:border-red-800 dark:bg-red-950/40"
>
<h2 class="text-lg font-semibold text-red-700 dark:text-red-300">Danger Zone</h2>
<p class="mt-2 text-sm text-red-600 dark:text-red-400">
Once you delete this instance, there is no going back. Please be certain.
</p>
<button
type="button"
on:click={() => (showDeleteModal = true)}
class="mt-4 flex items-center gap-2 rounded-lg border border-red-300 bg-white px-4 py-2 text-sm font-medium text-red-700 transition-colors hover:bg-red-50 dark:border-red-700 dark:bg-neutral-900 dark:text-red-300 dark:hover:bg-red-900"
>
<Trash2 size={14} />
Delete Instance
</button>
<form
bind:this={deleteFormElement}
method="POST"
action="?/delete"
class="hidden"
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'failure' && result.data) {
toastStore.add(
'error',
(result.data as { error?: string }).error || 'Failed to delete instance'
);
} else if (result.type === 'redirect') {
toastStore.add('success', 'Instance deleted successfully');
}
await update();
};
}}
>
<!-- Empty form, just for submission -->
</form>
</div>
{/if}
</div>
<!-- Delete Confirmation Modal -->