feat(home): route signed-in users and refresh landing page
This commit is contained in:
+24
-340
@@ -1,342 +1,26 @@
|
||||
"use client";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import LandingPage from "@/components/landing-page";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ArrowUpRight, ArrowRight, Plus, Minus } from "lucide-react";
|
||||
import { Brand } from "@/components/brand";
|
||||
import { Button } from "@/components/ui/button";
|
||||
const scenes = [
|
||||
"收藏生活的切片",
|
||||
"遇见新的工作方法",
|
||||
"记下突然出现的灵感",
|
||||
"给想法一点自由",
|
||||
"做一件属于你的作品",
|
||||
];
|
||||
export default function Home() {
|
||||
const [faq, setFaq] = useState<number | null>(0);
|
||||
return (
|
||||
<div className="landing">
|
||||
<header className="site-nav">
|
||||
<Brand />
|
||||
<nav aria-label="主导航">
|
||||
<a href="#tools">创作工具</a>
|
||||
<a href="#ideas">知识与灵感</a>
|
||||
<a href="#workflow">工作方式</a>
|
||||
</nav>
|
||||
<div className="nav-actions">
|
||||
<Link href="/login">登录</Link>
|
||||
<Button asChild>
|
||||
<Link href="/login">
|
||||
进入工作台 <ArrowUpRight size={15} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<section className="hero container">
|
||||
<div className="eyebrow">
|
||||
<span className="status-dot" /> A SPACE FOR HUMAN IDEAS & AGENTS
|
||||
</div>
|
||||
<h1>
|
||||
想法不该停在对话里。
|
||||
<br />
|
||||
让灵感,<span>有形。</span>
|
||||
<svg
|
||||
className="hero-stroke"
|
||||
viewBox="0 0 230 18"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 13 Q100 0 225 9"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</h1>
|
||||
<p className="hero-copy">
|
||||
从一闪而过的念头,到值得分享的作品。
|
||||
<br />
|
||||
在一张画布上,把你的创作继续下去。
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/login">
|
||||
开始创作 <ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</Button>
|
||||
<a className="text-link" href="#space">
|
||||
探索你的创作空间 <ArrowRight size={16} />
|
||||
</a>
|
||||
</div>
|
||||
<div className="people-row">
|
||||
{scenes.map((s, i) => (
|
||||
<figure key={s}>
|
||||
<div className="person-slot">
|
||||
<img
|
||||
src={"/assets/landingcontext-imgFrame" + (i + 1) + ".svg"}
|
||||
width="200"
|
||||
height="220"
|
||||
alt={s}
|
||||
/>
|
||||
</div>
|
||||
<figcaption>
|
||||
<span>0{i + 1}</span>
|
||||
{s}
|
||||
</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
<div className="hero-footnote">
|
||||
<span>COLLECT. CONNECT. CREATE.</span>
|
||||
<span>给好奇心一点空间 ↓</span>
|
||||
</div>
|
||||
</section>
|
||||
<section id="space" className="space-section">
|
||||
<div className="container">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<div className="eyebrow">YOUR IDEAS, ALL TOGETHER</div>
|
||||
<h2>
|
||||
一个念头,
|
||||
<br />
|
||||
可以去很远。
|
||||
</h2>
|
||||
</div>
|
||||
<p>
|
||||
笔记、参考、还没想好的方向。
|
||||
<br />
|
||||
放在一起,看见它们之间的新可能。
|
||||
</p>
|
||||
</div>
|
||||
<div className="product-preview">
|
||||
<div className="preview-sidebar">
|
||||
<Brand />
|
||||
<p>
|
||||
个人空间 <span>⌄</span>
|
||||
</p>
|
||||
<div className="preview-active">▦ 城市清晨</div>
|
||||
<div>▧ 灵感收集</div>
|
||||
<div>+ 新建画布</div>
|
||||
<small>你的下一件作品,从这里开始。</small>
|
||||
</div>
|
||||
<div className="preview-canvas">
|
||||
<div className="preview-bar">
|
||||
☀ 城市清晨 <span>↗ 分享</span>
|
||||
</div>
|
||||
<div className="preview-note">
|
||||
<small>01 / THE IDEA</small>
|
||||
<h3>
|
||||
把日常,
|
||||
<br />
|
||||
拍成一场电影。
|
||||
</h3>
|
||||
<p>
|
||||
一束光、一段声音、
|
||||
<br />
|
||||
一个值得留下的瞬间。
|
||||
</p>
|
||||
</div>
|
||||
<div className="preview-art">
|
||||
<div className="light-shape" />
|
||||
<span>光的形状</span>
|
||||
</div>
|
||||
<div className="preview-sticky">
|
||||
慢一点。
|
||||
<br />
|
||||
让生活里的细节
|
||||
<br />
|
||||
成为下一次灵感。<span>✳</span>
|
||||
</div>
|
||||
<svg
|
||||
viewBox="0 0 800 500"
|
||||
preserveAspectRatio="none"
|
||||
className="preview-connect"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M290 235 C420 235 310 360 480 360"
|
||||
fill="none"
|
||||
stroke="#a98b72"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="5 5"
|
||||
/>
|
||||
</svg>
|
||||
<div className="preview-tools">↖ ✋ T ▧ □ ↗ +</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="tools" className="tools-section container">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<div className="eyebrow">TWO TOOLS. ONE CREATIVE FLOW.</div>
|
||||
<h2>
|
||||
给不同的想法,
|
||||
<br />
|
||||
恰好的空间。
|
||||
</h2>
|
||||
</div>
|
||||
<p>
|
||||
从整理思路,到亲手完成作品。
|
||||
<br />
|
||||
每一步,都有你的空间。
|
||||
</p>
|
||||
</div>
|
||||
<div className="tool-grid">
|
||||
<article className="tool-card">
|
||||
<div className="tool-meta">
|
||||
01 / GRAPHIC <ArrowUpRight size={20} />
|
||||
</div>
|
||||
<div className="type-art">
|
||||
MAKE
|
||||
<br />
|
||||
ROOM<span>for ideas.</span>
|
||||
<i>✳</i>
|
||||
</div>
|
||||
<h3>把灵感排成作品。</h3>
|
||||
<p>
|
||||
让文字、图形与图片自由编排。打磨每个细节,留下属于你的表达。
|
||||
</p>
|
||||
<Link href="/login" className="text-link">
|
||||
打开图形编辑器 <ArrowRight size={16} />
|
||||
</Link>
|
||||
</article>
|
||||
<article className="tool-card">
|
||||
<div className="tool-meta">
|
||||
02 / INFINITE CANVAS <ArrowUpRight size={20} />
|
||||
</div>
|
||||
<div className="canvas-art">
|
||||
<span>一个念头</span>
|
||||
<i>⤳</i>
|
||||
<span>一张参考</span>
|
||||
<b>新的可能 ✧</b>
|
||||
</div>
|
||||
<h3>让思路,自由生长。</h3>
|
||||
<p>
|
||||
把笔记、参考图与提示词连成线索。在无限画布上,找到下一次创作的方向。
|
||||
</p>
|
||||
<Link href="/login" className="text-link">
|
||||
打开无限画布 <ArrowRight size={16} />
|
||||
</Link>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="ideas" className="ideas-section">
|
||||
<div className="container">
|
||||
<div className="eyebrow">GOOD IDEAS DESERVE GOOD COMPANY</div>
|
||||
<h2>
|
||||
不必每一次,
|
||||
<br />
|
||||
都从空白开始。
|
||||
</h2>
|
||||
<div className="idea-grid">
|
||||
{[
|
||||
["留住一闪而过", "一句念头、一张参考图。先留下,再慢慢展开。"],
|
||||
["让线索相遇", "把不同的灵感放上画布,用连接看见新的可能。"],
|
||||
["把创作继续下去", "用熟悉的方法探索,再亲手调整细节。"],
|
||||
].map(([title, copy], i) => (
|
||||
<article key={title}>
|
||||
<img
|
||||
src={"/assets/landingcontext-imgFrame" + (i + 6) + ".svg"}
|
||||
width="140"
|
||||
height="110"
|
||||
alt=""
|
||||
/>
|
||||
<h3>{title}</h3>
|
||||
<p>{copy}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="workflow" className="workflow container">
|
||||
<div>
|
||||
<div className="eyebrow">YOUR AGENT. YOUR WORKFLOW.</div>
|
||||
<h2>
|
||||
熟悉的工作方式。
|
||||
<br />
|
||||
新的创作可能。
|
||||
</h2>
|
||||
<p>
|
||||
从一个小小的起点出发,让工具跟上你的想法。
|
||||
<br />
|
||||
整理、连接、亲手打磨,把创作继续下去。
|
||||
</p>
|
||||
<Link href="/login" className="text-link">
|
||||
进入创作空间 <ArrowUpRight size={17} />
|
||||
</Link>
|
||||
</div>
|
||||
<ol>
|
||||
{[
|
||||
["找到起点", "留住一个念头,或者从一张空白画布开始。"],
|
||||
["连接工作方式", "把相关的笔记与参考放在一起。"],
|
||||
["亲手打磨,保存作品", "调整细节,让想法成为自己的表达。"],
|
||||
].map(([t, c], i) => (
|
||||
<li key={t}>
|
||||
<span>0{i + 1}</span>
|
||||
<div>
|
||||
<h3>{t}</h3>
|
||||
<p>{c}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
<section className="faq container">
|
||||
<div className="eyebrow">A FEW THINGS TO KNOW</div>
|
||||
<h2>开始之前,你也许想知道。</h2>
|
||||
{[
|
||||
[
|
||||
"我可以在这里做什么?",
|
||||
"收集灵感、整理笔记、在画布上连接想法,逐步把零散的线索变成作品。",
|
||||
],
|
||||
[
|
||||
"需要先想好完整的作品吗?",
|
||||
"不用。可以从一段文字、一张参考或一个还没有想清楚的念头开始。",
|
||||
],
|
||||
[
|
||||
"我的内容会自动公开吗?",
|
||||
"你的工作空间需要登录访问。分享内容前,请确认工作空间的成员与权限设置。",
|
||||
],
|
||||
].map(([q, a], i) => (
|
||||
<div className="faq-item" key={q}>
|
||||
<button
|
||||
aria-expanded={faq === i}
|
||||
onClick={() => setFaq(faq === i ? null : i)}
|
||||
>
|
||||
{q}
|
||||
{faq === i ? <Minus size={17} /> : <Plus size={17} />}
|
||||
</button>
|
||||
{faq === i && <p>{a}</p>}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<section className="closing">
|
||||
<div className="eyebrow">YOUR NEXT IDEA STARTS HERE</div>
|
||||
<h2>
|
||||
给下一个念头,
|
||||
<br />
|
||||
留一个起点。
|
||||
</h2>
|
||||
<p>不用想好全部。带着一点好奇,先开始。</p>
|
||||
<Button asChild size="lg">
|
||||
<Link href="/login">
|
||||
进入你的工作台 <ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</Button>
|
||||
<span className="closing-star">✳</span>
|
||||
</section>
|
||||
</main>
|
||||
<footer className="site-footer">
|
||||
<Brand />
|
||||
<p>给想法留白,给创作空间。</p>
|
||||
<a href="#workflow">
|
||||
工作方式 <ArrowUpRight size={14} />
|
||||
</a>
|
||||
<span>© {new Date().getFullYear()} Endoleap</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
export default async function Home() {
|
||||
const session = (await cookies()).get("foundry_session")?.value;
|
||||
let authenticated = false;
|
||||
if (session) {
|
||||
const origin = process.env.FOUNDRY_API_ORIGIN || "https://api.foundry.entroia.com";
|
||||
const project = process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap";
|
||||
try {
|
||||
const response = await fetch(`${origin}/projects/${encodeURIComponent(project)}/profile`, {
|
||||
headers: { Authorization: `Bearer ${session}` },
|
||||
cache: "no-store",
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
authenticated = response.ok;
|
||||
await response.body?.cancel();
|
||||
} catch {
|
||||
// An unverified session must not hide the public landing page.
|
||||
}
|
||||
}
|
||||
if (authenticated) redirect("/dashboard");
|
||||
return <LandingPage />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
.page { --ink: #21221f; --muted: #797b73; --line: #e5e6df; background: #fff; color: var(--ink); font-family: Inter, -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||
.page a, .page button { -webkit-tap-highlight-color: transparent; }
|
||||
.page a:focus-visible, .page button:focus-visible { outline: 2px solid #6879c2; outline-offset: 4px; }
|
||||
.nav { display: flex; align-items: center; gap: 40px; height: 88px; max-width: 1320px; padding: 0 48px; margin: auto; }
|
||||
.page :global(.brand) { color: var(--ink); gap: 8px; font-size: 19px; letter-spacing: -0.6px; }
|
||||
.page :global(.brand img) { width: 30px; height: 30px; }
|
||||
.page :global(.brand small) { color: #92958c; font-size: 11px; font-weight: 400; letter-spacing: 0; }
|
||||
.nav nav { display: flex; gap: 28px; font-size: 13px; color: #72766c; }
|
||||
.nav nav a:hover { color: var(--ink); }
|
||||
.navActions { margin-left: auto; display: flex; align-items: center; gap: 24px; font-size: 13px; }
|
||||
.smallCta { display: inline-flex; align-items: center; gap: 12px; padding: 10px 15px; border: 1px solid #cfd2c8; border-radius: 5px; }
|
||||
.smallCta:hover { background: #f5f6f0; }
|
||||
.hero { max-width: 1320px; margin: auto; padding: 82px 48px 90px; display: grid; grid-template-columns: 1fr 1.04fr; align-items: center; gap: 46px; }
|
||||
.eyebrow { font: 10px/1.6 ui-monospace, "SFMono-Regular", monospace; letter-spacing: 1.25px; color: #90958a; }
|
||||
.heroCopy .eyebrow { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||||
.statusDot { width: 5px; height: 5px; background: #83966c; border-radius: 50%; }
|
||||
.hero h1 { font-size: clamp(38px, 4.4vw, 61px); font-weight: 600; line-height: 1.23; letter-spacing: -3px; margin: 22px 0; }
|
||||
.hero h1 > span { position: relative; white-space: nowrap; }
|
||||
.hero h1 > span::after { content: ""; position: absolute; left: 0; bottom: -3px; width: 100%; height: 5px; border-bottom: 2px solid #b9c87c; border-radius: 50%; rotate: -2deg; }
|
||||
.heroCopy > p { color: #6f7369; font-size: 15px; line-height: 1.9; font-weight: 400; }
|
||||
.heroActions { display: flex; flex-wrap: wrap; align-items: center; gap: 22px; margin-top: 30px; }
|
||||
.primary { display: inline-flex; align-items: center; justify-content: center; gap: 16px; padding: 14px 20px; background: var(--ink); color: #fff; border: 1px solid var(--ink); border-radius: 5px; font-size: 13px; font-weight: 500; box-shadow: 0 2px 2px #21221f08; transition: background 160ms, translate 160ms; }
|
||||
.primary:hover { background: #41453b; translate: 0 -1px; }
|
||||
.textLink { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: #6e7565; }
|
||||
.textLink:hover { color: #21221f; }
|
||||
.heroNote { margin-top: 34px; font-family: "Kaiti SC", "STKaiti", Georgia, serif; color: #969a8b; font-size: 13px; display: flex; align-items: center; gap: 8px; }
|
||||
.scribbleStar { font-family: Georgia, serif; color: #929a74; font-size: 24px; }
|
||||
.preview { min-width: 0; border: 1px solid #e0e3d9; border-radius: 9px; background: #fafafa; box-shadow: 0 12px 50px #3c442508, 0 2px 8px #3c442505; rotate: -1deg; }
|
||||
.previewHeading { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; font-size: 10px; color: #777d70; }
|
||||
.previewHeading > span:first-child { display: flex; align-items: center; gap: 7px; color: #69745e; }
|
||||
.previewHeading > span:last-child { font: 8px ui-monospace, monospace; letter-spacing: 1px; }
|
||||
.tinyLogo { font-size: 20px; color: #4c5841; }
|
||||
.scene { position: relative; aspect-ratio: 600 / 440; background-image: radial-gradient(#dce1d2 0.65px, transparent 0.65px); background-size: 17px 17px; }
|
||||
.wires { position: absolute; inset: 0; width: 100%; height: 100%; stroke: #a3ad90; stroke-width: 1; }
|
||||
.wires circle { fill: #a3ad90; }
|
||||
.scene > button { position: absolute; display: flex; flex-direction: column; text-align: left; border: 1px solid #e1e4d8; border-radius: 5px; padding: 14px; background: #fff; color: #373a35; box-shadow: 0 2px 5px #32471504; transition: box-shadow 180ms, border-color 180ms, translate 180ms; }
|
||||
.scene > button:hover { translate: 0 -3px; }
|
||||
.scene .activeCard { border-color: #9aa77d; box-shadow: 0 0 0 2px #9aa77d15, 0 4px 12px #32471508; }
|
||||
.cardLabel { display: flex; align-items: center; gap: 5px; font-size: 9px; color: #9aab88; }
|
||||
.ideaCard { left: 5%; top: 17%; width: 42%; min-height: 41%; rotate: -3deg; }
|
||||
.ideaCard strong { margin-top: 14px; margin-bottom: 13px; font-size: clamp(13px, 1.6vw, 22px); font-family: "Songti SC", Georgia, serif; font-weight: 400; line-height: 1.55; }
|
||||
.cardText { font-size: 9px; line-height: 1.8; color: #9ba48f; }
|
||||
.paperCorner { position: absolute; bottom: 4px; right: 4px; width: 10px; height: 10px; border-right: 1px solid #d9dece; border-bottom: 1px solid #d9dece; }
|
||||
.scene .artCard { top: 8%; right: 7%; width: 29%; min-height: 49%; background: #edeaf7; rotate: 4deg; }
|
||||
.artCard svg { width: 100%; max-height: 150px; flex: 1; stroke: #53584d; stroke-width: 1.2; fill: none; }
|
||||
.artCaption { font: italic 8px Georgia, serif; text-align: center; color: #909e79; }
|
||||
.scene .noteCard { left: 38%; bottom: 9%; width: 37%; min-height: 27%; background: #f0eedf; rotate: 2deg; }
|
||||
.noteCard strong { margin: 9px 0 12px; font-size: clamp(11px, 1.4vw, 18px); font-family: "Songti SC", Georgia, serif; font-weight: 400; line-height: 1.5; }
|
||||
.noteTag { align-self: flex-start; font-size: 8px; padding: 3px 5px; border: 1px solid #d6d6ba; border-radius: 3px; color: #8f9278; }
|
||||
.looseNote { position: absolute; left: 8%; bottom: 12%; color: #aab39c; rotate: -10deg; font: italic 14px Georgia, serif; }
|
||||
.cursor { position: absolute; right: 17%; bottom: 20%; color: #879567; rotate: -12deg; pointer-events: none; }
|
||||
.cursor span { position: absolute; top: 20px; left: 12px; background: #879567; color: white; border-radius: 3px; padding: 3px 7px; font-size: 9px; }
|
||||
.previewFooter { display: flex; align-items: center; gap: 10px; padding: 13px 18px; border-top: 1px solid var(--line); font-size: 9px; color: #a1a992; }
|
||||
.previewFooter > div { display: flex; gap: 5px; }
|
||||
.previewFooter button { width: 5px; height: 5px; border-radius: 50%; background: #d8dfcc; }
|
||||
.previewFooter button[aria-pressed="true"] { background: #879567; }
|
||||
.previewFooter > span:last-child { margin-left: auto; font: 9px ui-monospace, monospace; }
|
||||
.manifestoStrip { display: flex; align-items: center; justify-content: space-between; gap: 20px; max-width: 1224px; margin: 0 auto; padding: 25px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
||||
.manifestoStrip > span { font: 8px ui-monospace, monospace; letter-spacing: 0.6px; color: #a0a494; }
|
||||
.manifestoStrip p { font-size: 13px; color: #646c58; letter-spacing: 0.3px; }
|
||||
.manifestoStrip p span { color: #9ba28e; }
|
||||
.space { max-width: 1320px; margin: auto; padding: 105px 48px; }
|
||||
.sectionHeading { display: flex; align-items: flex-end; justify-content: space-between; gap: 30px; margin-bottom: 48px; }
|
||||
.sectionHeading h2, .faq h2 { font-size: 34px; line-height: 1.5; letter-spacing: -1px; font-weight: 500; margin-top: 18px; }
|
||||
.sectionHeading > p { padding-bottom: 5px; color: #6c7166; line-height: 1.9; font-size: 13px; }
|
||||
.featureGrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; }
|
||||
.featureGrid article { min-width: 0; }
|
||||
.featureArt { position: relative; height: 185px; border: 1px solid #eaede4; border-radius: 6px; background: #f9faf6; overflow: hidden; margin-bottom: 25px; }
|
||||
.collectArt { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; }
|
||||
.collectArt span { display: flex; align-items: center; gap: 9px; padding: 8px 12px; min-width: 70%; border: 1px solid #e1e6d7; border-radius: 4px; background: #fff; font-size: 10px; color: #859471; rotate: -2deg; }
|
||||
.collectArt span:nth-child(2) { rotate: 2deg; translate: 8px 0; }
|
||||
.collectArt span:nth-child(3) { rotate: -1deg; translate: -5px 0; }
|
||||
.collectArt svg { width: 14px; }
|
||||
.connectArt svg { width: 100%; height: 100%; stroke: #b1b99e; }
|
||||
.connectArt span { position: absolute; top: 39px; left: 23px; padding: 10px 13px; border: 1px solid #e1e5d6; border-radius: 5px; background: #fff; color: #8a9877; font-size: 11px; }
|
||||
.connectArt span:nth-of-type(2) { top: 110px; left: auto; right: 35px; background: #edf0e1; }
|
||||
.connectArt i { position: absolute; top: 38px; right: 25px; color: #a0ad86; font: 32px Georgia, serif; }
|
||||
.createArt { display: grid; place-items: center; }
|
||||
.createArt > div { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px; background: #f1efe3; width: 72%; rotate: -2deg; border: 1px solid #e0dfcf; }
|
||||
.createArt > div > span { font: 7px ui-monospace, monospace; letter-spacing: 1px; color: #9da184; }
|
||||
.createArt strong { font: 25px "Songti SC", Georgia, serif; color: #6e7857; letter-spacing: 1px; }
|
||||
.editBadge { position: absolute; right: 14px; bottom: 24px; display: flex; align-items: center; gap: 5px; padding: 5px 8px; border: 1px solid #dce3cf; background: #fff; border-radius: 4px; font-size: 8px; color: #889874; }
|
||||
.featureNumber { color: #a4ab98; font: 9px ui-monospace, monospace; letter-spacing: 0.7px; }
|
||||
.featureGrid h3 { font-size: 18px; font-weight: 500; margin: 12px 0; letter-spacing: -0.4px; }
|
||||
.featureGrid p { font-size: 12px; color: #6c7166; line-height: 1.9; }
|
||||
.workflow { background: #f7f7f4; border-top: 1px solid #e9ece1; border-bottom: 1px solid #e9ece1; display: grid; grid-template-columns: 1fr 1fr; gap: 90px; padding: 85px max(48px, calc((100vw - 1224px) / 2)); }
|
||||
.quote { position: relative; }
|
||||
.quoteMark { font: 80px/1 Georgia, serif; position: absolute; top: -30px; left: -12px; color: #d9dfca; }
|
||||
.quote h2 { position: relative; font: 31px/1.7 "Songti SC", Georgia, serif; color: #586449; }
|
||||
.quote em { display: block; font-style: normal; }
|
||||
.quote p { font-size: 12px; color: #747a6d; line-height: 2; margin-top: 18px; }
|
||||
.signature { display: block; font: italic 13px Georgia, serif; color: #a3ad94; margin-top: 30px; }
|
||||
.workflowList { display: flex; flex-direction: column; justify-content: center; }
|
||||
.workflowList > div { display: flex; gap: 18px; padding: 24px 0; border-bottom: 1px solid #dfe5d4; }
|
||||
.workflowList > div:first-child { padding-top: 0; }
|
||||
.workflowList > div > span { font: 9px/22px ui-monospace, monospace; color: #a4ad95; }
|
||||
.workflowList h3 { font-size: 15px; font-weight: 500; color: #5e6e4e; margin-bottom: 8px; }
|
||||
.workflowList p { font-size: 11px; line-height: 1.8; color: #747a6d; }
|
||||
.workflowList svg { flex-shrink: 0; color: #a5b38e; margin-left: auto; }
|
||||
.faq { max-width: 1320px; padding: 105px 48px; margin: auto; display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 90px; }
|
||||
.faqItem { border-bottom: 1px solid var(--line); }
|
||||
.faqItem button { display: flex; align-items: center; justify-content: space-between; gap: 20px; width: 100%; padding: 23px 0; font-size: 14px; text-align: left; }
|
||||
.faqItem svg { flex-shrink: 0; color: #929b84; transition: rotate 160ms; }
|
||||
.faqItem p { padding: 0 28px 24px 0; font-size: 12px; line-height: 1.9; color: #8b947e; }
|
||||
.closing { position: relative; border-top: 1px solid var(--line); text-align: center; padding: 85px 24px 90px; }
|
||||
.closing h2 { font-size: 38px; letter-spacing: -1.3px; font-weight: 500; margin: 18px 0; }
|
||||
.closing p { font-size: 13px; color: #74796d; margin-bottom: 28px; }
|
||||
.closingDoodle { position: absolute; left: 12%; top: 75px; color: #b6c198; font: 75px Georgia, serif; rotate: -16deg; }
|
||||
.footer { display: flex; align-items: center; gap: 30px; max-width: 1320px; padding: 30px 48px; margin: auto; border-top: 1px solid var(--line); }
|
||||
.footer > p { font-size: 11px; color: #a1a992; }
|
||||
.footer > a:not(:first-child) { margin-left: auto; display: flex; gap: 7px; align-items: center; font-size: 11px; color: #7e8b6a; }
|
||||
.footer > span { color: #abb39d; font: 8px ui-monospace, monospace; }
|
||||
.mobileBreak { display: none; }
|
||||
@media (min-width: 1400px) { .hero { padding-top: 105px; padding-bottom: 110px; } }
|
||||
@media (max-width: 1050px) {
|
||||
.nav, .hero, .space, .faq, .footer { padding-left: 30px; padding-right: 30px; }
|
||||
.hero { gap: 26px; }
|
||||
.hero h1 { font-size: 43px; }
|
||||
.heroActions { gap: 15px; }
|
||||
.primary { font-size: 12px; padding: 12px 15px; gap: 10px; }
|
||||
.manifestoStrip { margin: 0 30px; }
|
||||
.manifestoStrip > span { display: none; }
|
||||
.workflow { gap: 50px; padding-left: 30px; padding-right: 30px; }
|
||||
.featureGrid { gap: 20px; }
|
||||
.faq { gap: 45px; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.nav { height: 74px; gap: 15px; padding-left: 22px; padding-right: 22px; }
|
||||
.nav nav { display: none; }
|
||||
.navActions { gap: 15px; }
|
||||
.smallCta { padding: 8px 10px; gap: 6px; }
|
||||
.hero { grid-template-columns: 1fr; padding: 52px 24px 55px; gap: 45px; }
|
||||
.hero h1 { font-size: clamp(37px, 8.2vw, 58px); letter-spacing: -2px; }
|
||||
.heroCopy .eyebrow { font-size: 8px; letter-spacing: 0.8px; }
|
||||
.heroCopy > p { font-size: 14px; }
|
||||
.heroNote { margin-top: 22px; }
|
||||
.preview { width: 100%; max-width: 600px; margin: auto; }
|
||||
.ideaCard strong { font-size: clamp(14px, 3.4vw, 23px); }
|
||||
.noteCard strong { font-size: clamp(12px, 2.9vw, 19px); }
|
||||
.scene > button { padding: 12px; }
|
||||
.manifestoStrip { margin: 0 24px; justify-content: center; }
|
||||
.manifestoStrip p { font-size: 11px; }
|
||||
.space { padding: 65px 24px; }
|
||||
.sectionHeading { align-items: flex-start; gap: 20px; margin-bottom: 30px; }
|
||||
.sectionHeading h2, .faq h2 { font-size: 27px; }
|
||||
.sectionHeading > p { font-size: 10px; padding-top: 32px; }
|
||||
.featureGrid { grid-template-columns: 1fr; gap: 35px; }
|
||||
.featureArt { height: 210px; margin-bottom: 18px; }
|
||||
.collectArt span { min-width: 60%; padding: 12px; font-size: 12px; }
|
||||
.workflow { grid-template-columns: 1fr; gap: 35px; padding: 65px 24px; }
|
||||
.quote h2 { font-size: 30px; }
|
||||
.quote em { display: inline; }
|
||||
.faq { padding: 65px 24px; grid-template-columns: 1fr; gap: 20px; }
|
||||
.faqItem button { font-size: 13px; }
|
||||
.closing { padding: 65px 24px; }
|
||||
.closing h2 { font-size: 31px; }
|
||||
.closingDoodle { left: auto; right: 22px; top: 20px; font-size: 35px; }
|
||||
.footer { flex-wrap: wrap; gap: 18px; padding: 25px 24px; }
|
||||
.footer > p { display: none; }
|
||||
.footer > span { flex-basis: 100%; }
|
||||
}
|
||||
@media (max-width: 380px) {
|
||||
.page :global(.brand small) { display: none; }
|
||||
.hero { padding-left: 20px; padding-right: 20px; }
|
||||
.heroCopy .eyebrow { font-size: 7px; letter-spacing: 0.3px; }
|
||||
.heroActions { gap: 16px; }
|
||||
.scene > button { padding: 9px; }
|
||||
.cardLabel { font-size: 8px; }
|
||||
.cardText, .artCaption { font-size: 7px; }
|
||||
.ideaCard strong { margin-top: 8px; margin-bottom: 8px; }
|
||||
.noteTag { font-size: 7px; padding: 2px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .page a, .page button, .faqItem svg { transition: none; } }
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight, ArrowRight, ChevronDown, FileText, Image as ImageIcon, Layers, MousePointer2, Plus, Sparkles, Type } from "lucide-react";
|
||||
import { Brand } from "@/components/brand";
|
||||
import styles from "./landing-page.module.css";
|
||||
|
||||
const cards = ["先留住一个念头", "让不同的线索相遇", "慢慢成为你的作品"];
|
||||
const questions = [
|
||||
["还没想好要做什么,也可以开始吗?", "当然。一个词、一张参考图、一段还不成形的想法,都可以成为起点。先放下来,再慢慢找到它们之间的联系。"],
|
||||
["我可以放些什么在画布里?", "文字、笔记、图片、脚本和图标。你可以自由移动、调整样式、用线连接,也可以用分区把相关内容放在一起。"],
|
||||
["我的创作会自动公开吗?", "不会。工作空间需要登录访问,内容的访问范围由空间和材料权限控制。"],
|
||||
["可以先体验,再注册吗?", "可以。点击「先逛逛工作台」进入本地演示。演示内容保留在当前浏览器,登录后可以建立自己的工作空间。"],
|
||||
];
|
||||
|
||||
export default function LandingPage() {
|
||||
const [active, setActive] = useState(0);
|
||||
const [faq, setFaq] = useState<number | null>(null);
|
||||
return <div className="landing"><div className={styles.page}>
|
||||
<header className={styles.nav}>
|
||||
<Brand />
|
||||
<nav aria-label="主导航"><a href="#space">创作空间</a><a href="#workflow">工作方式</a><a href="#questions">一些问题</a></nav>
|
||||
<div className={styles.navActions}><Link href="/login">登录</Link><Link href="/login" className={styles.smallCta}>开始创作 <ArrowUpRight size={15} /></Link></div>
|
||||
</header>
|
||||
<main>
|
||||
<section className={styles.hero}>
|
||||
<div className={styles.heroCopy}>
|
||||
<div className={styles.eyebrow}><span className={styles.statusDot} /> A LITTLE SPACE. ENDLESS POSSIBILITIES.</div>
|
||||
<h1>给想法一点<br /><span>自由生长</span>的空间。</h1>
|
||||
<p>不必一开始就井井有条。<br />把零散的灵感放下来,连接起来,<br className={styles.mobileBreak} />慢慢做成自己的作品。</p>
|
||||
<div className={styles.heroActions}><Link href="/login" className={styles.primary}>打开你的创作空间 <ArrowUpRight size={18} /></Link><Link href="/dashboard?demo=1" className={styles.textLink}>先逛逛工作台 <ArrowRight size={16} /></Link></div>
|
||||
<div className={styles.heroNote}><span className={styles.scribbleStar}>✳</span> 留一点空白,给还没发生的好想法。</div>
|
||||
</div>
|
||||
<div className={styles.preview} aria-label="创作空间交互预览">
|
||||
<div className={styles.previewHeading}><span><span className={styles.tinyLogo}>↗</span> 一些还不错的想法 <ChevronDown size={11} /></span><span>PRIVATE SPACE</span></div>
|
||||
<div className={styles.scene}>
|
||||
<svg className={styles.wires} viewBox="0 0 600 440" fill="none" aria-hidden="true"><path d="M250 177 C335 177 315 137 393 137" /><path d="M416 220 C416 300 255 290 255 336" /><circle cx="250" cy="177" r="3" /><circle cx="393" cy="137" r="3" /><circle cx="416" cy="220" r="3" /><circle cx="255" cy="336" r="3" /></svg>
|
||||
<button className={`${styles.ideaCard} ${active === 0 ? styles.activeCard : ""}`} aria-pressed={active === 0} onClick={() => setActive(0)}><span className={styles.cardLabel}><FileText size={12} /> 随手记下</span><strong>如果灵感<br />有自己的形状?</strong><span className={styles.cardText}>也许从一片叶子,<br />或一个突然冒出来的问题开始。</span><span className={styles.paperCorner} /></button>
|
||||
<button className={`${styles.artCard} ${active === 1 ? styles.activeCard : ""}`} aria-pressed={active === 1} onClick={() => setActive(1)}><span className={styles.cardLabel}><ImageIcon size={12} /> 一点视觉线索</span><svg viewBox="0 0 180 200" aria-hidden="true"><path d="M92 164C89 133 94 111 94 88M94 125C72 125 59 110 57 94C78 91 90 105 94 125M94 143C118 143 134 129 137 110C112 107 96 123 94 143" /><path d="M93 88C47 109 32 78 60 62C35 32 67 11 87 43C96 7 130 18 116 50C151 37 168 66 132 79C158 104 119 121 101 89" /><circle cx="95" cy="68" r="13" /><path d="M67 173C85 168 108 170 126 175" /></svg><span className={styles.artCaption}>ordinary things, new eyes.</span></button>
|
||||
<button className={`${styles.noteCard} ${active === 2 ? styles.activeCard : ""}`} aria-pressed={active === 2} onClick={() => setActive(2)}><span className={styles.cardLabel}><Sparkles size={12} /> 连接之后</span><strong>把日常,<br />做成一点不日常。</strong><span className={styles.noteTag}>下一个小作品 ↗</span></button>
|
||||
<span className={styles.looseNote}>keep wondering.</span><div className={styles.cursor} aria-hidden="true"><MousePointer2 size={21} fill="currentColor" /><span>你</span></div>
|
||||
</div>
|
||||
<div className={styles.previewFooter}><div aria-label="预览选择">{cards.map((label, index) => <button key={label} aria-label={label} aria-pressed={active === index} onClick={() => setActive(index)} />)}</div><span>{cards[active]}</span><span>100%</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<div className={styles.manifestoStrip}><span>FOR THE CURIOUS MINDS.</span><p>认真生活的人,<span>总有一些想法值得留下。</span></p><span>COLLECT / CONNECT / CREATE</span></div>
|
||||
<section id="space" className={styles.space}>
|
||||
<div className={styles.sectionHeading}><div><div className={styles.eyebrow}>01 — YOUR THINKING SPACE</div><h2>思路可以自由,<br />也可以有迹可循。</h2></div><p>从随手收集,到看见联系。<br />把整理变成创作的一部分。</p></div>
|
||||
<div className={styles.featureGrid}>
|
||||
<article><div className={`${styles.featureArt} ${styles.collectArt}`} aria-hidden="true"><span><Type size={18} /> 一句还没写完的话</span><span><ImageIcon size={18} /> 某个让你停下来的画面</span><span><Sparkles size={18} /> 一点突如其来的好奇心</span></div><span className={styles.featureNumber}>01 / COLLECT</span><h3>先留下,再想一想。</h3><p>文字、图片、参考和碎片。灵感来的时候,<br />不需要先决定它应该放在哪个文件夹。</p></article>
|
||||
<article><div className={`${styles.featureArt} ${styles.connectArt}`} aria-hidden="true"><svg viewBox="0 0 320 170" fill="none"><path d="M68 57C150 57 130 115 220 115M220 115C240 115 250 60 280 60" /></svg><span>一个念头</span><span>另一种可能</span><i>✳</i></div><span className={styles.featureNumber}>02 / CONNECT</span><h3>让线索,自己说话。</h3><p>在无限画布上自由摆放、连接与分组。<br />原本无关的东西,也许正好能碰出火花。</p></article>
|
||||
<article><div className={`${styles.featureArt} ${styles.createArt}`} aria-hidden="true"><div><span>UNTITLED PROJECT</span><strong>想法,有形。</strong><span>文字 × 图像 × 你自己的表达</span></div><span className={styles.editBadge}><Layers size={13} /> 持续打磨中</span></div><span className={styles.featureNumber}>03 / CREATE</span><h3>慢慢变成,你的作品。</h3><p>调整文字、图标和画面细节。<br />让收集的每一点灵感,都有继续生长的可能。</p></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="workflow" className={styles.workflow}>
|
||||
<div className={styles.quote}><span className={styles.quoteMark}>“</span><h2>不急着有答案。<br />先和自己的想法,<em>待一会儿。</em></h2><p>一个属于你的工作空间,<br />也是一张可以反复涂改的草稿纸。</p><span className={styles.signature}>a work in progress, always.</span></div>
|
||||
<div className={styles.workflowList}>{[["01", "带着一点好奇,开始。", "从一张空白画布,或一个还没想好的念头出发。"], ["02", "把线索,放在一起。", "拖动、连接、分区,让自己的思路慢慢清晰。"], ["03", "继续打磨,不必着急完成。", "随时回来,加一点新想法,留下新的可能。"]].map(([number, title, copy]) => <div key={number}><span>{number}</span><div><h3>{title}</h3><p>{copy}</p></div><ArrowUpRight size={18} /></div>)}</div>
|
||||
</section>
|
||||
<section id="questions" className={styles.faq}><div><div className={styles.eyebrow}>A FEW THINGS TO KNOW</div><h2>开始之前,<br />你也许想知道。</h2></div><div>{questions.map(([question, answer], index) => <div className={styles.faqItem} key={question}><button aria-expanded={faq === index} aria-controls={`landing-faq-${index}`} onClick={() => setFaq(faq === index ? null : index)}><span>{question}</span><Plus size={17} style={{ rotate: faq === index ? "45deg" : "0deg" }} /></button><div id={`landing-faq-${index}`} hidden={faq !== index}><p>{answer}</p></div></div>)}</div></section>
|
||||
<section className={styles.closing}><span className={styles.closingDoodle} aria-hidden="true">✳</span><div className={styles.eyebrow}>YOUR NEXT IDEA STARTS HERE</div><h2>好想法,<br className={styles.mobileBreak} />值得一个好地方。</h2><p>不用准备好全部。带着一点好奇,先开始。</p><Link href="/login" className={styles.primary}>给想法留个位置 <ArrowUpRight size={18} /></Link></section>
|
||||
</main>
|
||||
<footer className={styles.footer}><Brand /><p>给想法留白,给创作空间。</p><a href="#space">回到创作空间 <ArrowUpRight size={14} /></a><span>© 2026 ENDOLEAP</span></footer>
|
||||
</div></div>;
|
||||
}
|
||||
Reference in New Issue
Block a user