File size: 11,652 Bytes
f74c934
00a4363
 
f74c934
00a4363
f74c934
00a4363
 
 
 
 
4c105cb
00a4363
 
 
4c105cb
00a4363
 
 
 
 
 
0d81e80
4c105cb
00a4363
 
 
 
 
 
 
 
4c105cb
 
 
 
 
 
00a4363
 
 
 
 
 
 
 
 
4c105cb
 
00a4363
 
 
 
 
 
 
 
 
4c105cb
00a4363
4c105cb
 
 
 
 
 
 
00a4363
 
 
 
 
 
 
 
 
 
2823b28
00a4363
 
 
 
f74c934
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2823b28
f74c934
 
 
 
 
 
 
 
0d81e80
 
f74c934
 
 
 
 
2823b28
f74c934
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00a4363
 
 
 
f74c934
00a4363
 
 
f74c934
 
00a4363
f74c934
 
 
 
 
 
00a4363
 
 
 
 
 
f74c934
00a4363
 
 
 
 
0c11338
 
 
 
 
 
00a4363
2823b28
00a4363
f74c934
00a4363
 
2823b28
 
00a4363
 
 
2823b28
00a4363
 
 
 
2823b28
00a4363
 
 
 
 
 
3d10303
00a4363
 
 
2823b28
 
 
 
00a4363
f74c934
 
 
 
 
 
 
00a4363
 
 
 
f74c934
00a4363
 
 
 
 
 
 
 
f74c934
 
00a4363
 
 
 
 
 
 
 
 
 
 
 
f74c934
 
 
 
 
 
 
 
00a4363
 
 
 
 
f74c934
 
00a4363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f74c934
 
00a4363
 
 
 
 
 
 
 
 
 
 
f74c934
00a4363
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
import { useEffect, useRef, useState, type FormEvent } from "react";
import { Link, useNavigate } from "react-router-dom";
import SkyCanvas from "../components/SkyCanvas";
import FeatureShowcase from "../components/FeatureShowcase";
import PricingTiers from "../components/PricingTiers";
import "../styles/showcase.css";
import { PublicFooter, PublicNav } from "../components/PublicChrome";
import { blogPosts } from "../content/blogPosts";
import { useAuth } from "../hooks/useAuth";
import {
  ArrowRightIcon,
  ConstellationIcon,
  HelmetIcon,
  LaunchWindowIcon,
  LaunchpadIcon,
  OrbitIcon,
  RocketIcon,
  SignalIcon,
  TelescopeIcon,
} from "../components/icons/Icons";

export const PENDING_SOURCE_KEY = "ns_pending_source";
// Bump after re-rendering public/demo.mp4 so browsers and the CDN fetch the new file.
const DEMO_VERSION = 5;

const FEATURES = [
  {
    icon: TelescopeIcon,
    label: "Research chat",
    title: "Ask your archive anything",
    body: "Every answer cites the exact passage it came from. If you never wrote it, Notestack says so.",
  },
  {
    icon: ConstellationIcon,
    label: "Mind Constellation",
    title: "Fly through your ideas",
    body: "Open a notebook as a galaxy of topics. Switch the shape, dive in and see the posts behind every star.",
  },
  {
    icon: SignalIcon,
    label: "Audio overview",
    title: "Two hosts, your ideas",
    body: "Turn a post or a whole notebook into a natural two host conversation, voiced by ElevenLabs.",
  },
  {
    icon: LaunchWindowIcon,
    label: "Video",
    title: "Narrated videos you can edit",
    body: "Pick a post and a template, then edit every scene with a live preview. Landscape for YouTube, vertical for Reels.",
  },
  {
    icon: RocketIcon,
    label: "Launch Kit",
    title: "One post, every platform",
    body: "Threads, LinkedIn, Substack Notes, hooks, quote cards and an SEO pack in one click.",
  },
  {
    icon: HelmetIcon,
    label: "Voices",
    title: "Sounds like you",
    body: "Notestack learns your tone from your posts, and you can describe, pick or clone a voice for narration.",
  },
  {
    icon: OrbitIcon,
    label: "Memory",
    title: "Remembers who you write for",
    body: "Short notes about your audience and style, learned from your chats and yours to edit anytime.",
  },
  {
    icon: LaunchpadIcon,
    label: "Launchpad",
    title: "Schedule the countdown",
    body: "Plan every launch on one calendar and resurface evergreen posts when they matter again.",
  },
];

