refactor: replace button text changes with toast notifications

- Update all admin forms to use toast messages
- Remove temporary "Saving..." button text changes
- Remove inline error/success message displays
- Keep buttons disabled during operations
- Show loading, success, and error toasts appropriately

Updated components:
- AlbumForm: Save operations with descriptive messages
- StatusDropdown: Remove loading text from buttons
- MediaDetailsModal: Save, delete, and copy operations
- ProjectForm: Create and update operations
- EssayForm: Publish and save draft operations
- SimplePostForm: Create and update posts
- PhotoPostForm: Publish photo posts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-06-24 01:56:26 +01:00
parent 1a155e5657
commit e305bf15ef
7 changed files with 76 additions and 101 deletions

View file

@ -10,6 +10,7 @@
import SmartImage from '../SmartImage.svelte'
import EnhancedComposer from './EnhancedComposer.svelte'
import { authenticatedFetch } from '$lib/admin-auth'
import { toast } from '$lib/stores/toast'
import type { Album } from '@prisma/client'
import type { JSONContent } from '@tiptap/core'
@ -34,8 +35,6 @@
// State
let isLoading = $state(mode === 'edit')
let isSaving = $state(false)
let error = $state('')
let successMessage = $state('')
let validationErrors = $state<Record<string, string>>({})
let showBulkAlbumModal = $state(false)
let albumMedia = $state<any[]>([])
@ -132,14 +131,14 @@
async function handleSave() {
if (!validateForm()) {
error = 'Please fix the validation errors'
toast.error('Please fix the validation errors')
return
}
const loadingToastId = toast.loading(`${mode === 'edit' ? 'Saving' : 'Creating'} album...`)
try {
isSaving = true
error = ''
successMessage = ''
const payload = {
title: formData.title,
@ -172,6 +171,9 @@
const savedAlbum = await response.json()
toast.dismiss(loadingToastId)
toast.success(`Album ${mode === 'edit' ? 'saved' : 'created'} successfully!`)
if (mode === 'create') {
goto(`/admin/albums/${savedAlbum.id}/edit`)
} else if (mode === 'edit' && album) {
@ -180,10 +182,12 @@
populateFormData(savedAlbum)
}
} catch (err) {
error =
toast.dismiss(loadingToastId)
toast.error(
err instanceof Error
? err.message
: `Failed to ${mode === 'edit' ? 'save' : 'create'} album`
)
console.error(err)
} finally {
isSaving = false
@ -252,10 +256,6 @@
{#if isLoading}
<div class="loading">Loading album...</div>
{:else}
{#if error}
<div class="error-message">{error}</div>
{/if}
<div class="tab-panels">
<!-- Metadata Panel -->
<div class="panel content-wrapper" class:active={activeTab === 'metadata'}>
@ -464,16 +464,6 @@
color: $grey-40;
}
.error-message {
background-color: #fee;
color: #d33;
padding: $unit-3x;
border-radius: $unit;
margin-bottom: $unit-4x;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.form-section {
display: flex;

View file

@ -5,6 +5,7 @@
import Editor from './Editor.svelte'
import Button from './Button.svelte'
import Input from './Input.svelte'
import { toast } from '$lib/stores/toast'
import type { JSONContent } from '@tiptap/core'
interface Props {
@ -24,8 +25,6 @@
// State
let isLoading = $state(false)
let isSaving = $state(false)
let error = $state('')
let successMessage = $state('')
let activeTab = $state('metadata')
let showPublishMenu = $state(false)
@ -80,14 +79,14 @@
}
if (!title) {
error = 'Title is required'
toast.error('Title is required')
return
}
const loadingToastId = toast.loading(`${mode === 'edit' ? 'Saving' : 'Creating'} essay...`)
try {
isSaving = true
error = ''
successMessage = ''
const auth = localStorage.getItem('admin_auth')
if (!auth) {
@ -121,16 +120,16 @@
}
const savedPost = await response.json()
successMessage = `Essay ${mode === 'edit' ? 'saved' : 'created'} successfully!`
toast.dismiss(loadingToastId)
toast.success(`Essay ${mode === 'edit' ? 'saved' : 'created'} successfully!`)
setTimeout(() => {
successMessage = ''
if (mode === 'create') {
goto(`/admin/posts/${savedPost.id}/edit`)
}
}, 1500)
if (mode === 'create') {
goto(`/admin/posts/${savedPost.id}/edit`)
}
} catch (err) {
error = `Failed to ${mode === 'edit' ? 'save' : 'create'} essay`
toast.dismiss(loadingToastId)
toast.error(`Failed to ${mode === 'edit' ? 'save' : 'create'} essay`)
console.error(err)
} finally {
isSaving = false
@ -196,7 +195,7 @@
<div class="header-actions">
<div class="save-actions">
<Button variant="primary" onclick={handleSave} disabled={isSaving} class="save-button">
{isSaving ? 'Saving...' : status === 'published' ? 'Save' : 'Save Draft'}
{status === 'published' ? 'Save' : 'Save Draft'}
</Button>
<Button
variant="primary"

View file

@ -7,6 +7,7 @@
import AlbumSelector from './AlbumSelector.svelte'
import AlbumIcon from '$icons/album.svg?component'
import { authenticatedFetch } from '$lib/admin-auth'
import { toast } from '$lib/stores/toast'
import type { Media } from '@prisma/client'
interface Props {
@ -22,8 +23,6 @@
let description = $state('')
let isPhotography = $state(false)
let isSaving = $state(false)
let error = $state('')
let successMessage = $state('')
// Usage tracking state
let usage = $state<
@ -51,8 +50,6 @@
if (media) {
description = media.description || ''
isPhotography = media.isPhotography || false
error = ''
successMessage = ''
showExif = false
loadUsage()
// Only load albums for images
@ -109,8 +106,6 @@
function handleClose() {
description = ''
isPhotography = false
error = ''
successMessage = ''
isOpen = false
onClose()
}
@ -118,9 +113,10 @@
async function handleSave() {
if (!media) return
const loadingToastId = toast.loading('Saving changes...')
try {
isSaving = true
error = ''
const response = await authenticatedFetch(`/api/media/${media.id}`, {
method: 'PUT',
@ -139,14 +135,17 @@
const updatedMedia = await response.json()
onUpdate(updatedMedia)
successMessage = 'Media updated successfully!'
toast.dismiss(loadingToastId)
toast.success('Media updated successfully!')
// Auto-close after success
setTimeout(() => {
handleClose()
}, 1500)
} catch (err) {
error = 'Failed to update media. Please try again.'
toast.dismiss(loadingToastId)
toast.error('Failed to update media. Please try again.')
console.error('Failed to update media:', err)
} finally {
isSaving = false
@ -161,9 +160,10 @@
return
}
const loadingToastId = toast.loading('Deleting media...')
try {
isSaving = true
error = ''
const response = await authenticatedFetch(`/api/media/${media.id}`, {
method: 'DELETE'
@ -173,11 +173,15 @@
throw new Error('Failed to delete media')
}
toast.dismiss(loadingToastId)
toast.success('Media deleted successfully')
// Close modal and let parent handle the deletion
handleClose()
// Note: Parent component should refresh the media list
} catch (err) {
error = 'Failed to delete media. Please try again.'
toast.dismiss(loadingToastId)
toast.error('Failed to delete media. Please try again.')
console.error('Failed to delete media:', err)
} finally {
isSaving = false
@ -189,16 +193,10 @@
navigator.clipboard
.writeText(media.url)
.then(() => {
successMessage = 'URL copied to clipboard!'
setTimeout(() => {
successMessage = ''
}, 2000)
toast.success('URL copied to clipboard!')
})
.catch(() => {
error = 'Failed to copy URL'
setTimeout(() => {
error = ''
}, 2000)
toast.error('Failed to copy URL')
})
}
}
@ -548,15 +546,8 @@
</div>
<div class="footer-right">
{#if error}
<span class="error-text">{error}</span>
{/if}
{#if successMessage}
<span class="success-text">{successMessage}</span>
{/if}
<Button variant="primary" onclick={handleSave} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save Changes'}
Save Changes
</Button>
</div>
</div>
@ -1035,16 +1026,6 @@
display: flex;
align-items: center;
gap: $unit-2x;
.error-text {
color: $red-60;
font-size: 0.875rem;
}
.success-text {
color: #16a34a; // green-600 equivalent
font-size: 0.875rem;
}
}
}

View file

@ -5,6 +5,7 @@
import Input from './Input.svelte'
import ImageUploader from './ImageUploader.svelte'
import Editor from './Editor.svelte'
import { toast } from '$lib/stores/toast'
import type { JSONContent } from '@tiptap/core'
import type { Media } from '@prisma/client'
@ -24,7 +25,6 @@
// State
let isSaving = $state(false)
let error = $state('')
let status = $state<'draft' | 'published'>(initialData?.status || 'draft')
// Form data
@ -81,18 +81,19 @@
async function handleSave() {
// Validate required fields
if (!featuredImage) {
error = 'Please upload a photo for this post'
toast.error('Please upload a photo for this post')
return
}
if (!title.trim()) {
error = 'Please enter a title for this post'
toast.error('Please enter a title for this post')
return
}
const loadingToastId = toast.loading(`${status === 'published' ? 'Publishing' : 'Saving'} photo post...`)
try {
isSaving = true
error = ''
// Get editor content
let editorContent = content
@ -145,6 +146,9 @@
const savedPost = await response.json()
toast.dismiss(loadingToastId)
toast.success(`Photo post ${status === 'published' ? 'published' : 'saved'} successfully!`)
// Redirect to posts list or edit page
if (mode === 'create') {
goto(`/admin/posts/${savedPost.id}/edit`)
@ -152,7 +156,8 @@
goto('/admin/posts')
}
} catch (err) {
error = `Failed to ${mode === 'edit' ? 'update' : 'create'} photo post`
toast.dismiss(loadingToastId)
toast.error(`Failed to ${mode === 'edit' ? 'update' : 'create'} photo post`)
console.error(err)
} finally {
isSaving = false
@ -192,7 +197,7 @@
onclick={handlePublish}
disabled={!featuredImage || !title.trim()}
>
{isSaving ? 'Publishing...' : 'Publish'}
Publish
</Button>
{/if}
</div>

View file

@ -11,6 +11,7 @@
import Button from './Button.svelte'
import StatusDropdown from './StatusDropdown.svelte'
import { projectSchema } from '$lib/schemas/project'
import { toast } from '$lib/stores/toast'
import type { Project, ProjectFormData } from '$lib/types/project'
import { defaultProjectFormData } from '$lib/types/project'
@ -24,8 +25,6 @@
// State
let isLoading = $state(mode === 'edit')
let isSaving = $state(false)
let error = $state('')
let successMessage = $state('')
let activeTab = $state('metadata')
let validationErrors = $state<Record<string, string>>({})
@ -117,14 +116,14 @@
}
if (!validateForm()) {
error = 'Please fix the validation errors'
toast.error('Please fix the validation errors')
return
}
const loadingToastId = toast.loading(`${mode === 'edit' ? 'Saving' : 'Creating'} project...`)
try {
isSaving = true
error = ''
successMessage = ''
const auth = localStorage.getItem('admin_auth')
if (!auth) {
@ -173,16 +172,16 @@
}
const savedProject = await response.json()
successMessage = `Project ${mode === 'edit' ? 'saved' : 'created'} successfully!`
toast.dismiss(loadingToastId)
toast.success(`Project ${mode === 'edit' ? 'saved' : 'created'} successfully!`)
setTimeout(() => {
successMessage = ''
if (mode === 'create') {
goto(`/admin/projects/${savedProject.id}/edit`)
}
}, 1500)
if (mode === 'create') {
goto(`/admin/projects/${savedProject.id}/edit`)
}
} catch (err) {
error = `Failed to ${mode === 'edit' ? 'save' : 'create'} project`
toast.dismiss(loadingToastId)
toast.error(`Failed to ${mode === 'edit' ? 'save' : 'create'} project`)
console.error(err)
} finally {
isSaving = false

View file

@ -5,6 +5,7 @@
import Editor from './Editor.svelte'
import Button from './Button.svelte'
import Input from './Input.svelte'
import { toast } from '$lib/stores/toast'
interface Props {
postType: 'post'
@ -23,7 +24,6 @@
// State
let isSaving = $state(false)
let error = $state('')
let status = $state<'draft' | 'published'>(initialData?.status || 'draft')
// Form data
@ -53,19 +53,20 @@
async function handleSave(publishStatus: 'draft' | 'published') {
if (isOverLimit) {
error = 'Post is too long'
toast.error('Post is too long')
return
}
// For link posts, URL is required
if (linkUrl && !linkUrl.trim()) {
error = 'Link URL is required'
toast.error('Link URL is required')
return
}
const loadingToastId = toast.loading(`${publishStatus === 'published' ? 'Publishing' : 'Saving'} post...`)
try {
isSaving = true
error = ''
const auth = localStorage.getItem('admin_auth')
if (!auth) {
@ -104,10 +105,14 @@
const savedPost = await response.json()
toast.dismiss(loadingToastId)
toast.success(`Post ${publishStatus === 'published' ? 'published' : 'saved'} successfully!`)
// Redirect back to posts list after creation
goto('/admin/posts')
} catch (err) {
error = `Failed to ${mode === 'edit' ? 'save' : 'create'} post`
toast.dismiss(loadingToastId)
toast.error(`Failed to ${mode === 'edit' ? 'save' : 'create'} post`)
console.error(err)
} finally {
isSaving = false
@ -146,16 +151,12 @@
onclick={() => handleSave('published')}
disabled={isSaving || !hasContent() || (postType === 'microblog' && isOverLimit)}
>
{isSaving ? 'Posting...' : 'Post'}
Post
</Button>
</div>
</header>
<div class="composer-container">
{#if error}
<div class="error-message">{error}</div>
{/if}
<div class="composer">
{#if postType === 'microblog'}
<div class="post-composer">

View file

@ -76,7 +76,7 @@
onclick={handlePrimaryAction}
disabled={disabled || isLoading}
>
{isLoading ? `${primaryAction.label.replace(/e$/, 'ing')}...` : primaryAction.label}
{primaryAction.label}
</Button>
{#if hasDropdownContent}