fix(picture): resolve all TypeScript type errors

- Migrate stores from Database types to API types (camelCase)
- Fix snake_case to camelCase property access across components
- Remove deprecated userId params from API calls (now auth-based)
- Fix Konva type comparisons in BoardCanvas
- Fix async onMount return type issues
- Add emoji property to ThemeVariantDefinition type
- Fix Input autocomplete type to use AutoFill
- Remove invalid 'downloading' status checks

Reduces type errors from 120 to 0.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-29 09:17:55 +01:00
parent 8a4cc298f6
commit b9608bd5d2
16 changed files with 70 additions and 81 deletions

View file

@ -146,7 +146,8 @@
// Stage click to deselect
stage.on('click', (e) => {
console.log('[Canvas] Stage clicked, target:', e.target.getType());
if (e.target === stage || e.target === backgroundLayer) {
const targetType = e.target.getType();
if (targetType === 'Stage' || e.target.name() === 'background') {
console.log('[Canvas] Deselecting all');
deselectAll();
transformer.nodes([]);
@ -437,7 +438,7 @@
textarea.style.width = `${textNode.width() * textNode.scaleX()}px`;
textarea.style.fontSize = `${textNode.fontSize()}px`;
textarea.style.fontFamily = textNode.fontFamily();
textarea.style.color = textNode.fill();
textarea.style.color = String(textNode.fill());
textarea.style.border = '2px solid #4A90E2';
textarea.style.padding = '4px';
textarea.style.margin = '0px';

View file

@ -46,15 +46,12 @@
const hasSelection = $derived($selectedItemIds.length > 0);
async function handleExport() {
// Get the stage element
const stage = document.querySelector('.konvajs-content canvas') as HTMLCanvasElement;
if (!stage) return;
// Get the canvas element
const canvas = document.querySelector('.konvajs-content canvas') as HTMLCanvasElement;
if (!canvas) return;
// Create download link
const dataURL = stage.toDataURL({
pixelRatio: 2, // Retina quality
mimeType: 'image/png',
});
// Create download link using standard canvas API
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = `${boardName}.png`;

View file

@ -103,8 +103,6 @@
generationProgress.set(`Warte auf Verarbeitung... (${i + 1}/${totalImages})`);
} else if (progress.status === 'processing') {
generationProgress.set(`Verarbeite Bild ${i + 1}/${totalImages}...`);
} else if (progress.status === 'downloading') {
generationProgress.set(`Speichere Bild ${i + 1}/${totalImages}...`);
} else if (progress.status === 'completed') {
unsubscribe();
resolve();

View file

@ -68,8 +68,6 @@
generationProgress.set('Queued for processing...');
} else if (progress.status === 'processing') {
generationProgress.set('Processing...');
} else if (progress.status === 'downloading') {
generationProgress.set('Saving image...');
} else if (progress.status === 'completed') {
unsubscribe();
resolve();

View file

@ -13,7 +13,7 @@
import { images } from '$lib/stores/images';
import { archivedImages } from '$lib/stores/archive';
import { showToast } from '$lib/stores/toast';
import type { Database } from '@picture/shared/types';
import type { Tag } from '$lib/api/tags';
import {
DownloadSimple,
Link,
@ -26,21 +26,19 @@
Check,
} from '@manacore/shared-icons';
type TagType = Database['public']['Tables']['tags']['Row'];
let tagSubmenuElement = $state<HTMLElement | null>(null);
let imageTags = $state<TagType[]>([]);
let imageTags = $state<Tag[]>([]);
// Check if current image is archived
const isArchived = $derived(
$contextMenu.image?.archived_at !== null && $contextMenu.image?.archived_at !== undefined
$contextMenu.image?.archivedAt !== null && $contextMenu.image?.archivedAt !== undefined
);
// Check if current image is favorite
const isFavorite = $derived($contextMenu.image?.is_favorite === true);
const isFavorite = $derived($contextMenu.image?.isFavorite === true);
// Check if current image belongs to current user
const isOwnImage = $derived($contextMenu.image?.user_id === authStore.user?.id);
const isOwnImage = $derived($contextMenu.image?.userId === authStore.user?.id);
type IconName = 'download' | 'link' | 'heart' | 'tag' | 'archive' | 'restore' | 'trash';
@ -72,7 +70,7 @@
showTagSubmenu(rect.right, rect.top);
}
async function handleAddTag(tag: TagType) {
async function handleAddTag(tag: Tag) {
if (!$contextMenu.image) return;
try {
@ -85,7 +83,7 @@
}
}
async function handleRemoveTag(tag: TagType) {
async function handleRemoveTag(tag: Tag) {
if (!$contextMenu.image) return;
try {
@ -99,10 +97,10 @@
}
function handleDownload() {
if (!$contextMenu.image?.public_url) return;
if (!$contextMenu.image?.publicUrl) return;
const link = document.createElement('a');
link.href = $contextMenu.image.public_url;
link.href = $contextMenu.image.publicUrl;
link.download = $contextMenu.image.filename || 'image.png';
link.click();
hideContextMenu();
@ -110,9 +108,9 @@
}
function handleCopyLink() {
if (!$contextMenu.image?.public_url) return;
if (!$contextMenu.image?.publicUrl) return;
navigator.clipboard.writeText($contextMenu.image.public_url);
navigator.clipboard.writeText($contextMenu.image.publicUrl);
hideContextMenu();
showToast('Link kopiert', 'success');
}
@ -171,12 +169,12 @@
// Update in all stores
images.update((current) =>
current.map((img) =>
img.id === $contextMenu.image?.id ? { ...img, is_favorite: newFavoriteStatus } : img
img.id === $contextMenu.image?.id ? { ...img, isFavorite: newFavoriteStatus } : img
)
);
archivedImages.update((current) =>
current.map((img) =>
img.id === $contextMenu.image?.id ? { ...img, is_favorite: newFavoriteStatus } : img
img.id === $contextMenu.image?.id ? { ...img, isFavorite: newFavoriteStatus } : img
)
);

View file

@ -10,7 +10,7 @@
class?: string;
id?: string;
name?: string;
autocomplete?: string;
autocomplete?: AutoFill;
onchange?: (e: Event) => void;
oninput?: (e: Event) => void;
}
@ -26,7 +26,7 @@
class: className = '',
id = '',
name = '',
autocomplete = '',
autocomplete,
onchange,
oninput,
}: Props = $props();

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { viewMode, cycleViewMode, type ViewMode } from '$lib/stores/view';
import { List, SquaresFour, Squares } from '@manacore/shared-icons';
import { List, SquaresFour, Square } from '@manacore/shared-icons';
function getLabel(mode: ViewMode) {
switch (mode) {
@ -24,7 +24,7 @@
{:else if $viewMode === 'grid3'}
<SquaresFour size={20} />
{:else}
<Squares size={20} />
<Square size={20} />
{/if}
<span class="hidden sm:inline">{getLabel($viewMode)}</span>
</button>

View file

@ -14,7 +14,7 @@
let { children, data } = $props();
onMount(async () => {
onMount(() => {
// Initialize theme (applies CSS variables and loads from localStorage)
const cleanupTheme = theme.initialize();
@ -22,14 +22,14 @@
initPostHog();
// Initialize auth with Mana Core
await authStore.initialize();
// Identify user in PostHog if logged in
if (authStore.user) {
analytics.identify(authStore.user.id, {
email: authStore.user.email,
});
}
authStore.initialize().then(() => {
// Identify user in PostHog if logged in
if (authStore.user) {
analytics.identify(authStore.user.id, {
email: authStore.user.email,
});
}
});
return () => {
cleanupTheme();

View file

@ -6,16 +6,13 @@
hasMoreArchive,
currentArchivePage,
} from '$lib/stores/archive';
import { getImages } from '$lib/api/images';
import { getImages, type Image } from '$lib/api/images';
import ArchivedImageCard from '$lib/components/archive/ArchivedImageCard.svelte';
import ArchivedImageModal from '$lib/components/archive/ArchivedImageModal.svelte';
import ImageSkeleton from '$lib/components/ui/ImageSkeleton.svelte';
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import { Archive } from '@manacore/shared-icons';
import { onMount } from 'svelte';
import type { Database } from '@picture/shared/types';
type Image = Database['public']['Tables']['images']['Row'];
let loadingMore = $state(false);
let observer: IntersectionObserver | null = null;
@ -52,7 +49,7 @@
isLoadingArchive.set(true);
try {
const data = await getImages({ userId: authStore.user.id, page: 1, archived: true });
const data = await getImages({ page: 1, archived: true });
archivedImages.set(data);
currentArchivePage.set(1);
hasMoreArchive.set(data.length === 20);
@ -70,7 +67,7 @@
const nextPage = $currentArchivePage + 1;
try {
const newImages = await getImages({ userId: authStore.user.id, page: nextPage, archived: true });
const newImages = await getImages({ page: nextPage, archived: true });
if (newImages.length > 0) {
archivedImages.update((current) => [...current, ...newImages]);
currentArchivePage.set(nextPage);

View file

@ -32,9 +32,9 @@
let showDeleteModal = $state(false);
let deletingBoard = $state<string | null>(null);
onMount(async () => {
onMount(() => {
resetBoardsState();
await loadInitialBoards();
loadInitialBoards();
// Setup Intersection Observer for infinite scroll
observer = new IntersectionObserver(
@ -63,7 +63,7 @@
isLoadingBoards.set(true);
try {
const data = await getBoards({ userId: authStore.user.id, page: 1 });
const data = await getBoards({ page: 1 });
boards.set(data);
currentBoardsPage.set(1);
hasBoardsMore.set(data.length === 20);
@ -82,7 +82,7 @@
const nextPage = $currentBoardsPage + 1;
try {
const newBoards = await getBoards({ userId: authStore.user.id, page: nextPage });
const newBoards = await getBoards({ page: nextPage });
if (newBoards.length > 0) {
boards.update((current) => [...current, ...newBoards]);
currentBoardsPage.set(nextPage);
@ -104,11 +104,10 @@
try {
const { createBoard } = await import('$lib/api/boards');
const newBoard = await createBoard({
user_id: authStore.user.id,
name: boardName,
description: boardDescription || null,
description: boardDescription || undefined,
});
addBoard({ ...newBoard, item_count: 0 });
addBoard({ ...newBoard, itemCount: 0 });
showCreateBoardModal.set(false);
boardName = '';
boardDescription = '';
@ -140,8 +139,8 @@
if (!authStore.user) return;
try {
const newBoard = await duplicateBoard(boardId, authStore.user.id);
addBoard({ ...newBoard, item_count: 0 });
const newBoard = await duplicateBoard(boardId);
addBoard({ ...newBoard, itemCount: 0 });
showToast('Board dupliziert', 'success');
} catch (error) {
console.error('Error duplicating board:', error);
@ -214,11 +213,11 @@
<button
onclick={() => openBoard(board.id)}
class="block w-full overflow-hidden bg-gray-100 dark:bg-gray-700"
style="aspect-ratio: 4/3; background-color: {board.background_color || '#ffffff'}"
style="aspect-ratio: 4/3; background-color: {board.backgroundColor || '#ffffff'}"
>
{#if board.thumbnail_url}
{#if board.thumbnailUrl}
<img
src={board.thumbnail_url}
src={board.thumbnailUrl}
alt={board.name}
class="h-full w-full object-cover transition-transform group-hover:scale-105"
/>
@ -245,8 +244,8 @@
<div
class="mt-3 flex items-center justify-between text-sm text-gray-500 dark:text-gray-400"
>
<span>{board.item_count} {board.item_count === 1 ? 'Bild' : 'Bilder'}</span>
<span>{new Date(board.updated_at).toLocaleDateString('de-DE')}</span>
<span>{board.itemCount} {board.itemCount === 1 ? 'Bild' : 'Bilder'}</span>
<span>{new Date(board.updatedAt).toLocaleDateString('de-DE')}</span>
</div>
<!-- Actions -->

View file

@ -26,13 +26,13 @@
let showImagePicker = $state(false);
let isLoading = $state(true);
onMount(async () => {
onMount(() => {
if (!boardId) {
goto('/app/board');
return;
}
await loadBoard();
loadBoard();
return () => {
resetCanvasState();
@ -41,7 +41,7 @@
});
async function loadBoard() {
if (!authStore.user) return;
if (!authStore.user || !boardId) return;
isLoading = true;
try {
@ -49,7 +49,7 @@
const board = await getBoardById(boardId);
// Check if user has access
if (board.user_id !== authStore.user.id && !board.is_public) {
if (board.userId !== authStore.user.id && !board.isPublic) {
showToast('Zugriff verweigert', 'error');
goto('/app/board');
return;

View file

@ -18,11 +18,9 @@
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
import { MagnifyingGlass, X, Heart } from '@manacore/shared-icons';
import { onMount } from 'svelte';
import type { Database } from '@picture/shared/types';
import type { Image } from '$lib/api/images';
import type { ViewMode } from '$lib/stores/view';
type Image = Database['public']['Tables']['images']['Row'];
let loadingMore = $state(false);
let observer: IntersectionObserver | null = null;
let loadMoreTrigger = $state<HTMLElement | null>(null);

View file

@ -26,9 +26,10 @@
let observer: IntersectionObserver | null = null;
let loadMoreTrigger = $state<HTMLElement | null>(null);
onMount(async () => {
await loadTags();
loadInitialImages();
onMount(() => {
loadTags().then(() => {
loadInitialImages();
});
// Setup Intersection Observer for infinite scroll
observer = new IntersectionObserver(
@ -74,7 +75,6 @@
isLoading.set(true);
try {
const data = await getImages({
userId: authStore.user.id,
page: 1,
tagIds: $selectedTags.length > 0 ? $selectedTags : undefined,
favoritesOnly: $showFavoritesOnly,
@ -97,7 +97,6 @@
try {
const newImages = await getImages({
userId: authStore.user.id,
page: nextPage,
tagIds: $selectedTags.length > 0 ? $selectedTags : undefined,
favoritesOnly: $showFavoritesOnly,

View file

@ -1,16 +1,13 @@
<script lang="ts">
import { onMount } from 'svelte';
import { tags, isLoadingTags } from '$lib/stores/tags';
import { getAllTags, createTag, updateTag, deleteTag } from '$lib/api/tags';
import { getAllTags, createTag, updateTag, deleteTag, type Tag } from '$lib/api/tags';
import { showToast } from '$lib/stores/toast';
import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons';
import type { Database } from '@picture/shared/types';
type TagType = Database['public']['Tables']['tags']['Row'];
let showCreateModal = $state(false);
let showEditModal = $state(false);
let editingTag = $state<TagType | null>(null);
let editingTag = $state<Tag | null>(null);
let newTagName = $state('');
let newTagColor = $state('#3B82F6');
let editTagName = $state('');
@ -63,7 +60,7 @@
}
}
function openEditModal(tag: TagType) {
function openEditModal(tag: Tag) {
editingTag = tag;
editTagName = tag.name;
editTagColor = tag.color || '#3B82F6';
@ -158,7 +155,7 @@
<div>
<h3 class="font-medium text-gray-900 dark:text-gray-100">{tag.name}</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{tag.created_at ? new Date(tag.created_at).toLocaleDateString('de-DE') : ''}
{tag.createdAt ? new Date(tag.createdAt).toLocaleDateString('de-DE') : ''}
</p>
</div>
</div>

View file

@ -202,6 +202,7 @@ export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
name: 'lume',
label: 'Lume',
emoji: '✨',
icon: 'sparkle',
hue: 47,
light: lumeLight,
dark: lumeDark,
@ -210,6 +211,7 @@ export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
name: 'nature',
label: 'Nature',
emoji: '🌿',
icon: 'leaf',
hue: 122,
light: natureLight,
dark: natureDark,
@ -218,6 +220,7 @@ export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
name: 'stone',
label: 'Stone',
emoji: '🪨',
icon: 'hexagon',
hue: 200,
light: stoneLight,
dark: stoneDark,
@ -226,6 +229,7 @@ export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
name: 'ocean',
label: 'Ocean',
emoji: '🌊',
icon: 'waves',
hue: 199,
light: oceanLight,
dark: oceanDark,

View file

@ -77,7 +77,10 @@ export interface ThemeColors {
export interface ThemeVariantDefinition {
name: string;
label: string;
/** Emoji representation of the theme */
emoji: string;
/** Icon name for the theme (e.g., 'sparkle', 'leaf', 'hexagon', 'waves') */
icon: string;
/** The primary hue for this variant (used for accent colors) */
hue: number;
light: ThemeColors;