fix: replace any types in frontend components

- use Leaflet types (L.Map, L.Marker, L.LeafletEvent) for map components
- use Post and Project types from Prisma for form components
- use JSONContent type for editor instances
- use Snippet type for Svelte 5 render functions
- use EditorView type for TipTap/ProseMirror views
- use proper type guards for error handling
- add editor interface types for save/clear methods
This commit is contained in:
Justin Edmund 2025-11-23 05:32:09 -08:00
parent 8ec4c582c1
commit 9c746d51c0
12 changed files with 50 additions and 42 deletions

View file

@ -19,9 +19,9 @@
}: Props = $props() }: Props = $props()
let mapContainer: HTMLDivElement let mapContainer: HTMLDivElement
let map: any let map: L.Map | null = null
let marker: any let marker: L.Marker | null = null
let leaflet: any let leaflet: typeof L | null = null
// Load Leaflet dynamically // Load Leaflet dynamically
async function loadLeaflet() { async function loadLeaflet() {

View file

@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'
interface Props { interface Props {
left?: any left?: Snippet
right?: any right?: Snippet
} }
let { left, right }: Props = $props() let { left, right }: Props = $props()

View file

@ -37,8 +37,8 @@
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<any[]>([]) let albumMedia = $state<Array<{ media: Media; displayOrder: number }>>([])
let editorInstance = $state<any>() let editorInstance = $state<{ save: () => Promise<JSONContent>; clear: () => void } | undefined>()
let activeTab = $state('metadata') let activeTab = $state('metadata')
let pendingMediaIds = $state<number[]>([]) // Photos to add after album creation let pendingMediaIds = $state<number[]>([]) // Photos to add after album creation

View file

@ -10,7 +10,8 @@
import { makeDraftKey, saveDraft, loadDraft, clearDraft, timeAgo } from '$lib/admin/draftStore' import { makeDraftKey, saveDraft, loadDraft, clearDraft, timeAgo } from '$lib/admin/draftStore'
import { createAutoSaveStore } from '$lib/admin/autoSave.svelte' import { createAutoSaveStore } from '$lib/admin/autoSave.svelte'
import AutoSaveStatus from './AutoSaveStatus.svelte' import AutoSaveStatus from './AutoSaveStatus.svelte'
import type { JSONContent } from '@tiptap/core' import type { JSONContent, Editor as TipTapEditor } from '@tiptap/core'
import type { Post } from '@prisma/client'
interface Props { interface Props {
postId?: number postId?: number
@ -43,7 +44,7 @@
let tagInput = $state('') let tagInput = $state('')
// Ref to the editor component // Ref to the editor component
let editorRef: any let editorRef: { save: () => Promise<JSONContent> } | undefined
// Draft backup // Draft backup
const draftKey = $derived(makeDraftKey('post', postId ?? 'new')) const draftKey = $derived(makeDraftKey('post', postId ?? 'new'))
@ -80,8 +81,8 @@ let autoSave = mode === 'edit' && postId
if (!response.ok) throw new Error('Failed to save') if (!response.ok) throw new Error('Failed to save')
return await response.json() return await response.json()
}, },
onSaved: (saved: any, { prime }) => { onSaved: (saved: Post, { prime }) => {
updatedAt = saved.updatedAt updatedAt = saved.updatedAt.toISOString()
prime(buildPayload()) prime(buildPayload())
if (draftKey) clearDraft(draftKey) if (draftKey) clearDraft(draftKey)
} }
@ -144,7 +145,7 @@ $effect(() => {
// Show restore prompt if a draft exists // Show restore prompt if a draft exists
$effect(() => { $effect(() => {
const draft = loadDraft<any>(draftKey) const draft = loadDraft<ReturnType<typeof buildPayload>>(draftKey)
if (draft) { if (draft) {
showDraftPrompt = true showDraftPrompt = true
draftTimestamp = draft.ts draftTimestamp = draft.ts
@ -152,7 +153,7 @@ $effect(() => {
}) })
function restoreDraft() { function restoreDraft() {
const draft = loadDraft<any>(draftKey) const draft = loadDraft<ReturnType<typeof buildPayload>>(draftKey)
if (!draft) return if (!draft) return
const p = draft.payload const p = draft.payload
title = p.title ?? title title = p.title ?? title

View file

@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'
interface Props { interface Props {
label: string label: string
name?: string name?: string
type?: string type?: string
value?: any value?: string | number
placeholder?: string placeholder?: string
required?: boolean required?: boolean
error?: string error?: string
helpText?: string helpText?: string
disabled?: boolean disabled?: boolean
onchange?: (e: Event) => void onchange?: (e: Event) => void
children?: any children?: Snippet
} }
let { let {

View file

@ -42,7 +42,7 @@
content: [{ type: 'paragraph' }] content: [{ type: 'paragraph' }]
} }
let characterCount = 0 let characterCount = 0
let editorInstance: any let editorInstance: { save: () => Promise<JSONContent>; clear: () => void } | undefined
// Essay metadata // Essay metadata
let essayTitle = '' let essayTitle = ''

View file

@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import GenericMetadataPopover, { type MetadataConfig } from './GenericMetadataPopover.svelte' import GenericMetadataPopover, { type MetadataConfig } from './GenericMetadataPopover.svelte'
import type { Post } from '@prisma/client'
type Props = { type Props = {
post: any post: Post
postType: 'post' | 'essay' postType: 'post' | 'essay'
slug: string slug: string
tags: string[] tags: string[]
@ -12,7 +13,7 @@
onRemoveTag: (tag: string) => void onRemoveTag: (tag: string) => void
onDelete: () => void onDelete: () => void
onClose?: () => void onClose?: () => void
onFieldUpdate?: (key: string, value: any) => void onFieldUpdate?: (key: string, value: unknown) => void
} }
let { let {
@ -29,11 +30,11 @@
onFieldUpdate onFieldUpdate
}: Props = $props() }: Props = $props()
function handleFieldUpdate(key: string, value: any) { function handleFieldUpdate(key: string, value: unknown) {
if (key === 'slug') { if (key === 'slug' && typeof value === 'string') {
slug = value slug = value
onFieldUpdate?.(key, value) onFieldUpdate?.(key, value)
} else if (key === 'tagInput') { } else if (key === 'tagInput' && typeof value === 'string') {
tagInput = value tagInput = value
} }
} }

View file

@ -17,6 +17,7 @@
import { useFormGuards } from '$lib/admin/useFormGuards.svelte' import { useFormGuards } from '$lib/admin/useFormGuards.svelte'
import { makeDraftKey, saveDraft, clearDraft } from '$lib/admin/draftStore' import { makeDraftKey, saveDraft, clearDraft } from '$lib/admin/draftStore'
import type { ProjectFormData } from '$lib/types/project' import type { ProjectFormData } from '$lib/types/project'
import type { JSONContent } from '@tiptap/core'
interface Props { interface Props {
project?: Project | null project?: Project | null
@ -37,7 +38,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: any let editorRef: { save: () => Promise<JSONContent> } | undefined
// 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)
@ -50,7 +51,7 @@
save: async (payload, { signal }) => { save: async (payload, { signal }) => {
return await api.put(`/api/projects/${project?.id}`, payload, { signal }) return await api.put(`/api/projects/${project?.id}`, payload, { signal })
}, },
onSaved: (savedProject: any, { prime }) => { onSaved: (savedProject: Project, { prime }) => {
project = savedProject project = savedProject
formStore.populateFromProject(savedProject) formStore.populateFromProject(savedProject)
prime(formStore.buildPayload()) prime(formStore.buildPayload())
@ -112,7 +113,7 @@
} }
}) })
function handleEditorChange(content: any) { function handleEditorChange(content: JSONContent) {
formStore.setField('caseStudyContent', content) formStore.setField('caseStudyContent', content)
} }
@ -158,7 +159,7 @@
} }
} catch (err) { } catch (err) {
toast.dismiss(loadingToastId) toast.dismiss(loadingToastId)
if ((err as any)?.status === 409) { if (err && typeof err === 'object' && 'status' in err && (err as { status: number }).status === 409) {
toast.error('This project has changed in another tab. Please reload.') toast.error('This project has changed in another tab. Please reload.')
} else { } else {
toast.error(`Failed to ${mode === 'edit' ? 'save' : 'create'} project`) toast.error(`Failed to ${mode === 'edit' ? 'save' : 'create'} project`)

View file

@ -1,4 +1,4 @@
import type { Editor } from '@tiptap/core' import type { Editor, EditorView } from '@tiptap/core'
import type { ComposerMediaHandler } from './ComposerMediaHandler.svelte' import type { ComposerMediaHandler } from './ComposerMediaHandler.svelte'
import { focusEditor } from '$lib/components/edra/utils' import { focusEditor } from '$lib/components/edra/utils'
@ -12,7 +12,7 @@ export interface UseComposerEventsOptions {
export function useComposerEvents(options: UseComposerEventsOptions) { export function useComposerEvents(options: UseComposerEventsOptions) {
// Handle paste events // Handle paste events
function handlePaste(view: any, event: ClipboardEvent): boolean { function handlePaste(view: EditorView, event: ClipboardEvent): boolean {
const clipboardData = event.clipboardData const clipboardData = event.clipboardData
if (!clipboardData) return false if (!clipboardData) return false
@ -30,7 +30,7 @@ export function useComposerEvents(options: UseComposerEventsOptions) {
event.preventDefault() event.preventDefault()
// Use editor commands to insert HTML content // Use editor commands to insert HTML content
const editorInstance = (view as any).editor const editorInstance = options.editor
if (editorInstance) { if (editorInstance) {
editorInstance editorInstance
.chain() .chain()
@ -66,7 +66,7 @@ export function useComposerEvents(options: UseComposerEventsOptions) {
} }
// Handle drag and drop for images // Handle drag and drop for images
function handleDrop(view: any, event: DragEvent): boolean { function handleDrop(view: EditorView, event: DragEvent): boolean {
if (!options.features.imageUpload || !options.mediaHandler) return false if (!options.features.imageUpload || !options.mediaHandler) return false
const files = event.dataTransfer?.files const files = event.dataTransfer?.files

View file

@ -189,7 +189,7 @@
} }
// Block manipulation functions // Block manipulation functions
function convertBlockType(type: string, attrs?: any) { function convertBlockType(type: string, attrs?: Record<string, unknown>) {
console.log('convertBlockType called:', type, attrs) console.log('convertBlockType called:', type, attrs)
// Use menuNode which was captured when menu was opened // Use menuNode which was captured when menu was opened
const nodeToConvert = menuNode || currentNode const nodeToConvert = menuNode || currentNode
@ -486,10 +486,11 @@
// Find the existing drag handle created by the plugin and add click listener // Find the existing drag handle created by the plugin and add click listener
const checkForDragHandle = setInterval(() => { const checkForDragHandle = setInterval(() => {
const existingDragHandle = document.querySelector('.drag-handle') const existingDragHandle = document.querySelector('.drag-handle')
if (existingDragHandle && !(existingDragHandle as any).__menuListener) { const element = existingDragHandle as HTMLElement & { __menuListener?: boolean }
if (existingDragHandle && !element.__menuListener) {
console.log('Found drag handle, adding click listener') console.log('Found drag handle, adding click listener')
existingDragHandle.addEventListener('click', handleMenuClick) existingDragHandle.addEventListener('click', handleMenuClick)
;(existingDragHandle as any).__menuListener = true element.__menuListener = true
// Update our reference to use the existing drag handle // Update our reference to use the existing drag handle
dragHandleContainer = existingDragHandle as HTMLElement dragHandleContainer = existingDragHandle as HTMLElement

View file

@ -7,9 +7,9 @@
let { node, updateAttributes }: Props = $props() let { node, updateAttributes }: Props = $props()
let mapContainer: HTMLDivElement let mapContainer: HTMLDivElement
let map: any let map: L.Map | null = null
let marker: any let marker: L.Marker | null = null
let leaflet: any let leaflet: typeof L | null = null
let isEditing = $state(false) let isEditing = $state(false)
// Extract attributes // Extract attributes

View file

@ -19,9 +19,9 @@
// Map picker state // Map picker state
let showMapPicker = $state(false) let showMapPicker = $state(false)
let mapContainer: HTMLDivElement let mapContainer: HTMLDivElement
let pickerMap: any let pickerMap: L.Map | null = null
let pickerMarker: any let pickerMarker: L.Marker | null = null
let leaflet: any let leaflet: typeof L | null = null
// Load Leaflet for map picker // Load Leaflet for map picker
async function loadLeaflet() { async function loadLeaflet() {
@ -77,15 +77,15 @@
.addTo(pickerMap) .addTo(pickerMap)
// Update coordinates on marker drag // Update coordinates on marker drag
pickerMarker.on('dragend', (e: any) => { pickerMarker.on('dragend', (e: L.LeafletEvent) => {
const position = e.target.getLatLng() const position = (e.target as L.Marker).getLatLng()
latitude = position.lat.toFixed(6) latitude = position.lat.toFixed(6)
longitude = position.lng.toFixed(6) longitude = position.lng.toFixed(6)
}) })
// Update marker on map click // Update marker on map click
pickerMap.on('click', (e: any) => { pickerMap.on('click', (e: L.LeafletMouseEvent) => {
pickerMarker.setLatLng(e.latlng) pickerMarker!.setLatLng(e.latlng)
latitude = e.latlng.lat.toFixed(6) latitude = e.latlng.lat.toFixed(6)
longitude = e.latlng.lng.toFixed(6) longitude = e.latlng.lng.toFixed(6)
}) })