templating dashboard & add user route to api client

This commit is contained in:
grngxd 2025-06-21 17:02:52 +01:00
parent 0d29d59937
commit f47a1b2226
9 changed files with 58 additions and 329 deletions

View file

@ -1,88 +0,0 @@
import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api";
import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
import { StereoFile } from "~/lib/types";
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "../misc/Icons";
import StereoLogo from "../misc/StereoLogo";
export default component$(() => {
const loaded = useNanostore$<boolean>(areFilesLoaded);
const files = useNanostore$<StereoFile[]>(dashboardFiles);
const fileInputRef = useSignal<HTMLInputElement>();
const uploadingFiles = useSignal<NoSerialize<File[]> | undefined>();
const now = useSignal(new Date());
useVisibleTask$(() => {
const interval = setInterval(() => {
now.value = new Date();
}, 500);
return () => clearInterval(interval);
});
const uploadFiles = $(async () => {
if (!uploadingFiles.value) {
console.error("No file(s) selected for upload.");
return;
}
try {
const ufiles = uploadingFiles.value as File[];
for (const file of ufiles) {
const name = file.name.replace(/[^a-zA-Z0-9_.-]/g, "_");
const f = new File([file], name, { type: file.type });
await api.upload(f);
}
files.value = await api.list();
} catch (error) {
console.error("Error uploading file:", error);
}
})
return (
<div class="z-[999999999] fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-neutral-700/10 backdrop-blur-3xl lg:w-1/3 md:w-2/3 w-4/5 p-2 pr-4 rounded-lg flex items-center justify-between">
<input
type="file"
ref={fileInputRef}
style="display: none;"
onChange$={async (e: Event) => {
uploadingFiles.value = noSerialize(Object.values((e.target as HTMLInputElement).files || {}));
await uploadFiles();
}}
multiple
/>
<div class="flex items-center gap-2">
{/* TODO: replace this button with a modal with options like settings log out etc */}
<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
class="duration-100 hover:bg-white text-white hover:text-black p-2 rounded-lg"
onClick$={() => { fileInputRef.value?.click() }}
>
<SolarUploadLinear class="w-6 h-6" />
</button>
</div>
<p class="text-white font-medium">{now.value.toLocaleTimeString()}</p>
</div>
)
})

View file

