make the landing page look beautiful 😍😍😍

This commit is contained in:
grngxd 2025-06-14 17:05:49 +01:00
parent a72ebe853f
commit d36c98cb49
22 changed files with 481 additions and 29 deletions

View file

@ -3,8 +3,8 @@ 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 "./Icons";
import StereoLogo from "./StereoLogo";
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "../misc/Icons";
import StereoLogo from "../misc/StereoLogo";
export default component$(() => {
const loaded = useNanostore$<boolean>(areFilesLoaded);

View file

@ -3,7 +3,7 @@ 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 "./Icons";
import { SolarClipboardAddBold, SolarDownloadMinimalisticBold, SolarTrashBin2Bold } from "../misc/Icons";
type FileProps = {
file: StereoFile;

View file

@ -0,0 +1,23 @@
import { component$ } from "@builder.io/qwik";
import { OAUTH_LINK } from "~/lib/constants";
export default component$(() => (
<div
data-aos="fade-up"
data-aos-duration="1000"
class="flex flex-col gap-1.5 items-center justify-center w-4/5 bg-gradient-to-b from-stereo/30 to-transparent rounded-2xl h-96 py-4"
>
<p class="text-4xl text-center">
ready to try the <span class="text-stereo">stereo</span> experience?
</p>
<p class="text-xl text-white/80 text-center">
join over <span class="text-stereo">100k</span> other people hosting their files with <span class="text-stereo">stereo</span>!
</p>
<a
href={OAUTH_LINK}
class="px-12 py-1.5 mt-1.5 text-lg font-medium text-white bg-stereo rounded-full hover:text-stereo hover:bg-white transition duration-300"
>
get started
</a>
</div>
));

View file

@ -0,0 +1,29 @@
import { component$ } from "@builder.io/qwik";
import StereoLogo from "../misc/StereoLogo";
export default component$(() => {
return (
<div class="w-screen flex py-16 px-16">
<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]">
<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>
<span class="text-white/80 font-light text-lg">
store all your precious moments with <span class="text-stereo font-medium">stereo</span>.
</span>
<span class="text-white/80 font-light text-md">
copyright © {new Date().getFullYear()} stereo.cat - all rights reserved.
</span>
</div>
</div>
<div class="flex flex-grow justify-end items-start gap-12">
</div>
</div>
)
})

View file

@ -0,0 +1,83 @@
import { component$, useSignal } from "@builder.io/qwik";
import { useRelativeMouse } from "~/hooks/mouse";
import { OAUTH_LINK } from "~/lib/constants";
import GradientBorder from "../misc/GradientBorder";
export default component$(() => {
const ref1 = useSignal<HTMLElement>();
const mouse1 = useRelativeMouse(ref1);
const ref2 = useSignal<HTMLElement>();
const mouse2 = useRelativeMouse(ref2);
return (
<div class="flex flex-col w-full bg-gradient-to-b from-stereo/30 to-transparent overflow-x-clip select-none">
<div class="mt-62 flex flex-col justify-center w-full h-full text-center">
<div class="flex flex-col items-center justify-center gap-2 font-light">
<p class="text-6xl">
you bring the files, we'll bring the <span class="text-stereo">magic</span>.
</p>
<p class="text-2xl text-white/80">
stereo is no-bs file-host inspired by Tixte, that you'll love to use
</p>
<div class="flex gap-4 mt-1">
<a
href={OAUTH_LINK}
class="px-6 py-1 text-lg font-medium text-white bg-stereo rounded-full hover:text-stereo hover:bg-white transition duration-300"
>
get started
</a>
<a
href="discord.gg"
target="_blank"
rel="noopener noreferrer"
class="px-6 py-1 text-lg font-medium text-stereo rounded-full border border-stereo hover:bg-stereo hover:text-white transition duration-300"
>
learn more
</a>
</div>
<div class="mt-10 flex items-center" style={{ perspective: "1000px" }}>
<GradientBorder
ref={ref1}
size="3px"
from="#ff264e"
to="transparent"
direction="to bottom"
style={{
transformStyle: "preserve-3d",
transformOrigin: "center center",
transform: `
rotateX(${(mouse1.y / 120) + 30}deg)
rotateY(${(-mouse1.x / 120) + 25}deg)
rotateZ(-10deg)
`,
}}
class="rounded-2xl -mr-12"
>
<img src="dashboard 1.png" class="h-[32rem] rounded-2xl shadow-2xl shadow-stereo/10" />
</GradientBorder>
<GradientBorder
ref={ref2}
size="3px"
from="#ff264e"
to="transparent"
direction="to bottom"
style={{
transformStyle: "preserve-3d",
transformOrigin: "center center",
transform: `
rotateX(${(mouse2.y / 120) + 30}deg)
rotateY(${(-mouse2.x / 60) - 25}deg)
rotateZ(10deg)
`,
}}
class="rounded-2xl -ml-12 shadow-2xl shadow-stereo/10"
>
<img src="dashboard 2.png" class="h-[36rem] rounded-2xl" />
</GradientBorder>
</div>
</div>
</div>
</div>
);
});

View file

@ -0,0 +1,41 @@
import { component$ } from "@builder.io/qwik";
import StereoLogo from "../misc/StereoLogo";
export default component$(() => {
const items = [
{ text: "Synopsis", href: "#" },
{ text: "Discord", href: "#" },
{ text: "Dashboard", href: "/dashboard", highlighted: true },
{ text: "Pricing", href: "#" },
{ text: "Terms", href: "#" }
];
return (
<div
data-aos="fade-down"
data-aos-anchor-placement="top-center"
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">
<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 }) => (
<a
key={text}
href={href}
class={
h ? "px-4 py-0.5 hover:text-stereo hover:bg-white rounded-full font-medium bg-stereo text-white transition duration-300 text-lg"
: "text-white font-light hover:text-stereo transition duration-300 text-lg"
}
>
{text}
</a>
))}
</div>
<div class="flex flex-1/3 items-center justify-end">
<span class="text-white group hover:text-stereo transition duration-300 font-medium text-2xl">stereo<span class="text-stereo group-hover:text-white transition duration-300 font-bold">.</span>cat</span>
</div>
</div>
)
});

