mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-11 22:51:05 +00:00
landing page complete ✅
This commit is contained in:
parent
ff8ec14940
commit
24bececdc0
21 changed files with 1004 additions and 217 deletions
17
apps/web-v2/components.json
Normal file
17
apps/web-v2/components.json
Normal 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
45
apps/web-v2/env.d.ts
vendored
|
|
@ -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 {};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
44
apps/web-v2/src/app/(landing)/Cta.tsx
Normal file
44
apps/web-v2/src/app/(landing)/Cta.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
67
apps/web-v2/src/app/(landing)/Hero.tsx
Normal file
67
apps/web-v2/src/app/(landing)/Hero.tsx
Normal 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;
|
||||
37
apps/web-v2/src/app/(landing)/Navbar.tsx
Normal file
37
apps/web-v2/src/app/(landing)/Navbar.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
47
apps/web-v2/src/app/(landing)/formSubmitAction.ts
Normal file
47
apps/web-v2/src/app/(landing)/formSubmitAction.ts
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
129
apps/web-v2/src/components/ui/toast.tsx
Normal file
129
apps/web-v2/src/components/ui/toast.tsx
Normal 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,
|
||||
};
|
||||
35
apps/web-v2/src/components/ui/toaster.tsx
Normal file
35
apps/web-v2/src/components/ui/toaster.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
191
apps/web-v2/src/components/ui/use-toast.ts
Normal file
191
apps/web-v2/src/components/ui/use-toast.ts
Normal 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 };
|
||||
6
apps/web-v2/src/lib/utils.ts
Normal file
6
apps/web-v2/src/lib/utils.ts
Normal 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));
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue