From fa6600d5a98b67aad199d6f5aa72ac0952fe718e Mon Sep 17 00:00:00 2001 From: Adam Cameron Date: Thu, 27 Nov 2025 11:50:01 +0000 Subject: [PATCH 1/7] FL-53: Uploading files to Supabase --- README.md | 7 + pages/fl-53/index.html | 13 ++ src/fl-53/ImageManager.tsx | 133 +++++++++++++++++++ src/fl-53/main.tsx | 17 +++ src/{profiles => }/lib/ErrorBoundary.tsx | 0 src/{profiles => }/lib/supabase.ts | 0 src/{profiles => }/lib/testCredentials.ts | 0 src/main.tsx | 2 +- src/profiles/add/Form.tsx | 2 +- src/profiles/gallery/Gallery.tsx | 2 +- src/profiles/login/LoginForm.tsx | 4 +- tests/integration/profiles/gallery.test.tsx | 4 +- tests/unit/profiles/add/form.test.tsx | 2 +- tests/unit/profiles/gallery/gallery.test.tsx | 2 +- vite.config.ts | 1 + 15 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 pages/fl-53/index.html create mode 100644 src/fl-53/ImageManager.tsx create mode 100644 src/fl-53/main.tsx rename src/{profiles => }/lib/ErrorBoundary.tsx (100%) rename src/{profiles => }/lib/supabase.ts (100%) rename src/{profiles => }/lib/testCredentials.ts (100%) diff --git a/README.md b/README.md index 5295e93..8648757 100644 --- a/README.md +++ b/README.md @@ -98,3 +98,10 @@ VALUES ``` This lot can be added via the Supabase admin, or straight into the `public` schema of the `postgres` DB. + +Login for the admin app is just the defaults: + +``` +DASHBOARD_USERNAME=supabase +DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated +``` diff --git a/pages/fl-53/index.html b/pages/fl-53/index.html new file mode 100644 index 0000000..38de23e --- /dev/null +++ b/pages/fl-53/index.html @@ -0,0 +1,13 @@ + + + + + + + app + + +
+ + + diff --git a/src/fl-53/ImageManager.tsx b/src/fl-53/ImageManager.tsx new file mode 100644 index 0000000..e37bf0e --- /dev/null +++ b/src/fl-53/ImageManager.tsx @@ -0,0 +1,133 @@ +import React, { type FormEvent, useState } from 'react' +import { supabaseClient } from '../lib/supabase.ts' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +export default function ImageManager() { + const queryClient = useQueryClient() + const [selectedFile, setSelectedFile] = useState(null) + + const uploadMutation = useMutation({ + mutationFn: uploadFile, + onSuccess: () => { + console.log('File uploaded successfully') + void queryClient.invalidateQueries({ queryKey: ['ALL_IMAGES'] }) + void imageUrls.refetch() + }, + onError: (error) => { + console.error('Error uploading file:', error) + }, + }) + + const deleteMutation = useMutation({ + mutationFn: deleteImages, + onSuccess: () => { + console.log('All files deleted successfully') + void queryClient.invalidateQueries({ queryKey: ['ALL_IMAGES'] }) + void imageUrls.refetch() + }, + onError: (error) => { + console.error('Error deleting files:', error) + }, + }) + + const imageUrls = useQuery({ + queryKey: ['ALL_IMAGES'], + queryFn: fetchImageUrls, + }) + + const handleChange = (event: React.ChangeEvent) => { + setSelectedFile(event.target.files![0] || null) + } + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + if (selectedFile === null) { + return console.log("Ain't no file selected") + } + + uploadMutation.mutate(selectedFile) + event.currentTarget.reset() + } + + const handleDelete = (event: React.MouseEvent) => { + event.preventDefault() + deleteMutation.mutate() + } + + return ( +
+
+ + +
+ +
+
+ {imageUrls.data === undefined + ? 'nothing' + : imageUrls.data.map((imageUrl: string) => ( + <> + +
+ {getFilenameFromUrl(imageUrl)} +
+ + ))} +
+ ) +} + +async function uploadFile(file: File) { + const { data, error } = await supabaseClient.storage + .from('images') + .upload(`private/${file.name}`, file) + if (error === null) { + return Promise.resolve(data) + } + + return Promise.reject(error) +} + +async function fetchImageUrls(): Promise { + const { data, error } = await supabaseClient.storage + .from('images') + .list('private/') + if (error !== null) { + return Promise.reject(error) + } + + const urls = data + .filter((file) => file.name.endsWith('.png')) + .map(async (file) => { + const { data } = await supabaseClient.storage + .from('images') + .createSignedUrl(`private/${file.name}`, 60) + + return data === null ? '' : data.signedUrl + }) + return Promise.all(urls) +} + +async function deleteImages() { + const { data: files } = await supabaseClient.storage + .from('images') + .list('private/') + const paths = files!.map((file) => `private/${file.name}`) + + const { data, error } = await supabaseClient.storage + .from('images') + .remove(paths) + if (error !== null) { + return Promise.reject(error) + } + return Promise.resolve(data) +} + +function getFilenameFromUrl(url: string) { + return url.split('?')[0].split('/').pop() +} diff --git a/src/fl-53/main.tsx b/src/fl-53/main.tsx new file mode 100644 index 0000000..70fb4fd --- /dev/null +++ b/src/fl-53/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import ImageManager from './ImageManager.tsx' +import ErrorBoundary from '../lib/ErrorBoundary.tsx' + +const queryClient = new QueryClient() + +createRoot(document.getElementById('root')!).render( + + + + + + + +) diff --git a/src/profiles/lib/ErrorBoundary.tsx b/src/lib/ErrorBoundary.tsx similarity index 100% rename from src/profiles/lib/ErrorBoundary.tsx rename to src/lib/ErrorBoundary.tsx diff --git a/src/profiles/lib/supabase.ts b/src/lib/supabase.ts similarity index 100% rename from src/profiles/lib/supabase.ts rename to src/lib/supabase.ts diff --git a/src/profiles/lib/testCredentials.ts b/src/lib/testCredentials.ts similarity index 100% rename from src/profiles/lib/testCredentials.ts rename to src/lib/testCredentials.ts diff --git a/src/main.tsx b/src/main.tsx index 56913f5..d6ecf99 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,7 +2,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import ErrorBoundary from './profiles/lib/ErrorBoundary.tsx' +import ErrorBoundary from './lib/ErrorBoundary.tsx' import Home from './profiles/home/Home.tsx' import LoginForm from './profiles/login/LoginForm.tsx' import Gallery from './profiles/gallery/Gallery.tsx' diff --git a/src/profiles/add/Form.tsx b/src/profiles/add/Form.tsx index d1e717d..b8cf915 100644 --- a/src/profiles/add/Form.tsx +++ b/src/profiles/add/Form.tsx @@ -3,7 +3,7 @@ import { StatusCodes } from 'http-status-codes' import { useNavigate } from 'react-router-dom' import { useMutation, useQueryClient } from '@tanstack/react-query' import { type Mugshot, type UnsavedMugshot } from '../mugshot.tsx' -import { supabaseClient } from '../lib/supabase.ts' +import { supabaseClient } from '../../lib/supabase.ts' import './styles.css' diff --git a/src/profiles/gallery/Gallery.tsx b/src/profiles/gallery/Gallery.tsx index 1ea4f3e..0f71d3e 100644 --- a/src/profiles/gallery/Gallery.tsx +++ b/src/profiles/gallery/Gallery.tsx @@ -2,7 +2,7 @@ import { type ReactNode } from 'react' import { useQuery } from '@tanstack/react-query' import './gallery.css' import { type Mugshot } from '../mugshot.tsx' -import { supabaseClient } from '../lib/supabase.ts' +import { supabaseClient } from '../../lib/supabase.ts' import { StatusCodes } from 'http-status-codes' function Profile({ src, alt }: { src: string; alt: string }) { diff --git a/src/profiles/login/LoginForm.tsx b/src/profiles/login/LoginForm.tsx index 7e5e9ae..b40b778 100644 --- a/src/profiles/login/LoginForm.tsx +++ b/src/profiles/login/LoginForm.tsx @@ -1,8 +1,8 @@ import { type ChangeEvent, type FormEvent, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useMutation } from '@tanstack/react-query' -import { supabaseClient } from '../lib/supabase.ts' -import { loginCredentials } from '../lib/testCredentials.ts' +import { supabaseClient } from '../../lib/supabase.ts' +import { loginCredentials } from '../../lib/testCredentials.ts' type User = { email: string; password: string } export default function LoginForm() { diff --git a/tests/integration/profiles/gallery.test.tsx b/tests/integration/profiles/gallery.test.tsx index 0325870..07faa4c 100644 --- a/tests/integration/profiles/gallery.test.tsx +++ b/tests/integration/profiles/gallery.test.tsx @@ -1,8 +1,8 @@ import { render, screen, waitFor } from '@testing-library/react' import { describe, it, expect } from 'vitest' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { supabaseClient } from '@/profiles/lib/supabase.ts' -import { loginCredentials } from '@/profiles/lib/testCredentials' +import { supabaseClient } from '@/lib/supabase' +import { loginCredentials } from '@/lib/testCredentials' import Gallery from '@/profiles/gallery/Gallery.tsx' describe('Testing Gallery component with live data', () => { diff --git a/tests/unit/profiles/add/form.test.tsx b/tests/unit/profiles/add/form.test.tsx index 5074b70..b80b3f0 100644 --- a/tests/unit/profiles/add/form.test.tsx +++ b/tests/unit/profiles/add/form.test.tsx @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { StatusCodes } from 'http-status-codes' import { MemoryRouter } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { supabaseClient } from '@/profiles/lib/supabase.ts' +import { supabaseClient } from '@/lib/supabase' import Form from '@/profiles/add/Form.tsx' const mockedUsedNavigate = vi.fn() diff --git a/tests/unit/profiles/gallery/gallery.test.tsx b/tests/unit/profiles/gallery/gallery.test.tsx index 8e1df3d..1ae6c47 100644 --- a/tests/unit/profiles/gallery/gallery.test.tsx +++ b/tests/unit/profiles/gallery/gallery.test.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react' import { describe, it, expect, vi, afterEach } from 'vitest' import { StatusCodes } from 'http-status-codes' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { supabaseClient } from '@/profiles/lib/supabase.ts' +import { supabaseClient } from '@/lib/supabase' import { type UserResponse } from '@supabase/supabase-js' import Gallery from '@/profiles/gallery/Gallery.tsx' diff --git a/vite.config.ts b/vite.config.ts index ab330a2..197ccc5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -34,6 +34,7 @@ export default defineConfig({ __dirname, 'pages/fl-51/baseline/index.html' ), + fl53_image_manager: resolve(__dirname, 'pages/fl-53/index.html'), }, }, }, From 4939f5f667ec92ba1e700441e0a3d80228cfa4b7 Mon Sep 17 00:00:00 2001 From: Adam Cameron Date: Thu, 27 Nov 2025 12:01:40 +0000 Subject: [PATCH 2/7] Update src/fl-53/ImageManager.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/fl-53/ImageManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fl-53/ImageManager.tsx b/src/fl-53/ImageManager.tsx index e37bf0e..2b680e7 100644 --- a/src/fl-53/ImageManager.tsx +++ b/src/fl-53/ImageManager.tsx @@ -36,7 +36,7 @@ export default function ImageManager() { }) const handleChange = (event: React.ChangeEvent) => { - setSelectedFile(event.target.files![0] || null) + setSelectedFile(event.target.files?.[0] || null) } const handleSubmit = (event: FormEvent) => { From f6205aabd58c51541f8677b3ee8fe1f7a3a76af3 Mon Sep 17 00:00:00 2001 From: Adam Cameron Date: Thu, 27 Nov 2025 12:02:29 +0000 Subject: [PATCH 3/7] Update src/fl-53/ImageManager.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/fl-53/ImageManager.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fl-53/ImageManager.tsx b/src/fl-53/ImageManager.tsx index 2b680e7..6e838c5 100644 --- a/src/fl-53/ImageManager.tsx +++ b/src/fl-53/ImageManager.tsx @@ -104,10 +104,13 @@ async function fetchImageUrls(): Promise { const urls = data .filter((file) => file.name.endsWith('.png')) .map(async (file) => { - const { data } = await supabaseClient.storage + const { data, error } = await supabaseClient.storage .from('images') .createSignedUrl(`private/${file.name}`, 60) - + if (error !== null) { + console.error(`Failed to create signed URL for ${file.name}:`, error) + throw error + } return data === null ? '' : data.signedUrl }) return Promise.all(urls) From bcd50ef9f634d4a2aeb0406b499002b58e3b3915 Mon Sep 17 00:00:00 2001 From: Adam Cameron Date: Thu, 27 Nov 2025 12:05:43 +0000 Subject: [PATCH 4/7] Update src/fl-53/ImageManager.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/fl-53/ImageManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fl-53/ImageManager.tsx b/src/fl-53/ImageManager.tsx index 6e838c5..abaa4f4 100644 --- a/src/fl-53/ImageManager.tsx +++ b/src/fl-53/ImageManager.tsx @@ -72,7 +72,7 @@ export default function ImageManager() { ? 'nothing' : imageUrls.data.map((imageUrl: string) => ( <> - + {getFilenameFromUrl(imageUrl)
{getFilenameFromUrl(imageUrl)}
From b5c6e3be5768c2d69dd27912a79acb74bc10acb7 Mon Sep 17 00:00:00 2001 From: Adam Cameron Date: Thu, 27 Nov 2025 12:10:08 +0000 Subject: [PATCH 5/7] FL-53: conflix --- src/fl-53/ImageManager.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/fl-53/ImageManager.tsx b/src/fl-53/ImageManager.tsx index abaa4f4..0723f11 100644 --- a/src/fl-53/ImageManager.tsx +++ b/src/fl-53/ImageManager.tsx @@ -1,4 +1,4 @@ -import React, { type FormEvent, useState } from 'react' +import React, { type FormEvent, useState, Fragment } from 'react' import { supabaseClient } from '../lib/supabase.ts' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' @@ -11,7 +11,6 @@ export default function ImageManager() { onSuccess: () => { console.log('File uploaded successfully') void queryClient.invalidateQueries({ queryKey: ['ALL_IMAGES'] }) - void imageUrls.refetch() }, onError: (error) => { console.error('Error uploading file:', error) @@ -71,12 +70,16 @@ export default function ImageManager() { {imageUrls.data === undefined ? 'nothing' : imageUrls.data.map((imageUrl: string) => ( - <> - {getFilenameFromUrl(imageUrl) + + {getFilenameFromUrl(imageUrl)
{getFilenameFromUrl(imageUrl)}
- +
))} ) From 4abce98456f0b203bb5a34e7b320f06fa75f6f44 Mon Sep 17 00:00:00 2001 From: Adam Cameron Date: Thu, 27 Nov 2025 12:12:16 +0000 Subject: [PATCH 6/7] Update src/fl-53/ImageManager.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/fl-53/ImageManager.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fl-53/ImageManager.tsx b/src/fl-53/ImageManager.tsx index 0723f11..09fd0b3 100644 --- a/src/fl-53/ImageManager.tsx +++ b/src/fl-53/ImageManager.tsx @@ -120,10 +120,13 @@ async function fetchImageUrls(): Promise { } async function deleteImages() { - const { data: files } = await supabaseClient.storage + const { data: files, error: listError } = await supabaseClient.storage .from('images') .list('private/') - const paths = files!.map((file) => `private/${file.name}`) + if (listError !== null || files === null) { + return Promise.reject(listError || new Error('Failed to list files')) + } + const paths = files.map((file) => `private/${file.name}`) const { data, error } = await supabaseClient.storage .from('images') From 2be915ec6b27aded780a4fe283add96afaba3f06 Mon Sep 17 00:00:00 2001 From: Adam Cameron Date: Thu, 27 Nov 2025 12:22:26 +0000 Subject: [PATCH 7/7] FL-53: code review --- src/fl-53/ImageManager.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fl-53/ImageManager.tsx b/src/fl-53/ImageManager.tsx index 09fd0b3..96487fd 100644 --- a/src/fl-53/ImageManager.tsx +++ b/src/fl-53/ImageManager.tsx @@ -22,7 +22,6 @@ export default function ImageManager() { onSuccess: () => { console.log('All files deleted successfully') void queryClient.invalidateQueries({ queryKey: ['ALL_IMAGES'] }) - void imageUrls.refetch() }, onError: (error) => { console.error('Error deleting files:', error) @@ -59,7 +58,7 @@ export default function ImageManager() {