View file

@ -0,0 +1,40 @@
import { component$ } from "@builder.io/qwik";
export default component$(() => {
type StatProps = {
stat: string,
description: string;
}
const Stat = component$(({ stat, description }: StatProps) => (
<div class="group hover:scale-105 transition-all duration-300 bg-gradient-to-t from-stereo/30 to-transparent p-8 px-12 rounded-2xl h-96 flex flex-col items-center text-center shadow-2xl shadow-stereo/15">
<div class="flex-grow flex items-center justify-center">
<p class="text-7xl font-bold text-stereo group-hover:text-white transition-colours duration-300">
{stat.includes("+") ? stat.substring(0, stat.length - 1) : stat}
{stat.endsWith("+") ? <span class="text-stereo/80 text-4xl align-super">+</span> : ""}
</p>
</div>
<p class="text-xl w-3/4 text-wrap text-white/30 group-hover:text-white/60 transition-colours duration-300">{description}</p>
</div>
) );
return (
<div
data-aos="fade-up"
data-aos-duration="1000"
class="flex flex-col gap-12 items-center justify-center w-full"
>
<div class="flex flex-col gap-1 items-center justify-center">
<p class="text-lg text-stereo font-bold uppercase">statistics</p>
<p class="text-2xl text-white/80">
we know what you're thinking, and we have the numbers to prove it.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<Stat stat="1.5M+" description="files hosted on stereo" />
<Stat stat="100k+" description="active users on stereo" />
<Stat stat="99.9%" description="uptime guarantee for all files" />
</div>
</div>
)
});

View file

@ -0,0 +1,112 @@
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
import ky from "ky";
export default component$(() => {
type TestimonialProps = {
nickname: string;
pfp?: string;
id?: string;
quote: string;
};
type LanyardResponse = {
data: {
discord_status: "online" | "idle" | "dnd";
discord_user: {
avatar: string;
}
},
success: boolean;
}
const Testimonial = component$(({ nickname, id, quote, pfp }: TestimonialProps) => {
const lanyard = useSignal<LanyardResponse>();
useOnDocument("DOMContentLoaded", $(async () => {
if (!id) return;
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);
}
}));
return (
<div
data-aos="fade-up"
data-aos-duration="1000"
class="flex gap-6 items-center bg-gradient-to-t from-stereo/15 to-stereo/5 rounded-2xl p-6 max-w-2xl shadow-2xl shadow-stereo/15"
>
<div class="relative h-30 aspect-square flex-shrink-0 rounded-full"
style={{
border: lanyard.value ? `3px solid ${
lanyard.value.data.discord_status === "online"
? "#43b581"
: lanyard.value.data.discord_status === "idle"
? "#faa61a"
: lanyard.value.data.discord_status === "dnd"
? "#f04747"
: "#7289da"
}` : "",
padding: lanyard.value ? `3px` : "0px",
}}>
<img
src={pfp ? pfp : (
lanyard.value
? `https://cdn.discordapp.com/avatars/${id}/${lanyard.value.data.discord_user.avatar}.png`
: `https://api.dicebear.com/9.x/shapes/svg?seed=${Math.random().toString(36).substring(2, 15)}.png`
)}
class="rounded-full h-full w-full object-cover"
/>
</div>
<div class="flex flex-col gap-2">
<p class="text-stereo text-9xl h-min -mb-18"></p>
<p class="text-lg text-white/90">
{quote}
</p>
<p class="text-white/60 font-light italic"> {nickname}</p>
</div>
</div>
);
});
return (
<div
data-aos="fade-up"
data-aos-duration="1000"
class="flex flex-col gap-6 items-center justify-center w-4/5"
>
<div class="flex flex-col gap-1 items-center justify-center">
<p class="text-lg text-stereo font-bold uppercase">testimonials</p>
<p class="text-2xl text-white/80">
don't just take our word for it, hear it from our users.
</p>
</div>
<div class="flex flex-col gap-6">
<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!"
/>
<Testimonial
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
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">
and many, many more...
</p>
</div>
);
});

