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:
parent
1a155e5657
commit
e305bf15ef
7 changed files with 76 additions and 101 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
onclick={handlePrimaryAction}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{isLoading ? `${primaryAction.label.replace(/e$/, 'ing')}...` : primaryAction.label}
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
|
||||
{#if hasDropdownContent}
|
||||
|
|
|
|||
Loading…
Reference in a new issue