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..96487fd
--- /dev/null
+++ b/src/fl-53/ImageManager.tsx
@@ -0,0 +1,141 @@
+import React, { type FormEvent, useState, Fragment } 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'] })
+ },
+ 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'] })
+ },
+ 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, 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)
+}
+
+async function deleteImages() {
+ const { data: files, error: listError } = await supabaseClient.storage
+ .from('images')
+ .list('private/')
+ 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')
+ .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'),
},
},
},