View file

@ -0,0 +1,40 @@
import { component$, CSSProperties, QwikIntrinsicElements, Slot } from "@builder.io/qwik";
type GradientProps = {
size: string;
from: string;
to: string;
direction?: string;
};
export default component$((props: GradientProps & QwikIntrinsicElements["div"]) => {
const {
size,
from,
to,
direction,
style: userStyle,
...rest
} = props;
const borderStyle: CSSProperties = {
padding: size,
background: `linear-gradient(${direction || "to bottom"}, ${from}, ${to})`,
display: "inline-block",
};
// Only spread userStyle if it's an object
const mergedStyle =
userStyle && typeof userStyle === "object"
? { ...borderStyle, ...userStyle }
: borderStyle;
return (
<div
style={mergedStyle}
{...rest}
>
<Slot />
</div>
);
});

View file

@ -3,6 +3,5 @@
@theme {
--font-sans: 'DM Sans', sans-serif;
}
@plugin 'tailwind-scrollbar';
--color-stereo: #ff264e;
}

27
src/hooks/mouse.ts Normal file
View file

@ -0,0 +1,27 @@
import { $, Signal, useOnDocument, useStore } from "@builder.io/qwik";
export const useMouse = () => {
const pos = useStore({ x: 0, y: 0 });
useOnDocument("mousemove", $((event: MouseEvent) => {
const { clientX, clientY } = event;
pos.x = clientX;
pos.y = clientY;
}));
return pos
}
export const useRelativeMouse = (ref: Signal<HTMLElement | undefined>) => {
const pos = useStore({ x: 0, y: 0 });
useOnDocument("mousemove", $((event: MouseEvent) => {
const el = ref.value;
if (!el) return;
const rect = el.getBoundingClientRect();
pos.x = event.clientX - rect.left;
pos.y = event.clientY - rect.top;
}));
return pos;
};

View file

@ -1,9 +1,9 @@
import { component$, useVisibleTask$ } from "@builder.io/qwik";
import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
import Controlbar from "~/components/Controlbar";
import Controlbar from "~/components/dashboard/Controlbar";
// import Dropzone from "~/components/Dropzone";
import File from "~/components/File";
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/Icons";
import File from "~/components/dashboard/File";
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/misc/Icons";
import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api";
import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
@ -12,7 +12,6 @@ import { StereoFile } from "~/lib/types";
export const useAuthCheck = routeLoader$(({ cookie, redirect: r }) => {
const jwt = cookie.get("jwt");
if (jwt) return {};
throw r(302, "/");
});

View file

@ -1,18 +1,44 @@
/* eslint-disable qwik/jsx-img */
import { component$ } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";
import { OAUTH_LINK } from "~/lib/constants";
import { DocumentHead, routeLoader$ } from "@builder.io/qwik-city";
import CallToAction from "~/components/landing/CallToAction";
import Footer from "~/components/landing/Footer";
import Hero from "~/components/landing/Hero";
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 }) => {
const jwt = cookie.get("jwt");
if (!jwt) return {};
export const useAuthCheck = routeLoader$(({ cookie, redirect: r, query }) => {
const jwt = cookie.get("jwt");
const set = Boolean(query.get("jwt_set"));
throw r(302, "/dashboard");
if (jwt && set) {
throw r(302, "/dashboard");
}
return {};
});
export default component$(() => {
return (
<div class="flex flex-col items-center justify-center flex-grow">
<a href={OAUTH_LINK} class="bg-white text-black p-2 rounded-lg">clik heir 2 auth!!!!!!</a>
</div>
);
});
return (
<>
<Navbar />
<div class="flex flex-col flex-grow w-screen items-center mb-24 gap-56">
<Hero />
<Stats />
<Testimonials />
<CallToAction />
</div>
<Footer />
</>
);
});
export const head: DocumentHead = {
title: "Welcome to Qwik",
meta: [
{
name: "description",
content: "Qwik site description",
},
],
};

View file

@ -1,9 +1,25 @@
import { component$, Slot } from '@builder.io/qwik';
import { $, component$, Slot, useOnDocument } from '@builder.io/qwik';
import AOS from 'aos';
import 'aos/dist/aos.css';
export default component$(() => {
return (
<div class="flex flex-col min-h-screen bg-neutral-950 text-white scrollbar-thin overflow-scroll">
<Slot />
</div>
);
useOnDocument("DOMContentLoaded", $(() => {
AOS.init({
once: true,
duration: 1000,
offset: 100,
easing: 'ease-in-out',
});
}))
return (
<div
class="
flex flex-col
min-h-screen w-full overflow-x-clip
bg-neutral-950 text-white
">
<Slot />
</div>
);
});