From aaaebc20a53c12ed06f78def9a210aba8d7dd14f Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:05:55 +0100 Subject: [PATCH] file upload --- src/components/dashboard/Actionbar.tsx | 47 ++++++++++++++++++++++++-- src/hooks/nanostores.ts | 4 +-- src/lib/api.ts | 21 ++++++------ src/lib/stores.ts | 3 +- src/routes/dashboard/index.tsx | 4 ++- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/components/dashboard/Actionbar.tsx b/src/components/dashboard/Actionbar.tsx index 26d32f9..cec5881 100644 --- a/src/components/dashboard/Actionbar.tsx +++ b/src/components/dashboard/Actionbar.tsx @@ -1,6 +1,9 @@ -import { component$ } from "@builder.io/qwik"; +/* eslint-disable qwik/jsx-a */ +import { $, component$, useSignal } from "@builder.io/qwik"; import { useNanostore$ } from "~/hooks/nanostores"; -import { isSettingsOpen } from "~/lib/stores"; +import { api } from "~/lib/api"; +import { isSettingsOpen, loadedFiles } from "~/lib/stores"; +import { StereoFile } from "~/lib/types"; import { SolarLibraryLinear, SolarQuestionCircleLinear, SolarRoundedMagniferLinear, SolarSettingsLinear, SolarUploadMinimalisticLinear, StereoCircularProgress, StereoLogoLinear } from "../misc/Icons"; export default component$(() => { @@ -8,6 +11,30 @@ export default component$(() => { const total = 15; const settingsOpen = useNanostore$(isSettingsOpen); + const fileInputRef = useSignal(); + const files = useNanostore$(loadedFiles, []); + + const handleFileChange = $(async (event: Event) => { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const fi: File[] = Array.from(input.files); + + const metas: StereoFile[] = await Promise.all( + fi.map(async (file) => { + try { + const id = (await (await api.upload(file)).json()).id; + return await api.meta(id); + } catch (error) { + console.error("actionbar: file upload failed:", error); + throw new Error("File upload failed"); + } + }) + ); + + input.value = ""; + files.value = [...files.value, ...metas]; + } + }); return (
@@ -31,11 +58,25 @@ export default component$(() => { }} class="flex items-center justify-center px-6 py-4 gap-5 text-white text-3xl absolute left-1/2 transform -translate-x-1/2"> settingsOpen.value = true}> - + { + e.preventDefault(); + fileInputRef.value?.click(); + }} + > + +
+ +
(store: Atom | WritableAtom): store is WritableAtom).set === 'function'; } -export function useNanostoreQrl(qrl: QRL | Atom>): Signal { - const signal = useSignal(undefined); +export function useNanostoreQrl(qrl: QRL | Atom>, defaultValue?: T): Signal { + const signal = useSignal(defaultValue); const storeSignal = useSignal | Atom> | undefined>(undefined); useTask$(async ({ track }) => { diff --git a/src/lib/api.ts b/src/lib/api.ts index 476c1ad..2d46f7e 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,25 +1,26 @@ -import ky from 'ky'; -import { StereoFile } from './types'; +import ky from "ky"; +import { StereoFile, StereoUser } from "./types"; export const client = ky.create({ - prefixUrl: '/api', - credentials: 'include' + prefixUrl: "/api", + credentials: "include" }); export const api = { file: async (uid: string) => await client.get(uid), + meta: async (uid: string) => await client.get(uid + "/meta").json(), list: async (page?: number, size?: number) => { const searchParams = new URLSearchParams(); - if (page !== undefined) searchParams.append('page', String(page)); - if (size !== undefined) searchParams.append('size', String(size)); + if (page !== undefined) searchParams.append("page", String(page)); + if (size !== undefined) searchParams.append("size", String(size)); - return await client.get('list', { searchParams }).json(); + return await client.get("list", { searchParams }).json(); }, upload: async (file: File) => { const formData = new FormData(); - formData.append('file', file); - return await client.post('upload', { body: formData }); + formData.append("file", file); + return await client.post<{id: string, message: string}>("upload", { body: formData }); }, delete: async (uid: string) => await client.delete(uid).json(), - me: async () => (await client.get('auth/me').json() as any).user, + me: async () => (await client.get("auth/me").json()).user as StereoUser, } \ No newline at end of file diff --git a/src/lib/stores.ts b/src/lib/stores.ts index d467cee..ed2bd87 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -10,4 +10,5 @@ export const userInfo = atom({ email: "user@example.com", created_at: Date.now().toString(), }); -export const isSettingsOpen = atom(false); \ No newline at end of file +export const isSettingsOpen = atom(false); +export const loadedFiles = atom([]); \ No newline at end of file diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 22461fb..1c190c8 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -4,9 +4,11 @@ import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city"; import Actionbar from "~/components/dashboard/Actionbar"; import Settings from "~/components/dashboard/Settings"; import Titlebar from "~/components/dashboard/Titlebar"; +import { useNanostore$ } from "~/hooks/nanostores"; // import Dropzone from "~/components/Dropzone"; import { api } from "~/lib/api"; import { debounce } from "~/lib/misc"; +import { loadedFiles } from "~/lib/stores"; import { StereoFile } from "~/lib/types"; export const useAuthenticated = routeLoader$(({ cookie, redirect: r }) => { @@ -111,7 +113,7 @@ const Files = component$(() => { const hasMore = useSignal(true); const sentinel = useSignal(); - const files = useSignal([]); + const files = useNanostore$(loadedFiles, []); const page = useSignal(1); // TODO: make it load enough images to fill the viewport instead