mock dashboard + 2 way sync in nanostores (i hate ctx)

This commit is contained in:
grngxd 2025-06-08 14:45:40 +01:00
parent d792cec873
commit 31594891e5
5 changed files with 150 additions and 70 deletions

View file

@ -1,35 +1,57 @@
import { component$, useSignal } from "@builder.io/qwik"; import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import { atom } from "nanostores"; import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api";
import { DashboardFiles } from "~/lib/stores";
import { StereoFile } from "~/lib/types";
import { SolarUploadLinear } from "./Icons"; import { SolarUploadLinear } from "./Icons";
const a = atom(0);
export default component$(() => { export default component$(() => {
const files = useNanostore$<StereoFile[]>(DashboardFiles);
const fileInputRef = useSignal<HTMLInputElement>(); const fileInputRef = useSignal<HTMLInputElement>();
const uploadingFile = useSignal<NoSerialize<File> | 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 ( return (
<div class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-red-400 w-1/3 p-2 rounded-lg flex items-center justify-start"> <div class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gray-950/50 backdrop-blur-3xl w-1/3 p-2 rounded-lg flex items-center justify-between">
{/* Hidden file input */}
<input <input
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
style="display: none;" style="display: none;"
onChange$={(e) => { onChange$={async (e: Event) => {
// You can handle the selected file here or emit an event uploadingFile.value = noSerialize((e.target as HTMLInputElement).files![0]);
const files = (e.target as HTMLInputElement).files; await uploadFile();
if (files && files.length > 0) {
console.log("File selected:", files[0]);
}
}} }}
/> />
{/* Button that triggers the file dialog */} <div class="flex items-center gap-1">
<button <button
class="duration-100 hover:bg-white 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() }}
> >
<SolarUploadLinear class="w-6 h-6 text-black" /> <SolarUploadLinear class="w-6 h-6" />
</button> </button>
</div> </div>
<p class="text-white font-medium">{now.value.toLocaleTimeString()}</p>
</div>
) )
}) })

View file

@ -1,15 +1,44 @@
import { component$ } from "@builder.io/qwik"; import { component$ } from "@builder.io/qwik";
import { StereoFile } from "~/lib/types"; 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 ( return (
<div key={file.ID}> <div class="rounded-xl bg-slate-900 flex flex-col overflow-hidden">
<h2>Owner: {file.Owner}</h2>
<p>File ID: {file.ID}</p>
<p>Created: {new Date(file.CreatedAt).toLocaleString()}</p>
{ file.Base64 && (file.ID.endsWith(".png") || file.ID.endsWith(".jpg") || file.ID.endsWith(".jpeg")) && ( { file.Base64 && (file.ID.endsWith(".png") || file.ID.endsWith(".jpg") || file.ID.endsWith(".jpeg")) && (
<img src={`data:image/png;base64,${file.Base64}`} alt="Stereo File" class="w-full h-auto" /> <img
width={400}
height={300}
src={`data:image/png;base64,${file.Base64}`}
alt={file.ID}
class="w-full h-80 object-cover bg-gray-800 flex-grow"
/>
)} )}
<div class="p-4 flex-grow-0 text-center">
<p class="text-lg font-semibold text-white w-full truncate">
{file.ID.split("_").slice(1).join("_") || "Untitled"}
</p>
<div class="flex gap-1 text-sm text-gray-400 items-center justify-center">
<span>{formatSize(getBase64Size(file.Base64))}</span>
<span class="text-gray-600"></span>
<p>Uploaded on {new Date(file.CreatedAt).toLocaleDateString()}</p>
</div>
</div>
</div> </div>
) )
}) })

View file

@ -1,17 +1,67 @@
import { isServer, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik'; import {
import type { ReadableAtom } from 'nanostores'; implicit$FirstArg,
noSerialize,
NoSerialize,
QRL,
Signal,
useSignal,
useTask$,
useVisibleTask$,
} from "@builder.io/qwik";
import { Atom, WritableAtom } from "nanostores";
export function useNanostore<T>(atom: ReadableAtom<T>) { function writeable<T = any>(store: Atom<T> | WritableAtom<T>): store is WritableAtom<T> {
if (isServer) return return typeof (store as WritableAtom<T>).set === 'function';
}
const state = useSignal<T>(atom.get()); export function useNanostoreQrl<T>(qrl: QRL<WritableAtom<T> | Atom<T>>): Signal<T> {
const store = useSignal<NoSerialize<ReadableAtom<T>> | undefined>(undefined); const signal = useSignal<T | undefined>(undefined);
const storeSignal = useSignal<NoSerialize<WritableAtom<T> | Atom<T>> | undefined>(undefined);
store.value = noSerialize(atom); useTask$(async ({ track }) => {
const unsubscribe = atom.subscribe((value) => { let store: WritableAtom<T> | Atom<T> | undefined = storeSignal.value;
state.value = 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()); // eslint-disable-next-line qwik/no-use-visible-task
return state; useVisibleTask$(async ({ cleanup }) => {
let store: WritableAtom<T> | Atom<T> | 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<T>;
}
export const useNanostore$ = implicit$FirstArg(useNanostoreQrl);

4
src/lib/stores.ts Normal file
View file

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

View file

@ -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 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";
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 { 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 = useSignal<StereoFile[]>([]); const files = useNanostore$<StereoFile[]>(DashboardFiles);
const loaded = useSignal(false); const loaded = useSignal(false);
const uploadingFile = useSignal<NoSerialize<File> | undefined>();
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;
}) });
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 ( return (
<> <>
<Controlbar /> <Controlbar />
<a href={OAUTH_LINK}>oauth</a> <a href={OAUTH_LINK}>oauth</a>
<input
onChange$={(e: Event) => uploadingFile.value = noSerialize((e.target as HTMLInputElement).files![0])}
type="file"
/>
<button <div class="grid grid-cols-4 gap-4 p-4 mb-18">
onClick$={uploadFile}
>
upload
</button>
<div class="grid grid-cols-3 gap-4 p-4">
{/* TODO: make ts better :broken_heart: */} {/* TODO: make ts better :broken_heart: */}
{!loaded.value ? <p>Loading...</p> : ( {!loaded.value ? <p>Loading...</p> : (
files.value.length === 0 ? ( <p> no files found fr </p> ) files.value.length === 0 ? ( <p> no files found fr </p> )