Compare commits
No commits in common. "31594891e5493b4e122b40d016c799db2c3e3656" and "7edcd0a49cb13159653d7c1a1b6651bd750824ee" have entirely different histories.
31594891e5
...
7edcd0a49c
8 changed files with 43 additions and 176 deletions
3
bun.lock
3
bun.lock
|
@ -5,7 +5,6 @@
|
||||||
"name": "my-qwik-empty-starter",
|
"name": "my-qwik-empty-starter",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ky": "^1.8.1",
|
"ky": "^1.8.1",
|
||||||
"nanostores": "^1.0.1",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@builder.io/qwik": "^1.14.1",
|
"@builder.io/qwik": "^1.14.1",
|
||||||
|
@ -752,8 +751,6 @@
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="],
|
|
||||||
|
|
||||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
"vite-tsconfig-paths": "^4.2.1"
|
"vite-tsconfig-paths": "^4.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ky": "^1.8.1",
|
"ky": "^1.8.1"
|
||||||
"nanostores": "^1.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,10 @@
|
||||||
import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
import { component$ } 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";
|
|
||||||
|
|
||||||
|
// TODO: add upload button and other stuff fr
|
||||||
export default component$(() => {
|
export default component$(() => {
|
||||||
const files = useNanostore$<StereoFile[]>(DashboardFiles);
|
|
||||||
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-gray-950/50 backdrop-blur-3xl w-1/3 p-2 rounded-lg flex items-center justify-between">
|
<div class="absolute fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-red-400 w-1/4 p-4 rounded-lg flex items-center justify-center">
|
||||||
<input
|
hi
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
style="display: none;"
|
|
||||||
onChange$={async (e: Event) => {
|
|
||||||
uploadingFile.value = noSerialize((e.target as HTMLInputElement).files![0]);
|
|
||||||
await uploadFile();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<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>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -1,44 +1,15 @@
|
||||||
import { component$ } from "@builder.io/qwik";
|
import { component$ } from "@builder.io/qwik";
|
||||||
import { StereoFile } from "~/lib/types";
|
import { StereoFile } from "~/lib/types";
|
||||||
|
|
||||||
type FileProps = {
|
export default component$(({ file }: { file: StereoFile }) => {
|
||||||
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 class="rounded-xl bg-slate-900 flex flex-col overflow-hidden">
|
<div key={file.ID}>
|
||||||
|
<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
|
<img src={`data:image/png;base64,${file.Base64}`} alt="Stereo File" class="w-full h-auto" />
|
||||||
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>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -1,7 +0,0 @@
|
||||||
import { QwikIntrinsicElements } from "@builder.io/qwik";
|
|
||||||
|
|
||||||
export function SolarUploadLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
|
||||||
return (
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path d="M17 9.002c2.175.012 3.353.109 4.121.877C22 10.758 22 12.172 22 15v1c0 2.829 0 4.243-.879 5.122C20.243 22 18.828 22 16 22H8c-2.828 0-4.243 0-5.121-.878C2 20.242 2 18.829 2 16v-1c0-2.828 0-4.242.879-5.121c.768-.768 1.946-.865 4.121-.877"></path><path stroke-linejoin="round" d="M12 15V2m0 0l3 3.5M12 2L9 5.5"></path></g></svg>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
import {
|
|
||||||
implicit$FirstArg,
|
|
||||||
noSerialize,
|
|
||||||
NoSerialize,
|
|
||||||
QRL,
|
|
||||||
Signal,
|
|
||||||
useSignal,
|
|
||||||
useTask$,
|
|
||||||
useVisibleTask$,
|
|
||||||
} from "@builder.io/qwik";
|
|
||||||
import { Atom, WritableAtom } from "nanostores";
|
|
||||||
|
|
||||||
function writeable<T = any>(store: Atom<T> | WritableAtom<T>): store is WritableAtom<T> {
|
|
||||||
return typeof (store as WritableAtom<T>).set === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNanostoreQrl<T>(qrl: QRL<WritableAtom<T> | Atom<T>>): Signal<T> {
|
|
||||||
const signal = useSignal<T | undefined>(undefined);
|
|
||||||
const storeSignal = useSignal<NoSerialize<WritableAtom<T> | Atom<T>> | undefined>(undefined);
|
|
||||||
|
|
||||||
useTask$(async ({ track }) => {
|
|
||||||
let store: WritableAtom<T> | Atom<T> | 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line qwik/no-use-visible-task
|
|
||||||
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);
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { atom } from "nanostores";
|
|
||||||
import { StereoFile } from "./types";
|
|
||||||
|
|
||||||
export const DashboardFiles = atom<StereoFile[]>([]);
|
|
|
@ -1,32 +1,57 @@
|
||||||
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
import { $, component$, noSerialize, NoSerialize, 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 = useNanostore$<StereoFile[]>(DashboardFiles);
|
const files = useSignal<StereoFile[]>([]);
|
||||||
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"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="grid grid-cols-4 gap-4 p-4 mb-18">
|
<button
|
||||||
|
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> )
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue