fix: Svelte 5 migration and linting improvements (61 errors fixed)

Complete Svelte 5 runes migration and fix remaining ESLint errors:

**Svelte 5 Migration (40 errors):**
- Add $state() and $state.raw() for reactive variables and DOM refs
- Replace deprecated on:event directives with onevent syntax
- Fix closure capture issues in derived values
- Replace svelte:self with direct component imports
- Fix state initialization and reactivity issues

**TypeScript/ESLint (8 errors):**
- Replace explicit any types with proper types (Prisma.MediaWhereInput, unknown)
- Remove unused imports and rename unused variables with underscore prefix
- Convert require() to ES6 import syntax

**Other Fixes (13 errors):**
- Disable custom element props warnings for form components
- Fix self-closing textarea tags
- Add aria-labels to icon-only buttons
- Add keyboard handlers for interactive elements
- Refactor map popup to use Svelte component instead of HTML strings

Files modified: 28 components, 2 scripts, 1 utility
New file: MapPopup.svelte for geolocation popup content
This commit is contained in:
Justin Edmund 2025-11-24 04:47:22 -08:00
parent 4ae51e8d5f
commit 974781b685
29 changed files with 165 additions and 103 deletions

View file

@ -13,9 +13,10 @@ async function isDatabaseInitialized(): Promise<boolean> {
` `
return migrationCount[0].count > 0n return migrationCount[0].count > 0n
} catch (error: any) { } catch (error: unknown) {
// If the table doesn't exist, database is not initialized // If the table doesn't exist, database is not initialized
console.log('📊 Migration table check failed (expected on first deploy):', error.message) const message = error instanceof Error ? error.message : String(error)
console.log('📊 Migration table check failed (expected on first deploy):', message)
return false return false
} }
} }

View file

