Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/[locale]/(landing)/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Logo } from "@workspace/ui/components/landing/logo";
const menuItems = [
{ name: "Docs", href: "/docs" },
{ name: "Pricing", href: "/" },
{ name: "Download", href: "/" },
{ name: "Download", href: "/download" },
{ name: "Showcase", href: "/" },
];

Expand Down
34 changes: 2 additions & 32 deletions apps/web/app/[locale]/(landing)/components/hero-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,10 @@ import Image from "next/image";
import { ArrowRight, Play, Rocket } from "lucide-react";
import { fetchLatestGithubVersion } from "@workspace/core/lib/utils";
import { Button } from "@workspace/ui/components/button";
import {
AnimatedGroup,
type AnimatedGroupProps,
} from "@workspace/ui/components/landing/animated-group";
import { AnimatedGroup } from "@workspace/ui/components/landing/animated-group";
import { LogoCloud } from "@workspace/ui/components/landing/logo-cloud";
import { TextEffect } from "@workspace/ui/components/landing/text-effect";

const transitionVariants: AnimatedGroupProps["variants"] = {
container: {
visible: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
},
},
},
item: {
hidden: {
opacity: 0,
filter: "blur(12px)",
y: 12,
},
visible: {
opacity: 1,
filter: "blur(0px)",
y: 0,
transition: {
type: "spring",
bounce: 0.3,
duration: 1.5,
},
},
},
};
import { transitionVariants } from "@/lib/animations";

export default function HeroSection() {
const [latestTag, setLatestTag] = useState<string | null>(null);
Expand Down
113 changes: 113 additions & 0 deletions apps/web/app/[locale]/(landing)/components/platform-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import Link from "next/link";
import { Card, CardContent, CardHeader } from "@workspace/ui/components/card";
import { Button } from "@workspace/ui/components/button";
import { ReactNode } from "react";
import {
platformCards,
type PlatformCardData,
} from "../download/platform-mappings";

interface PlatformCardsProps {
assets: Record<string, string>;
}

function DownloadButton({
href,
label,
ext,
}: {
href: string | undefined;
label: string;
ext: string;
}) {
if (!href) return null;
return (
<Button
asChild
variant="outline"
className="w-full cursor-pointer justify-between"
>
<Link href={href}>
<span className="text-sm">{label}</span>
<span className="text-muted-foreground text-xs font-mono">{ext}</span>
</Link>
</Button>
);
}

const colSpanClass = {
2: "lg:col-span-2",
3: "lg:col-span-3",
} as const;

function PlatformCard({
platform,
assets,
}: {
platform: PlatformCardData;
assets: Record<string, string>;
}) {
return (
<Card
className={`group overflow-hidden bg-background shadow-foreground/5 text-center ${colSpanClass[platform.colSpan]}`}
>
<CardHeader className="pb-3">
<CardDecorator>{platform.icon}</CardDecorator>
<h3 className="mt-6 font-medium">{platform.name}</h3>
</CardHeader>
<CardContent className="space-y-3">
{platform.downloads.length > 0 ? (
platform.downloads.map((dl) => (
<DownloadButton
key={dl.assetKey}
href={assets[dl.assetKey]}
label={dl.label}
ext={dl.ext}
/>
))
) : (
<p className="text-sm text-muted-foreground py-2">Coming soon</p>
)}
</CardContent>
</Card>
);
}

export default function PlatformCards({ assets }: PlatformCardsProps) {
return (
<section className="py-16 md:py-32">
<div className="mx-auto max-w-5xl px-6">
<div className="text-center">
<h2 className="text-balance text-4xl font-semibold lg:text-5xl">
Available Platforms
</h2>
<p className="mt-4 text-muted-foreground">
Download TNTStack for your platform.
</p>
</div>
<div className="mx-auto mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-6 md:mt-16">
{platformCards.map((platform) => (
<PlatformCard
key={platform.name}
platform={platform}
assets={assets}
/>
))}
</div>
</div>
</section>
);
}

const CardDecorator = ({ children }: { children: ReactNode }) => (
<div className="mask-radial-from-40% mask-radial-to-60% relative mx-auto size-36 duration-200 [--color-border:color-mix(in_oklab,var(--color-foreground)10%,transparent)] group-hover:[--color-border:color-mix(in_oklab,var(--color-foreground)20%,transparent)] dark:[--color-border:color-mix(in_oklab,var(--color-foreground)15%,transparent)] dark:group-hover:[--color-border:color-mix(in_oklab,var(--color-foreground)20%,transparent)]">
<div
aria-hidden
className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-border)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-border)_1px,transparent_1px)] bg-size-[24px_24px] dark:opacity-50"
/>

<div className="bg-background absolute inset-0 m-auto flex size-12 items-center justify-center border-l border-t">
{children}
</div>
</div>
);
176 changes: 176 additions & 0 deletions apps/web/app/[locale]/(landing)/download/download-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"use client";