@ -1,199 +0,0 @@
import { $, component$, Signal, useSignal, useTask$ } from "@builder.io/qwik";
import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api";
import { dashboardFiles } from "~/lib/stores";
import { StereoFile } from "~/lib/types";
import { SolarClipboardAddBold, SolarDownloadMinimalisticBold, SolarTrashBin2Bold } from "../misc/Icons";
type FileProps = {
file: StereoFile;
}
const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
export default component$(({ file }: FileProps) => {
const files = useNanostore$<StereoFile[]>(dashboardFiles);
const deleteFile = $(async (id: string) => {
if (!confirm("Are you sure you want to delete this file?")) return;
await api.delete(id);
files.value = await api.list();
});
const addFileToClipboard = $(async () => {
const response = await api.file(file.Name);
const data = await response.blob();
let mime = data.type || "application/octet-stream";
let clip;
if (navigator.clipboard && window.ClipboardItem) {
if (mime === "image/jpeg" || mime === "image/jpg") {
const img = document.createElement("img");
img.src = URL.createObjectURL(data);
await new Promise((res) => (img.onload = res));
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx?.drawImage(img, 0, 0);
const png = await new Promise<Blob>((resolve) =>
canvas.toBlob((b) => resolve(b!), "image/png")
);
mime = "image/png";
clip = new ClipboardItem({ [mime]: png });
} else {
clip = new ClipboardItem({ [mime]: data });
}
try {
await navigator.clipboard.write([clip]);
alert("File added to clipboard successfully!");
} catch (error) {
console.error("Failed to add file to clipboard:", error);
alert("Failed to add file to clipboard. Please try again.");
}
} else {
alert("Clipboard API not supported in this browser.");
}
});
return (
<div class="rounded-xl bg-neutral-900 flex flex-col group overflow-hidden hover:bg-neutral-800 transition-all duration-200">
<div class="relative">
<a
href={`/api/${file.ID}`}
target="_blank"
class="flex w-full h-60 overflow-clip"
>
<div class="flex flex-grow group-hover:scale-105 transition-all duration-500 bg-neutral-800">
<FilePreview file={file} />
</div>
</a>
<div class="absolute bottom-2 right-2 gap-2 z-10 group-hover:flex hidden duration-200 transition-all">
<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"
href={`/api/${file.ID}`}
target="_blank"
>
<SolarDownloadMinimalisticBold class="w-6 h-6"/>
</a>
<button
class="bg-green-600/50 backdrop-blur-lg hover:bg-green-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
onClick$={async () => await addFileToClipboard()}
>
<SolarClipboardAddBold class="w-6 h-6"/>
</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"
onClick$={async () => await deleteFile(file.ID)}
>
<SolarTrashBin2Bold class="w-6 h-6"/>
</button>
</div>
</div>
<div class="flex justify-center items-center h-full">
<div class="p-4 flex flex-col w-full text-center">
<p class="text-lg font-semibold text-white w-full truncate">
{ file.Name || "Untitled" }
</p>
<div class="flex gap-1 text-sm text-neutral-500 items-center justify-center">
<span>{ formatSize(file.Size) }</span>
<span class="text-neutral-600"></span>
<span>Uploaded on { new Date(file.CreatedAt).toLocaleDateString() }</span>
</div>
</div>
</div>
</div>
)
})
const FilePreview = component$(({ file }: FileProps) => {
type FileType =
| "image"
| "video"
| "audio"
| "other";
const type: Signal<FileType> = useSignal<FileType>("other");
const extension = file.Name.split('.').pop()?.toLowerCase() || "";
useTask$(async () => {
if (
["png", "jpg", "jpeg", "gif"]
.includes(extension)) type.value = "image";
else if (
["mp4", "webm", "ogg", "avi", "mov", "mkv"]
.includes(extension)) type.value = "video";
else if (
["mp3", "wav", "ogg", "flac", "aac"]
.includes(extension)) type.value = "audio";
else type.value = "other";
});
switch (type.value) {
case "image":
return (
<div class="w-full h-60 object-cover flex-grow relative">
<img
width={400}
height={300}
src={`/api/${file.ID}`}
alt={file.Name}
class="w-full h-60 object-cover flex-grow"
/>
</div>
);
case "video":
return (
<div class="w-full h-60 object-cover flex-grow relative">
<video
width={400}
height={300}
src={`/api/${file.ID}`}
class="w-full h-60 object-cover flex-grow"
controls
autoplay
muted
>
<div class="w-full h-60 flex items-center justify-center">
<p class="text-white/50 text-lg font-light">
Preview not available
</p>
</div>
</video>
</div>
);
case "audio":
return (
<div class="w-full h-60 flex items-center justify-center p-2">
<audio
controls
class="w-full h-12"
src={`/api/${file.ID}`}
>
Your browser does not support the audio element.
</audio>
</div>
);
case "other":
default:
return (
<div class="w-full h-60 flex items-center justify-center">
<p class="text-white/50 text-lg font-light">
Preview not available
</p>
</div>
);
}
});

View file

@ -0,0 +1,9 @@
import { component$ } from "@builder.io/qwik";
export default component$(() => {
return (
<div class="flex w-full h-48 bg-red-500 align-bottom">
a
</div>
)
})

View file

@ -0,0 +1,9 @@
import { component$ } from "@builder.io/qwik";
export default component$(() => {
return (
<div class="bg-blue-500">
<p>whats on the agenda today, @hexlocation?</p>
</div>
)
})

View file

@ -15,4 +15,5 @@ export const api = {
return await client.post('upload', { body: formData }); return await client.post('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,
} }

View file

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

