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 ( <>