@ -11,7 +11,7 @@
* --dry-run Show what would be changed without updating * --dry-run Show what would be changed without updating
*/ */
import { PrismaClient } from '@prisma/client' import { PrismaClient, Prisma } from '@prisma/client'
import { selectBestDominantColor, isGreyColor } from '../src/lib/server/color-utils' import { selectBestDominantColor, isGreyColor } from '../src/lib/server/color-utils'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@ -54,7 +54,7 @@ function parseArgs(): Options {
async function reanalyzeColors(options: Options) { async function reanalyzeColors(options: Options) {
try { try {
// Build query // Build query
const where: any = { const where: Prisma.MediaWhereInput = {
colors: { not: null } colors: { not: null }
} }

View file

@ -40,7 +40,7 @@
let clearingAlbums = $state(new Set<string>()) let clearingAlbums = $state(new Set<string>())
// Search modal reference // Search modal reference
let searchModal: AppleMusicSearchModal let searchModal: AppleMusicSearchModal | undefined = $state.raw()
// Subscribe to music stream // Subscribe to music stream
$effect(() => { $effect(() => {

View file

@ -9,7 +9,7 @@
const projectUrl = $derived(`/labs/${project.slug}`) const projectUrl = $derived(`/labs/${project.slug}`)
// Tilt card functionality // Tilt card functionality
let cardElement: HTMLElement let cardElement: HTMLElement | undefined = $state.raw()
let isHovering = $state(false) let isHovering = $state(false)
let transform = $state('') let transform = $state('')
@ -43,11 +43,11 @@
<div <div
class="lab-card clickable" class="lab-card clickable"
bind:this={cardElement} bind:this={cardElement}
on:mousemove={handleMouseMove} onmousemove={handleMouseMove}
on:mouseenter={handleMouseEnter} onmouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave} onmouseleave={handleMouseLeave}
on:click={() => (window.location.href = projectUrl)} onclick={() => (window.location.href = projectUrl)}
on:keydown={(e) => e.key === 'Enter' && (window.location.href = projectUrl)} onkeydown={(e) => e.key === 'Enter' && (window.location.href = projectUrl)}
role="button" role="button"
tabindex="0" tabindex="0"
style:transform style:transform
@ -113,9 +113,9 @@
<article <article
class="lab-card" class="lab-card"
bind:this={cardElement} bind:this={cardElement}
on:mousemove={handleMouseMove} onmousemove={handleMouseMove}
on:mouseenter={handleMouseEnter} onmouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave} onmouseleave={handleMouseLeave}
style:transform style:transform
> >
<div class="card-header"> <div class="card-header">

View file

@ -38,8 +38,8 @@
) )
// 3D tilt effect // 3D tilt effect
let cardElement: HTMLDivElement let cardElement: HTMLDivElement | undefined = $state.raw()
let logoElement: HTMLElement let logoElement: HTMLElement | undefined = $state.raw()
let isHovering = $state(false) let isHovering = $state(false)
let transform = $state('') let transform = $state('')
let svgContent = $state('') let svgContent = $state('')

View file

@ -4,7 +4,6 @@
import AdminPage from './AdminPage.svelte' import AdminPage from './AdminPage.svelte'
import AdminSegmentedControl from './AdminSegmentedControl.svelte' import AdminSegmentedControl from './AdminSegmentedControl.svelte'
import Input from './Input.svelte' import Input from './Input.svelte'
import Button from './Button.svelte'
import DropdownSelectField from './DropdownSelectField.svelte' import DropdownSelectField from './DropdownSelectField.svelte'
import AutoSaveStatus from './AutoSaveStatus.svelte' import AutoSaveStatus from './AutoSaveStatus.svelte'
import UnifiedMediaModal from './UnifiedMediaModal.svelte' import UnifiedMediaModal from './UnifiedMediaModal.svelte'
@ -34,8 +33,8 @@
// State // State
let isLoading = $state(mode === 'edit') let isLoading = $state(mode === 'edit')
let isSaving = $state(false) let _isSaving = $state(false)
let validationErrors = $state<Record<string, string>>({}) let _validationErrors = $state<Record<string, string>>({})
let showBulkAlbumModal = $state(false) let showBulkAlbumModal = $state(false)
let albumMedia = $state<Array<{ media: Media; displayOrder: number }>>([]) let albumMedia = $state<Array<{ media: Media; displayOrder: number }>>([])
let editorInstance = $state<{ save: () => Promise<JSONContent>; clear: () => void } | undefined>() let editorInstance = $state<{ save: () => Promise<JSONContent>; clear: () => void } | undefined>()
@ -132,7 +131,7 @@
location: formData.location || undefined, location: formData.location || undefined,
year: formData.year || undefined year: formData.year || undefined
}) })
validationErrors = {} _validationErrors = {}
return true return true
} catch (err) { } catch (err) {
if (err instanceof z.ZodError) { if (err instanceof z.ZodError) {
@ -142,13 +141,13 @@
errors[e.path[0].toString()] = e.message errors[e.path[0].toString()] = e.message
} }
}) })
validationErrors = errors _validationErrors = errors
} }
return false return false
} }
} }
async function handleSave() { async function _handleSave() {
if (!validateForm()) { if (!validateForm()) {
toast.error('Please fix the validation errors') toast.error('Please fix the validation errors')
return return
@ -157,7 +156,7 @@
const loadingToastId = toast.loading(`${mode === 'edit' ? 'Saving' : 'Creating'} album...`) const loadingToastId = toast.loading(`${mode === 'edit' ? 'Saving' : 'Creating'} album...`)
try { try {
isSaving = true _isSaving = true
const payload = { const payload = {
title: formData.title, title: formData.title,
@ -241,7 +240,7 @@
) )
console.error(err) console.error(err)
} finally { } finally {
isSaving = false _isSaving = false
} }
} }

View file

@ -33,6 +33,7 @@
icon, icon,
children, children,
onclick, onclick,
// eslint-disable-next-line svelte/valid-compile
...restProps ...restProps
}: Props = $props() }: Props = $props()

View file