const STEPS = [
  { n: "01", title: "Point it at your writing", body: "Any blog, newsletter or site URL, or upload markdown, text, HTML and PDF files." },
  { n: "02", title: "Watch them come into orbit", body: "Posts are cleaned, indexed and embedded in minutes." },
  { n: "03", title: "Launch", body: "Ask, listen, render and publish, all grounded in what you wrote." },
];

/** The narrated demo. Browsers only autoplay muted, so a button turns the captain's log on. */
function DemoVideo() {
  const ref = useRef<HTMLVideoElement>(null);
  const [muted, setMuted] = useState(true);
  const toggle = () => {
    const v = ref.current;
    if (!v) return;
    if (muted) {
      v.currentTime = 0; // start the narration from the top
      v.muted = false;
      void v.play();
    } else {
      v.muted = true;
    }
    setMuted(!muted);
  };
  return (
    <div className="demo-frame card arrive">
      <div className="demo-bar mono">
        <span>Mission log · 00:46</span>
        <button type="button" className="demo-sound" onClick={toggle} aria-pressed={!muted}>
          {muted ? "Play with sound" : "Mute"}
        </button>
      </div>
      <div className="demo-stage">
        <video
          ref={ref}
          className="demo-video"
          src={`/demo.mp4?v=${DEMO_VERSION}`}
          poster={`/demo-poster.jpg?v=${DEMO_VERSION}`}
          autoPlay
          muted
          loop
          playsInline
          preload="metadata"
          aria-label="Notestack product demo, narrated: connect any blog, site or markdown, ask your archive, hear an audio overview and launch posts"
        />
        {muted && (
          <button type="button" className="demo-unmute" onClick={toggle} aria-label="Play the demo with sound">
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <path d="M4 9v6h4l5 4V5L8 9H4z" />
              <path d="M16.5 8.5a5 5 0 0 1 0 7M19 6a8.5 8.5 0 0 1 0 12" />
            </svg>
            Hear the captain's log
          </button>
        )}
      </div>
    </div>
  );
}

/** Sections drift in out of the depth as they reach the viewport, like planets on approach. */
function useArrivals() {
  useEffect(() => {
    const els = Array.from(document.querySelectorAll<HTMLElement>(".arrive"));
    if (!("IntersectionObserver" in window) || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      els.forEach((el) => el.classList.add("arrived"));
      return;
    }
    const io = new IntersectionObserver(
      (entries) => {
        for (const e of entries) {
          if (e.isIntersecting) {
            e.target.classList.add("arrived");
            io.unobserve(e.target);
          }
        }
      },
      { threshold: 0.35, rootMargin: "0px 0px -12% 0px" },
    );
    els.forEach((el) => io.observe(el));
    return () => io.disconnect();
  }, []);
}

