Skip to content
Closed
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
20 changes: 20 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.11/schema.json",

Check notice on line 2 in biome.json

View workflow job for this annotation

GitHub Actions / JS lint & format

deserialize

The configuration schema version does not match the CLI version 2.4.13
"files": {
"includes": [
"host/client_app/**",
Expand Down Expand Up @@ -42,6 +42,26 @@
}
}
}
},
{
"includes": ["packages/ui/src/lacowiki/**"],
"linter": {
"rules": {
"a11y": {
"noSvgWithoutTitle": "off",
"useKeyWithClickEvents": "off",
"useSemanticElements": "off",
"noStaticElementInteractions": "off",
"useValidAnchor": "off"
},
"correctness": {
"useJsxKeyInIterable": "off"
},
"suspicious": {
"noArrayIndexKey": "off"
}
}
}
}
],
"javascript": {
Expand Down
321 changes: 132 additions & 189 deletions host/client_app/pages/Landing.tsx
Original file line number Diff line number Diff line change
@@ -1,213 +1,156 @@
import { usePage } from '@inertiajs/react';
import { Link, usePage } from '@inertiajs/react';
import { keys, useT } from '@simple-module-py/i18n';
import { Badge } from '@simple-module-py/ui/components/ui/badge';
import { Button } from '@simple-module-py/ui/components/ui/button';
import { Card, CardContent } from '@simple-module-py/ui/components/ui/card';
import { Separator } from '@simple-module-py/ui/components/ui/separator';
import { PublicLayout } from '@simple-module-py/ui/layouts/PublicLayout';

interface Props {
isAuthenticated: boolean;
}

const STATS = ['14k+ datasets', '·', '2.4M validated points', '·', 'open source'];
const FEATURE_GRID: [string, string][] = [
['Datasets', 'Upload COG/PMTiles'],
['Sampling', '7 strategies'],
['Validation', 'Map-side classify'],
['Reports', 'Confusion matrix'],
];

const SAMPLE_DOTS: [number, number][] = [
[30, 40],
[60, 55],
[75, 30],
[45, 70],
[20, 55],
];

const MAP_BG = `radial-gradient(ellipse at 30% 40%, oklch(0.92 0.03 155) 0%, transparent 35%),
radial-gradient(ellipse at 70% 60%, oklch(0.88 0.04 200) 0%, transparent 40%),
repeating-linear-gradient(45deg, transparent 0 14px, rgba(0,0,0,0.025) 14px 15px),
repeating-linear-gradient(-45deg, transparent 0 14px, rgba(0,0,0,0.025) 14px 15px),
oklch(0.93 0.005 250)`;

function Landing() {
const { isAuthenticated } = usePage<{ props: Props }>().props as unknown as Props;
const { t } = useT();

const features = [
{
icon: (
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"
/>
</svg>
),
title: t(keys.host.landing.features.module_system_title),
description: t(keys.host.landing.features.module_system_description),
},
{
icon: (
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"
/>
</svg>
),
title: t(keys.host.landing.features.auth_title),
description: t(keys.host.landing.features.auth_description),
},
{
icon: (
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"
/>
</svg>
),
title: t(keys.host.landing.features.schema_title),
description: t(keys.host.landing.features.schema_description),
},
{
icon: (
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"
/>
</svg>
),
title: t(keys.host.landing.features.inertia_title),
description: t(keys.host.landing.features.inertia_description),
},
{
icon: (
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
/>
</svg>
),
title: t(keys.host.landing.features.diagnostics_title),
description: t(keys.host.landing.features.diagnostics_description),
},
{
icon: (
<svg
aria-hidden="true"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z"
/>
</svg>
),
title: t(keys.host.landing.features.devtools_title),
description: t(keys.host.landing.features.devtools_description),
},
];
const ctaHref = isAuthenticated ? '/dashboard/' : '/auth/login';
const ctaLabel = isAuthenticated
? t(keys.host.landing.cta_dashboard)
: t(keys.host.landing.cta_get_started);

return (
<>
{/* Hero */}
<section className="max-w-5xl mx-auto px-4 pt-12 pb-12 text-center sm:px-8 sm:pt-24 sm:pb-20">
<Badge
variant="outline"
className="border-primary-400/20 bg-primary-400/10 text-primary-300 mb-6 sm:mb-8 gap-2"
>
<span className="w-2 h-2 rounded-full bg-primary-400" />
{t(keys.host.landing.badge)}
</Badge>

<h1 className="text-3xl font-extrabold tracking-tight leading-tight font-[var(--font-display)] sm:text-5xl lg:text-6xl">
{t(keys.host.landing.hero_title_line1)}
<br />
<span className="bg-gradient-to-r from-primary-300 via-primary-400 to-primary-200 bg-clip-text text-transparent">
{t(keys.host.landing.hero_title_line2)}
</span>
</h1>

<p className="mt-4 text-base text-dark-text-muted max-w-2xl mx-auto leading-relaxed sm:mt-6 sm:text-lg">
{t(keys.host.landing.hero_subtitle)}
</p>

<div className="mt-8 flex flex-col items-center gap-3 sm:flex-row sm:justify-center sm:gap-4 sm:mt-10">
<Button asChild size="lg" className="w-full sm:w-auto">
<a href="/users/login">
{isAuthenticated
? t(keys.host.landing.cta_dashboard)
: t(keys.host.landing.cta_get_started)}
<div className="min-h-screen bg-background text-foreground">
{/* Top nav */}
<header className="border-b border-border/60">
<div className="mx-auto flex max-w-6xl items-center gap-8 px-6 py-3.5">
<Link href="/" className="flex items-center gap-2.5">
<div className="h-5 w-5 rounded bg-primary" />
<span className="text-[13px] font-semibold tracking-tight">SimpleModule</span>
</Link>
<nav className="hidden gap-5 text-xs text-muted-foreground sm:flex">
<span>Product</span>
<span>Datasets</span>
<span>Methodology</span>
<span>Docs</span>
</nav>
<div className="ml-auto flex items-center gap-2">
<a
href="/auth/login"
className="rounded border border-border bg-card px-3 py-1.5 text-xs font-medium hover:border-primary-400"
>
Sign in
</a>
</Button>
<Button
asChild
variant="outline"
size="lg"
className="w-full sm:w-auto bg-transparent border-dark-border text-white hover:text-white hover:border-dark-border-hover hover:bg-white/10"
>
<a href="https://github.com">{t(keys.host.landing.cta_docs)}</a>
</Button>
<a
href={ctaHref}
className="rounded border border-foreground bg-foreground px-3 py-1.5 text-xs font-medium text-background"
>
Request access
</a>
</div>
</div>
</section>
</header>

<Separator className="max-w-6xl mx-auto bg-white/[0.06]" />
{/* Hero */}
<section className="mx-auto max-w-6xl px-6 py-16">
<div className="grid grid-cols-1 items-center gap-10 lg:grid-cols-[1.1fr_1fr]">
<div className="flex flex-col gap-4">
<span className="inline-flex w-fit items-center gap-1.5 rounded-full border border-primary-300 bg-primary-50 px-2.5 py-0.5 font-mono text-[11px] text-primary-800">
{t(keys.host.landing.badge)}
</span>
<h1 className="font-[var(--font-display)] text-4xl font-semibold leading-tight tracking-tight sm:text-5xl">
{t(keys.host.landing.hero_title_line1)}
<br />
{t(keys.host.landing.hero_title_line2)}
</h1>
<p className="max-w-md text-sm text-muted-foreground sm:text-[15px]">
{t(keys.host.landing.hero_subtitle)}
</p>
<div className="flex flex-wrap items-center gap-2">
<a
href={ctaHref}
className="inline-flex items-center gap-1.5 rounded border border-foreground bg-foreground px-3.5 py-2 text-xs font-medium text-background"
>
{ctaLabel}
</a>
<a
href="https://github.com"
className="inline-flex items-center gap-1.5 rounded border border-border bg-card px-3.5 py-2 text-xs font-medium"
>
{t(keys.host.landing.cta_docs)}
</a>
</div>
<div className="mt-2 flex flex-wrap items-center gap-3 font-mono text-[11px] text-muted-foreground">
{STATS.map((s) => (
<span key={s}>{s}</span>
))}
</div>
</div>

{/* Features */}
<section className="max-w-6xl mx-auto px-4 py-16 sm:px-8 sm:py-24">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{features.map((feature) => (
<Card
key={feature.title}
className="group border-white/[0.06] bg-white/[0.03] backdrop-blur-sm hover:border-primary-400/20 hover:bg-white/[0.06] transition-colors duration-200"
{/* Map preview + accuracy callout */}
<div className="relative">
<div
className="relative h-72 overflow-hidden rounded-lg border bg-secondary"
style={{ backgroundImage: MAP_BG }}
>
<CardContent className="pt-6">
<div className="w-10 h-10 rounded-lg bg-primary-400/10 flex items-center justify-center text-primary-400 mb-4 transition-colors duration-200 group-hover:bg-primary-400/15">
{feature.icon}
</div>
<h3 className="text-lg font-semibold text-white mb-2 font-[var(--font-display)]">
{feature.title}
</h3>
<p className="text-sm text-dark-text-muted leading-relaxed">
{feature.description}
</p>
</CardContent>
</Card>
{SAMPLE_DOTS.map(([x, y]) => (
<span
key={`${x}-${y}`}
className="absolute h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-[1.5px] border-foreground"
style={{
left: `${x}%`,
top: `${y}%`,
background: x === 75 ? 'oklch(0.62 0.12 200)' : 'white',
}}
/>
))}
</div>
<div className="absolute -bottom-4 -right-4 w-52 rounded-md border bg-card p-3 text-[11px] shadow-card">
<div className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
Overall accuracy
</div>
<div className="mt-1 text-2xl font-semibold tabular-nums">87.3%</div>
<div className="text-muted-foreground">±2.1% at 95% CI</div>
</div>
</div>
</div>
</section>

{/* Feature row */}
<section className="mx-auto max-w-6xl px-6 pb-16">
<div className="grid grid-cols-2 gap-4 border-t border-border/60 pt-6 sm:grid-cols-4">
{FEATURE_GRID.map(([title, sub]) => (
<div key={title}>
<div className="text-xs font-semibold">{title}</div>
<div className="text-[11px] text-muted-foreground">{sub}</div>
</div>
))}
</div>
</section>
</>

<footer className="border-t border-border/60 py-6 text-center text-xs text-muted-foreground">
SimpleModule — built with FastAPI, Inertia.js, React, Tailwind
</footer>
</div>
);
}

Landing.layout = (page: React.ReactNode) => <PublicLayout>{page}</PublicLayout>;
Landing.layout = (page: React.ReactNode) => page;
export default Landing;
2 changes: 1 addition & 1 deletion host/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<title>SimpleModule</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
{% inertia_head %}
{% if request.app.state.sm.inertia_config.environment == "development" %}
<script type="module">
Expand Down
Loading
Loading