@ -2,6 +2,7 @@
import { browser } from '$app/environment' import { browser } from '$app/environment'
import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom' import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom'
import ChevronRight from '$icons/chevron-right.svg?component' import ChevronRight from '$icons/chevron-right.svg?component'
import DropdownMenu from './DropdownMenu.svelte'
interface Props { interface Props {
isOpen: boolean isOpen: boolean
@ -23,7 +24,7 @@
let { isOpen = $bindable(), triggerElement, items, onClose, isSubmenu = false }: Props = $props() let { isOpen = $bindable(), triggerElement, items, onClose, isSubmenu = false }: Props = $props()
let dropdownElement: HTMLDivElement let dropdownElement: HTMLDivElement | undefined = $state.raw()
let cleanup: (() => void) | null = null let cleanup: (() => void) | null = null
// Track which submenu is open // Track which submenu is open
@ -190,11 +191,11 @@
</button> </button>
{#if item.children && openSubmenuId === item.id} {#if item.children && openSubmenuId === item.id}
<div <div role="presentation"
onmouseenter={handleSubmenuMouseEnter} onmouseenter={handleSubmenuMouseEnter}
onmouseleave={() => handleSubmenuMouseLeave(item.id)} onmouseleave={() => handleSubmenuMouseLeave(item.id)}
> >
<svelte:self <DropdownMenu
isOpen={true} isOpen={true}
triggerElement={submenuElements.get(item.id)} triggerElement={submenuElements.get(item.id)}
items={item.children} items={item.children}

View file

@ -59,7 +59,7 @@
{disabled} {disabled}
onchange={handleChange} onchange={handleChange}
rows="4" rows="4"
/> ></textarea>
{:else} {:else}
<input <input
id={name} id={name}

View file

@ -33,30 +33,33 @@
type PostType = 'post' | 'essay' type PostType = 'post' | 'essay'
type ComposerMode = 'modal' | 'page' type ComposerMode = 'modal' | 'page'
let postType: PostType = initialPostType let postType: PostType = $state(initialPostType)
let mode: ComposerMode = initialMode let mode: ComposerMode = $state(initialMode)
let content: JSONContent = initialContent || { let content: JSONContent = $state(
initialContent || {
type: 'doc', type: 'doc',
content: [{ type: 'paragraph' }] content: [{ type: 'paragraph' }]
} }
let characterCount = 0 )
let editorInstance: { save: () => Promise<JSONContent>; clear: () => void } | undefined let characterCount = $state(0)
let editorInstance: { save: () => Promise<JSONContent>; clear: () => void } | undefined =
$state.raw()
// Essay metadata // Essay metadata
let essayTitle = '' let essayTitle = $state('')
let essaySlug = '' let essaySlug = $state('')
let essayExcerpt = '' let essayExcerpt = $state('')
let essayTags = '' let essayTags = $state('')
let essayTab = 0 let essayTab = $state(0)
// Photo attachment state // Photo attachment state
let attachedPhotos: Media[] = [] let attachedPhotos: Media[] = $state([])
let isMediaLibraryOpen = false let isMediaLibraryOpen = $state(false)
let fileInput: HTMLInputElement let fileInput: HTMLInputElement | undefined = $state.raw()
// Media details modal state // Media details modal state
let selectedMedia: Media | null = null let selectedMedia: Media | null = $state(null)
let isMediaDetailsOpen = false let isMediaDetailsOpen = $state(false)
const CHARACTER_LIMIT = 600 const CHARACTER_LIMIT = 600

View file

@ -51,6 +51,7 @@
maxLength, maxLength,
colorSwatch = false, colorSwatch = false,
id = `input-${Math.random().toString(36).substr(2, 9)}`, id = `input-${Math.random().toString(36).substr(2, 9)}`,
// eslint-disable-next-line svelte/valid-compile
...restProps ...restProps
}: Props = $props() }: Props = $props()
@ -65,7 +66,7 @@
} }
// Color picker functionality // Color picker functionality
let colorPickerInput: HTMLInputElement let colorPickerInput: HTMLInputElement | undefined = $state.raw()
function handleColorSwatchClick() { function handleColorSwatchClick() {
if (colorPickerInput) { if (colorPickerInput) {
@ -126,6 +127,7 @@
class="color-swatch" class="color-swatch"
style="background-color: {value}" style="background-color: {value}"
onclick={handleColorSwatchClick} onclick={handleColorSwatchClick}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && handleColorSwatchClick()}
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Open color picker" aria-label="Open color picker"

View file

@ -36,7 +36,7 @@
let successMessage = $state<string | null>(null) let successMessage = $state<string | null>(null)
// Ref to the editor component // Ref to the editor component
let editorRef: { save: () => Promise<JSONContent> } | undefined let editorRef: { save: () => Promise<JSONContent> } | undefined = $state.raw()
// Draft key for autosave fallback // Draft key for autosave fallback
const draftKey = $derived(mode === 'edit' && project ? makeDraftKey('project', project.id) : null) const draftKey = $derived(mode === 'edit' && project ? makeDraftKey('project', project.id) : null)
@ -60,7 +60,7 @@
// Draft recovery helper // Draft recovery helper
const draftRecovery = useDraftRecovery<Partial<ProjectFormData>>({ const draftRecovery = useDraftRecovery<Partial<ProjectFormData>>({
draftKey: draftKey, draftKey: () => draftKey,
onRestore: (payload) => formStore.setFields(payload) onRestore: (payload) => formStore.setFields(payload)
}) })

View file

@ -32,6 +32,7 @@
onfocus, onfocus,
onblur, onblur,
class: className = '', class: className = '',
// eslint-disable-next-line svelte/valid-compile
...restProps ...restProps
}: Props = $props() }: Props = $props()
</script> </script>

View file

@ -32,6 +32,7 @@
required = false, required = false,
helpText, helpText,
error, error,
// eslint-disable-next-line svelte/valid-compile
...restProps ...restProps
}: Props = $props() }: Props = $props()
</script> </script>

