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) +
+ {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'), }, }, },