export default function Landing() {
  const [url, setUrl] = useState("");
  const navigate = useNavigate();
  const { user } = useAuth();
  useArrivals();

  const onSubmit = (e: FormEvent) => {
    e.preventDefault();
    // Always go to sign in / sign up. A pasted URL rides along and starts connecting after signup
    // (Welcome picks it up); signed in writers go straight to Mission Control, which does the same.
    const value = url.trim();
    if (value) {
      try {
        sessionStorage.setItem(PENDING_SOURCE_KEY, value);
      } catch {
        /* ignore */
      }
    }
    navigate(user ? "/app" : "/auth?mode=signup");
  };

  return (
    <div className="page">
      <SkyCanvas travel />
      <PublicNav />

      <main>
        <section className="hero">
          <div className="container hero-inner">
            <p className="eyebrow">
              <Link to="/notebooklm-alternative" className="eyebrow-link">
                The ultimate NotebookLM alternative
              </Link>{" "}
              for writers, bloggers and newsletters
            </p>
            <h1 className="hero-title">
              Your knowledge,
              <br />
              <span className="glow-text shimmer">in orbit.</span>
            </h1>
            <p className="hero-sub muted">
              Notestack turns your blog, newsletter, site or markdown files into a research notebook you can question,
              podcasts you can publish and launch kits for every platform. Every word traced back to what you actually wrote.
            </p>
            <form className="hero-form" onSubmit={onSubmit}>
              <label htmlFor="hero-url" className="sr-only">
                Your blog, newsletter or site URL
              </label>
              <input
                id="hero-url"
                className="input"
                placeholder="yourblog.com or yourname.substack.com"
                value={url}
                onChange={(e) => setUrl(e.target.value)}
                autoComplete="url"
                inputMode="url"
              />
              <button type="submit" className="btn btn-primary">
                Start Exploring
                <ArrowRightIcon size={18} />
              </button>
            </form>
            <p className="mono hero-meta">
              Works with Substack, Ghost, WordPress, Medium, any RSS feed or site, and markdown files.{" "}
              <Link to="/auth?mode=signup">Upload files instead</Link>
            </p>
          </div>
          <div className="horizon" aria-hidden="true">
            <span className="horizon-flare" />
            <span className="horizon-glint g1" />
            <span className="horizon-glint g2" />
            <span className="horizon-glint g3" />
          </div>
          <div className="hero-sweep" aria-hidden="true" />
        </section>

        <section className="section demo">
          <div className="container">
            <DemoVideo />
          </div>
        </section>

        <section id="features" className="section">
          <div className="container">
            <p className="eyebrow">What you get</p>
            <h2 className="section-title">Everything your writing can become</h2>
            <div className="feature-grid">
              {FEATURES.map(({ icon: Icon, label, title, body }, i) => (
                <article key={label} className="card feature arrive" style={{ ["--i" as string]: i % 3 }}>
                  <div className="feature-label">
                    <Icon />
                    <span className="mono">{label}</span>
                  </div>
                  <h3>{title}</h3>
                  <p className="muted">{body}</p>
                </article>
              ))}
            </div>
          </div>
        </section>

        <section id="tour" className="section tour">
          <div className="container">
            <p className="eyebrow">The tour</p>
            <h2 className="section-title">Seven stops across your archive</h2>
            <FeatureShowcase />
          </div>
        </section>

        <section className="section">
          <div className="container">
            <p className="eyebrow">How it works</p>
            <h2 className="section-title">Three steps to liftoff</h2>
            <ol className="steps">
              {STEPS.map((s, i) => (
                <li key={s.n} className="step arrive" style={{ ["--i" as string]: i }}>
                  <span className="step-n mono">{s.n}</span>
                  <h3>{s.title}</h3>
                  <p className="muted">{s.body}</p>
                </li>
              ))}
            </ol>
          </div>
        </section>

        <section id="pricing" className="section">
          <div className="container">
            <p className="eyebrow">Pricing</p>
            <h2 className="section-title">Pick your trajectory</h2>
            <PricingTiers />
          </div>
        </section>

        <section className="section">
          <div className="container">
            <p className="eyebrow">From the blog</p>
            <h2 className="section-title">Field notes</h2>
            <div className="blog-grid">
              {blogPosts.slice(0, 3).map((p, i) => (
                <Link key={p.slug} to={`/blogs/${p.slug}`} className="card blog-card arrive" style={{ ["--i" as string]: i }}>
                  <span className="mono muted">
                    {p.category} · {p.readTime}
                  </span>
                  <h3>{p.title}</h3>
                  <p className="muted">{p.description}</p>
                </Link>
              ))}
            </div>
          </div>
        </section>

        <section className="section final-cta arrive">
          <div className="container">
            <h2 className="section-title">Clear skies ahead.</h2>
            <p className="muted">Paste your URL above or start with an empty notebook.</p>
            <Link to="/auth?mode=signup" className="btn btn-primary">
              Start free
            </Link>
          </div>
        </section>
      </main>

      <PublicFooter />
    </div>
  );
}