Compare commits
No commits in common. "dashboard-refresh" and "main" have entirely different histories.
dashboard-
...
main
15 changed files with 351 additions and 346 deletions
|
@ -1,11 +1,9 @@
|
|||
# stereo.cat frontend
|
||||
|
||||
written in typescript with qwik & bun
|
||||
written in typescript with qwik
|
||||
|
||||
## development
|
||||
https://bun.sh/docs/installation
|
||||
|
||||
```bash
|
||||
## running in dev env
|
||||
```
|
||||
git clone https://git.iwakura.rip/stereo.cat/frontend.git
|
||||
git submodule update --init --recursive
|
||||
bun install
|
||||
|
@ -13,5 +11,4 @@ bun dev
|
|||
```
|
||||
|
||||
## disclaimer
|
||||
|
||||
All graphic assets belonging to stereo.cat may not be used in unofficial instances, forks or versions of our software. Please replace them if you are hosting our software yourself, they can be found in the ``public`` folder in this repository. More information (like the full license) can be found [here](https://git.iwakura.rip/stereo.cat/public)
|
||||
|
|
88
src/components/dashboard/Controlbar.tsx
Normal file
88
src/components/dashboard/Controlbar.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
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>
|
||||
)
|
||||
})
|
199
src/components/dashboard/File.tsx
Normal file
199
src/components/dashboard/File.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,45 +0,0 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import { SolarLibraryLinear, SolarQuestionCircleLinear, SolarRoundedMagniferLinear, SolarSettingsLinear, SolarUploadMinimalisticLinear, StereoCircularProgress, StereoLogoLinear } from "../misc/Icons";
|
||||
|
||||
export default component$(() => {
|
||||
const used = 3.8;
|
||||
const total = 15;
|
||||
return (
|
||||
<div class="absolute bottom-0 left-0 flex items-center justify-between p-7 px-16 w-full">
|
||||
<div style={{
|
||||
borderRadius: "999px",
|
||||
border: "0.5px solid #FF264E",
|
||||
background: "rgba(255, 38, 78, 0.15)",
|
||||
boxShadow: "0px 4px 20px 0px rgba(255, 38, 78, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 1px 0px rgba(0, 0, 0, 0.04), 0px 4px 8px 0px rgba(255, 38, 78, 0.12) inset, 0px 1px 3px 0px rgba(255, 38, 78, 0.24) inset",
|
||||
backdropFilter: "blur(12px)",
|
||||
}} class="flex items-center justify-center px-6 py-4 gap-2 text-white text-xl">
|
||||
<StereoCircularProgress value={used/total} class="text-2xl"/>
|
||||
<p>{used} / {total} GB</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
borderRadius: "999px",
|
||||
border: "0.5px solid #FF264E",
|
||||
background: "rgba(255, 38, 78, 0.15)",
|
||||
boxShadow: "0px 4px 20px 0px rgba(255, 38, 78, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 1px 0px rgba(0, 0, 0, 0.04), 0px 4px 8px 0px rgba(255, 38, 78, 0.12) inset, 0px 1px 3px 0px rgba(255, 38, 78, 0.24) inset",
|
||||
backdropFilter: "blur(12px)",
|
||||
}} class="flex items-center justify-center px-6 py-4 gap-5 text-white text-3xl absolute left-1/2 transform -translate-x-1/2">
|
||||
<StereoLogoLinear />
|
||||
<SolarLibraryLinear />
|
||||
<SolarUploadMinimalisticLinear />
|
||||
<SolarRoundedMagniferLinear />
|
||||
<SolarSettingsLinear />
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
borderRadius: "999px",
|
||||
border: "0.5px solid #FF264E",
|
||||
background: "rgba(255, 38, 78, 0.15)",
|
||||
boxShadow: "0px 4px 20px 0px rgba(255, 38, 78, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 1px 0px rgba(0, 0, 0, 0.04), 0px 4px 8px 0px rgba(255, 38, 78, 0.12) inset, 0px 1px 3px 0px rgba(255, 38, 78, 0.24) inset",
|
||||
backdropFilter: "blur(12px)",
|
||||
}} class="flex items-center justify-center px-5 py-5 gap-2 text-white text-3xl h-full">
|
||||
<SolarQuestionCircleLinear />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -1,45 +0,0 @@
|
|||
import { component$, useTask$ } from "@builder.io/qwik";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { userInfo } from "~/lib/stores";
|
||||
import { StereoUser } from "~/lib/types";
|
||||
|
||||
export default component$(() => {
|
||||
const greetings = [
|
||||
"what's on the agenda today, |?",
|
||||
"what's on your mind, |?",
|
||||
"what's the plan, |?",
|
||||
"ready to rock, |?",
|
||||
"what's brewing, |?",
|
||||
"what's the latest, |?",
|
||||
"how's your day going, |?",
|
||||
"need some inspiration, |?",
|
||||
"let's make some noise, |!",
|
||||
"welcome back, |!",
|
||||
"good to see you, |!",
|
||||
"what are we making today, |?",
|
||||
"time to make some magic, |!",
|
||||
"let's get creative, |?",
|
||||
"what's the vibe today, |?",
|
||||
]
|
||||
|
||||
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
|
||||
const user = useNanostore$<StereoUser>(userInfo);
|
||||
|
||||
const splits = greeting.split("|");
|
||||
|
||||
useTask$(({ track }) => {
|
||||
track(() => user.value);
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="rounded-lg">
|
||||
<p class="font-medium text-4xl text-stereo/45">
|
||||
{splits[0]}
|
||||
<span class="text-stereo">
|
||||
@{user.value?.username || "..."}
|
||||
</span>
|
||||
{splits[1]}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import { StereoLogoBold } from "../misc/Icons";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
|
@ -7,7 +7,7 @@ export default component$(() => {
|
|||
<div class="flex flex-col flex-shrink h-full justify-start items-start gap-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="flex gap-[1ch]">
|
||||
<StereoLogoBold class="w-8 h-8 text-stereo" />
|
||||
<StereoLogo class="w-8 h-8 text-stereo" />
|
||||
<span class="text-white font-medium text-2xl">stereo<span class="text-stereo font-bold">.</span>cat</span>
|
||||
</span>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import { StereoLogoBold } from "../misc/Icons";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
|
||||
export default component$(() => {
|
||||
const items = [
|
||||
|
@ -17,7 +17,7 @@ export default component$(() => {
|
|||
data-aos-duration="1000"
|
||||
class="fixed flex items-center justify-start top-6 left-1/2 transform -translate-x-1/2 bg-neutral-950 p-8 h-10 rounded-full lg:w-2/3 md:w-4/5 w-4/5 z-[9999999] shadow-lg">
|
||||
<div class="flex flex-1/3">
|
||||
<StereoLogoBold class="w-10 h-10 text-stereo hover:text-white transition-all duration-300 hover:cursor-pointer" />
|
||||
<StereoLogo class="w-10 h-10 text-stereo hover:text-white transition-all duration-300 hover:cursor-pointer" />
|
||||
</div>
|
||||
<div class="w-full hidden md:flex flex-grow gap-4 items-center justify-center">
|
||||
{items.map(({ text, href, highlighted: h }) => (
|
||||
|
|
|
@ -28,6 +28,7 @@ export default component$(() => {
|
|||
try {
|
||||
const response = await ky.get(`https://api.lanyard.rest/v1/users/${id}`).json<LanyardResponse>();
|
||||
lanyard.value = response;
|
||||
console.log("Lanyard data:", lanyard.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching lanyard data:", error);
|
||||
}
|
||||
|
@ -93,16 +94,14 @@ export default component$(() => {
|
|||
/>
|
||||
|
||||
<Testimonial
|
||||
pfp="https://git.iwakura.rip/avatars/38bbf57a26f2891c59102582240386a4e2fa52b3999374673e0f4c4249ed4149?size=512"
|
||||
nickname="hexlocation"
|
||||
id="1325924978805440522"
|
||||
quote="I've been using stereo for a while now, and I can't imagine going back to any other file host. It's just that good!"
|
||||
/>
|
||||
|
||||
<Testimonial
|
||||
pfp="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fclipartcraft.com%2Fimages%2Ffire-emoji-transparent-snapchat-4.png&f=1&nofb=1&ipt=d59b80eec4d535f7f10b618a0daa9c0689a408643eaa6c1a054c0a03e7ca1835"
|
||||
nickname="typed"
|
||||
quote="stereo.cat saved my house from a fire, because when I was signing up I realized the email I had to log into needed my phone for 2fa, so when I went to the kitchen and grabbed my phone I noticed the stove was on, I thankfully turned it off. Thank you stereo.cat"
|
||||
nickname="an anonymous user"
|
||||
quote="stereo has changed the way I share files, it's so easy to use and the performance is top-notch. Highly recommend!"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xl text-white/50">
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { QwikIntrinsicElements } from "@builder.io/qwik";
|
||||
|
||||
// Solar - https://icones.js.org/collection/solar
|
||||
|
||||
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>
|
||||
|
@ -28,6 +26,7 @@ export function SolarLinkRoundBold(props: QwikIntrinsicElements['svg'], key: str
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarDownloadMinimalisticBold(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}><path fill="currentColor" d="M12.554 16.506a.75.75 0 0 1-1.107 0l-4-4.375a.75.75 0 0 1 1.107-1.012l2.696 2.95V3a.75.75 0 0 1 1.5 0v11.068l2.697-2.95a.75.75 0 1 1 1.107 1.013z" /><path fill="currentColor" d="M3.75 15a.75.75 0 0 0-1.5 0v.055c0 1.367 0 2.47.117 3.337c.12.9.38 1.658.981 2.26c.602.602 1.36.86 2.26.982c.867.116 1.97.116 3.337.116h6.11c1.367 0 2.47 0 3.337-.116c.9-.122 1.658-.38 2.26-.982s.86-1.36.982-2.26c.116-.867.116-1.97.116-3.337V15a.75.75 0 0 0-1.5 0c0 1.435-.002 2.436-.103 3.192c-.099.734-.28 1.122-.556 1.399c-.277.277-.665.457-1.4.556c-.755.101-1.756.103-3.191.103H9c-1.435 0-2.437-.002-3.192-.103c-.734-.099-1.122-.28-1.399-.556c-.277-.277-.457-.665-.556-1.4c-.101-.755-.103-1.756-.103-3.191" /></svg>
|
||||
|
@ -38,92 +37,3 @@ export function SvgSpinnersBarsRotateFade(props: QwikIntrinsicElements['svg'], k
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".14" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".29" transform="rotate(30 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".43" transform="rotate(60 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".57" transform="rotate(90 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".71" transform="rotate(120 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".86" transform="rotate(150 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" transform="rotate(180 12 12)" /><animateTransform attributeName="transform" calcMode="discrete" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12;360 12 12" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SolarLibraryLinear(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-width="1.5"><path d="M19.562 7a2.132 2.132 0 0 0-2.1-2.5H6.538a2.132 2.132 0 0 0-2.1 2.5M17.5 4.5c.028-.26.043-.389.043-.496a2 2 0 0 0-1.787-1.993C15.65 2 15.52 2 15.26 2H8.74c-.26 0-.391 0-.497.011a2 2 0 0 0-1.787 1.993c0 .107.014.237.043.496" /><path stroke-linecap="round" d="M15 18H9" /><path d="M2.384 13.793c-.447-3.164-.67-4.745.278-5.77C3.61 7 5.298 7 8.672 7h6.656c3.374 0 5.062 0 6.01 1.024s.724 2.605.278 5.769l-.422 3c-.35 2.48-.525 3.721-1.422 4.464s-2.22.743-4.867.743h-5.81c-2.646 0-3.97 0-4.867-.743s-1.072-1.983-1.422-4.464z" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SolarUploadMinimalisticLinear(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}><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 15c0 2.828 0 4.243.879 5.121C4.757 21 6.172 21 9 21h6c2.828 0 4.243 0 5.121-.879C21 19.243 21 17.828 21 15m-9 1V3m0 0l4 4.375M12 3L8 7.375" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarRoundedMagniferLinear(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-width="1.5"><circle cx="11" cy="11" r="9" /><path stroke-linecap="round" d="M21.812 20.975c-.063.095-.176.208-.403.434c-.226.227-.34.34-.434.403a1.13 1.13 0 0 1-1.62-.408c-.053-.1-.099-.254-.19-.561c-.101-.335-.151-.503-.161-.621a1.13 1.13 0 0 1 1.218-1.218c.118.01.285.06.621.16c.307.092.46.138.56.192a1.13 1.13 0 0 1 .409 1.619Z" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarSettingsLinear(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-width="1.5"><circle cx="12" cy="12" r="3" /><path d="M13.765 2.152C13.398 2 12.932 2 12 2s-1.398 0-1.765.152a2 2 0 0 0-1.083 1.083c-.092.223-.129.484-.143.863a1.62 1.62 0 0 1-.79 1.353a1.62 1.62 0 0 1-1.567.008c-.336-.178-.579-.276-.82-.308a2 2 0 0 0-1.478.396C4.04 5.79 3.806 6.193 3.34 7s-.7 1.21-.751 1.605a2 2 0 0 0 .396 1.479c.148.192.355.353.676.555c.473.297.777.803.777 1.361s-.304 1.064-.777 1.36c-.321.203-.529.364-.676.556a2 2 0 0 0-.396 1.479c.052.394.285.798.75 1.605c.467.807.7 1.21 1.015 1.453a2 2 0 0 0 1.479.396c.24-.032.483-.13.819-.308a1.62 1.62 0 0 1 1.567.008c.483.28.77.795.79 1.353c.014.38.05.64.143.863a2 2 0 0 0 1.083 1.083C10.602 22 11.068 22 12 22s1.398 0 1.765-.152a2 2 0 0 0 1.083-1.083c.092-.223.129-.483.143-.863c.02-.558.307-1.074.79-1.353a1.62 1.62 0 0 1 1.567-.008c.336.178.579.276.819.308a2 2 0 0 0 1.479-.396c.315-.242.548-.646 1.014-1.453s.7-1.21.751-1.605a2 2 0 0 0-.396-1.479c-.148-.192-.355-.353-.676-.555A1.62 1.62 0 0 1 19.562 12c0-.558.304-1.064.777-1.36c.321-.203.529-.364.676-.556a2 2 0 0 0 .396-1.479c-.052-.394-.285-.798-.75-1.605c-.467-.807-.7-1.21-1.015-1.453a2 2 0 0 0-1.479-.396c-.24.032-.483.13-.82.308a1.62 1.62 0 0 1-1.566-.008a1.62 1.62 0 0 1-.79-1.353c-.014-.38-.05-.64-.143-.863a2 2 0 0 0-1.083-1.083Z" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarQuestionCircleLinear(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"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" /><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M10.125 8.875a1.875 1.875 0 1 1 2.828 1.615c-.475.281-.953.708-.953 1.26V13" /><circle cx="12" cy="16" r="1" fill="currentColor" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Stereo
|
||||
|
||||
export function StereoLogoBold(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg width="1em" height="1em" viewBox="0 0 281 248" fill="none" xmlns="http://www.w3.org/2000/svg" {...props} key={key}><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>
|
||||
)
|
||||
}
|
||||
|
||||
export function StereoLogoLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg width="1em" height="1em" viewBox="0 0 21 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...props} key={key}><path d="M1.26893 2.381C1.21059 1.92943 1.28782 1.4706 1.49067 1.06358C1.58237 0.879581 1.8316 0.851009 1.96205 1.00954L6.83479 6.93126H14.1652L19.038 1.00954C19.1684 0.851008 19.4176 0.879581 19.5093 1.06358C19.7122 1.4706 19.7894 1.92943 19.7311 2.381L18.7758 9.77571C18.235 13.9617 14.693 17.0937 10.5 17.0937C6.30697 17.0937 2.76496 13.9617 2.2242 9.77571L1.26893 2.381Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function StereoCircularProgress(
|
||||
{ value, ...svgProps }: QwikIntrinsicElements['svg'] & { value: number },
|
||||
key: string
|
||||
) {
|
||||
const radius = 10;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const dashOffset = circumference * (1 - value);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...svgProps}
|
||||
key={key}
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-opacity={0.25}
|
||||
stroke-width={2}
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width={2}
|
||||
stroke-dasharray={`${circumference} ${circumference}`}
|
||||
stroke-dashoffset={dashOffset}
|
||||
transform="rotate(-90 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
9
src/components/misc/StereoLogo.tsx
Normal file
9
src/components/misc/StereoLogo.tsx
Normal 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>
|
||||
)
|
||||
})
|
|
@ -15,5 +15,4 @@ export const api = {
|
|||
return await client.post('upload', { body: formData });
|
||||
},
|
||||
delete: async (uid: string) => await client.delete(uid).json(),
|
||||
me: async () => (await client.get('auth/me').json() as any).user,
|
||||
}
|
|
@ -1,12 +1,5 @@
|
|||
import { atom } from "nanostores";
|
||||
import { StereoFile, StereoUser } from "./types";
|
||||
import { StereoFile } from "./types";
|
||||
|
||||
export const areFilesLoaded = atom<boolean>(false);
|
||||
export const dashboardFiles = atom<StereoFile[]>([]);
|
||||
export const userInfo = atom<StereoUser>({
|
||||
id: "1",
|
||||
username: "user",
|
||||
blacklisted: false,
|
||||
email: "user@example.com",
|
||||
created_at: Date.now().toString(),
|
||||
});
|
|
@ -6,11 +6,3 @@ export type StereoFile = {
|
|||
CreatedAt: string;
|
||||
Mime: string;
|
||||
}
|
||||
|
||||
export type StereoUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
blacklisted: boolean;
|
||||
email: string;
|
||||
created_at: string;
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
import { component$, Signal, useSignal, useTask$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
|
||||
import OSBar from "~/components/dashboard/OSBar";
|
||||
import TitleBar from "~/components/dashboard/TitleBar";
|
||||
import Controlbar from "~/components/dashboard/Controlbar";
|
||||
// import Dropzone from "~/components/Dropzone";
|
||||
import File from "~/components/dashboard/File";
|
||||
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/misc/Icons";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { api } from "~/lib/api";
|
||||
import { OAUTH_LINK } from "~/lib/constants";
|
||||
import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
|
||||
export const useAuthCheck = routeLoader$(({ cookie, redirect: r }) => {
|
||||
const jwt = cookie.get("jwt");
|
||||
if (jwt) return {};
|
||||
throw r(302, OAUTH_LINK);
|
||||
throw r(302, "/");
|
||||
});
|
||||
|
||||
export default component$(() => {
|
||||
|
@ -22,127 +22,47 @@ export default component$(() => {
|
|||
useVisibleTask$(async () => {
|
||||
loaded.value = false;
|
||||
files.value = await api.list();
|
||||
console.log("Files loaded:", files.value);
|
||||
loaded.value = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex flex-col w-full h-screen p-8 gap-6 bg-gradient-to-b from-stereo/20 to-transparent justify-self-end">
|
||||
<TitleBar />
|
||||
<Files files={files} loaded={loaded} />
|
||||
<OSBar />
|
||||
<>
|
||||
{/* <Dropzone /> */}
|
||||
<Controlbar />
|
||||
{!loaded.value ? (
|
||||
<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>
|
||||
);
|
||||
});
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
const Files = component$<{
|
||||
files: Signal<StereoFile[]>;
|
||||
loaded: Signal<boolean>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
}>(({ files, loaded }) => {
|
||||
|
||||
const File = component$(({ file }: { file: StereoFile }) => {
|
||||
const Preview = component$(() => {
|
||||
type FileType =
|
||||
| "image"
|
||||
| "video"
|
||||
| "audio"
|
||||
| "other";
|
||||
|
||||
const fileType: Signal<FileType> = useSignal<FileType>("other");
|
||||
const type = file.Mime.split("/")[1];
|
||||
|
||||
useTask$(async () => {
|
||||
if (
|
||||
["jpeg", "jpg", "png", "gif", "webp"]
|
||||
.includes(type)
|
||||
) fileType.value = "image";
|
||||
|
||||
else if (
|
||||
["mp4", "webm", "ogg", "avi", "mov"]
|
||||
.includes(type)
|
||||
) fileType.value = "video";
|
||||
|
||||
else if (
|
||||
["mp3", "wav", "flac", "aac"]
|
||||
.includes(type)
|
||||
) fileType.value = "audio";
|
||||
|
||||
else fileType.value = "other";
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="w-full h-60 object-cover flex-grow relative">
|
||||
{fileType.value === "image" && (
|
||||
<img
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.ID}`}
|
||||
alt={file.Name}
|
||||
class="w-full h-60 object-cover flex-grow"
|
||||
/>
|
||||
)}
|
||||
|
||||
{fileType.value === "video" && (
|
||||
<video
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.ID}`}
|
||||
controls
|
||||
class="w-full h-60 object-cover flex-grow"
|
||||
/>
|
||||
)}
|
||||
|
||||
{fileType.value === "audio" && (
|
||||
<audio
|
||||
src={`/api/${file.ID}`}
|
||||
controls
|
||||
class="w-full h-60 object-cover flex-grow"
|
||||
/>
|
||||
)}
|
||||
|
||||
{fileType.value === "other" && (
|
||||
<div class="w-full h-60 flex items-center justify-center bg-gray-200 text-gray-500">
|
||||
<p>Unsupported file type</p>
|
||||
</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>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: "linear-gradient(180deg, rgba(255, 38, 78, 0.00) 57.21%, rgba(255, 38, 78, 0.17) 100%); ",
|
||||
boxShadow: "0px 4px 21.2px 2px rgba(255, 38, 78, 0.05)",
|
||||
}} class="transition-all rounded-3xl flex flex-col overflow-clip items-center justify-center">
|
||||
<Preview />
|
||||
|
||||
<div class="flex flex-col items-center justify-center text-center w-full h-full p-5">
|
||||
<p class="text-xl">{file.Name}</p>
|
||||
<p class="text-stereo/50 text-lg">
|
||||
{formatSize(file.Size)}
|
||||
<span class="text-stereo/40"> • </span>
|
||||
Uploaded on {new Date(file.CreatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="mb-6 flex flex-col w-full max-h-full overflow-y-auto overflow-x-hidden mask-clip-content rounded-3xl">
|
||||
<div class="w-full h-full grid grid-cols-4 gap-2">
|
||||
: (
|
||||
<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.ID} file={file} />
|
||||
<File key={file.Name} file={file} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
import { $, component$, Slot, useOnDocument, useVisibleTask$ } from '@builder.io/qwik';
|
||||
import { $, component$, Slot, useOnDocument } from '@builder.io/qwik';
|
||||
import AOS from 'aos';
|
||||
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$(() => {
|
||||
const info = useNanostore$<StereoUser>(userInfo);
|
||||
|
||||
useVisibleTask$(async () => {
|
||||
info.value = await api.me();
|
||||
})
|
||||
|
||||
useOnDocument("DOMContentLoaded", $(async () => {
|
||||
|
||||
useOnDocument("DOMContentLoaded", $(() => {
|
||||
AOS.init({
|
||||
once: true,
|
||||
duration: 1000,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue