From 31594891e5493b4e122b40d016c799db2c3e3656 Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Sun, 8 Jun 2025 14:45:40 +0100 Subject: [PATCH] mock dashboard + 2 way sync in nanostores (i hate ctx) --- src/components/Controlbar.tsx | 62 +++++++++++++++++++---------- src/components/File.tsx | 41 ++++++++++++++++--- src/hooks/nanostores.ts | 74 +++++++++++++++++++++++++++++------ src/lib/stores.ts | 4 ++ src/routes/index.tsx | 39 ++++-------------- 5 files changed, 150 insertions(+), 70 deletions(-) create mode 100644 src/lib/stores.ts diff --git a/src/components/Controlbar.tsx b/src/components/Controlbar.tsx index 7f4478c..6988740 100644 --- a/src/components/Controlbar.tsx +++ b/src/components/Controlbar.tsx @@ -1,35 +1,57 @@ -import { component$, useSignal } from "@builder.io/qwik"; -import { atom } from "nanostores"; +import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } 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 { SolarUploadLinear } from "./Icons"; -const a = atom(0); - export default component$(() => { + const files = useNanostore$(DashboardFiles); const fileInputRef = useSignal(); - + const uploadingFile = useSignal | undefined>(); + const now = useSignal(new Date()); + + useVisibleTask$(() => { + const interval = setInterval(() => { + now.value = new Date(); + }, 500); + return () => clearInterval(interval); + }); + + const uploadFile = $(async () => { + if (!uploadingFile.value) { + console.error("No file selected for upload."); + return; + } + + try { + await api.upload(uploadingFile.value as File) + files.value = await api.list(); + } catch (error) { + console.error("Error uploading file:", error); + } + }) return ( -
- {/* Hidden file input */} +
{ - // You can handle the selected file here or emit an event - const files = (e.target as HTMLInputElement).files; - if (files && files.length > 0) { - console.log("File selected:", files[0]); - } + onChange$={async (e: Event) => { + uploadingFile.value = noSerialize((e.target as HTMLInputElement).files![0]); + await uploadFile(); }} /> - {/* Button that triggers the file dialog */} - +
+ +
+

{now.value.toLocaleTimeString()}

) }) \ No newline at end of file diff --git a/src/components/File.tsx b/src/components/File.tsx index 714de15..34eabde 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -1,15 +1,44 @@ import { component$ } from "@builder.io/qwik"; import { StereoFile } from "~/lib/types"; -export default component$(({ file }: { file: StereoFile }) => { +type FileProps = { + file: StereoFile; +} + +const getBase64Size = (b: string) => { + if (!b) return 0; + const padding = (b.match(/=+$/) || [""])[0].length; + return Math.floor((b.length * 3) / 4) - padding; +}; + +const formatSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +} + +export default component$(({ file }: FileProps) => { return ( -
-

Owner: {file.Owner}

-

File ID: {file.ID}

-

Created: {new Date(file.CreatedAt).toLocaleString()}

+
{ file.Base64 && (file.ID.endsWith(".png") || file.ID.endsWith(".jpg") || file.ID.endsWith(".jpeg")) && ( - Stereo File + {file.ID} )} +
+

+ {file.ID.split("_").slice(1).join("_") || "Untitled"} +

+
+ {formatSize(getBase64Size(file.Base64))} + +

Uploaded on {new Date(file.CreatedAt).toLocaleDateString()}

+
+
) }) \ No newline at end of file diff --git a/src/hooks/nanostores.ts b/src/hooks/nanostores.ts index c5b6eb8..3896ec4 100644 --- a/src/hooks/nanostores.ts +++ b/src/hooks/nanostores.ts @@ -1,17 +1,67 @@ -import { isServer, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik'; -import type { ReadableAtom } from 'nanostores'; +import { + implicit$FirstArg, + noSerialize, + NoSerialize, + QRL, + Signal, + useSignal, + useTask$, + useVisibleTask$, +} from "@builder.io/qwik"; +import { Atom, WritableAtom } from "nanostores"; -export function useNanostore(atom: ReadableAtom) { - if (isServer) return +function writeable(store: Atom | WritableAtom): store is WritableAtom { + return typeof (store as WritableAtom).set === 'function'; +} - const state = useSignal(atom.get()); - const store = useSignal> | undefined>(undefined); +export function useNanostoreQrl(qrl: QRL | Atom>): Signal { + const signal = useSignal(undefined); + const storeSignal = useSignal | Atom> | undefined>(undefined); - store.value = noSerialize(atom); - const unsubscribe = atom.subscribe((value) => { - state.value = value; + useTask$(async ({ track }) => { + let store: WritableAtom | Atom | undefined = storeSignal.value; + + if (!store) { + const modified = await qrl.resolve(); + storeSignal.value = noSerialize(modified); + store = modified; + } + + if (signal.value === undefined && store.value !== undefined) { + signal.value = store.value; + } + + const v = track(signal); + + if (writeable(store) && v !== undefined && store.value !== v) { + store.set(v); + } }); - window.addEventListener('beforeunload', () => unsubscribe()); - return state; -} \ No newline at end of file + // eslint-disable-next-line qwik/no-use-visible-task + useVisibleTask$(async ({ cleanup }) => { + let store: WritableAtom | Atom | undefined = storeSignal.value; + + if (!store) { + const modified = await qrl.resolve(); + storeSignal.value = noSerialize(modified); + store = modified; + } + + if (store.value !== undefined && signal.value !== store.value) { + signal.value = store.value; + } + + const unsub = store.subscribe((value) => { + if (signal.value !== value) { + signal.value = value; + } + }); + + cleanup(unsub); + }); + + return signal as Signal; +} + +export const useNanostore$ = implicit$FirstArg(useNanostoreQrl); \ No newline at end of file diff --git a/src/lib/stores.ts b/src/lib/stores.ts new file mode 100644 index 0000000..dc9c0b3 --- /dev/null +++ b/src/lib/stores.ts @@ -0,0 +1,4 @@ +import { atom } from "nanostores"; +import { StereoFile } from "./types"; + +export const DashboardFiles = atom([]); \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 3188d33..5ad3524 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,57 +1,32 @@ -import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik"; +import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"; import type { DocumentHead } from "@builder.io/qwik-city"; import Controlbar from "~/components/Controlbar"; import File from "~/components/File"; +import { useNanostore$ } from "~/hooks/nanostores"; import { api } from "~/lib/api"; import { OAUTH_LINK } from "~/lib/constants"; +import { DashboardFiles } from "~/lib/stores"; import { StereoFile } from "~/lib/types"; // TODO: move this to dashboard/index.tsx export default component$(() => { - const files = useSignal([]); + const files = useNanostore$(DashboardFiles); const loaded = useSignal(false); - const uploadingFile = useSignal | undefined>(); - useVisibleTask$(async () => { loaded.value = false; files.value = await api.list(); + console.log("Files loaded:", files.value); loaded.value = true; - }) - - const uploadFile = $( - async () => { - if (!uploadingFile.value) { - console.error("No file selected for upload."); - return; - } - - try { - await api.upload(uploadingFile.value as File) - files.value = await api.list(); - } catch (error) { - console.error("Error uploading file:", error); - } - } - ) + }); return ( <> oauth - uploadingFile.value = noSerialize((e.target as HTMLInputElement).files![0])} - type="file" - /> - - -
+
{/* TODO: make ts better :broken_heart: */} {!loaded.value ?

Loading...

: ( files.value.length === 0 ? (

no files found fr

)