refactor: update file handling and improve component structure

- fix api client
- make controlbar look better
- fix api client again
This commit is contained in:
grngxd 2025-06-10 00:10:59 +01:00
parent 50b1bc2b24
commit 98a582c8d4
10 changed files with 62 additions and 31 deletions

3
public/logo.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="281" height="248" viewBox="0 0 281 248" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.744835 19.1645C0.340705 16.0258 0.876443 12.8377 2.2843 10.0034L6.37392 1.7703C7.01957 0.470487 8.7912 0.269553 9.71155 1.39175L85.375 93.6495H195.875L271.538 1.39175C272.459 0.269551 274.23 0.470487 274.876 1.77029L278.966 10.0034C280.374 12.8377 280.909 16.0258 280.505 19.1645L264.378 144.419C256.8 203.277 206.688 247.35 147.344 247.35H133.906C74.5619 247.35 24.4504 203.277 16.872 144.419L0.744835 19.1645Z" fill="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View file

@ -1,12 +1,14 @@
import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik"; import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import { useNanostore$ } from "~/hooks/nanostores"; import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { DashboardFiles } from "~/lib/stores"; import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
import { StereoFile } from "~/lib/types"; import { StereoFile } from "~/lib/types";
import { SolarUploadLinear } from "./Icons"; import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "./Icons";
import StereoLogo from "./StereoLogo";
export default component$(() => { export default component$(() => {
const files = useNanostore$<StereoFile[]>(DashboardFiles); const loaded = useNanostore$<boolean>(areFilesLoaded);
const files = useNanostore$<StereoFile[]>(dashboardFiles);
const fileInputRef = useSignal<HTMLInputElement>(); const fileInputRef = useSignal<HTMLInputElement>();
const uploadingFile = useSignal<NoSerialize<File> | undefined>(); const uploadingFile = useSignal<NoSerialize<File> | undefined>();
const now = useSignal(new Date()); const now = useSignal(new Date());
@ -28,7 +30,7 @@ export default component$(() => {
const unsafe = uploadingFile.value as File; const unsafe = uploadingFile.value as File;
const name = unsafe.name.replace(/[^a-zA-Z0-9_.-]/g, "_"); const name = unsafe.name.replace(/[^a-zA-Z0-9_.-]/g, "_");
const f = new File([unsafe], name, { type: unsafe.type }); const f = new File([unsafe], name, { type: unsafe.type });
await api.upload(f); await api.upload(f);
files.value = await api.list(); files.value = await api.list();
} catch (error) { } catch (error) {
@ -36,7 +38,7 @@ export default component$(() => {
} }
}) })
return ( return (
<div class="z-[999999999] fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-neutral-950/50 backdrop-blur-3xl w-1/3 p-2 rounded-lg flex items-center justify-between"> <div class="z-[999999999] fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-neutral-700/10 backdrop-blur-3xl w-1/3 p-2 pr-4 rounded-lg flex items-center justify-between">
<input <input
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
@ -47,7 +49,26 @@ export default component$(() => {
}} }}
/> />
<div class="flex items-center gap-1"> <div class="flex items-center gap-2">
<button
class="duration-100 hover:bg-white text-white hover:text-black p-2 rounded-lg"
onClick$={async () => {
loaded.value = false;
files.value = await api.list()
loaded.value = true;
}}
>
{
loaded.value ? (
<StereoLogo class="w-6 h-6" />
) : (
<SvgSpinnersBarsRotateFade class="w-6 h-6" />
)
}
</button>
<p class="text-white/25 font-light text-xl"> | </p>
<button <button
class="duration-100 hover:bg-white text-white hover:text-black p-2 rounded-lg" class="duration-100 hover:bg-white text-white hover:text-black p-2 rounded-lg"
onClick$={() => { fileInputRef.value?.click() }} onClick$={() => { fileInputRef.value?.click() }}

View file

@ -1,7 +1,7 @@
import { $, component$ } from "@builder.io/qwik"; import { $, component$ } from "@builder.io/qwik";
import { useNanostore$ } from "~/hooks/nanostores"; import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { DashboardFiles } from "~/lib/stores"; import { dashboardFiles } from "~/lib/stores";
import { StereoFile } from "~/lib/types"; import { StereoFile } from "~/lib/types";
import { SolarClipboardAddBold, SolarDownloadMinimalisticBold, SolarTrashBin2Bold } from "./Icons"; import { SolarClipboardAddBold, SolarDownloadMinimalisticBold, SolarTrashBin2Bold } from "./Icons";
@ -17,16 +17,16 @@ const formatSize = (bytes: number) => {
} }
export default component$(({ file }: FileProps) => { export default component$(({ file }: FileProps) => {
const files = useNanostore$<StereoFile[]>(DashboardFiles); const files = useNanostore$<StereoFile[]>(dashboardFiles);
const deleteFile = $(async (id: string) => { const deleteFile = $(async (uid: string, name: string) => {
if (!confirm("Are you sure you want to delete this file?")) return; if (!confirm("Are you sure you want to delete this file?")) return;
console.log(await api.delete(id)) console.log(await api.delete(uid, name));
files.value = await api.list(); files.value = await api.list();
}); });
const addFileToClipboard = $(async () => { const addFileToClipboard = $(async () => {
const response = await api.file(file.ID); const response = await api.file(file.Name);
const data = await response.blob(); const data = await response.blob();
let mime = data.type || "application/octet-stream"; let mime = data.type || "application/octet-stream";
let clip; let clip;
@ -65,13 +65,13 @@ export default component$(({ file }: FileProps) => {
return ( return (
<div class="rounded-xl bg-neutral-900 flex flex-col group overflow-hidden hover:bg-neutral-800 transition-all duration-200"> <div class="rounded-xl bg-neutral-900 flex flex-col group overflow-hidden hover:bg-neutral-800 transition-all duration-200">
<div class="relative"> <div class="relative">
<a href={`/api/${file.ID}`} target="_blank"> <a href={`/api/${file.Owner}/${file.Name}`} target="_blank">
{ (file.ID.endsWith(".png") || file.ID.endsWith(".jpg") || file.ID.endsWith(".jpeg")) && ( { (file.Name.endsWith(".png") || file.Name.endsWith(".jpg") || file.Name.endsWith(".jpeg")) && (
<img <img
width={400} width={400}
height={300} height={300}
src={`/api/${file.ID}`} src={`/api/${file.Owner}/${file.Name}`}
alt={file.ID} alt={file.Name}
class="w-full h-60 object-cover bg-neutral-800 flex-grow" class="w-full h-60 object-cover bg-neutral-800 flex-grow"
/> />
)} )}
@ -80,7 +80,7 @@ export default component$(({ file }: FileProps) => {
<div class="absolute bottom-2 right-2 gap-2 z-10 group-hover:flex hidden duration-200 transition-all"> <div class="absolute bottom-2 right-2 gap-2 z-10 group-hover:flex hidden duration-200 transition-all">
<a <a
class="bg-neutral-600/40 backdrop-blur-lg hover:bg-neutral-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95" class="bg-neutral-600/40 backdrop-blur-lg hover:bg-neutral-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
href={`/api/${file.ID}`} href={`/api/${file.Owner}/${file.Name}`}
target="_blank" target="_blank"
> >
<SolarDownloadMinimalisticBold class="w-6 h-6"/> <SolarDownloadMinimalisticBold class="w-6 h-6"/>
@ -95,7 +95,7 @@ export default component$(({ file }: FileProps) => {
<button <button
class="bg-red-600/50 backdrop-blur-lg hover:bg-red-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95" class="bg-red-600/50 backdrop-blur-lg hover:bg-red-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
onClick$={async () => await deleteFile(file.ID)} onClick$={async () => await deleteFile(file.Owner, file.Name)}
> >
<SolarTrashBin2Bold class="w-6 h-6"/> <SolarTrashBin2Bold class="w-6 h-6"/>
</button> </button>
@ -105,7 +105,7 @@ export default component$(({ file }: FileProps) => {
<div class="flex justify-center items-center h-full"> <div class="flex justify-center items-center h-full">
<div class="p-4 flex flex-col w-full text-center"> <div class="p-4 flex flex-col w-full text-center">
<p class="text-lg font-semibold text-white w-full truncate"> <p class="text-lg font-semibold text-white w-full truncate">
{ file.ID.split("_").slice(1).join("_") || "Untitled" } { file.Name || "Untitled" }
</p> </p>
<div class="flex gap-1 text-sm text-neutral-500 items-center justify-center"> <div class="flex gap-1 text-sm text-neutral-500 items-center justify-center">
<span>{ formatSize(file.Size) }</span> <span>{ formatSize(file.Size) }</span>

View file

@ -0,0 +1,9 @@
import { component$, QwikIntrinsicElements } from "@builder.io/qwik";
export default component$((props: QwikIntrinsicElements['svg']) => {
return (
<svg width="281" height="248" viewBox="0 0 281 248" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M0.744835 19.1645C0.340705 16.0258 0.876443 12.8377 2.2843 10.0034L6.37392 1.7703C7.01957 0.470487 8.7912 0.269553 9.71155 1.39175L85.375 93.6495H195.875L271.538 1.39175C272.459 0.269551 274.23 0.470487 274.876 1.77029L278.966 10.0034C280.374 12.8377 280.909 16.0258 280.505 19.1645L264.378 144.419C256.8 203.277 206.688 247.35 147.344 247.35H133.906C74.5619 247.35 24.4504 203.277 16.872 144.419L0.744835 19.1645Z" fill="currentColor"/>
</svg>
)
})

View file

@ -10,7 +10,7 @@ import {
} from "@builder.io/qwik"; } from "@builder.io/qwik";
import { Atom, WritableAtom } from "nanostores"; import { Atom, WritableAtom } from "nanostores";
function writeable<T = any>(store: Atom<T> | WritableAtom<T>): store is WritableAtom<T> { 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';
} }

View file

@ -14,10 +14,7 @@ export const api = {
formData.append('file', file); formData.append('file', file);
return await client.post('upload', { body: formData }); return await client.post('upload', { body: formData });
}, },
delete: async (file_id: string) => { delete: async (uid: string, file: string) => {
console.log("Deleting file with ID:", file_id); return await client.delete(`${uid}/${file}`).json();
return await client.delete(`delete`, {
json: { file_id }
}).json();
}, },
} }

View file

@ -1,4 +1,5 @@
import { atom } from "nanostores"; import { atom } from "nanostores";
import { StereoFile } from "./types"; import { StereoFile } from "./types";
export const DashboardFiles = atom<StereoFile[]>([]); export const areFilesLoaded = atom<boolean>(false);
export const dashboardFiles = atom<StereoFile[]>([]);

View file

@ -1,5 +1,5 @@
export type StereoFile = { export type StereoFile = {
ID: string; Name: string;
Owner: string; Owner: string;
CreatedAt: string; CreatedAt: string;
Size: number; Size: number;

View file

@ -1,4 +1,4 @@
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"; import { component$, useVisibleTask$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city"; import type { DocumentHead } from "@builder.io/qwik-city";
import Controlbar from "~/components/Controlbar"; import Controlbar from "~/components/Controlbar";
import File from "~/components/File"; import File from "~/components/File";
@ -6,14 +6,14 @@ import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/Icons
import { useNanostore$ } from "~/hooks/nanostores"; import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { OAUTH_LINK } from "~/lib/constants"; import { OAUTH_LINK } from "~/lib/constants";
import { DashboardFiles } from "~/lib/stores"; import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
import { StereoFile } from "~/lib/types"; import { StereoFile } from "~/lib/types";
// TODO: move this to dashboard/index.tsx // TODO: move this to dashboard/index.tsx
export default component$(() => { export default component$(() => {
const files = useNanostore$<StereoFile[]>(DashboardFiles); const files = useNanostore$<StereoFile[]>(dashboardFiles);
const loaded = useSignal(false); const loaded = useNanostore$<boolean>(areFilesLoaded);
useVisibleTask$(async () => { useVisibleTask$(async () => {
loaded.value = false; loaded.value = false;
@ -43,7 +43,7 @@ export default component$(() => {
: ( : (
<div class="grid grid-cols-4 gap-4 p-4 mb-18"> <div class="grid grid-cols-4 gap-4 p-4 mb-18">
{files.value.map((file) => ( {files.value.map((file) => (
<File key={file.ID} file={file} /> <File key={file.Name} file={file} />
))} ))}
</div> </div>
) )