landing page complete

This commit is contained in:
Dhravya 2024-05-19 19:45:49 -05:00
parent ff8ec14940
commit 24bececdc0
21 changed files with 1004 additions and 217 deletions

View file

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

45
apps/web-v2/env.d.ts vendored
View file

@ -1,4 +1,47 @@
// Generated by Wrangler
// by running `wrangler types --env-interface CloudflareEnv env.d.ts`
interface CloudflareEnv {}
interface CloudflareEnv {
RATELIMITER: RateLimitBinding;
}
export interface RateLimitBinding {
limit: LimitFunc;
}
export interface LimitFunc {
(options: LimitOptions): Promise<RateLimitResult>;
}
interface RateLimitResult {
success: boolean;
}
export interface LimitOptions {
key: string;
}
export interface RateLimitResponse {
key: string;
success: boolean;
}
export interface RateLimitOptions {
continueOnRateLimit: boolean;
}
export type RateLimitKeyFunc = {
(c: Context): string;
};
declare global {
namespace NodeJS {
interface ProcessEnv {
// [key: string]: string | undefined;
RATELIMITER: RateLimitBinding;
CLOUDFLARE_TURNSTILE_TOKEN: string;
}
}
}
export {};

View file

@ -13,9 +13,15 @@
"cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
},
"dependencies": {
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.378.0",
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"next": "14.1.0"
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@cloudflare/next-on-pages": "1",

View file

@ -0,0 +1,44 @@
import Image from "next/image";
import React from "react";
import EmailInput from "./EmailInput";
function Cta() {
return (
<section
id="try"
className="relative mb-32 mt-40 flex w-full flex-col items-center justify-center gap-8"
>
<div className="absolute left-0 z-[-1] h-full w-full">
{/* a blue gradient line that's slightly tilted with blur (a lotof blur)*/}
<div className="overflow-hidden">
<div
className="absolute left-0 h-32 w-full overflow-hidden bg-[#369DFD] bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
</div>
<Image
src="/landing-ui-2.png"
alt="Landing page background"
width={1512}
height={1405}
priority
draggable="false"
className="absolute z-[-2] hidden select-none rounded-3xl bg-black md:block lg:w-[80%]"
/>
<h1 className="z-20 mt-4 text-center text-5xl font-light text-white">
Your bookmarks are collecting dust.
</h1>
<div className="text-center text-sm text-zinc-500">
Launching July 1st, 2024
</div>
<p className="text-soft-foreground-text z-20 text-center">
Time to change that. <br /> Sign up for the waitlist and be the first to
try Supermemory
</p>
<EmailInput />
</section>
);
}
export default Cta;

View file

@ -1,15 +1,77 @@
import React from "react";
"use client";
import { FormEvent, useState } from "react";
import formSubmitAction from "./formSubmitAction";
import { useToast } from "@/components/ui/use-toast";
function EmailInput() {
const [email, setEmail] = useState("");
const { toast } = useToast();
return (
<div className="z-20 w-full rounded-2xl bg-gradient-to-br from-gray-200/70 to-transparent p-[0.7px] md:w-1/2">
<input
type="email"
className="flex w-full items-center rounded-2xl bg-[#37485E] px-4 py-2 focus:outline-none"
placeholder="Enter your email"
autoFocus
/>
</div>
<form
onSubmit={async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const value = await formSubmitAction(email, "token" as string);
if (value.success) {
setEmail("");
toast({
title: "You are now on the waitlist! 🎉",
description:
"We will notify you when we launch. Check your inbox and spam folder for a surprise! 🎁",
});
} else {
console.error("email submission failed: ", value.value);
toast({
variant: "destructive",
title: "Uh oh! Something went wrong.",
description: `${value.value}`,
});
}
}}
className="flex w-full items-center justify-center gap-2"
>
<div
className={`transition-width z-20 rounded-2xl bg-gradient-to-br from-gray-200/70 to-transparent p-[0.7px] duration-300 ease-in-out ${email ? "w-[90%] md:w-[42%]" : "w-full md:w-1/2"}`}
>
<input
type="email"
name="email"
className={`transition-width flex w-full items-center rounded-2xl bg-[#37485E] px-4 py-2 duration-300 focus:outline-none`}
placeholder="Enter your email"
value={email}
required
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div
className="cf-turnstile"
data-sitekey="0x4AAAAAAAakohhUeXc99J7E"
></div>
{email && (
<button
type="submit"
className="transition-width rounded-xl bg-gray-700 p-2 text-white duration-300"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
/>
</svg>
</button>
)}
</form>
);
}

