mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-25 04:12:26 +01:00
refactor(alerts): move to lib/client
- also remove reusable request wrapper
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Check, X, Loader2, Save, Wifi, Trash2 } from 'lucide-svelte';
|
||||
import { apiRequest } from '$api';
|
||||
import TagInput from '$components/form/TagInput.svelte';
|
||||
import Modal from '$components/modal/Modal.svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import type { ArrInstance } from '$db/queries/arrInstances.ts';
|
||||
|
||||
// Props
|
||||
@@ -56,14 +55,20 @@
|
||||
connectionError = '';
|
||||
|
||||
try {
|
||||
await apiRequest<{ success: boolean; error?: string }>('/arr/test', {
|
||||
const response = await fetch('/arr/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type, url, apiKey }),
|
||||
showSuccessToast: true,
|
||||
successMessage: 'Connection successful!'
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type, url, apiKey })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.message || 'Connection test failed');
|
||||
}
|
||||
|
||||
connectionStatus = 'success';
|
||||
alertStore.add('success', 'Connection successful!');
|
||||
} catch (error) {
|
||||
connectionStatus = 'error';
|
||||
connectionError = error instanceof Error ? error.message : 'Connection test failed';
|
||||
@@ -106,9 +111,9 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add('error', (result.data as { error?: string }).error || errorMessage);
|
||||
alertStore.add('error', (result.data as { error?: string }).error || errorMessage);
|
||||
} else if (result.type === 'redirect') {
|
||||
toastStore.add('success', successMessage);
|
||||
alertStore.add('success', successMessage);
|
||||
}
|
||||
await update();
|
||||
};
|
||||
@@ -351,12 +356,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to delete instance'
|
||||
);
|
||||
} else if (result.type === 'redirect') {
|
||||
toastStore.add('success', 'Instance deleted successfully');
|
||||
alertStore.add('success', 'Instance deleted successfully');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import DiscordConfiguration from './DiscordConfiguration.svelte';
|
||||
import { siDiscord } from 'simple-icons';
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || `Failed to ${mode} service`
|
||||
);
|
||||
} else if (result.type === 'redirect') {
|
||||
toastStore.add('success', `Notification service ${mode === 'create' ? 'created' : 'updated'} successfully`);
|
||||
alertStore.add('success', `Notification service ${mode === 'create' ? 'created' : 'updated'} successfully`);
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { toastStore } from '$stores/toast';
|
||||
import Toast from './Toast.svelte';
|
||||
</script>
|
||||
|
||||
<div class="pointer-events-none fixed top-20 right-4 z-50 flex flex-col gap-3">
|
||||
{#each $toastStore as toast (toast.id)}
|
||||
<div class="pointer-events-auto">
|
||||
<Toast id={toast.id} type={toast.type} message={toast.message} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-svelte';
|
||||
import type { ToastType } from '$stores/toast';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import type { AlertType } from './store';
|
||||
import { alertStore } from './store';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
|
||||
export let id: string;
|
||||
export let type: ToastType;
|
||||
export let type: AlertType;
|
||||
export let message: string;
|
||||
|
||||
// Icon mapping
|
||||
@@ -38,7 +38,7 @@
|
||||
const Icon = icons[type];
|
||||
|
||||
function dismiss() {
|
||||
toastStore.remove(id);
|
||||
alertStore.remove(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
12
src/lib/client/alerts/AlertContainer.svelte
Normal file
12
src/lib/client/alerts/AlertContainer.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { alertStore } from './store';
|
||||
import Alert from './Alert.svelte';
|
||||
</script>
|
||||
|
||||
<div class="pointer-events-none fixed top-20 right-4 z-50 flex flex-col gap-3">
|
||||
{#each $alertStore as alert (alert.id)}
|
||||
<div class="pointer-events-auto">
|
||||
<Alert id={alert.id} type={alert.type} message={alert.message} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
41
src/lib/client/alerts/store.ts
Normal file
41
src/lib/client/alerts/store.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type AlertType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
type: AlertType;
|
||||
message: string;
|
||||
duration?: number; // Auto-dismiss duration in ms (default: 5000)
|
||||
}
|
||||
|
||||
function createAlertStore() {
|
||||
const { subscribe, update } = writable<Alert[]>([]);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
add: (type: AlertType, message: string, duration = 5000) => {
|
||||
const id = crypto.randomUUID();
|
||||
const alert: Alert = { id, type, message, duration };
|
||||
|
||||
update((alerts) => [...alerts, alert]);
|
||||
|
||||
// Auto-dismiss after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
update((alerts) => alerts.filter((a) => a.id !== id));
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
remove: (id: string) => {
|
||||
update((alerts) => alerts.filter((a) => a.id !== id));
|
||||
},
|
||||
clear: () => {
|
||||
update(() => []);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const alertStore = createAlertStore();
|
||||
@@ -1,41 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
duration?: number; // Auto-dismiss duration in ms (default: 5000)
|
||||
}
|
||||
|
||||
function createToastStore() {
|
||||
const { subscribe, update } = writable<Toast[]>([]);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
add: (type: ToastType, message: string, duration = 5000) => {
|
||||
const id = crypto.randomUUID();
|
||||
const toast: Toast = { id, type, message, duration };
|
||||
|
||||
update((toasts) => [...toasts, toast]);
|
||||
|
||||
// Auto-dismiss after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
update((toasts) => toasts.filter((t) => t.id !== id));
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
remove: (id: string) => {
|
||||
update((toasts) => toasts.filter((t) => t.id !== id));
|
||||
},
|
||||
clear: () => {
|
||||
update(() => []);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const toastStore = createToastStore();
|
||||
@@ -3,7 +3,7 @@
|
||||
import logo from '$assets/logo.svg';
|
||||
import Navbar from '$components/navigation/navbar/navbar.svelte';
|
||||
import PageNav from '$components/navigation/pageNav/pageNav.svelte';
|
||||
import ToastContainer from '$components/toast/ToastContainer.svelte';
|
||||
import AlertContainer from '$alerts/AlertContainer.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<Navbar />
|
||||
<PageNav />
|
||||
<ToastContainer />
|
||||
<AlertContainer />
|
||||
|
||||
<main class="pt-16 pl-72">
|
||||
<slot />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import { Download, Plus, Trash2, RotateCcw, Database, Upload } from 'lucide-svelte';
|
||||
import Modal from '$components/modal/Modal.svelte';
|
||||
import type { PageData } from './$types';
|
||||
@@ -93,12 +93,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to upload backup'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Backup uploaded successfully');
|
||||
alertStore.add('success', 'Backup uploaded successfully');
|
||||
fileInput.value = ''; // Reset file input
|
||||
}
|
||||
await update();
|
||||
@@ -135,12 +135,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to create backup'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Backup created successfully');
|
||||
alertStore.add('success', 'Backup created successfully');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
@@ -247,12 +247,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to restore backup'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'success',
|
||||
'Backup restored successfully. Please restart the application.'
|
||||
);
|
||||
@@ -282,12 +282,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to delete backup'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Backup deleted successfully');
|
||||
alertStore.add('success', 'Backup deleted successfully');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import { Save } from 'lucide-svelte';
|
||||
import NumberInput from '$components/form/NumberInput.svelte';
|
||||
import type { BackupSettings } from './types';
|
||||
@@ -29,9 +29,9 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add('error', (result.data as { error?: string }).error || 'Failed to save');
|
||||
alertStore.add('error', (result.data as { error?: string }).error || 'Failed to save');
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Backup settings saved successfully!');
|
||||
alertStore.add('success', 'Backup settings saved successfully!');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import { Save, RotateCcw } from 'lucide-svelte';
|
||||
import NumberInput from '$components/form/NumberInput.svelte';
|
||||
import type { LogSettings } from './types';
|
||||
@@ -47,9 +47,9 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add('error', (result.data as { error?: string }).error || 'Failed to save');
|
||||
alertStore.add('error', (result.data as { error?: string }).error || 'Failed to save');
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Log settings saved successfully!');
|
||||
alertStore.add('success', 'Log settings saved successfully!');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import { Save } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to update job'
|
||||
);
|
||||
} else if (result.type === 'redirect') {
|
||||
toastStore.add('success', 'Job updated successfully!');
|
||||
alertStore.add('success', 'Job updated successfully!');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import { Play, CheckCircle, XCircle, AlertCircle, Edit2, Power } from 'lucide-svelte';
|
||||
|
||||
export let job: {
|
||||
@@ -107,12 +107,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to trigger job'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', `Job "${job.name}" triggered successfully`);
|
||||
alertStore.add('success', `Job "${job.name}" triggered successfully`);
|
||||
}
|
||||
await update();
|
||||
};
|
||||
@@ -136,12 +136,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to update job'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', `Job ${job.enabled ? 'disabled' : 'enabled'}`);
|
||||
alertStore.add('success', `Job ${job.enabled ? 'disabled' : 'enabled'}`);
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Search, Download, RefreshCw, Eye, Copy } from 'lucide-svelte';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import Modal from '$components/modal/Modal.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@@ -80,9 +80,9 @@
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(logText);
|
||||
toastStore.add('success', 'Log entry copied to clipboard');
|
||||
alertStore.add('success', 'Log entry copied to clipboard');
|
||||
} catch (err) {
|
||||
toastStore.add('error', 'Failed to copy to clipboard');
|
||||
alertStore.add('error', 'Failed to copy to clipboard');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { toastStore } from '$stores/toast';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import { Plus, Trash2, Bell, BellOff, MessageSquare, Send, Loader2, Pencil } from 'lucide-svelte';
|
||||
import Modal from '$components/modal/Modal.svelte';
|
||||
import NotificationHistory from '$components/notifications/NotificationHistory.svelte';
|
||||
@@ -206,12 +206,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to update service'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Service updated successfully');
|
||||
alertStore.add('success', 'Service updated successfully');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
@@ -277,12 +277,12 @@
|
||||
testingServiceId = service.id;
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to send test notification'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Test notification sent successfully');
|
||||
alertStore.add('success', 'Test notification sent successfully');
|
||||
}
|
||||
testingServiceId = null;
|
||||
await update();
|
||||
@@ -320,12 +320,12 @@
|
||||
use:enhance={() => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'failure' && result.data) {
|
||||
toastStore.add(
|
||||
alertStore.add(
|
||||
'error',
|
||||
(result.data as { error?: string }).error || 'Failed to delete service'
|
||||
);
|
||||
} else if (result.type === 'success') {
|
||||
toastStore.add('success', 'Service deleted successfully');
|
||||
alertStore.add('success', 'Service deleted successfully');
|
||||
}
|
||||
await update();
|
||||
};
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { toastStore } from '$stores/toast';
|
||||
|
||||
export interface ApiRequestOptions extends RequestInit {
|
||||
showSuccessToast?: boolean; // Show toast on success (default: false)
|
||||
showErrorToast?: boolean; // Show toast on error (default: true)
|
||||
successMessage?: string; // Custom success message
|
||||
errorMessage?: string; // Custom error message (overrides server error)
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public data?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for fetch API with automatic toast notifications
|
||||
* @param url - Request URL
|
||||
* @param options - Request options with toast configuration
|
||||
* @returns Response data (automatically parsed as JSON)
|
||||
*/
|
||||
export async function apiRequest<T = unknown>(
|
||||
url: string,
|
||||
options: ApiRequestOptions = {}
|
||||
): Promise<T> {
|
||||
const {
|
||||
showSuccessToast = false,
|
||||
showErrorToast = true,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
headers,
|
||||
...fetchOptions
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// Set default headers
|
||||
const defaultHeaders: HeadersInit = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...headers
|
||||
}
|
||||
});
|
||||
|
||||
// Parse response
|
||||
const data = await response.json();
|
||||
|
||||
// Handle HTTP errors
|
||||
if (!response.ok) {
|
||||
const message = errorMessage || data.error || data.message || `HTTP ${response.status}`;
|
||||
|
||||
if (showErrorToast) {
|
||||
toastStore.add('error', message);
|
||||
}
|
||||
|
||||
throw new ApiError(message, response.status, data);
|
||||
}
|
||||
|
||||
// Show success toast if requested
|
||||
if (showSuccessToast && successMessage) {
|
||||
toastStore.add('success', successMessage);
|
||||
}
|
||||
|
||||
return data as T;
|
||||
} catch (error) {
|
||||
// Handle network errors or other exceptions
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const message = errorMessage || (error instanceof Error ? error.message : 'Network error');
|
||||
|
||||
if (showErrorToast) {
|
||||
toastStore.add('error', message);
|
||||
}
|
||||
|
||||
throw new ApiError(message, 0);
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,11 @@ const config = {
|
||||
$stores: './src/lib/client/stores',
|
||||
$components: './src/components',
|
||||
$assets: './src/lib/client/assets',
|
||||
$alerts: './src/lib/client/alerts',
|
||||
$server: './src/server',
|
||||
$db: './src/db',
|
||||
$arr: './src/utils/arr',
|
||||
$http: './src/utils/http',
|
||||
$api: './src/utils/api/request.ts',
|
||||
$utils: './src/utils',
|
||||
$notifications: './src/notifications',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user