fix part 1

This commit is contained in:
Danilo Reyes
2025-11-02 18:09:07 -06:00
parent ce353f8b49
commit 376ac1dec9
8 changed files with 637 additions and 161 deletions

View File

@@ -9,32 +9,14 @@ import type { Image, BoardImage, ImageListResponse } from '$lib/types/images';
* Upload a single image
*/
export async function uploadImage(file: File): Promise<Image> {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post<Image>('/images/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response;
return await apiClient.uploadFile<Image>('/images/upload', file);
}
/**
* Upload multiple images from a ZIP file
*/
export async function uploadZip(file: File): Promise<Image[]> {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post<Image[]>('/images/upload-zip', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response;
return await apiClient.uploadFile<Image[]>('/images/upload-zip', file);
}
/**

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
onMount(() => {
// Client-side redirect based on auth token
if (browser) {
const token = localStorage.getItem('auth_token');
if (token) {
goto('/boards');
} else {
goto('/login');
}
}
});
</script>
<svelte:head>
<title>Reference Board Viewer</title>
</svelte:head>
<div class="loading-container">
<div class="spinner"></div>
<p>Loading...</p>
</div>
<style>
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
gap: 1rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
p {
color: #6b7280;
font-size: 1rem;
}
</style>

View File

@@ -0,0 +1,2 @@
// Disable server-side rendering for the root page
export const ssr = false;

View File

@@ -0,0 +1,506 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { boards, currentBoard } from '$lib/stores/boards';
import {
boardImages,
loadBoardImages,
uploadSingleImage,
uploadZipFile,
addImageToBoard,
} from '$lib/stores/images';
let loading = true;
let error = '';
let uploadError = '';
let uploadSuccess = '';
let uploading = false;
let fileInput: HTMLInputElement;
$: boardId = $page.params.id;
onMount(async () => {
try {
await boards.loadBoard(boardId);
await loadBoardImages(boardId);
loading = false;
} catch (err: any) {
error = err.error || 'Failed to load board';
loading = false;
}
});
async function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement;
if (!target.files || target.files.length === 0) return;
await processFiles(Array.from(target.files));
// Reset input
if (target) target.value = '';
}
async function handleDrop(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
if (!event.dataTransfer?.files) return;
await processFiles(Array.from(event.dataTransfer.files));
}
function handleDragOver(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
}
async function processFiles(files: File[]) {
if (files.length === 0) return;
uploading = true;
uploadError = '';
uploadSuccess = '';
try {
let totalUploaded = 0;
for (const file of files) {
// Upload to library first
if (file.name.toLowerCase().endsWith('.zip')) {
const images = await uploadZipFile(file);
// Add each image to board
for (const img of images) {
await addImageToBoard(
boardId,
img.id,
{ x: Math.random() * 500, y: Math.random() * 500 },
0
);
}
totalUploaded += images.length;
} else if (file.type.startsWith('image/')) {
const image = await uploadSingleImage(file);
// Add to board
await addImageToBoard(
boardId,
image.id,
{ x: Math.random() * 500, y: Math.random() * 500 },
0
);
totalUploaded++;
}
}
// Reload board images to show new uploads
await loadBoardImages(boardId);
uploadSuccess = `Successfully uploaded ${totalUploaded} image(s)`;
setTimeout(() => {
uploadSuccess = '';
}, 3000);
} catch (err: any) {
uploadError = err.message || 'Upload failed';
} finally {
uploading = false;
}
}
function openFilePicker() {
fileInput.click();
}
function handleEditBoard() {
goto(`/boards/${boardId}/edit`);
}
function handleBackToBoards() {
goto('/boards');
}
</script>
<svelte:head>
<title>{$currentBoard?.title || 'Board'} - Reference Board Viewer</title>
</svelte:head>
<div
class="board-canvas-page"
on:drop={handleDrop}
on:dragover={handleDragOver}
role="presentation"
>
{#if loading}
<div class="loading">
<div class="spinner"></div>
<p>Loading board...</p>
</div>
{:else if error}
<div class="error-message">
<p>{error}</p>
<button on:click={handleBackToBoards}>Back to Boards</button>
</div>
{:else if $currentBoard}
<!-- Top Toolbar -->
<div class="toolbar-top">
<div class="toolbar-left">
<button class="btn-icon" on:click={handleBackToBoards} title="Back to boards">
← Boards
</button>
<div class="title-group">
<h1 class="board-title">{$currentBoard.title}</h1>
{#if $currentBoard.description}
<span class="board-desc">{$currentBoard.description}</span>
{/if}
</div>
</div>
<div class="toolbar-right">
<button
class="btn-upload"
on:click={openFilePicker}
disabled={uploading}
title="Upload images"
>
{#if uploading}
<span class="spinner-small"></span>
Uploading...
{:else}
📤 Upload Images
{/if}
</button>
<button class="btn-secondary" on:click={handleEditBoard} title="Edit board settings">
⚙️ Settings
</button>
</div>
</div>
<!-- Hidden file input -->
<input
bind:this={fileInput}
type="file"
accept="image/*,.zip"
multiple
on:change={handleFileSelect}
style="display: none;"
/>
{#if uploadSuccess}
<div class="success-banner">{uploadSuccess}</div>
{/if}
{#if uploadError}
<div class="error-banner">{uploadError}</div>
{/if}
<!-- Canvas Area -->
<div class="canvas-container">
{#if $boardImages.length === 0}
<div class="empty-state">
<div class="empty-icon">🖼️</div>
<h2>No images yet</h2>
<p>Click "Upload Images" or drag & drop files anywhere to get started</p>
<button class="btn-upload-large" on:click={openFilePicker}>
Upload Your First Image
</button>
</div>
{:else}
<div class="canvas-info">
<p>{$boardImages.length} image(s) on board</p>
<p class="hint">Pan: Drag canvas | Zoom: Mouse wheel | Drag images to move</p>
</div>
<!-- TODO: Render Konva canvas with images -->
<div class="temp-image-list">
{#each $boardImages as boardImage}
<div class="image-placeholder">
<p>{boardImage.image?.filename || 'Image'}</p>
<small>Position: ({boardImage.position.x}, {boardImage.position.y})</small>
</div>
{/each}
</div>
{/if}
</div>
<!-- Status Bar -->
<div class="status-bar">
<span>Board: {$currentBoard.title}</span>
<span>|</span>
<span>Images: {$boardImages.length}</span>
<span>|</span>
<span class="status-ready">● Ready</span>
</div>
{:else}
<div class="error-message">
<p>Board not found</p>
<button on:click={handleBackToBoards}>Back to Boards</button>
</div>
{/if}
</div>
<style>
.board-canvas-page {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background: #f3f4f6;
}
.loading,
.error-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
gap: 1rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.spinner-small {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Toolbar */
.toolbar-top {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.5rem;
background: white;
border-bottom: 1px solid #e5e7eb;
z-index: 100;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.title-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.toolbar-right {
display: flex;
gap: 0.75rem;
}
.board-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #111827;
}
.board-desc {
font-size: 0.875rem;
color: #6b7280;
}
.btn-icon,
.btn-upload,
.btn-secondary {
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
border: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-icon {
background: none;
border: 1px solid #d1d5db;
color: #374151;
}
.btn-icon:hover {
background: #f9fafb;
}
.btn-upload {
background: #10b981;
color: white;
}
.btn-upload:hover:not(:disabled) {
background: #059669;
}
.btn-upload:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-upload-large {
background: #3b82f6;
color: white;
padding: 0.75rem 2rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
border: none;
cursor: pointer;
}
.btn-upload-large:hover {
background: #2563eb;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.success-banner,
.error-banner {
padding: 0.75rem 1.5rem;
text-align: center;
font-size: 0.875rem;
font-weight: 500;
}
.success-banner {
background: #d1fae5;
color: #065f46;
border-bottom: 1px solid #a7f3d0;
}
.error-banner {
background: #fee2e2;
color: #991b1b;
border-bottom: 1px solid #fecaca;
}
/* Canvas Container */
.canvas-container {
flex: 1;
position: relative;
overflow: auto;
background: #ffffff;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 1.5rem;
}
.empty-icon {
font-size: 4rem;
}
.empty-state h2 {
margin: 0;
font-size: 1.5rem;
color: #111827;
}
.empty-state p {
margin: 0;
font-size: 1rem;
color: #6b7280;
max-width: 400px;
text-align: center;
}
.canvas-info {
padding: 1rem;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.canvas-info p {
margin: 0.25rem 0;
font-size: 0.875rem;
color: #6b7280;
}
.hint {
font-style: italic;
}
.temp-image-list {
padding: 2rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.image-placeholder {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
background: white;
}
.image-placeholder p {
margin: 0 0 0.5rem 0;
font-weight: 500;
color: #111827;
font-size: 0.875rem;
}
.image-placeholder small {
color: #6b7280;
font-size: 0.75rem;
}
/* Status Bar */
.status-bar {
display: flex;
gap: 0.5rem;
align-items: center;
padding: 0.5rem 1.5rem;
background: white;
border-top: 1px solid #e5e7eb;
font-size: 0.75rem;
color: #6b7280;
z-index: 100;
}
.status-ready {
color: #10b981;
font-weight: 500;
}
button {
font-family: inherit;
}
</style>

11
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
port: 5173,
strictPort: false,
},
});