mirror of
https://github.com/FranP-code/Reflecto.git
synced 2025-10-13 00:43:31 +00:00
feat: add DotGrid component for enhanced visual effects on the homepage
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
"appwrite": "^14.0.1",
|
"appwrite": "^14.0.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
"lucide-react": "^0.473.0",
|
"lucide-react": "^0.473.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.2",
|
||||||
|
|||||||
22
apps/web/src/components/DotGrid.css
Normal file
22
apps/web/src/components/DotGrid.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.dot-grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-grid__wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-grid__canvas {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
268
apps/web/src/components/DotGrid.jsx
Normal file
268
apps/web/src/components/DotGrid.jsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
'use client';
|
||||||
|
import { useRef, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import { gsap } from 'gsap';
|
||||||
|
import { InertiaPlugin } from 'gsap/InertiaPlugin';
|
||||||
|
|
||||||
|
import './DotGrid.css';
|
||||||
|
|
||||||
|
gsap.registerPlugin(InertiaPlugin);
|
||||||
|
|
||||||
|
const throttle = (func, limit) => {
|
||||||
|
let lastCall = 0;
|
||||||
|
return function (...args) {
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - lastCall >= limit) {
|
||||||
|
lastCall = now;
|
||||||
|
func.apply(this, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
|
||||||
|
if (!m) return { r: 0, g: 0, b: 0 };
|
||||||
|
return {
|
||||||
|
r: parseInt(m[1], 16),
|
||||||
|
g: parseInt(m[2], 16),
|
||||||
|
b: parseInt(m[3], 16)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DotGrid = ({
|
||||||
|
dotSize = 16,
|
||||||
|
gap = 32,
|
||||||
|
baseColor = '#5227FF',
|
||||||
|
activeColor = '#5227FF',
|
||||||
|
proximity = 150,
|
||||||
|
speedTrigger = 100,
|
||||||
|
shockRadius = 250,
|
||||||
|
shockStrength = 5,
|
||||||
|
maxSpeed = 5000,
|
||||||
|
resistance = 750,
|
||||||
|
returnDuration = 1.5,
|
||||||
|
className = '',
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
|
const wrapperRef = useRef(null);
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
const dotsRef = useRef([]);
|
||||||
|
const pointerRef = useRef({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
speed: 0,
|
||||||
|
lastTime: 0,
|
||||||
|
lastX: 0,
|
||||||
|
lastY: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]);
|
||||||
|
const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]);
|
||||||
|
|
||||||
|
const circlePath = useMemo(() => {
|
||||||
|
if (typeof window === 'undefined' || !window.Path2D) return null;
|
||||||
|
|
||||||
|
const p = new window.Path2D();
|
||||||
|
p.arc(0, 0, dotSize / 2, 0, Math.PI * 2);
|
||||||
|
return p;
|
||||||
|
}, [dotSize]);
|
||||||
|
|
||||||
|
const buildGrid = useCallback(() => {
|
||||||
|
const wrap = wrapperRef.current;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!wrap || !canvas) return;
|
||||||
|
|
||||||
|
const { width, height } = wrap.getBoundingClientRect();
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
canvas.width = width * dpr;
|
||||||
|
canvas.height = height * dpr;
|
||||||
|
canvas.style.width = `${width}px`;
|
||||||
|
canvas.style.height = `${height}px`;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
const cols = Math.floor((width + gap) / (dotSize + gap));
|
||||||
|
const rows = Math.floor((height + gap) / (dotSize + gap));
|
||||||
|
const cell = dotSize + gap;
|
||||||
|
|
||||||
|
const gridW = cell * cols - gap;
|
||||||
|
const gridH = cell * rows - gap;
|
||||||
|
|
||||||
|
const extraX = width - gridW;
|
||||||
|
const extraY = height - gridH;
|
||||||
|
|
||||||
|
const startX = extraX / 2 + dotSize / 2;
|
||||||
|
const startY = extraY / 2 + dotSize / 2;
|
||||||
|
|
||||||
|
const dots = [];
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
const cx = startX + x * cell;
|
||||||
|
const cy = startY + y * cell;
|
||||||
|
dots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dotsRef.current = dots;
|
||||||
|
}, [dotSize, gap]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!circlePath) return;
|
||||||
|
|
||||||
|
let rafId;
|
||||||
|
const proxSq = proximity * proximity;
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const { x: px, y: py } = pointerRef.current;
|
||||||
|
|
||||||
|
for (const dot of dotsRef.current) {
|
||||||
|
const ox = dot.cx + dot.xOffset;
|
||||||
|
const oy = dot.cy + dot.yOffset;
|
||||||
|
const dx = dot.cx - px;
|
||||||
|
const dy = dot.cy - py;
|
||||||
|
const dsq = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
let style = baseColor;
|
||||||
|
if (dsq <= proxSq) {
|
||||||
|
const dist = Math.sqrt(dsq);
|
||||||
|
const t = 1 - dist / proximity;
|
||||||
|
const r = Math.round(baseRgb.r + (activeRgb.r - baseRgb.r) * t);
|
||||||
|
const g = Math.round(baseRgb.g + (activeRgb.g - baseRgb.g) * t);
|
||||||
|
const b = Math.round(baseRgb.b + (activeRgb.b - baseRgb.b) * t);
|
||||||
|
style = `rgb(${r},${g},${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(ox, oy);
|
||||||
|
ctx.fillStyle = style;
|
||||||
|
ctx.fill(circlePath);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(draw);
|
||||||
|
};
|
||||||
|
|
||||||
|
draw();
|
||||||
|
return () => cancelAnimationFrame(rafId);
|
||||||
|
}, [proximity, baseColor, activeRgb, baseRgb, circlePath]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
buildGrid();
|
||||||
|
let ro = null;
|
||||||
|
if ('ResizeObserver' in window) {
|
||||||
|
ro = new ResizeObserver(buildGrid);
|
||||||
|
wrapperRef.current && ro.observe(wrapperRef.current);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('resize', buildGrid);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (ro) ro.disconnect();
|
||||||
|
else window.removeEventListener('resize', buildGrid);
|
||||||
|
};
|
||||||
|
}, [buildGrid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = e => {
|
||||||
|
const now = performance.now();
|
||||||
|
const pr = pointerRef.current;
|
||||||
|
const dt = pr.lastTime ? now - pr.lastTime : 16;
|
||||||
|
const dx = e.clientX - pr.lastX;
|
||||||
|
const dy = e.clientY - pr.lastY;
|
||||||
|
let vx = (dx / dt) * 1000;
|
||||||
|
let vy = (dy / dt) * 1000;
|
||||||
|
let speed = Math.hypot(vx, vy);
|
||||||
|
if (speed > maxSpeed) {
|
||||||
|
const scale = maxSpeed / speed;
|
||||||
|
vx *= scale;
|
||||||
|
vy *= scale;
|
||||||
|
speed = maxSpeed;
|
||||||
|
}
|
||||||
|
pr.lastTime = now;
|
||||||
|
pr.lastX = e.clientX;
|
||||||
|
pr.lastY = e.clientY;
|
||||||
|
pr.vx = vx;
|
||||||
|
pr.vy = vy;
|
||||||
|
pr.speed = speed;
|
||||||
|
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
pr.x = e.clientX - rect.left;
|
||||||
|
pr.y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
for (const dot of dotsRef.current) {
|
||||||
|
const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y);
|
||||||
|
if (speed > speedTrigger && dist < proximity && !dot._inertiaApplied) {
|
||||||
|
dot._inertiaApplied = true;
|
||||||
|
gsap.killTweensOf(dot);
|
||||||
|
const pushX = dot.cx - pr.x + vx * 0.005;
|
||||||
|
const pushY = dot.cy - pr.y + vy * 0.005;
|
||||||
|
gsap.to(dot, {
|
||||||
|
inertia: { xOffset: pushX, yOffset: pushY, resistance },
|
||||||
|
onComplete: () => {
|
||||||
|
gsap.to(dot, {
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
duration: returnDuration,
|
||||||
|
ease: 'elastic.out(1,0.75)'
|
||||||
|
});
|
||||||
|
dot._inertiaApplied = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = e => {
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
const cx = e.clientX - rect.left;
|
||||||
|
const cy = e.clientY - rect.top;
|
||||||
|
for (const dot of dotsRef.current) {
|
||||||
|
const dist = Math.hypot(dot.cx - cx, dot.cy - cy);
|
||||||
|
if (dist < shockRadius && !dot._inertiaApplied) {
|
||||||
|
dot._inertiaApplied = true;
|
||||||
|
gsap.killTweensOf(dot);
|
||||||
|
const falloff = Math.max(0, 1 - dist / shockRadius);
|
||||||
|
const pushX = (dot.cx - cx) * shockStrength * falloff;
|
||||||
|
const pushY = (dot.cy - cy) * shockStrength * falloff;
|
||||||
|
gsap.to(dot, {
|
||||||
|
inertia: { xOffset: pushX, yOffset: pushY, resistance },
|
||||||
|
onComplete: () => {
|
||||||
|
gsap.to(dot, {
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
duration: returnDuration,
|
||||||
|
ease: 'elastic.out(1,0.75)'
|
||||||
|
});
|
||||||
|
dot._inertiaApplied = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const throttledMove = throttle(onMove, 50);
|
||||||
|
window.addEventListener('mousemove', throttledMove, { passive: true });
|
||||||
|
window.addEventListener('click', onClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', throttledMove);
|
||||||
|
window.removeEventListener('click', onClick);
|
||||||
|
};
|
||||||
|
}, [maxSpeed, speedTrigger, proximity, resistance, returnDuration, shockRadius, shockStrength]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`dot-grid ${className}`} style={style}>
|
||||||
|
<div ref={wrapperRef} className="dot-grid__wrap">
|
||||||
|
<canvas ref={canvasRef} className="dot-grid__canvas" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DotGrid;
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
|
import DotGrid from "@/components/DotGrid";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { trpc } from "@/utils/trpc";
|
import { trpc } from "@/utils/trpc";
|
||||||
@@ -264,129 +265,151 @@ const FEATURES: Feature[] = [
|
|||||||
|
|
||||||
function HomeComponent() {
|
function HomeComponent() {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<>
|
||||||
{/* Hero */}
|
<div
|
||||||
<section className="container mx-auto max-w-7xl px-4 pt-14 pb-24 sm:pt-20 sm:pb-32">
|
style={{
|
||||||
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
|
width: "100%",
|
||||||
<HealthBadge />
|
height: "100%",
|
||||||
<h1 className="mt-6 animate-shine bg-[linear-gradient(120deg,white,white_40%,rgba(255,255,255,0.7)_60%,rgba(255,255,255,0.3))] bg-clip-text font-semibold text-5xl text-transparent leading-tight [background-size:200%_100%] sm:text-6xl md:text-7xl lg:text-8xl">
|
position: "fixed",
|
||||||
Reflecto
|
}}
|
||||||
</h1>
|
>
|
||||||
<p className="mt-5 text-balance text-lg text-muted-foreground sm:text-xl">
|
<DotGrid
|
||||||
Your AI‑powered second brain. Capture voice, images, and
|
activeColor="#525151"
|
||||||
text—Reflecto turns it into a searchable, connected knowledge base.
|
baseColor="#281E37"
|
||||||
</p>
|
dotSize={5}
|
||||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
gap={15}
|
||||||
<Button asChild size="lg">
|
proximity={120}
|
||||||
<Link to="/login">
|
resistance={750}
|
||||||
<Sparkles className="mr-2 size-4" /> Get started
|
returnDuration={1.5}
|
||||||
</Link>
|
shockRadius={250}
|
||||||
</Button>
|
shockStrength={5}
|
||||||
<Button asChild size="lg" variant="secondary">
|
/>
|
||||||
<Link to="/dashboard">View dashboard</Link>
|
</div>
|
||||||
</Button>
|
<div className="relative">
|
||||||
</div>
|
{/* Hero */}
|
||||||
<div className="mt-8 flex items-center gap-3 text-muted-foreground text-xs">
|
<section className="container mx-auto max-w-7xl px-4 pt-14 pb-24 sm:pt-20 sm:pb-32">
|
||||||
<img
|
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
|
||||||
alt="Reflecto"
|
<HealthBadge />
|
||||||
className="h-5 w-5 rounded-sm"
|
<h1 className="mt-6 animate-shine bg-[linear-gradient(120deg,white,white_40%,rgba(255,255,255,0.7)_60%,rgba(255,255,255,0.3))] bg-clip-text font-semibold text-5xl text-transparent leading-tight [background-size:200%_100%] sm:text-6xl md:text-7xl lg:text-8xl">
|
||||||
height="20"
|
Reflecto
|
||||||
src="/logo.png"
|
</h1>
|
||||||
width="20"
|
<p className="mt-5 text-balance text-lg text-muted-foreground sm:text-xl">
|
||||||
/>
|
Your AI‑powered second brain. Capture voice, images, and
|
||||||
<span>Private by default • Powered by lightweight AI</span>
|
text—Reflecto turns it into a searchable, connected knowledge
|
||||||
</div>
|
base.
|
||||||
</div>
|
</p>
|
||||||
{/* Showcase preview */}
|
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||||
<div className="mt-12 sm:mt-16">
|
|
||||||
<PreviewCard />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Features */}
|
|
||||||
<section className="container mx-auto max-w-7xl px-4 py-16 sm:py-24">
|
|
||||||
{/* quick stats */}
|
|
||||||
<div className="mx-auto grid max-w-4xl grid-cols-2 gap-4 pb-8 text-center sm:grid-cols-4">
|
|
||||||
{[
|
|
||||||
{ n: "2x", l: "Faster capture" },
|
|
||||||
{ n: "100%", l: "Private by default" },
|
|
||||||
{ n: "< 1s", l: "Search latency" },
|
|
||||||
{ n: "∞", l: "Ideas connected" },
|
|
||||||
].map((s) => (
|
|
||||||
<div className="rounded-lg border bg-white/5 p-4" key={s.l}>
|
|
||||||
<div className="font-semibold text-3xl sm:text-4xl">{s.n}</div>
|
|
||||||
<div className="text-muted-foreground text-xs">{s.l}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 items-stretch gap-6 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8">
|
|
||||||
{FEATURES.map((f) => (
|
|
||||||
<FeatureTile feature={f} key={f.key} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* How it works */}
|
|
||||||
<section className="container mx-auto max-w-7xl px-4 py-24 sm:py-32">
|
|
||||||
<div className="mx-auto max-w-3xl text-center">
|
|
||||||
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-muted-foreground text-sm backdrop-blur-sm">
|
|
||||||
<Sparkles className="h-4 w-4" />
|
|
||||||
<span>How it works</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-balance font-bold text-4xl sm:text-5xl lg:text-6xl">
|
|
||||||
From chaos to clarity
|
|
||||||
</h2>
|
|
||||||
<p className="mt-6 text-lg text-muted-foreground sm:text-xl">
|
|
||||||
Transform scattered thoughts into organized knowledge in four
|
|
||||||
seamless steps.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<HowItWorks />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Call to action */}
|
|
||||||
<section className="container mx-auto max-w-6xl px-4 pt-6 pb-24">
|
|
||||||
<div className="relative overflow-hidden rounded-3xl border border-white/10 bg-white/[0.04] p-8 sm:p-10">
|
|
||||||
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-balance font-semibold text-2xl sm:text-3xl">
|
|
||||||
Build your second brain
|
|
||||||
</h3>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Start free. Your knowledge stays private.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Button asChild size="lg">
|
<Button asChild size="lg">
|
||||||
<Link to="/login">Create account</Link>
|
<Link to="/login">
|
||||||
|
<Sparkles className="mr-2 size-4" /> Get started
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild size="lg" variant="secondary">
|
<Button asChild size="lg" variant="secondary">
|
||||||
<Link to="/dashboard">Go to app</Link>
|
<Link to="/dashboard">View dashboard</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-8 flex items-center gap-3 text-muted-foreground text-xs">
|
||||||
|
<img
|
||||||
|
alt="Reflecto"
|
||||||
|
className="h-5 w-5 rounded-sm"
|
||||||
|
height="20"
|
||||||
|
src="/logo.png"
|
||||||
|
width="20"
|
||||||
|
/>
|
||||||
|
<span>Private by default • Powered by lightweight AI</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/* Showcase preview */}
|
||||||
</section>
|
<div className="mt-12 sm:mt-16">
|
||||||
|
<PreviewCard />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Features */}
|
||||||
<footer className="container mx-auto max-w-7xl px-4 pt-4 pb-10 text-muted-foreground text-xs">
|
<section className="container mx-auto max-w-7xl px-4 py-16 sm:py-24">
|
||||||
<div className="flex flex-col items-center justify-between gap-2 sm:flex-row">
|
{/* quick stats */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="mx-auto grid max-w-4xl grid-cols-2 gap-4 pb-8 text-center sm:grid-cols-4">
|
||||||
<img
|
{[
|
||||||
alt="Reflecto"
|
{ n: "2x", l: "Faster capture" },
|
||||||
className="h-4 w-4 rounded-sm"
|
{ n: "100%", l: "Private by default" },
|
||||||
height="16"
|
{ n: "< 1s", l: "Search latency" },
|
||||||
src="/logo.png"
|
{ n: "∞", l: "Ideas connected" },
|
||||||
width="16"
|
].map((s) => (
|
||||||
/>
|
<div className="rounded-lg border bg-white/5 p-4" key={s.l}>
|
||||||
<span>Reflecto</span>
|
<div className="font-semibold text-3xl sm:text-4xl">{s.n}</div>
|
||||||
|
<div className="text-muted-foreground text-xs">{s.l}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="grid grid-cols-1 items-stretch gap-6 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8">
|
||||||
<Link to="/">Home</Link>
|
{FEATURES.map((f) => (
|
||||||
<Link to="/dashboard">Dashboard</Link>
|
<FeatureTile feature={f} key={f.key} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</footer>
|
|
||||||
</div>
|
{/* How it works */}
|
||||||
|
<section className="container mx-auto max-w-7xl px-4 py-24 sm:py-32">
|
||||||
|
<div className="mx-auto max-w-3xl text-center">
|
||||||
|
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-muted-foreground text-sm backdrop-blur-sm">
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
<span>How it works</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-balance font-bold text-4xl sm:text-5xl lg:text-6xl">
|
||||||
|
From chaos to clarity
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 text-lg text-muted-foreground sm:text-xl">
|
||||||
|
Transform scattered thoughts into organized knowledge in four
|
||||||
|
seamless steps.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<HowItWorks />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Call to action */}
|
||||||
|
<section className="container mx-auto max-w-6xl px-4 pt-6 pb-24">
|
||||||
|
<div className="relative overflow-hidden rounded-3xl border border-white/10 bg-white/[0.04] p-8 sm:p-10">
|
||||||
|
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-balance font-semibold text-2xl sm:text-3xl">
|
||||||
|
Build your second brain
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Start free. Your knowledge stays private.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link to="/login">Create account</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="secondary">
|
||||||
|
<Link to="/dashboard">Go to app</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="container mx-auto max-w-7xl px-4 pt-4 pb-10 text-muted-foreground text-xs">
|
||||||
|
<div className="flex flex-col items-center justify-between gap-2 sm:flex-row">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
alt="Reflecto"
|
||||||
|
className="h-4 w-4 rounded-sm"
|
||||||
|
height="16"
|
||||||
|
src="/logo.png"
|
||||||
|
width="16"
|
||||||
|
/>
|
||||||
|
<span>Reflecto</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
<Link to="/dashboard">Dashboard</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -105,6 +105,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
gsap:
|
||||||
|
specifier: ^3.13.0
|
||||||
|
version: 3.13.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.473.0
|
specifier: ^0.473.0
|
||||||
version: 0.473.0(react@19.1.1)
|
version: 0.473.0(react@19.1.1)
|
||||||
@@ -3198,6 +3201,9 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
gsap@3.13.0:
|
||||||
|
resolution: {integrity: sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==}
|
||||||
|
|
||||||
has-bigints@1.1.0:
|
has-bigints@1.1.0:
|
||||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -7606,6 +7612,8 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
gsap@3.13.0: {}
|
||||||
|
|
||||||
has-bigints@1.1.0: {}
|
has-bigints@1.1.0: {}
|
||||||
|
|
||||||
has-property-descriptors@1.0.2:
|
has-property-descriptors@1.0.2:
|
||||||
|
|||||||
Reference in New Issue
Block a user