TownieInputBox.tsx46 matches
2import { useRef, useState, useEffect } from "https://esm.sh/react@18.2.0?dev";
3import { PlusIcon, ArrowUpIcon, Square, XIcon } from "./icons.tsx";
4import { processFiles } from "../utils/images.ts";
56export function InputBox ({
11running,
12error,
13images,
14setImages,
15} : {
16value: string;
20running: boolean;
21error: any;
22images: (string|null)[];
23setImages: (images: (string|null)[]) => void;
24}) {
25const form = useRef(null);
57autoFocus={true}
58/>
59<ImageRow images={images} setImages={setImages} />
60<div className="toolbar">
61<UploadButton
62disabled={running}
63images={images}
64setImages={setImages}
65/>
66<div className="spacer" />
88}
8990export function ImageDropContainer ({
91images,
92setImages,
93running,
94children,
95}: {
96images: (string|null)[];
97setImages: (images: (string|null)[]) => void;
98running: boolean;
99children: React.ReactNode;
100}) {
101const dragging = useImageDrop({ images, setImages, running });
102103return (
105{children}
106{dragging && (
107<div className="image-drop-overlay">
108<div className="image-drop-inner">
109Drop images here to upload
110</div>
111</div>
115}
116117export function useImageDrop ({ images, setImages, running }: {
118images: (string|null)[];
119setImages(images: (string|null)[]) => void;
120running: boolean;
121}) {
143setDragging(false);
144if (e.dataTransfer?.files && !running) {
145processFiles(Array.from(e.dataTransfer.files), images, setImages);
146}
147}
164}
165166function ImageRow ({ images, setImages }: {
167images: (string|null)[];
168setImages: (images: (string|null)[]) => void;
169}) {
170return (
171<div className="image-row">
172{images.map((image, i) => (
173<Thumbnail
174key={i}
175image={image}
176onRemove={() => {
177setImages([
178...images.slice(0, i),
179...images.slice(i + 1),
180]);
181}}
186}
187188function Thumbnail ({ image, onRemove }: {
189image: string|null;
190onRemove: () => void;
191}) {
192if (!image) return null;
193194return (
195<div className="input-image">
196<img
197src={image}
198alt="User uploaded image"
199className="image-thumbnail"
200/>
201<button
202type="button"
203title="Remove image"
204className="remove-image-button"
205onClick={onRemove}
206>
212213function UploadButton ({
214images,
215setImages,
216disabled,
217}: {
218images: (string|null)[];
219setImages: (images: (string|null)[]) => void;
220disabled: boolean;
221}) {
226<button
227type="button"
228title="Upload image"
229disabled={disabled}
230onClick={e => {
234<PlusIcon />
235<div className="sr-only">
236Upload image
237</div>
238</button>
243onChange={e => {
244if (e.target.files) {
245processFiles(Array.from(e.target.files), images, setImages);
246}
247}}
12export const PROMPT_IMAGE_LIMIT = 5;
34export const processFiles = async (files: File[], images: (string | null)[], setImages: (images: (string | null)[]) => void) => {
5const imageFiles = files.filter(file => file.type.startsWith('image/'));
6const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
78if (filesToProcess.length === 0) return;
910const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
11setImages(newImages);
1213const processedImages = await Promise.all(
14filesToProcess.map(async (file) => {
15return await readFileAsDataURL(file);
17);
1819const updatedImages = [...images];
20processedImages.forEach((dataUrl, index) => {
21updatedImages[images.length + index] = dataUrl;
22});
2324setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
25};
2630reader.onload = () => {
31const result = reader.result as string;
32console.log("Image loaded, size:", result.length, "bytes");
33resolve(result);
34};
Towniefavicon.http.tsx1 match
13return new Response(svg, {
14headers: {
15"Content-Type": "image/svg+xml",
16},
17});
TownieChatRoute.tsx13 matches
10import { useUsageStats } from "../hooks/useUsageStats.ts";
11import { Messages } from "./Messages.tsx";
12import { InputBox, ImageDropContainer } from "./InputBox.tsx";
13import { PreviewFrame } from "./PreviewFrame.tsx";
14import { BranchSelect } from "./BranchSelect.tsx";
64}) {
65const { token, anthropicApiKey } = useAuth();
66const [images, setImages] = useState<(string|null)[]>([]);
67const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
68const { audio } = useContext(AppContext);
84bearerToken: token,
85selectedFiles,
86images,
87soundEnabled: audio,
88});
108109return (
110<ImageDropContainer
111running={running}
112images={images}
113setImages={setImages}>
114<div
115className="chat-container container">
131onSubmit={e => {
132handleSubmit(e);
133setImages([]);
134}}
135onCancel={handleStop}
136running={running}
137error={error}
138images={images}
139setImages={setImages}
140/>
141</div>
149rel="norefferer"
150className="block-link text-link lockup">
151{project.imageUrl ? (
152<img src={project.imageUrl} className="image-thumbnail" />
153) : (
154<div className="image-placeholder" />
155)}
156<div>
173</div>
174<pre hidden>{JSON.stringify(messages, null, 2)}</pre>
175</ImageDropContainer>
176);
177}
adportalcontinue.html149 matches
44cursor: not-allowed;
45}
46/* Image upload styles */
47#imagePreviewContainer {
48position: relative;
49display: inline-block;
50margin-top: 0.5rem;
51}
52#imagePreview {
53border: 1px solid #e5e7eb;
54border-radius: 0.375rem;
58line-height: 1;
59}
60#imagesList img {
61transition: transform 0.2s ease-in-out;
62}
63#imagesList img:hover {
64transform: scale(1.05);
65}
66.image-card {
67box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
68transition: box-shadow 0.2s ease-in-out;
69}
70.image-card:hover {
71box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
72}
136
137<div class="mt-6 border-t pt-6">
138<label class="block text-lg font-medium text-gray-800 mb-2">Upload Images</label>
139
140<div id="imageUploadLog" class="mb-4 p-3 bg-gray-50 border border-gray-200 rounded-md text-sm max-h-32 overflow-y-auto">
141<div class="text-gray-500">Image upload log:</div>
142</div>
143
144<div class="mb-4">
145<div class="flex items-center space-x-2">
146<label for="imageUpload" class="px-4 py-2 bg-blue-100 hover:bg-blue-200 rounded-md cursor-pointer text-sm font-medium border border-blue-300">
147Select Image
148</label>
149<span id="selectedFileName" class="text-sm text-gray-500">No file selected</span>
151<input
152type="file"
153id="imageUpload"
154accept="image/*"
155class="hidden"
156>
157</div>
158
159<div id="imagePreviewContainer" class="hidden mb-4">
160<div class="relative">
161<img id="imagePreview" class="max-w-full h-auto max-h-64 rounded-md border-2 border-blue-300" src="" alt="Preview">
162<button id="cancelUpload" class="absolute top-2 right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center">×</button>
163</div>
166<button
167type="button"
168id="uploadImageButton"
169class="px-4 py-2 bg-blue-500 text-white rounded-md disabled:bg-blue-300 disabled:cursor-not-allowed"
170disabled
171>
172Upload Image
173</button>
174
176</div>
177
178<div id="imagesContainer" class="mt-6 mb-6 p-4 bg-gray-50 border border-gray-200 rounded-md">
179<div class="flex justify-between items-center mb-3">
180<h3 class="text-lg font-medium text-gray-800">Image Gallery</h3>
181<div class="flex items-center space-x-2">
182<span id="imageCount" class="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-sm">0 images</span>
183<button id="refreshImages" class="px-2 py-1 bg-green-100 text-green-800 rounded-md text-sm">Refresh</button>
184<button id="testBlobStorage" class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded-md text-sm">Test Storage</button>
185</div>
186</div>
187<div id="noImagesMessage" class="text-gray-500 text-center py-4">No images uploaded yet</div>
188<div id="imagesList" class="grid grid-cols-2 gap-4"></div>
189</div>
190
217const submissionId = document.getElementById('submissionId').value;
218
219// Image upload elements
220const imageUploadInput = document.getElementById('imageUpload');
221const selectedFileName = document.getElementById('selectedFileName');
222const imagePreviewContainer = document.getElementById('imagePreviewContainer');
223const imagePreview = document.getElementById('imagePreview');
224const cancelUploadButton = document.getElementById('cancelUpload');
225const uploadImageButton = document.getElementById('uploadImageButton');
226const uploadStatus = document.getElementById('uploadStatus');
227const imagesContainer = document.getElementById('imagesContainer');
228const imagesList = document.getElementById('imagesList');
229const imageCount = document.getElementById('imageCount');
230const noImagesMessage = document.getElementById('noImagesMessage');
231const imageUploadLog = document.getElementById('imageUploadLog');
232
233// Helper function to log to UI
237logEntry.className = 'text-xs text-gray-700 mb-1';
238logEntry.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
239imageUploadLog.appendChild(logEntry);
240imageUploadLog.scrollTop = imageUploadLog.scrollHeight;
241}
242
251logToUI(`Page loaded for submission ID: ${submissionId}`);
252
253// Load existing images
254loadImages();
255
256// Add refresh button handler
257document.getElementById('refreshImages').addEventListener('click', () => {
258logToUI('Manual refresh requested');
259loadImages();
260});
261
274logToUI(`Test key worked: ${result.data.testKeyWorked}`);
275
276if (result.data.imageKeys && result.data.imageKeys.length > 0) {
277logToUI(`Found ${result.data.imageKeys.length} image keys:`);
278result.data.imageKeys.forEach(key => {
279logToUI(`- ${key}`);
280});
281} else {
282logToUI('No image keys found');
283}
284}
290
291// Handle file selection
292imageUploadInput.addEventListener('change', (e) => {
293const file = e.target.files[0];
294if (!file) {
295logToUI('File selection canceled');
296resetImageUpload();
297return;
298}
300logToUI(`File selected: ${file.name} (${file.type}, ${formatFileSize(file.size)})`);
301selectedFileName.textContent = file.name;
302uploadImageButton.disabled = false;
303
304// Show image preview
305const reader = new FileReader();
306reader.onload = (e) => {
307imagePreview.src = e.target.result;
308imagePreviewContainer.classList.remove('hidden');
309logToUI('Preview generated');
310};
315cancelUploadButton.addEventListener('click', () => {
316logToUI('Upload canceled by user');
317resetImageUpload();
318});
319
320// Upload image
321uploadImageButton.addEventListener('click', async () => {
322const file = imageUploadInput.files[0];
323if (!file) return;
324
325uploadImageButton.disabled = true;
326uploadStatus.textContent = 'Uploading...';
327uploadStatus.className = 'mt-2 text-sm text-blue-500';
337logToUI(`Sending file to server...`);
338
339const response = await fetch('/upload-image', {
340method: 'POST',
341body: formData
346
347if (response.ok && result.success) {
348logToUI(`Upload successful! Image ID: ${result.data.id}`);
349
350// Log debug info if available
351if (result.debug) {
352logToUI(`Debug info: Found ${result.debug.imagesFound} images`);
353if (result.debug.imageIds && result.debug.imageIds.length > 0) {
354result.debug.imageIds.forEach(id => {
355logToUI(`Debug: Image ID found: ${id}`);
356});
357}
358}
359
360uploadStatus.textContent = 'Image uploaded successfully!';
361uploadStatus.className = 'mt-2 text-sm text-green-500';
362resetImageUpload();
363loadImages(); // Refresh the images list
364} else {
365logToUI(`Upload failed: ${result.message || 'Unknown error'}`);
367logToUI(`Error details: ${result.error}`);
368}
369uploadStatus.textContent = result.message || 'Failed to upload image';
370uploadStatus.className = 'mt-2 text-sm text-red-500';
371uploadImageButton.disabled = false;
372}
373} catch (error) {
374console.error('Error uploading image:', error);
375logToUI(`Upload error: ${error.message}`);
376uploadStatus.textContent = 'An error occurred while uploading';
377uploadStatus.className = 'mt-2 text-sm text-red-500';
378uploadImageButton.disabled = false;
379}
380});
381
382// Reset image upload form
383function resetImageUpload() {
384imageUploadInput.value = '';
385selectedFileName.textContent = 'No file selected';
386imagePreviewContainer.classList.add('hidden');
387uploadImageButton.disabled = true;
388uploadStatus.textContent = '';
389logToUI('Upload form reset');
390}
391
392// Load images for this submission
393async function loadImages() {
394logToUI(`Loading images for submission: ${submissionId}`);
395try {
396logToUI(`Fetching from /list-images?submissionId=${submissionId}`);
397const response = await fetch(`/list-images?submissionId=${submissionId}`);
398
399if (!response.ok) {
400logToUI(`Error response: ${response.status} ${response.statusText}`);
401imagesContainer.classList.remove('hidden');
402noImagesMessage.classList.remove('hidden');
403noImagesMessage.textContent = `Error: ${response.status} ${response.statusText}`;
404return;
405}
409
410if (result.success) {
411const imageCount = result.data.length;
412logToUI(`Found ${imageCount} images`);
413
414if (result.data && Array.isArray(result.data)) {
415result.data.forEach((img, idx) => {
416logToUI(`Image ${idx + 1}: ID=${img.id}, Name=${img.originalName}, Type=${img.contentType}`);
417});
418}
419
420// Update image count display
421document.getElementById('imageCount').textContent = `${imageCount} image${imageCount !== 1 ? 's' : ''}`;
422
423if (imageCount > 0) {
424imagesContainer.classList.remove('hidden');
425noImagesMessage.classList.add('hidden');
426renderImages(result.data);
427} else {
428imagesContainer.classList.remove('hidden');
429noImagesMessage.classList.remove('hidden');
430imagesList.innerHTML = '';
431logToUI('No images found for this submission');
432}
433} else {
434logToUI(`Error in response: ${result.message || 'Unknown error'}`);
435imagesContainer.classList.remove('hidden');
436noImagesMessage.classList.remove('hidden');
437noImagesMessage.textContent = result.message || 'Error loading images';
438}
439} catch (error) {
440console.error('Error loading images:', error);
441logToUI(`Error loading images: ${error.message}`);
442imagesContainer.classList.remove('hidden');
443noImagesMessage.classList.remove('hidden');
444noImagesMessage.textContent = 'Error loading images';
445}
446}
447
448// Render images in the list
449function renderImages(images) {
450logToUI(`Rendering ${images.length} images in gallery`);
451imagesList.innerHTML = '';
452
453images.forEach((image, index) => {
454logToUI(`Rendering image ${index + 1}: ${image.originalName} (${image.id})`);
455
456const imageCard = document.createElement('div');
457imageCard.className = 'relative border rounded-md overflow-hidden image-card';
458imageCard.dataset.imageId = image.id;
459
460const img = document.createElement('img');
461img.src = `/image?id=${image.id}`;
462img.alt = image.originalName;
463img.className = 'w-full h-32 object-cover';
464img.onerror = () => {
465logToUI(`Failed to load image: ${image.id}`);
466img.src = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22286%22%20height%3D%22180%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20286%20180%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_17a3f093956%20text%20%7B%20fill%3A%23999%3Bfont-weight%3Anormal%3Bfont-family%3A-apple-system%2CBlinkMacSystemFont%2C%26quot%3BSegoe%20UI%26quot%3B%2CRoboto%2C%26quot%3BHelvetica%20Neue%26quot%3B%2CArial%2C%26quot%3BNoto%20Sans%26quot%3B%2Csans-serif%2C%26quot%3BApple%20Color%20Emoji%26quot%3B%2C%26quot%3BSegoe%20UI%20Emoji%26quot%3B%2C%26quot%3BSegoe%20UI%20Symbol%26quot%3B%2C%26quot%3BNoto%20Color%20Emoji%26quot%3B%2C%20monospace%3Bfont-size%3A14pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_17a3f093956%22%3E%3Crect%20width%3D%22286%22%20height%3D%22180%22%20fill%3D%22%23373940%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22108.5390625%22%20y%3D%2296.3%22%3EImage%20Error%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E';
467};
468img.onload = () => {
469logToUI(`Image loaded successfully: ${image.id}`);
470};
471
473deleteButton.className = 'absolute top-2 right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center';
474deleteButton.textContent = '×';
475deleteButton.addEventListener('click', () => deleteImage(image.id));
476
477const caption = document.createElement('div');
478caption.className = 'p-2 text-xs truncate';
479caption.textContent = image.originalName;
480
481const size = document.createElement('div');
482size.className = 'px-2 pb-2 text-xs text-gray-500';
483size.textContent = formatFileSize(image.size);
484
485imageCard.appendChild(img);
486imageCard.appendChild(deleteButton);
487imageCard.appendChild(caption);
488imageCard.appendChild(size);
489
490imagesList.appendChild(imageCard);
491});
492}
493
494// Delete an image
495async function deleteImage(imageId) {
496if (!confirm('Are you sure you want to delete this image?')) {
497logToUI(`Deletion canceled for image: ${imageId}`);
498return;
499}
500
501logToUI(`Deleting image: ${imageId}`);
502
503try {
504const response = await fetch('/image', {
505method: 'DELETE',
506headers: {
508},
509body: JSON.stringify({
510imageId,
511submissionId
512})
516
517if (response.ok && result.success) {
518logToUI(`Image deleted successfully: ${imageId}`);
519
520// Remove the image from the list
521const imageElement = imagesList.querySelector(`[data-image-id="${imageId}"]`);
522if (imageElement) {
523imageElement.remove();
524logToUI('Image removed from gallery');
525}
526
527// Update image count
528const remainingImages = imagesList.children.length;
529document.getElementById('imageCount').textContent = `${remainingImages} image${remainingImages !== 1 ? 's' : ''}`;
530
531// If no images left, show the no images message
532if (remainingImages === 0) {
533noImagesMessage.classList.remove('hidden');
534logToUI('No images remaining in gallery');
535}
536} else {
537logToUI(`Failed to delete image: ${result.message || 'Unknown error'}`);
538alert(result.message || 'Failed to delete image');
539}
540} catch (error) {
541console.error('Error deleting image:', error);
542logToUI(`Error deleting image: ${error.message}`);
543alert('An error occurred while deleting the image');
544}
545}
6import { generateUniqueId } from "./utils.ts";
7import {
8storeImage,
9getImage,
10getImageMetadata,
11listSubmissionImages,
12deleteImage,
13ImageMetadata,
14testBlobStorage
15} from "./image-storage.ts";
16import { blob } from "https://esm.town/v/std/blob";
17103
104// Remove id from the fields to update
105const { id: _, images: __, ...fieldsToUpdate } = body;
106
107// Update the submission in the database
119});
120121// Upload an image
122app.post("/upload-image", async (c) => {
123try {
124console.log("[SERVER] Received image upload request");
125
126// Parse the multipart form data
162console.log(`[SERVER] File read as Uint8Array, length: ${uint8Array.length} bytes`);
163
164// Store the image
165console.log(`[SERVER] Calling storeImage function`);
166const metadata = await storeImage(
167uint8Array,
168file.name,
171);
172
173console.log(`[SERVER] Image stored successfully with ID: ${metadata.id}`);
174
175// Immediately try to list images to verify
176console.log(`[SERVER] Verifying image listing immediately after upload`);
177const images = await listSubmissionImages(submissionId);
178console.log(`[SERVER] Verification found ${images.length} images`);
179
180return c.json({
181success: true,
182message: "Image uploaded successfully",
183data: metadata,
184debug: {
185imagesFound: images.length,
186imageIds: images.map(img => img.id)
187}
188});
189} catch (error) {
190console.error("[SERVER] Error uploading image:", error);
191return c.json({
192success: false,
193message: "An error occurred uploading your image",
194error: error.toString()
195}, 500);
197});
198199// Get an image by ID
200app.get("/image", async (c) => {
201try {
202const imageId = c.req.query("id");
203console.log(`[SERVER] Image request received for ID: ${imageId}`);
204
205if (!imageId) {
206console.log("[SERVER] Missing image ID in request");
207return c.json({ success: false, message: "Image ID is required" }, 400);
208}
209
210// Get image metadata
211const metadata = await getImageMetadata(imageId);
212if (!metadata) {
213console.log(`[SERVER] Metadata not found for image: ${imageId}`);
214return c.json({ success: false, message: "Image not found" }, 404);
215}
216
217console.log(`[SERVER] Metadata retrieved for image: ${imageId}`);
218console.log(`[SERVER] Image details: name=${metadata.originalName}, type=${metadata.contentType}, size=${metadata.size} bytes`);
219
220// Get the image data
221const imageData = await getImage(imageId);
222if (!imageData) {
223console.log(`[SERVER] Image data not found for ID: ${imageId}`);
224return c.json({ success: false, message: "Image data not found" }, 404);
225}
226
227console.log(`[SERVER] Image data retrieved successfully, size: ${imageData.length} bytes`);
228
229// Return the image with proper content type
230console.log(`[SERVER] Serving image with content type: ${metadata.contentType}`);
231return new Response(imageData, {
232headers: {
233"Content-Type": metadata.contentType,
236});
237} catch (error) {
238console.error("[SERVER] Error getting image:", error);
239return c.json({ success: false, message: "An error occurred getting the image" }, 500);
240}
241});
242243// List images for a submission
244app.get("/list-images", async (c) => {
245try {
246const submissionId = c.req.query("submissionId");
247console.log(`[SERVER] List images request for submission: ${submissionId}`);
248
249if (!submissionId) {
250console.log("[SERVER] Missing submission ID in list images request");
251return c.json({ success: false, message: "Submission ID is required" }, 400);
252}
253
254// Get all images for this submission
255const images = await listSubmissionImages(submissionId);
256console.log(`[SERVER] Found ${images.length} images for submission: ${submissionId}`);
257
258// Log details of each image
259images.forEach((img, index) => {
260console.log(`[SERVER] Image ${index + 1}: id=${img.id}, name=${img.originalName}, type=${img.contentType}, size=${img.size} bytes`);
261});
262
263return c.json({
264success: true,
265data: images
266});
267} catch (error) {
268console.error("[SERVER] Error listing images:", error);
269return c.json({
270success: false,
271message: "An error occurred listing images"
272}, 500);
273}
274});
275276// Delete an image
277app.delete("/image", async (c) => {
278try {
279const body = await c.req.json();
280const { imageId, submissionId } = body;
281console.log(`[SERVER] Delete image request: imageId=${imageId}, submissionId=${submissionId}`);
282
283if (!imageId || !submissionId) {
284console.log("[SERVER] Missing required parameters for image deletion");
285return c.json({
286success: false,
287message: "Image ID and Submission ID are required"
288}, 400);
289}
290
291// Get image metadata to verify it belongs to this submission
292const metadata = await getImageMetadata(imageId);
293if (!metadata) {
294console.log(`[SERVER] Metadata not found for image: ${imageId}`);
295return c.json({
296success: false,
297message: "Image not found"
298}, 404);
299}
300
301if (metadata.submissionId !== submissionId) {
302console.log(`[SERVER] Image ${imageId} does not belong to submission ${submissionId}`);
303console.log(`[SERVER] Image belongs to submission: ${metadata.submissionId}`);
304return c.json({
305success: false,
306message: "Image does not belong to this submission"
307}, 403);
308}
309
310console.log(`[SERVER] Verified image ${imageId} belongs to submission ${submissionId}`);
311
312// Delete the image
313const success = await deleteImage(imageId);
314
315if (!success) {
316console.log(`[SERVER] Failed to delete image: ${imageId}`);
317return c.json({ success: false, message: "Failed to delete image" }, 500);
318}
319
320console.log(`[SERVER] Successfully deleted image: ${imageId}`);
321
322return c.json({
323success: true,
324message: "Image deleted successfully"
325});
326} catch (error) {
327console.error("[SERVER] Error deleting image:", error);
328return c.json({
329success: false,
330message: "An error occurred deleting the image"
331}, 500);
332}
adportalimage-storage.ts75 matches
1import { blob } from "https://esm.town/v/std/blob";
23// Prefix for image keys in blob storage
4const IMAGE_PREFIX = "laura_mepson_image_";
56// Interface for image metadata
7export interface ImageMetadata {
8id: string;
9originalName: string;
14}
1516// Generate a unique ID for an image
17export function generateImageId(submissionId: string): string {
18const timestamp = Date.now().toString(36);
19const randomPart = Math.random().toString(36).substring(2, 8);
20return `${IMAGE_PREFIX}${submissionId}_${timestamp}${randomPart}`;
21}
2223// Store an image in blob storage
24export async function storeImage(
25imageData: Uint8Array, // Raw image data
26fileName: string,
27contentType: string,
28submissionId: string
29): Promise<ImageMetadata> {
30try {
31console.log(`[BLOB STORAGE] Storing image: ${fileName} (${contentType}) for submission: ${submissionId}`);
32console.log(`[BLOB STORAGE] Image size: ${imageData.length} bytes`);
33
34// Generate a unique ID for the image
35const imageId = generateImageId(submissionId);
36console.log(`[BLOB STORAGE] Generated image ID: ${imageId}`);
37
38// Store the image data directly
39await blob.set(imageId, imageData, { contentType });
40console.log(`[BLOB STORAGE] Image data stored successfully with ID: ${imageId}`);
41
42// Verify the image was stored
43const storedData = await blob.get(imageId);
44if (storedData) {
45console.log(`[BLOB STORAGE] Verified image data is stored (${storedData.length} bytes)`);
46} else {
47console.log(`[BLOB STORAGE] WARNING: Could not verify image data storage`);
48}
49
50// Create metadata
51const metadata: ImageMetadata = {
52id: imageId,
53originalName: fileName,
54contentType,
55size: imageData.length,
56uploadedAt: new Date().toISOString(),
57submissionId
60console.log(`[BLOB STORAGE] Storing metadata: ${JSON.stringify(metadata)}`);
61
62// Store metadata alongside the image
63await blob.setJSON(`${imageId}_metadata`, metadata);
64console.log(`[BLOB STORAGE] Image metadata stored successfully`);
65
66// Verify metadata was stored
67const storedMetadata = await blob.getJSON(`${imageId}_metadata`);
68if (storedMetadata) {
69console.log(`[BLOB STORAGE] Verified metadata is stored: ${JSON.stringify(storedMetadata)}`);
79return metadata;
80} catch (error) {
81console.error("[BLOB STORAGE] Error storing image:", error);
82throw error;
83}
84}
8586// Get an image from blob storage
87export async function getImage(imageId: string): Promise<Uint8Array | null> {
88try {
89console.log(`[BLOB STORAGE] Retrieving image with ID: ${imageId}`);
90const imageData = await blob.get(imageId);
91
92if (imageData) {
93console.log(`[BLOB STORAGE] Image retrieved successfully, size: ${imageData.length} bytes`);
94} else {
95console.log(`[BLOB STORAGE] Image not found with ID: ${imageId}`);
96}
97
98return imageData;
99} catch (error) {
100console.error(`[BLOB STORAGE] Error getting image ${imageId}:`, error);
101return null;
102}
103}
104105// Get image metadata
106export async function getImageMetadata(imageId: string): Promise<ImageMetadata | null> {
107try {
108console.log(`[BLOB STORAGE] Retrieving metadata for image: ${imageId}`);
109const metadata = await blob.getJSON(`${imageId}_metadata`);
110
111if (metadata) {
112console.log(`[BLOB STORAGE] Metadata retrieved successfully for image: ${imageId}`);
113} else {
114console.log(`[BLOB STORAGE] Metadata not found for image: ${imageId}`);
115}
116
117return metadata as ImageMetadata;
118} catch (error) {
119console.error(`[BLOB STORAGE] Error getting image metadata for ${imageId}:`, error);
120return null;
121}
122}
123124// List all images for a submission
125export async function listSubmissionImages(submissionId: string): Promise<ImageMetadata[]> {
126try {
127console.log(`[BLOB STORAGE] Listing images for submission: ${submissionId}`);
128console.log(`[BLOB STORAGE] Using prefix: ${IMAGE_PREFIX}${submissionId}_`);
129
130// First, list all keys to see what's in the blob storage
133allKeys.forEach(key => console.log(`[BLOB STORAGE] - ${key}`));
134
135// List all blobs with the image prefix for this submission
136const keys = await blob.list(`${IMAGE_PREFIX}${submissionId}_`);
137console.log(`[BLOB STORAGE] Found ${keys.length} keys for submission: ${submissionId}`);
138keys.forEach(key => console.log(`[BLOB STORAGE] - ${key}`));
139
140// Filter out metadata keys and get only image keys
141const imageKeys = keys.filter(key => !key.endsWith('_metadata'));
142console.log(`[BLOB STORAGE] Found ${imageKeys.length} image keys after filtering`);
143imageKeys.forEach(key => console.log(`[BLOB STORAGE] - Image key: ${key}`));
144
145// Get metadata for each image
146const metadataPromises = imageKeys.map(key => getImageMetadata(key));
147const metadataResults = await Promise.all(metadataPromises);
148
149// Filter out any null results
150const validMetadata = metadataResults.filter(metadata => metadata !== null) as ImageMetadata[];
151console.log(`[BLOB STORAGE] Retrieved ${validMetadata.length} valid metadata objects`);
152validMetadata.forEach(meta => console.log(`[BLOB STORAGE] - Metadata: ${meta.id}, ${meta.originalName}`));
154return validMetadata;
155} catch (error) {
156console.error(`[BLOB STORAGE] Error listing submission images for ${submissionId}:`, error);
157return [];
158}
159}
160161// Delete an image and its metadata
162export async function deleteImage(imageId: string): Promise<boolean> {
163try {
164console.log(`[BLOB STORAGE] Deleting image with ID: ${imageId}`);
165
166await blob.delete(imageId);
167console.log(`[BLOB STORAGE] Image deleted: ${imageId}`);
168
169await blob.delete(`${imageId}_metadata`);
170console.log(`[BLOB STORAGE] Image metadata deleted: ${imageId}_metadata`);
171
172return true;
173} catch (error) {
174console.error(`[BLOB STORAGE] Error deleting image ${imageId}:`, error);
175return false;
176}
218await blob.delete(testKey);
219
220// 6. List image keys for this submission
221console.log(`[BLOB STORAGE TEST] Listing image keys for submission: ${submissionId}`);
222const imageKeys = await blob.list(`${IMAGE_PREFIX}${submissionId}_`);
223console.log(`[BLOB STORAGE TEST] Found ${imageKeys.length} image keys`);
224
225return {
229totalKeys: allKeys.length,
230prefixedKeys: prefixedKeys.length,
231imageKeys: imageKeys,
232testKeyWorked: true
233}
blob_adminREADME.md1 match
3This is a lightweight Blob Admin interface to view and debug your Blob data.
45
67To use this, fork it to your account.
blob_adminmain.tsx2 matches
60const { ValTown } = await import("npm:@valtown/sdk");
61const vt = new ValTown();
62const { email: authorEmail, profileImageUrl, username } = await vt.me.profile.retrieve();
63// const authorEmail = me.email;
64128129c.set("email", email);
130c.set("profile", { profileImageUrl, username });
131await next();
132};
blob_adminapp.tsx3 matches
437{profile && (
438<div className="flex items-center space-x-4">
439<img src={profile.profileImageUrl} alt="Profile" className="w-8 h-8 rounded-full" />
440<span>{profile.username}</span>
441<a href="/auth/logout" className="text-blue-400 hover:text-blue-300">Logout</a>
580alt="Blob content"
581className="max-w-full h-auto"
582onError={() => console.error("Error loading image")}
583/>
584</div>
630<li>Create public shareable links for blobs</li>
631<li>View and manage public folder</li>
632<li>Preview images directly in the interface</li>
633</ul>
634</div>