View file

@ -5,4 +5,12 @@ export type StereoFile = {
Size: number; Size: number;
CreatedAt: string; CreatedAt: string;
Mime: string; Mime: string;
}
export type StereoUser = {
id: string;
username: string;
blacklisted: boolean;
email: string;
created_at: string;
} }

View file

@ -1,9 +1,8 @@
import { component$, useVisibleTask$ } from "@builder.io/qwik"; import { component$, Signal, useVisibleTask$ } from "@builder.io/qwik";
import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city"; import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
import Controlbar from "~/components/dashboard/Controlbar"; import OSBar from "~/components/dashboard/OSBar";
import TitleBar from "~/components/dashboard/TitleBar";
// import Dropzone from "~/components/Dropzone"; // import Dropzone from "~/components/Dropzone";
import File from "~/components/dashboard/File";
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/misc/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";
@ -23,50 +22,30 @@ export default component$(() => {
useVisibleTask$(async () => { useVisibleTask$(async () => {
loaded.value = false; loaded.value = false;
files.value = await api.list(); files.value = await api.list();
console.log("Files loaded:", files.value);
loaded.value = true; loaded.value = true;
}); });
return ( return (
<> <>
{/* <Dropzone /> */} <TitleBar />
<Controlbar /> <Files files={files} loaded={loaded} />
{!loaded.value ? ( <OSBar />
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
<p class="text-gray-500 text-8xl font-bold"><SvgSpinnersBarsRotateFade /></p>
<p class="text-gray-700 text-2xl font-light italic">loading your files...</p>
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">please wait... <span class="animate-spin"></span></span>
</div>
) : (
files.value.length === 0 ? (
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
<p class="text-gray-500 text-8xl font-bold">{
[
"┻━┻︵ \\(°□°)/ ︵ ┻━┻",
"┻━┻︵ヽ(`Д´)ノ︵ ┻━┻",
"ʕノ•ᴥ•ʔノ ︵ ┻━┻",
"(╯°Д°)╯︵ /(.□ . \\)",
"┬─┬ ︵ /(.□. \\",
"(/ .□.)\\ ︵╰(゜Д゜)╯︵ /(.□. \\)"
].sort(() => Math.random() - 0.5)[0]
}</p>
<p class="text-gray-700 text-2xl font-light italic">you haven't uploaded any files yet!</p>
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">click the <span class="animate-bounce p-0.5 bg-gray-500 rounded-sm"><SolarUploadLinear /></span> button to get started</span>
</div>
)
: (
<div class="grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-3 p-6 mb-14">
{files.value.map((file) => (
<File key={file.Name} file={file} />
))}
</div>
)
)}
</> </>
); );
}); });
const Files = component$<{
files: Signal<StereoFile[]>;
loaded: Signal<boolean>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}>(({ files, loaded }) => {
return (
<div class="flex flex-col flex-grow w-full h-full bg-green-500">
b
</div>
);
});
export const head: DocumentHead = { export const head: DocumentHead = {
title: "Welcome to Qwik", title: "Welcome to Qwik",
meta: [ meta: [

View file

@ -1,9 +1,18 @@
import { $, component$, Slot, useOnDocument } from '@builder.io/qwik'; import { $, component$, Slot, useOnDocument } from '@builder.io/qwik';
import AOS from 'aos'; import AOS from 'aos';
import 'aos/dist/aos.css'; import 'aos/dist/aos.css';
import { useNanostore$ } from '~/hooks/nanostores';
import { api } from '~/lib/api';
import { userInfo } from '~/lib/stores';
import { StereoUser } from '~/lib/types';
export default component$(() => { export default component$(() => {
useOnDocument("DOMContentLoaded", $(() => { const info = useNanostore$<StereoUser>(userInfo);
useOnDocument("DOMContentLoaded", $(async () => {
info.value = await api.me()
console.log(info.value);
AOS.init({ AOS.init({
once: true, once: true,
duration: 1000, duration: 1000,