View file

@ -4,6 +4,7 @@ import { useState, useRef, useEffect } from "react";
import { Transition } from "@headlessui/react";
import Image from "next/image";
import CarouselIllustration from "@/../public/images/carousel-illustration-01.png";
import { X } from "@/utils/icons";
export default function Features() {
const [tab, setTab] = useState<number>(1);
@ -23,7 +24,10 @@ export default function Features() {
<section className="relative w-full overflow-hidden after:pointer-events-none after:absolute after:right-0 after:top-0 after:h-full after:w-96 after:bg-gradient-to-l after:from-[#369DFD30] max-lg:after:hidden">
<div className="py-12 md:py-20">
{/* Carousel */}
<div className="mx-auto max-w-xl px-4 sm:px-6 lg:max-w-6xl">
<div
id="use-cases"
className="mx-auto max-w-xl px-4 sm:px-6 lg:max-w-6xl"
>
<div className="space-y-12 lg:flex lg:space-x-12 lg:space-y-0 xl:space-x-24">
{/* Content */}
<div className="lg:min-w-[524px] lg:max-w-none">
@ -62,8 +66,9 @@ export default function Features() {
For Researchers
</div>
<div className="text-zinc-500">
Save time and keep things consistent with reusable images,
and 3D assets in shared libraries.
Add content to collections and use it as a knowledge base
for your research, link multiple sources together to get a
better understanding of the topic.
</div>
</div>
</button>
@ -87,8 +92,8 @@ export default function Features() {
For Content writers
</div>
<div className="text-zinc-500">
Save time and keep things consistent with reusable images,
and 3D assets in shared libraries.
Save time and use the writing assistant to generate
content based on your own saved collections and sources.
</div>
</div>
</button>
@ -112,8 +117,8 @@ export default function Features() {
For Developers
</div>
<div className="text-zinc-500">
Save time and keep things consistent with reusable images,
and 3D assets in shared libraries.
Talk to documentation websites, code snippets, etc. so you
never have to google the same thing a hundred times.
</div>
</div>
</button>
@ -195,26 +200,22 @@ export default function Features() {
</div>
{/* Features blocks */}
<div className="mx-auto mt-24 max-w-6xl px-4 sm:px-6 lg:mt-32">
<div
id="features"
className="mx-auto mt-24 max-w-6xl px-4 sm:px-6 lg:mt-32"
>
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-16">
{/* Block #1 */}
<div>
<div className="mb-1 flex items-center">
<svg
className="mr-2 fill-zinc-400"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
>
<path d="M15 9a1 1 0 0 1 0 2c-.441 0-1.243.92-1.89 1.716.319 1.005.529 1.284.89 1.284a1 1 0 0 1 0 2 2.524 2.524 0 0 1-2.339-1.545A3.841 3.841 0 0 1 9 16a1 1 0 0 1 0-2c.441 0 1.243-.92 1.89-1.716C10.57 11.279 10.361 11 10 11a1 1 0 0 1 0-2 2.524 2.524 0 0 1 2.339 1.545A3.841 3.841 0 0 1 15 9Zm-5-1H7.51l-.02.142C6.964 11.825 6.367 16 3 16a3 3 0 0 1-3-3 1 1 0 0 1 2 0 1 1 0 0 0 1 1c1.49 0 1.984-2.48 2.49-6H3a1 1 0 1 1 0-2h2.793c.52-3.1 1.4-6 4.207-6a3 3 0 0 1 3 3 1 1 0 0 1-2 0 1 1 0 0 0-1-1C8.808 2 8.257 3.579 7.825 6H10a1 1 0 0 1 0 2Z" />
</svg>
<X className="mr-2" />
<h3 className="font-inter-tight font-semibold text-zinc-200">
Discussions
Import all your Twitter bookmarks
</h3>
</div>
<p className="text-sm text-zinc-500">
Keep workflows efficient with tools that give teams visibility
throughout the process.
Use all the knowledge you've saved on Twitter to train your own
supermemory.
</p>
</div>
{/* Block #2 */}
@ -229,35 +230,15 @@ export default function Features() {
<path d="M13 16c-.153 0-.306-.035-.447-.105l-3.851-1.926c-.231.02-.465.031-.702.031-4.411 0-8-3.14-8-7s3.589-7 8-7 8 3.14 8 7c0 1.723-.707 3.351-2 4.63V15a1.003 1.003 0 0 1-1 1Zm-4.108-4.054c.155 0 .308.036.447.105L12 13.382v-2.187c0-.288.125-.562.341-.752C13.411 9.506 14 8.284 14 7c0-2.757-2.691-5-6-5S2 4.243 2 7s2.691 5 6 5c.266 0 .526-.02.783-.048a1.01 1.01 0 0 1 .109-.006Z" />
</svg>
<h3 className="font-inter-tight font-semibold text-zinc-200">
Team views
Chat with collections
</h3>
</div>
<p className="text-sm text-zinc-500">
Keep workflows efficient with tools that give teams visibility
throughout the process.
Use collections to talk to specific knowledgebases like 'My
twitter bookmarks', or 'Learning web development'
</p>
</div>
{/* Block #3 */}
<div>
<div className="mb-1 flex items-center">
<svg
className="mr-2 fill-zinc-400"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="16"
>
<path d="M13 0H1C.4 0 0 .4 0 1v14c0 .6.4 1 1 1h8l5-5V1c0-.6-.4-1-1-1ZM2 2h10v8H8v4H2V2Z" />
</svg>
<h3 className="font-inter-tight font-semibold text-zinc-200">
Powerful search
</h3>
</div>
<p className="text-sm text-zinc-500">
Keep workflows efficient with tools that give teams visibility
throughout the process.
</p>
</div>
{/* Block #4 */}
<div>
<div className="mb-1 flex items-center">
<svg
@ -269,12 +250,32 @@ export default function Features() {
<path d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7ZM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5Zm8.707 12.293a.999.999 0 1 1-1.414 1.414L11.9 13.314a8.019 8.019 0 0 0 1.414-1.414l2.393 2.393Z" />
</svg>
<h3 className="font-inter-tight font-semibold text-zinc-200">
Enhancing
Powerful search
</h3>
</div>
<p className="text-sm text-zinc-500">
Keep workflows efficient with tools that give teams visibility
throughout the process.
Look up anything you've saved in your supermemory, and get the
information you need in seconds.
</p>
</div>
{/* Block #4 */}
<div>
<div className="mb-1 flex items-center">
<svg
className="mr-2 fill-zinc-400"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="16"
>
<path d="M13 0H1C.4 0 0 .4 0 1v14c0 .6.4 1 1 1h8l5-5V1c0-.6-.4-1-1-1ZM2 2h10v8H8v4H2V2Z" />
</svg>
<h3 className="font-inter-tight font-semibold text-zinc-200">
Knowledge canvas
</h3>
</div>
<p className="text-sm text-zinc-500">
Arrange your saved information in a way that makes sense to you
in a 2d canvas.
</p>
</div>
{/* Block #5 */}
@ -289,32 +290,32 @@ export default function Features() {
<path d="M14.6.085 8 2.885 1.4.085c-.5-.2-1.4-.1-1.4.9v11c0 .4.2.8.6.9l7 3c.3.1.5.1.8 0l7-3c.4-.2.6-.5.6-.9v-11c0-1-.9-1.1-1.4-.9ZM2 2.485l5 2.1v8.8l-5-2.1v-8.8Zm12 8.8-5 2.1v-8.7l5-2.1v8.7Z" />
</svg>
<h3 className="font-inter-tight font-semibold text-zinc-200">
Powerful search
Just... bookmarks
</h3>
</div>
<p className="text-sm text-zinc-500">
Keep workflows efficient with tools that give teams visibility
throughout the process.
AI is cool, but sometimes you just need a place to save your
stuff. Supermemory is that place.
</p>
</div>
{/* Block #6 */}
<div>
<div className="mb-1 flex items-center">
<svg
className="mr-2 fill-zinc-400"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="16"
viewBox="0 0 20 20"
fill="currentColor"
className="mr-2 h-5 w-5 fill-zinc-400"
>
<path d="M13 14a1 1 0 0 1 0 2H1a1 1 0 0 1 0-2h12Zm-6.707-2.293-5-5a1 1 0 0 1 1.414-1.414L6 8.586V1a1 1 0 1 1 2 0v7.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-5 5a1 1 0 0 1-1.414 0Z" />
<path d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" />
</svg>
<h3 className="font-inter-tight font-semibold text-zinc-200">
Team views
Writing assistant
</h3>
</div>
<p className="text-sm text-zinc-500">
Keep workflows efficient with tools that give teams visibility
throughout the process.
Use our markdown editor to write content based on your saved
data, with the help of AI.
</p>
</div>
</div>

View file

@ -0,0 +1,67 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import { Twitter } from "@/utils/icons";
import EmailInput from "./EmailInput";
const slap = {
initial: {
opacity: 0,
scale: 1.1,
},
whileInView: { opacity: 1, scale: 1 },
transition: {
duration: 0.5,
ease: "easeInOut",
},
viewport: { once: true },
};
function Hero() {
return (
<>
<section className="mt-24 flex max-w-xl flex-col items-center justify-center gap-8 md:mt-40">
<a
className="flex items-center justify-center gap-4 rounded-full bg-white/10 px-4 py-2 text-sm"
href="https://twitter.com/supermemoryai"
>
<Twitter className="h-4 w-4 text-white" /> Follow us on Twitter
</a>
<motion.h1
{...{
...slap,
transition: { ...slap.transition, delay: 0.2 },
}}
className="text-center text-4xl font-light text-white md:text-5xl"
>
Build your own second brain with Supermemory
</motion.h1>
<motion.p
{...{
...slap,
transition: { ...slap.transition, delay: 0.3 },
}}
className="text-soft-foreground-text text-center"
>
Bring saved information from all over the internet into one place
where you can connect it, and ask AI about it
</motion.p>
<EmailInput />
</section>
<motion.img
{...{
...slap,
transition: { ...slap.transition, delay: 0.35 },
}}
src="/landing-ui.svg"
alt="Landing page background"
width={1512}
height={1405}
draggable="false"
className="z-[-2] mt-16 h-full w-[80%] select-none"
/>
</>
);
}
export default Hero;

View file

@ -0,0 +1,37 @@
import Logo from "@/../public/logo.svg";
import { Github } from "@/utils/icons";
import Image from "next/image";
import Link from "next/link";
import React from "react";
function Navbar() {
return (
<nav className="fixed top-0 z-[99999] mt-12 hidden w-full px-24 text-sm md:flex">
<div className="flex w-full flex-row justify-between rounded-2xl bg-white/10 shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)] backdrop-blur-lg backdrop-filter">
<Link href={"/"} className="flex flex-row items-center p-3 opacity-50">
<Image src={Logo} alt="Supermemory logo" width={40} height={40} />
</Link>
<div className="flex flex-row items-center gap-8 p-3">
<Link href={"#use-cases"} className="text-soft-foreground-text">
Use cases
</Link>
<Link href={"#features"} className="text-soft-foreground-text">
Features
</Link>
<Link href={"#try"} className="text-soft-foreground-text">
Try supermemory
</Link>
</div>
<Link
href="https://git.new/memory"
className="m-2 flex items-center gap-3 rounded-xl bg-white/20 px-4 text-center text-white"
>
<Github className="h-4 w-4" />
Open source
</Link>
</div>
</nav>
);
}
export default Navbar;

View file

@ -24,52 +24,66 @@ const icons = [
const RotatingIcons: React.FC = () => {
return (
<div className="relative m-2 mx-auto h-96 w-96 scale-[70%] md:scale-100">
<div className="relative h-full w-full rounded-full border border-gray-800">
{icons.map((icon, index) => (
<motion.div
key={icon.props}
className="absolute top-1/2 -translate-x-10 transform"
style={{
originX: "200px",
originY: "-8px",
}}
animate={{
rotate: [0, 360],
}}
transition={{
repeat: Infinity,
duration: 5,
ease: "linear",
delay: index,
}}
>
<motion.div
style={{
rotate: index * 72,
}}
animate={{
rotate: [0, -360],
}}
transition={{
repeat: Infinity,
duration: 5,
ease: "linear",
delay: index,
}}
>
{icon}
</motion.div>
</motion.div>
))}
<Image
src="/logo.svg"
alt="Supermemory logo"
width={80}
height={80}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform text-white"
/>
<div className="relative flex w-full flex-col items-center justify-center gap-8 px-4 sm:px-6">
<h3 className="font-inter-tight mb-4 mt-8 text-center text-3xl font-bold text-zinc-200">
Collect data from <br />{" "}
<span className="bg-gradient-to-r from-blue-500 to-blue-300 bg-clip-text italic text-transparent ">
any website{" "}
</span>{" "}
on the internet
</h3>
<div className="flex items-center justify-center">
<div className="relative m-2 mx-auto h-96 w-96 scale-[70%] md:scale-100 ">
<div className="relative h-full w-full rounded-full border border-gray-800">
{icons.map((icon, index) => (
<motion.div
key={index}
className="absolute top-1/2 -translate-x-10 transform"
style={{
originX: "200px",
originY: "-8px",
}}
animate={{
rotate: [0, 360],
}}
transition={{
repeat: Infinity,
duration: 5,
ease: "linear",
delay: index,
}}
>
<motion.div
style={{
rotate: index * 72,
}}
animate={{
rotate: [0, -360],
}}
transition={{
repeat: Infinity,
duration: 5,
ease: "linear",
delay: index,
}}
>
{icon}
</motion.div>
</motion.div>
))}
<Image
src="/logo.svg"
alt="Supermemory logo"
width={80}
height={80}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform text-white"
/>
</div>
</div>
</div>
<p className="text-center text-sm text-zinc-500">
... and bring it into your second brain
</p>
</div>
);
};

View file

@ -0,0 +1,47 @@
"use server";
import { headers } from "next/headers";
const formSubmitAction = async (email: string, token: string) => {
console.log("email submitted:", email);
const formBody = `email=${encodeURIComponent(email)}`;
const h = await headers();
const ip = h.get("cf-connecting-ip");
if (ip) {
if (process.env.RATELIMITER) {
const { success } = await process.env.RATELIMITER.limit({
key: `waitlist-${ip}`,
});
if (!success) {
console.error("rate limit exceeded");
return { value: "Rate limit exceeded", success: false };
}
} else {
console.info("RATELIMITER not found in env");
}
} else {
console.info("cf-connecting-ip not found in headers");
}
const resp = await fetch(
"https://app.loops.so/api/newsletter-form/clwcn8dde0059m6hobbdw2rwe",
{
method: "POST",
body: formBody,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
);
if (resp.ok) {
console.log("email submitted successfully");
return { value: await resp.json(), success: true };
} else {
console.error("email submission failed");
return { value: await resp.text(), success: false };
}
};
export default formSubmitAction;

View file

@ -1,117 +1,46 @@
import { Github, Twitter } from "@/utils/icons";
import Image from "next/image";
import EmailInput from "./EmailInput";
import Features from "./Features";
import RotatingIcons from "./RotatingIcons";
import Logo from "@/../public/logo.svg";
import Hero from "./Hero";
import Navbar from "./Navbar";
import Cta from "./Cta";
import { Toaster } from "@/components/ui/toaster";
export const runtime = "edge";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center overflow-x-hidden">
<div className="fixed top-0 z-[99999] mt-12 hidden w-full px-24 text-sm md:flex">
<nav className="flex w-full flex-row justify-between rounded-2xl bg-white/10 shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)] backdrop-blur-lg backdrop-filter">
<div className="flex flex-row items-center p-3 opacity-50">
<Image src={Logo} alt="Supermemory logo" width={40} height={40} />
</div>
<div className="flex flex-row items-center gap-8 p-3">
<button className="text-soft-foreground-text">Home</button>
<button className="text-soft-foreground-text">Features</button>
<button className="text-soft-foreground-text">Use cases</button>
<button className="text-soft-foreground-text">Testimonials</button>
</div>
<a className="m-2 flex items-center gap-3 rounded-xl bg-white/20 px-4 text-center text-white">
<Github className="h-4 w-4" />
Open source
</a>
</nav>
</div>
<main className="dark flex min-h-screen flex-col items-center overflow-x-hidden px-2 md:px-0">
<Navbar />
{/* Background gradients */}
<div className="absolute left-0 top-0 z-[-1] h-full w-full">
<div className="overflow-none">
<div className="overflow-x-hidden">
<div
className="overflow-none absolute left-0 h-32 w-full bg-[#369DFD] bg-opacity-70 blur-[337.4px]"
className="absolute left-0 h-32 w-[95%] overflow-x-hidden bg-[#369DFD] bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
{/* a blue gradient line that's slightly tilted with blur (a lotof blur)*/}
<div className="overflow-none">
<div className="overflow-x-hidden">
<div
className="overflow-none absolute left-0 top-[100%] h-32 w-full bg-[#369DFD] bg-opacity-40 blur-[337.4px]"
className="absolute left-0 top-[100%] h-32 w-[90%] overflow-x-hidden bg-[#369DFD] bg-opacity-40 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
</div>
{/* Hero section */}
<section className="mt-40 flex max-w-xl flex-col items-center justify-center gap-8">
<a
className="flex items-center justify-center gap-4 rounded-full bg-white/10 px-4 py-2 text-sm"
href="https://twitter.com/supermemoryai"
>
<Twitter className="h-4 w-4 text-white" /> Follow us on Twitter
</a>
<h1 className="text-center text-5xl font-light text-white">
Build your own second brain with Supermemory
</h1>
<p className="text-soft-foreground-text text-center">
Bring saved information from all over the internet into one place
where you can connect it, and ask AI about it
</p>
<EmailInput />
</section>
<Image
src="/landing-ui.svg"
alt="Landing page background"
width={1512}
height={1405}
priority
draggable="false"
className="z-[-2] mt-16 h-full w-[80%] select-none"
/>
<Hero />
{/* Features section */}
<Features />
<div className="relative flex w-full flex-col items-center justify-center gap-8 px-4 sm:px-6">
<h3 className="font-inter-tight mb-4 mt-8 text-center text-3xl font-bold text-zinc-200">
Collect data from <br />{" "}
<span className="bg-gradient-to-r from-blue-500 to-blue-300 bg-clip-text italic text-transparent ">
any website{" "}
</span>{" "}
on the internet
</h3>
<RotatingIcons />
<p className="text-center text-sm text-zinc-500">
... and bring it into your second brain
</p>
</div>
<RotatingIcons />
<section className="relative mb-32 mt-40 flex w-full flex-col items-center justify-center gap-8">
<div className="absolute left-0 z-[-1] h-full w-full">
{/* a blue gradient line that's slightly tilted with blur (a lotof blur)*/}
<div className="overflow-hidden">
<div
className="absolute left-0 h-32 w-full overflow-hidden bg-[#369DFD] bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
</div>
<Image
src="/landing-ui-2.png"
alt="Landing page background"
width={1512}
height={1405}
priority
draggable="false"
className="absolute z-[-2] hidden select-none rounded-3xl bg-black md:block lg:w-[80%]"
/>
<h1 className="z-20 mt-4 text-center text-5xl font-light text-white">
Your bookmarks are collecting dust.
</h1>
<p className="text-soft-foreground-text z-20 text-center">
Time to change that. <br /> Sign up for the waitlist and be the first
to try Supermemory
</p>
<EmailInput />
</section>
<Cta />
<Toaster />
<footer className="mt-16 flex w-full items-center justify-between gap-4 border-t border-zinc-200/50 px-8 py-8 text-sm text-zinc-500">
<p>© 2024 Supermemory.ai</p>

View file

@ -2,13 +2,84 @@
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--black-bg: #0f1114;
--soft-foreground: #ffffff;
--soft-foreground-text: #b2bcca;
@layer base {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--black-bg: #0f1114;
--soft-foreground: #ffffff;
--soft-foreground-text: #b2bcca;
--background: 216, 14%, 7%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 216, 14%, 7%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
body {

View file

@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Script from "next/script";
const inter = Inter({ subsets: ["latin"] });
@ -17,6 +18,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" />
<body className={inter.className}>{children}</body>
</html>
);

View file

@ -0,0 +1,129 @@
"use client";
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"ring-offset-background hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"text-foreground/50 hover:text-foreground absolute right-2 top-2 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View file

@ -0,0 +1,35 @@
"use client";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View file

@ -0,0 +1,191 @@
"use client";
// Inspired by react-hot-toast library
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -259,3 +259,19 @@ export const Notion = (props: SVGProps<SVGSVGElement>) => (
<path d="M164.09.608 16.092 11.538C4.155 12.573 0 20.374 0 29.726v162.245c0 7.284 2.585 13.516 8.826 21.843l34.789 45.237c5.715 7.284 10.912 8.844 21.825 8.327l171.864-10.404c14.532-1.035 18.696-7.801 18.696-19.24V55.207c0-5.911-2.336-7.614-9.21-12.66l-1.185-.856L198.37 8.409C186.94.1 182.27-.952 164.09.608ZM69.327 52.22c-14.033.945-17.216 1.159-25.186-5.323L23.876 30.778c-2.06-2.086-1.026-4.69 4.163-5.207l142.274-10.395c11.947-1.043 18.17 3.12 22.842 6.758l24.401 17.68c1.043.525 3.638 3.637.517 3.637L71.146 52.095l-1.819.125Zm-16.36 183.954V81.222c0-6.767 2.077-9.887 8.3-10.413L230.02 60.93c5.724-.517 8.31 3.12 8.31 9.879v153.917c0 6.767-1.044 12.49-10.387 13.008l-161.487 9.361c-9.343.517-13.489-2.594-13.489-10.921ZM212.377 89.53c1.034 4.681 0 9.362-4.681 9.897l-7.783 1.542v114.404c-6.758 3.637-12.981 5.715-18.18 5.715-8.308 0-10.386-2.604-16.609-10.396l-50.898-80.079v77.476l16.1 3.646s0 9.362-12.989 9.362l-35.814 2.077c-1.043-2.086 0-7.284 3.63-8.318l9.351-2.595V109.823l-12.98-1.052c-1.044-4.68 1.55-11.439 8.826-11.965l38.426-2.585 52.958 81.113v-71.76l-13.498-1.552c-1.043-5.733 3.111-9.896 8.3-10.404l35.84-2.087Z" />
</svg>
);
export const X = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 1200 1227"
{...props}
>
<path
fill="#fff"
d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z"
/>
</svg>
);

View file

@ -1,12 +1,22 @@
import type { Config } from "tailwindcss";
const config: Config = {
const config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
@ -17,9 +27,62 @@ const config: Config = {
"black-bg": "var(--black-bg)",
"soft-foreground": "var(--foreground-bg)",
"soft-foreground-text": "var(--soft-foreground-text)",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [],
};
plugins: [require("tailwindcss-animate")],
} satisfies Config;
export default config;

View file

@ -4,6 +4,13 @@ compatibility_date = "2024-05-12"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
# [[unsafe.bindings]]
# name = "RATELIMITER"
# type = "ratelimit"
# namespace_id = "1011"
# simple = { limit = 25, period = 10 }
# Automatically place your workloads in an optimal location to minimize latency.
# If you are running back-end logic in a Pages Function, running it closer to your back-end infrastructure
# rather than the end user may result in better performance.

View file

@ -86,7 +86,7 @@
"remark-gfm": "^4.0.0",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-animate": "^1.0.7",
"wrangler": "3.56.0",
"wrangler": "^3.57.0",
"zod": "^3.23.8"
}
}