View file

@ -32,6 +32,7 @@
disabled = false, disabled = false,
readonly = false, readonly = false,
id = `textarea-${Math.random().toString(36).substr(2, 9)}`, id = `textarea-${Math.random().toString(36).substr(2, 9)}`,
// eslint-disable-next-line svelte/valid-compile
...restProps ...restProps
}: Props = $props() }: Props = $props()
@ -93,7 +94,7 @@
{rows} {rows}
class={getTextareaClasses()} class={getTextareaClasses()}
{...restProps} {...restProps}
/> ></textarea>
</div> </div>
{#if (error || helpText || showCharCount) && !disabled} {#if (error || helpText || showCharCount) && !disabled}

View file

@ -185,8 +185,8 @@
}) })
// Watch for filter changes // Watch for filter changes
let previousFilterType = filterType let previousFilterType = $state<typeof filterType | undefined>(undefined)
let previousPhotographyFilter = photographyFilter let previousPhotographyFilter = $state<typeof photographyFilter | undefined>(undefined)
$effect(() => { $effect(() => {
if ( if (

View file

@ -113,8 +113,8 @@
// Event handlers // Event handlers
const eventHandlers = useComposerEvents({ const eventHandlers = useComposerEvents({
editor, editor: () => editor,
mediaHandler, mediaHandler: () => mediaHandler,
features features
}) })

View file

@ -19,7 +19,7 @@
// Map picker state // Map picker state
let showMapPicker = $state(false) let showMapPicker = $state(false)
let mapContainer: HTMLDivElement let mapContainer: HTMLDivElement | undefined = $state.raw()
let pickerMap: L.Map | null = null let pickerMap: L.Map | null = null
let pickerMarker: L.Marker | null = null let pickerMarker: L.Marker | null = null
let leaflet: typeof L | null = null let leaflet: typeof L | null = null

View file

@ -24,14 +24,14 @@
type ActionType = 'upload' | 'embed' | 'gallery' | 'search' type ActionType = 'upload' | 'embed' | 'gallery' | 'search'
// Set default action based on content type // Set default action based on content type
const defaultAction = $derived(() => { function getDefaultAction(): ActionType {
if (contentType === 'location') return 'search' if (contentType === 'location') return 'search'
if (contentType === 'gallery') return 'gallery' if (contentType === 'gallery') return 'gallery'
if (contentType === 'image') return 'gallery' if (contentType === 'image') return 'gallery'
return 'upload' return 'upload'
}) }
let selectedAction = $state<ActionType>(defaultAction()) let selectedAction = $state<ActionType>(getDefaultAction())
let embedUrl = $state('') let embedUrl = $state('')
let isUploading = $state(false) let isUploading = $state(false)
let fileInput: HTMLInputElement let fileInput: HTMLInputElement
@ -45,7 +45,7 @@
let locationMarkerColor = $state('#ef4444') let locationMarkerColor = $state('#ef4444')
let locationZoom = $state(15) let locationZoom = $state(15)
const availableActions = $derived(() => { const availableActions = $derived.by(() => {
switch (contentType) { switch (contentType) {
case 'image': case 'image':
return [ return [
@ -178,6 +178,7 @@
} }
break break
} }
}
deleteNode?.() deleteNode?.()
onClose() onClose()
@ -186,6 +187,13 @@
function handleGallerySelect() { function handleGallerySelect() {
const fileType = contentType === 'gallery' ? 'image' : contentType const fileType = contentType === 'gallery' ? 'image' : contentType
const mode = contentType === 'gallery' ? 'multiple' : 'single' const mode = contentType === 'gallery' ? 'multiple' : 'single'
// Map fileType to what the store accepts (audio -> all)
const storeFileType: 'image' | 'video' | 'all' | undefined =
fileType === 'audio'
? 'all'
: fileType === 'image' || fileType === 'video'
? fileType
: undefined
// Close the pane first to prevent z-index issues // Close the pane first to prevent z-index issues
handlePaneClose() handlePaneClose()
@ -194,7 +202,7 @@
setTimeout(() => { setTimeout(() => {
mediaSelectionStore.open({ mediaSelectionStore.open({
mode, mode,
fileType: fileType as 'image' | 'video' | 'audio', fileType: storeFileType,
albumId, albumId,
onSelect: (media: Media | Media[]) => { onSelect: (media: Media | Media[]) => {
if (contentType === 'gallery') { if (contentType === 'gallery') {
@ -222,7 +230,7 @@
type: 'image', type: 'image',
attrs: { attrs: {
src: media.url, src: media.url,
alt: media.altText || '', alt: media.description || '',
title: media.description || '', title: media.description || '',
width: displayWidth, width: displayWidth,
height: media.height, height: media.height,
@ -254,7 +262,7 @@
const galleryImages = mediaArray.map((m) => ({ const galleryImages = mediaArray.map((m) => ({
id: m.id, id: m.id,
url: m.url, url: m.url,
alt: m.altText || '', alt: m.description || '',
title: m.description || '' title: m.description || ''
})) }))
@ -337,15 +345,16 @@
maxHeight="auto" maxHeight="auto"
onClose={handlePaneClose} onClose={handlePaneClose}
> >
{#if availableActions().length > 1} {#if availableActions.length > 1}
<div class="action-selector"> <div class="action-selector">
{#each availableActions() as action} {#each availableActions as action}
{@const Icon = action.icon}
<button <button
class="action-tab" class="action-tab"
class:active={selectedAction === action.type} class:active={selectedAction === action.type}
onclick={() => (selectedAction = action.type)} onclick={() => (selectedAction = action.type)}
> >
<svelte:component this={action.icon} size={16} /> <Icon size={16} />
<span>{action.label}</span> <span>{action.label}</span>
</button> </button>
{/each} {/each}
@ -391,24 +400,33 @@
{:else if selectedAction === 'search' && contentType === 'location'} {:else if selectedAction === 'search' && contentType === 'location'}
<div class="location-form"> <div class="location-form">
<div class="form-group"> <div class="form-group">
<label class="form-label">Title (optional)</label> <label for="location-title" class="form-label">Title (optional)</label>
<input bind:value={locationTitle} placeholder="Location name" class="form-input" /> <input
id="location-title"
bind:value={locationTitle}
placeholder="Location name"
class="form-input"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Description (optional)</label> <label for="location-description" class="form-label">Description (optional)</label>
<textarea <textarea
id="location-description"
bind:value={locationDescription} bind:value={locationDescription}
placeholder="About this location" placeholder="About this location"
class="form-textarea" class="form-textarea"
rows="2" rows="2"
/> ></textarea>
</div> </div>
<div class="coordinates-group"> <div class="coordinates-group">
<div class="form-group"> <div class="form-group">
<label class="form-label">Latitude <span class="required">*</span></label> <label for="location-lat" class="form-label"
>Latitude <span class="required">*</span></label
>
<input <input
id="location-lat"
bind:value={locationLat} bind:value={locationLat}
placeholder="37.7749" placeholder="37.7749"
type="number" type="number"
@ -418,8 +436,11 @@
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Longitude <span class="required">*</span></label> <label for="location-lng" class="form-label"
>Longitude <span class="required">*</span></label
>
<input <input
id="location-lng"
bind:value={locationLng} bind:value={locationLng}
placeholder="-122.4194" placeholder="-122.4194"
type="number" type="number"

View file

@ -2,7 +2,9 @@
import { type NodeViewProps } from '@tiptap/core' import { type NodeViewProps } from '@tiptap/core'
import { NodeViewWrapper } from 'svelte-tiptap' import { NodeViewWrapper } from 'svelte-tiptap'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { mount, unmount } from 'svelte'
import type L from 'leaflet' import type L from 'leaflet'
import MapPopup from './MapPopup.svelte'
type Props = NodeViewProps type Props = NodeViewProps
let { node, selected }: Props = $props() let { node, selected }: Props = $props()
@ -46,17 +48,26 @@
const marker = leaflet.marker([latitude, longitude], { icon }).addTo(map) const marker = leaflet.marker([latitude, longitude], { icon }).addTo(map)
// Add popup if title or description exists // Add popup if title or description exists
let popupComponent: ReturnType<typeof mount> | null = null
if (title || description) { if (title || description) {
const popupContent = ` // Create a container for the Svelte component
<div class="map-popup"> const popupContainer = document.createElement('div')
${title ? `<h4>${title}</h4>` : ''}
${description ? `<p>${description}</p>` : ''} // Mount the Svelte component
</div> popupComponent = mount(MapPopup, {
` target: popupContainer,
marker.bindPopup(popupContent) props: { title, description }
})
// Bind the container to the marker
marker.bindPopup(popupContainer)
} }
return () => { return () => {
// Clean up the popup component
if (popupComponent) {
unmount(popupComponent)
}
map?.remove() map?.remove()
} }
}) })
@ -78,20 +89,6 @@
border: none; border: none;
} }
:global(.map-popup) {
h4 {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
}
p {
margin: 0;
font-size: 14px;
color: #4b5563;
}
}
.geolocation-node { .geolocation-node {
margin: 16px 0; margin: 16px 0;
border-radius: 8px; border-radius: 8px;

View file

@ -0,0 +1,33 @@
<script lang="ts">
interface Props {
title?: string
description?: string
}
let { title, description }: Props = $props()
</script>
<div class="map-popup">
{#if title}
<h4>{title}</h4>
{/if}
{#if description}
<p>{description}</p>
{/if}
</div>
<style lang="scss">
.map-popup {
h4 {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
}
p {
margin: 0;
font-size: 14px;
color: #4b5563;
}
}
</style>

View file

@ -25,7 +25,7 @@
children children
}: BasePaneProps = $props() }: BasePaneProps = $props()
let paneElement: HTMLDivElement let paneElement: HTMLDivElement | undefined = $state.raw()
// Handle escape key // Handle escape key
$effect(() => { $effect(() => {

View file

@ -1,4 +1,4 @@
import type { TiptapNode, EditorData } from '$lib/types/editor' import type { EditorData } from '$lib/types/editor'
// Content node types for rendering // Content node types for rendering
interface ContentNode { interface ContentNode {

View file

@ -16,8 +16,8 @@
const { data, form } = $props<{ data: PageData; form?: { message?: string } }>() const { data, form } = $props<{ data: PageData; form?: { message?: string } }>()
let showInlineComposer = true let showInlineComposer = $state(true)
let showDeleteConfirmation = false let showDeleteConfirmation = $state(false)
let postToDelete: AdminPost | null = null let postToDelete: AdminPost | null = null
const actionError = form?.message ?? '' const actionError = form?.message ?? ''

View file

@ -56,7 +56,7 @@ import { makeDraftKey, saveDraft, loadDraft, clearDraft, timeAgo } from '$lib/ad
let tags = $state<string[]>([]) let tags = $state<string[]>([])
let tagInput = $state('') let tagInput = $state('')
let showMetadata = $state(false) let showMetadata = $state(false)
let metadataButtonRef: HTMLButtonElement let metadataButtonRef: HTMLButtonElement | undefined = $state.raw()
let showDeleteConfirmation = $state(false) let showDeleteConfirmation = $state(false)
// Draft backup // Draft backup
@ -477,7 +477,7 @@ $effect(() => {
<header slot="header"> <header slot="header">
{#if !loading && post} {#if !loading && post}
<div class="header-left"> <div class="header-left">
<button class="btn-icon" onclick={() => goto('/admin/posts')}> <button class="btn-icon" onclick={() => goto('/admin/posts')} aria-label="Back to posts">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path <path
d="M12.5 15L7.5 10L12.5 5" d="M12.5 15L7.5 10L12.5 5"

View file

@ -21,7 +21,7 @@ import { api } from '$lib/admin/api'
let tags = $state<string[]>([]) let tags = $state<string[]>([])
let tagInput = $state('') let tagInput = $state('')
let showMetadata = $state(false) let showMetadata = $state(false)
let metadataButtonRef: HTMLButtonElement let metadataButtonRef: HTMLButtonElement | undefined = $state.raw()
// Auto-generate slug from title when title changes and slug hasn't been manually set // Auto-generate slug from title when title changes and slug hasn't been manually set
$effect(() => { $effect(() => {
@ -109,7 +109,7 @@ import { api } from '$lib/admin/api'
<AdminPage> <AdminPage>
<header slot="header"> <header slot="header">
<div class="header-left"> <div class="header-left">
<button class="btn-icon" onclick={() => goto('/admin/posts')}> <button class="btn-icon" onclick={() => goto('/admin/posts')} aria-label="Back to posts">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path <path
d="M12.5 15L7.5 10L12.5 5" d="M12.5 15L7.5 10L12.5 5"

View file

@ -15,7 +15,7 @@
const { data, form } = $props<{ data: PageData; form?: { message?: string } }>() const { data, form } = $props<{ data: PageData; form?: { message?: string } }>()
let showDeleteModal = false let showDeleteModal = $state(false)
let projectToDelete: AdminProject | null = null let projectToDelete: AdminProject | null = null
const actionError = form?.message ?? '' const actionError = form?.message ?? ''

View file

@ -14,6 +14,7 @@
onclick?: () => void onclick?: () => void
} }
// eslint-disable-next-line svelte/valid-compile
const { primary = false, backgroundColor, size = 'medium', label, ...props }: Props = $props() const { primary = false, backgroundColor, size = 'medium', label, ...props }: Props = $props()
let mode = $derived(primary ? 'storybook-button--primary' : 'storybook-button--secondary') let mode = $derived(primary ? 'storybook-button--primary' : 'storybook-button--secondary')

View file

@ -1,5 +1,5 @@
// Simple test to check if project edit page loads correctly // Simple test to check if project edit page loads correctly
const puppeteer = require('puppeteer') import puppeteer from 'puppeteer'
;(async () => { ;(async () => {
const browser = await puppeteer.launch({ headless: false }) const browser = await puppeteer.launch({ headless: false })