diff --git a/src/components/landing/Testimonials.tsx b/src/components/landing/Testimonials.tsx
index fb73fe8..7731237 100644
--- a/src/components/landing/Testimonials.tsx
+++ b/src/components/landing/Testimonials.tsx
@@ -2,7 +2,7 @@ import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
import ky from "ky";
export default component$(() => {
- type TestimonialProps = {
+ type Testimonial = {
nickname: string;
pfp?: string;
id?: string;
@@ -19,7 +19,31 @@ export default component$(() => {
success: boolean;
}
- const Testimonial = component$(({ nickname, id, quote, pfp }: TestimonialProps) => {
+ // Array of testimonial objects
+ const testimonials: Testimonial[] = [
+ {
+ nickname: "grng",
+ id: "829372486780715018",
+ quote: "stereo is the best file host I've ever used, it's fast, reliable, and the interface is so clean and easy to use. I love it!"
+ },
+ {
+ nickname: "hexlocation",
+ pfp: "https://git.iwakura.rip/avatars/38bbf57a26f2891c59102582240386a4e2fa52b3999374673e0f4c4249ed4149?size=512",
+ 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!"
+ },
+ {
+ nickname: "typed",
+ 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",
+ 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: "starlo",
+ pfp: "https://cdn.discordapp.com/avatars/962173926849519716/04af851a9954ba623b5d2eb5dd189785.webp?size=512",
+ quote: "I was expecting that stereo would be a greater website for storing my images, and I wasn't wrong and my doubts are always correct, it's more cooler than I expected 🔥"
+ }
+ ];
+
+ const Testimonial = component$(({ nickname, id, quote, pfp }: Testimonial) => {
const lanyard = useSignal();
useOnDocument("DOMContentLoaded", $(async () => {
@@ -86,23 +110,9 @@ export default component$(() => {
-
-
-
-
-
+ {testimonials.map((t, i) => (
+
+ ))}
and many, many more...
diff --git a/src/lib/misc.ts b/src/lib/misc.ts
new file mode 100644
index 0000000..f322c68
--- /dev/null
+++ b/src/lib/misc.ts
@@ -0,0 +1,7 @@
+export function debounce void>(fn: T, delay = 200) {
+ let timer: ReturnType | null = null;
+ return (...args: Parameters) => {
+ if (timer) clearTimeout(timer);
+ timer = setTimeout(async () => await fn(...args), delay);
+ };
+}
\ No newline at end of file
diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx
index 22d044e..b601c8d 100644
--- a/src/routes/dashboard/index.tsx
+++ b/src/routes/dashboard/index.tsx
@@ -1,36 +1,28 @@
-import { $, component$, Signal, useSignal, useTask$, useVisibleTask$ } from "@builder.io/qwik";
+/* eslint-disable qwik/no-use-visible-task */
+import { component$, Signal, useSignal, useTask$, useVisibleTask$ } from "@builder.io/qwik";
import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
import Actionbar from "~/components/dashboard/Actionbar";
import Settings from "~/components/dashboard/Settings";
import Titlebar from "~/components/dashboard/Titlebar";
// import Dropzone from "~/components/Dropzone";
-import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api";
-import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
+import { debounce } from "~/lib/misc";
import { StereoFile } from "~/lib/types";
-export const useAuthCheck = routeLoader$(({ cookie, redirect: r }) => {
+export const useAuthenticated = routeLoader$(({ cookie, redirect: r }) => {
const jwt = cookie.get("jwt");
if (jwt) return {};
+
throw r(302, "/api/auth/login");
});
export default component$(() => {
- const files = useNanostore$(dashboardFiles);
- const loaded = useNanostore$(areFilesLoaded);
-
- useVisibleTask$(async () => {
- loaded.value = false;
- files.value = await api.list();
- loaded.value = true;
- });
-
return (
<>
>
@@ -45,11 +37,8 @@ const formatSize = (bytes: number) => {
}
-const Files = component$<{
- files: Signal;
- loaded: Signal;
-}>(({ files }) => {
- const File = component$(({ file }: { file: StereoFile }) => {
+const Files = component$(() => {
+ const File = component$(({ file }: { file: StereoFile }) => {
const Preview = component$(() => {
type FileType = "image" | "video" | "audio" | "other";
const fileType: Signal = useSignal("other");
@@ -90,7 +79,7 @@ const Files = component$<{
)}
{fileType.value === "other" && (
-
Unsupported file type
+
unsupported file type
)}
@@ -119,26 +108,68 @@ const Files = component$<{
});
const loadingMore = useSignal(false);
+ const hasMore = useSignal(true);
+ const sentinel = useSignal();
- const onScroll = $(async (e: Event) => {
- const element = e.target as HTMLElement;
- if (!element) return;
+ const files = useSignal([]);
+ const page = useSignal(1);
- const isAtBottom = element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
- if (isAtBottom) {
- console.log("Loading more files...");
- }
- });
+ // TODO: make it load enough images to fill the viewport instead
+ useVisibleTask$(({ cleanup }) => {
+ if (!sentinel.value) return;
- return (
-
-
- {files.value.map((file) => (
-
- ))}
-
-
- );
+ const observer = new IntersectionObserver((entries) => {
+ const entry = entries[0];
+ if (
+ entry.isIntersecting &&
+ !loadingMore.value &&
+ hasMore.value
+ ) {
+ loadingMore.value = true;
+
+ console.log("Loading more files...");
+
+ debounce(async () => {
+ const newFiles = await api.list(page.value, 4); // gotta be a multiple of 4 so pages dont look weird
+ if (newFiles.length === 0) {
+ hasMore.value = false;
+ if (sentinel.value) observer.unobserve(sentinel.value);
+ } else {
+ files.value = [...files.value, ...newFiles];
+ page.value++;
+ }
+ loadingMore.value = false;
+ }, 50)();
+ }
+ });
+
+ observer.observe(sentinel.value);
+
+ cleanup(() => {
+ if (observer && sentinel.value) observer.unobserve(sentinel.value);
+ });
+ });
+
+ return (
+
+
+ {files.value.map((file) => (
+
+ ))}
+
+ {hasMore.value && (
+
+ {/* {loadingMore.value && (
+ loading spinner?
+ )} */}
+
+ )}
+
+
+ );
});
export const head: DocumentHead = {
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 4fca559..c7b64bd 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,6 +1,6 @@
/* eslint-disable qwik/jsx-img */
import { component$ } from "@builder.io/qwik";
-import { DocumentHead, routeLoader$ } from "@builder.io/qwik-city";
+import { DocumentHead } from "@builder.io/qwik-city";
import CallToAction from "~/components/landing/CallToAction";
import Footer from "~/components/landing/Footer";
import Hero from "~/components/landing/Hero";
@@ -8,16 +8,6 @@ import Navbar from "~/components/landing/Navbar";
import Stats from "~/components/landing/Stats";
import Testimonials from "~/components/landing/Testimonials";
-export const useAuthCheck = routeLoader$(({ cookie, redirect: r, query }) => {
- const jwt = cookie.get("jwt");
- const set = Boolean(query.get("jwt_set"));
-
- if (jwt && set) {
- throw r(302, "/api/auth/login");
- }
- return {};
-});
-
export default component$(() => {
return (
<>