import React, { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { ArrowDown } from "lucide-react";
import { Button } from "@workspace/ui/components/button";
import { Logo } from "@workspace/ui/components/landing/logo";
import { AnimatedGroup } from "@workspace/ui/components/landing/animated-group";
import { TextEffect } from "@workspace/ui/components/landing/text-effect";
import { transitionVariants } from "@/lib/animations";
import { detectPlatform, type Platform } from "@/lib/detect-platform";
import { platformConfig } from "./platform-mappings";
import { type ReleaseData } from "@/lib/github-releases";
import PlatformCards from "../components/platform-cards";

interface DownloadContentProps {
release: ReleaseData | null;
}

export default function DownloadContent({ release }: DownloadContentProps) {
const [platform, setPlatform] = useState<Platform>("unknown");

useEffect(() => {
setPlatform(detectPlatform());
}, []);

const scrollToPlatforms = useCallback((e: React.MouseEvent) => {
e.preventDefault();
document
.getElementById("platforms")
?.scrollIntoView({ behavior: "smooth" });
}, []);

const { label, icon, primaryAssetKey } = platformConfig[platform];

const primaryUrl =
(release?.assets && primaryAssetKey && release.assets[primaryAssetKey]) ||
"#";

return (
<main className="overflow-hidden">
<div
className="absolute inset-0 z-0 text-stone-200 dark:text-white/10"
style={{
backgroundImage: `
linear-gradient(to right, currentColor 1px, transparent 1px),
linear-gradient(to bottom, currentColor 1px, transparent 1px)
`,
backgroundSize: "20px 20px",
backgroundPosition: "0 0, 0 0",
maskImage: `
repeating-linear-gradient(
to right,
black 0px,
black 3px,
transparent 3px,
transparent 8px
),
repeating-linear-gradient(
to bottom,
black 0px,
black 3px,
transparent 3px,
transparent 8px
),
radial-gradient(ellipse 70% 60% at 50% 0%, #000 60%, transparent 100%)
`,
WebkitMaskImage: `
repeating-linear-gradient(
to right,
black 0px,
black 3px,
transparent 3px,
transparent 8px
),
repeating-linear-gradient(
to bottom,
black 0px,
black 3px,
transparent 3px,
transparent 8px
),
radial-gradient(ellipse 70% 60% at 50% 0%, #000 60%, transparent 100%)
`,
maskComposite: "intersect",
WebkitMaskComposite: "source-in",
}}
/>
<section className="pt-24 md:pt-36">
<div className="mx-auto max-w-7xl px-6 text-center">
<AnimatedGroup variants={transitionVariants}>
<Logo className="mx-auto size-20" />
</AnimatedGroup>

<TextEffect
preset="fade-in-blur"
speedSegment={0.3}
as="h1"
className="mx-auto mt-8 max-w-4xl text-balance text-5xl max-md:font-semibold md:text-7xl lg:mt-16 xl:text-[5.25rem]"
>
Download TNTStack
</TextEffect>
<TextEffect
per="line"
preset="fade-in-blur"
speedSegment={0.3}
delay={0.5}
as="p"
className="mx-auto mt-8 max-w-3xl text-balance text-lg"
>
Get the latest version for your platform. One codebase for Web,
Desktop, and Mobile.
</TextEffect>

{release?.version && (
<AnimatedGroup variants={transitionVariants}>
<p className="mt-4 text-sm text-muted-foreground">
Latest release: v{release.version}
</p>
</AnimatedGroup>
)}

<AnimatedGroup
variants={{
container: {
visible: {
transition: {
staggerChildren: 0.05,
delayChildren: 0.75,
},
},
},
...transitionVariants,
}}
className="mt-12 flex flex-col items-center justify-center gap-2 md:flex-row"
>
<Button asChild size="lg" className="cursor-pointer text-base">
<Link href={primaryUrl}>
{icon}
<span className="text-nowrap">{label}</span>
</Link>
</Button>
<Button
key={2}
size="lg"
variant="outline"
className="cursor-pointer text-base"
onClick={scrollToPlatforms}
>
<ArrowDown />
<span className="text-nowrap">Other Platforms</span>
</Button>
</AnimatedGroup>
</div>

<AnimatedGroup
variants={{
container: {
visible: {
transition: {
staggerChildren: 0.05,
delayChildren: 0.75,
},
},
},
...transitionVariants,
}}
>
<div id="platforms">
<PlatformCards assets={release?.assets || {}} />
</div>
</AnimatedGroup>
</section>
</main>
);
}
7 changes: 7 additions & 0 deletions apps/web/app/[locale]/(landing)/download/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { fetchLatestReleaseWithAssets } from "@/lib/github-releases";
import DownloadContent from "./download-content";

export default async function DownloadPage() {
const release = await fetchLatestReleaseWithAssets();
return <DownloadContent release={release} />;
}
Loading
Loading