Add public DS-2 welcome page on root for unauthenticated visitors
Second-brain scheme (logo + six cards with rays) reworked from the old platform's infographic. Root stays role-redirecting for signed-in users; middleware opens "/" as an exact match only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -1,12 +1,13 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { WelcomePage } from "@/components/welcome/welcome-page";
|
||||
|
||||
export default async function HomePage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
|
||||
if (!session) {
|
||||
redirect("/login");
|
||||
return <WelcomePage />;
|
||||
}
|
||||
|
||||
const role = session.user.role;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
// Draws 2px rays from the edge of every [data-scheme-card] to the central
|
||||
// [data-scheme-logo] circle. Redraws on container resize and after fonts load.
|
||||
export function SchemeConnectors() {
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const svg = ref.current;
|
||||
const scheme = svg?.parentElement;
|
||||
if (!svg || !scheme) return;
|
||||
|
||||
function draw() {
|
||||
if (!svg || !scheme) return;
|
||||
const logo = scheme.querySelector<HTMLElement>("[data-scheme-logo]");
|
||||
if (!logo) return;
|
||||
const s = scheme.getBoundingClientRect();
|
||||
const l = logo.getBoundingClientRect();
|
||||
const cx = l.left + l.width / 2 - s.left;
|
||||
const cy = l.top + l.height / 2 - s.top;
|
||||
const r = l.width / 2 + 4;
|
||||
svg.setAttribute("viewBox", `0 0 ${s.width} ${s.height}`);
|
||||
let lines = "";
|
||||
scheme.querySelectorAll<HTMLElement>("[data-scheme-card]").forEach((card) => {
|
||||
const c = card.getBoundingClientRect();
|
||||
const isLeft = card.dataset.schemeCard === "left";
|
||||
const x1 = (isLeft ? c.right : c.left) - s.left;
|
||||
const y1 = c.top + c.height / 2 - s.top;
|
||||
const dx = cx - x1;
|
||||
const dy = cy - y1;
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
const x2 = cx - (dx / d) * r;
|
||||
const y2 = cy - (dy / d) * r;
|
||||
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />`;
|
||||
});
|
||||
svg.innerHTML = lines;
|
||||
}
|
||||
|
||||
draw();
|
||||
const ro = new ResizeObserver(draw);
|
||||
ro.observe(scheme);
|
||||
document.fonts?.ready.then(draw).catch(() => {});
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
aria-hidden="true"
|
||||
className="hidden md:block absolute inset-0 w-full h-full pointer-events-none"
|
||||
style={{ stroke: "var(--border)", strokeWidth: 2 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Public landing shown on "/" for unauthenticated visitors.
|
||||
// DS-2 rework of the old platform's "second brain" infographic.
|
||||
import Link from "next/link";
|
||||
import {
|
||||
StickyNote,
|
||||
BookOpen,
|
||||
Lightbulb,
|
||||
Database,
|
||||
GraduationCap,
|
||||
Wrench,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { SchemeConnectors } from "./scheme-connectors";
|
||||
|
||||
interface SchemeBlock {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
const BLOCKS_LEFT: SchemeBlock[] = [
|
||||
{
|
||||
icon: StickyNote,
|
||||
title: "Заметки",
|
||||
items: [
|
||||
"разбираемся, как создавать качественные заметки, а не просто писать всё подряд",
|
||||
"изучаем самые эффективные подходы в заметкоделии и заметковедении",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: "Знания",
|
||||
items: [
|
||||
"превращаем сырые данные и разрозненную информацию в устойчивые знания, готовые к повторному применению",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Lightbulb,
|
||||
title: "Идеи",
|
||||
items: [
|
||||
"фиксируем, развиваем и воплощаем гениальные идеи в жизнь",
|
||||
"и не даём им сгинуть в процессе",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const BLOCKS_RIGHT: SchemeBlock[] = [
|
||||
{
|
||||
icon: Database,
|
||||
title: "Базы знаний",
|
||||
items: [
|
||||
"создаём картотеку, строим структуру, наполняем её качественными заметками",
|
||||
"связываем отдельные элементы в единую сеть идей и размышлений",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: GraduationCap,
|
||||
title: "Обучение",
|
||||
items: [
|
||||
"учимся правильно учиться, осваиваем эффективные стратегии",
|
||||
"выстраиваем свой индивидуальный трек развития",
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
title: "Инструменты",
|
||||
items: [
|
||||
"разбираемся с особенностями инструментов и выбираем самые подходящие",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ROW_START = ["md:row-start-1", "md:row-start-2", "md:row-start-3"];
|
||||
|
||||
function SchemeCard({ block, side, row }: { block: SchemeBlock; side: "left" | "right"; row: number }) {
|
||||
const Icon = block.icon;
|
||||
const colClass = side === "left" ? "md:col-start-1" : "md:col-start-3";
|
||||
return (
|
||||
<div
|
||||
data-scheme-card={side}
|
||||
className={`card-aubade relative z-[1] p-5 ${colClass} ${ROW_START[row]}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-10 h-10 flex-shrink-0"
|
||||
style={{ backgroundColor: "var(--accent)", border: "2px solid var(--foreground)" }}
|
||||
>
|
||||
<Icon size={22} style={{ color: "var(--foreground)" }} />
|
||||
</span>
|
||||
<span className="font-bold uppercase" style={{ fontSize: "16px", letterSpacing: "0.08em" }}>
|
||||
{block.title}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{block.items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="relative pl-5 leading-normal"
|
||||
style={{ fontSize: "14.5px" }}
|
||||
>
|
||||
<span className="absolute left-0" style={{ color: "var(--muted-foreground)" }}>
|
||||
—
|
||||
</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WelcomePage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<header
|
||||
className="flex items-center justify-between gap-3 px-4 sm:px-8 py-3.5"
|
||||
style={{ borderBottom: "2px solid var(--border)" }}
|
||||
>
|
||||
<span className="font-bold text-lg">Second Brain</span>
|
||||
<nav className="flex gap-3">
|
||||
<Link href="/login" className="btn-aubade text-sm">
|
||||
Войти
|
||||
</Link>
|
||||
<Link href="/register" className="btn-aubade btn-aubade-accent text-sm">
|
||||
Зарегистрироваться
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 w-full max-w-[1240px] mx-auto px-4 sm:px-8 pt-10 md:pt-14 pb-16">
|
||||
<p
|
||||
className="text-center text-xs font-bold uppercase mb-3.5"
|
||||
style={{ color: "var(--muted-foreground)", letterSpacing: "0.18em" }}
|
||||
>
|
||||
Образовательная платформа Second Brain
|
||||
</p>
|
||||
<h1 className="text-center font-bold leading-tight text-[26px] md:text-[40px] mb-2.5">
|
||||
Создаём и развиваем
|
||||
<br />
|
||||
второй мозг
|
||||
</h1>
|
||||
<p
|
||||
className="text-center mb-8 md:mb-14"
|
||||
style={{ color: "var(--muted-foreground)", fontSize: "17px" }}
|
||||
>
|
||||
Заметки, знания и идеи — в одной системе, которая работает на вас
|
||||
</p>
|
||||
|
||||
<div className="relative grid grid-cols-1 md:grid-cols-[1fr_320px_1fr] gap-y-5 md:gap-y-7 gap-x-11 items-center">
|
||||
<SchemeConnectors />
|
||||
<div className="md:col-start-2 md:row-start-1 md:row-span-3 flex flex-col items-center gap-4 mb-4 md:mb-0">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Second Brain"
|
||||
data-scheme-logo
|
||||
className="w-[150px] h-[150px] md:w-[280px] md:h-[280px]"
|
||||
/>
|
||||
<span
|
||||
className="text-xs font-bold uppercase"
|
||||
style={{ color: "var(--muted-foreground)", letterSpacing: "0.18em" }}
|
||||
>
|
||||
Second Brain
|
||||
</span>
|
||||
</div>
|
||||
{BLOCKS_LEFT.map((block, i) => (
|
||||
<SchemeCard key={block.title} block={block} side="left" row={i} />
|
||||
))}
|
||||
{BLOCKS_RIGHT.map((block, i) => (
|
||||
<SchemeCard key={block.title} block={block} side="right" row={i} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4 mt-10 md:mt-14">
|
||||
<Link href="/register" className="btn-aubade btn-aubade-accent px-6 py-2.5">
|
||||
Начать учиться →
|
||||
</Link>
|
||||
<Link href="/login" className="btn-aubade px-6 py-2.5">
|
||||
У меня уже есть доступ
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer
|
||||
className="flex flex-col sm:flex-row justify-between gap-1.5 px-4 sm:px-8 py-4 text-sm"
|
||||
style={{ borderTop: "2px solid var(--border)", color: "var(--muted-foreground)" }}
|
||||
>
|
||||
<span>ИП Second Brain Production</span>
|
||||
<a href="https://second-brain.ru" className="underline" style={{ color: "var(--muted-foreground)" }}>
|
||||
second-brain.ru — все курсы и материалы
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
-1
@@ -14,9 +14,11 @@ export function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
if (
|
||||
pathname === "/" || // public welcome page; "/" must stay an EXACT match (startsWith would open everything)
|
||||
PUBLIC_ROUTES.some((route) => pathname.startsWith(route)) ||
|
||||
pathname.startsWith("/_next") ||
|
||||
pathname.startsWith("/favicon")
|
||||
pathname.startsWith("/favicon") ||
|
||||
pathname === "/logo.svg"
|
||||
) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user