make the landing page look beautiful 😍😍😍
This commit is contained in:
parent
a72ebe853f
commit
d36c98cb49
22 changed files with 481 additions and 29 deletions
|
@ -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);
|
|
@ -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;
|
23
src/components/landing/CallToAction.tsx
Normal file
23
src/components/landing/CallToAction.tsx
Normal 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>
|
||||
));
|
29
src/components/landing/Footer.tsx
Normal file
29
src/components/landing/Footer.tsx
Normal 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>
|
||||
)
|
||||
})
|
83
src/components/landing/Hero.tsx
Normal file
83
src/components/landing/Hero.tsx
Normal 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>
|
||||
);
|
||||
});
|
41
src/components/landing/Navbar.tsx
Normal file
41
src/components/landing/Navbar.tsx
Normal 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>
|
||||
)
|
||||
});
|
40
src/components/landing/Stats.tsx
Normal file
40
src/components/landing/Stats.tsx
Normal 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>
|
||||
)
|
||||
});
|
112
src/components/landing/Testimonials.tsx
Normal file
112
src/components/landing/Testimonials.tsx
Normal 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>
|
||||
);
|
||||
});
|
40
src/components/misc/GradientBorder.tsx
Normal file
40
src/components/misc/GradientBorder.tsx
Normal 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>
|
||||
);
|
||||
});
|
|
@ -3,6 +3,5 @@
|
|||
|
||||
@theme {
|
||||
--font-sans: 'DM Sans', sans-serif;
|
||||
}
|
||||
|
||||
@plugin 'tailwind-scrollbar';
|
||||
--color-stereo: #ff264e;
|
||||
}
|
27
src/hooks/mouse.ts
Normal file
27
src/hooks/mouse.ts
Normal 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;
|
||||
};
|
|
@ -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, "/");
|
||||
});
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue