file upload

This commit is contained in:
grngxd 2025-07-31 23:05:55 +01:00
parent 1599371df5
commit aaaebc20a5
5 changed files with 62 additions and 17 deletions

View file

@ -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 { 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"; import { SolarLibraryLinear, SolarQuestionCircleLinear, SolarRoundedMagniferLinear, SolarSettingsLinear, SolarUploadMinimalisticLinear, StereoCircularProgress, StereoLogoLinear } from "../misc/Icons";
export default component$(() => { export default component$(() => {
@ -8,6 +11,30 @@ export default component$(() => {
const total = 15; const total = 15;
const settingsOpen = useNanostore$<boolean>(isSettingsOpen); const settingsOpen = useNanostore$<boolean>(isSettingsOpen);
const fileInputRef = useSignal<HTMLInputElement>();
const files = useNanostore$<StereoFile[]>(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 ( return (
<div class="absolute bottom-0 left-0 flex items-center justify-between py-7 px-16 w-full"> <div class="absolute bottom-0 left-0 flex items-center justify-between py-7 px-16 w-full">
@ -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"> }} 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">
<a onClick$={() => settingsOpen.value = true}><StereoLogoLinear /></a> <a onClick$={() => settingsOpen.value = true}><StereoLogoLinear /></a>
<SolarLibraryLinear /> <SolarLibraryLinear />
<SolarUploadMinimalisticLinear /> <a
onClick$={(e) => {
e.preventDefault();
fileInputRef.value?.click();
}}
>
<SolarUploadMinimalisticLinear />
</a>
<SolarRoundedMagniferLinear /> <SolarRoundedMagniferLinear />
<SolarSettingsLinear /> <SolarSettingsLinear />
</div> </div>
<input
ref={fileInputRef}
type="file"
class="hidden"
onChange$={handleFileChange}
/>
<div style={{ <div style={{
borderRadius: "999px", borderRadius: "999px",
border: "0.5px solid #FF264E", border: "0.5px solid #FF264E",

View file

@ -14,8 +14,8 @@ function writeable<T>(store: Atom<T> | WritableAtom<T>): store is WritableAtom<T
return typeof (store as WritableAtom<T>).set === 'function'; return typeof (store as WritableAtom<T>).set === 'function';
} }
export function useNanostoreQrl<T>(qrl: QRL<WritableAtom<T> | Atom<T>>): Signal<T> { export function useNanostoreQrl<T>(qrl: QRL<WritableAtom<T> | Atom<T>>, defaultValue?: T): Signal<T> {
const signal = useSignal<T | undefined>(undefined); const signal = useSignal<T | undefined>(defaultValue);
const storeSignal = useSignal<NoSerialize<WritableAtom<T> | Atom<T>> | undefined>(undefined); const storeSignal = useSignal<NoSerialize<WritableAtom<T> | Atom<T>> | undefined>(undefined);
useTask$(async ({ track }) => { useTask$(async ({ track }) => {

View file

@ -1,25 +1,26 @@
import ky from 'ky'; import ky from "ky";
import { StereoFile } from './types'; import { StereoFile, StereoUser } from "./types";
export const client = ky.create({ export const client = ky.create({
prefixUrl: '/api', prefixUrl: "/api",
credentials: 'include' credentials: "include"
}); });
export const api = { export const api = {
file: async (uid: string) => await client.get<Blob>(uid), file: async (uid: string) => await client.get<Blob>(uid),
meta: async (uid: string) => await client.get<StereoFile>(uid + "/meta").json(),
list: async (page?: number, size?: number) => { list: async (page?: number, size?: number) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (page !== undefined) searchParams.append('page', String(page)); if (page !== undefined) searchParams.append("page", String(page));
if (size !== undefined) searchParams.append('size', String(size)); if (size !== undefined) searchParams.append("size", String(size));
return await client.get('list', { searchParams }).json<StereoFile[]>(); return await client.get("list", { searchParams }).json<StereoFile[]>();
}, },
upload: async (file: File) => { upload: async (file: File) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append("file", file);
return await client.post('upload', { body: formData }); return await client.post<{id: string, message: string}>("upload", { body: formData });
}, },
delete: async (uid: string) => await client.delete(uid).json(), 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<any>("auth/me").json()).user as StereoUser,
} }

View file

@ -10,4 +10,5 @@ export const userInfo = atom<StereoUser>({
email: "user@example.com", email: "user@example.com",
created_at: Date.now().toString(), created_at: Date.now().toString(),
}); });
export const isSettingsOpen = atom<boolean>(false); export const isSettingsOpen = atom<boolean>(false);
export const loadedFiles = atom<StereoFile[]>([]);

View file

@ -4,9 +4,11 @@ import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
import Actionbar from "~/components/dashboard/Actionbar"; import Actionbar from "~/components/dashboard/Actionbar";
import Settings from "~/components/dashboard/Settings"; import Settings from "~/components/dashboard/Settings";
import Titlebar from "~/components/dashboard/Titlebar"; import Titlebar from "~/components/dashboard/Titlebar";
import { useNanostore$ } from "~/hooks/nanostores";
// import Dropzone from "~/components/Dropzone"; // import Dropzone from "~/components/Dropzone";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { debounce } from "~/lib/misc"; import { debounce } from "~/lib/misc";
import { loadedFiles } from "~/lib/stores";
import { StereoFile } from "~/lib/types"; import { StereoFile } from "~/lib/types";
export const useAuthenticated = routeLoader$(({ cookie, redirect: r }) => { export const useAuthenticated = routeLoader$(({ cookie, redirect: r }) => {
@ -111,7 +113,7 @@ const Files = component$(() => {
const hasMore = useSignal(true); const hasMore = useSignal(true);
const sentinel = useSignal<HTMLDivElement>(); const sentinel = useSignal<HTMLDivElement>();
const files = useSignal<StereoFile[]>([]); const files = useNanostore$<StereoFile[]>(loadedFiles, []);
const page = useSignal(1); const page = useSignal(1);
// TODO: make it load enough images to fill the viewport instead // TODO: make it load enough images to fill the viewport instead