diff --git a/package.json b/package.json
index cf67b97..990c8e0 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
+ "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.3",
@@ -55,6 +56,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.33.0",
+ "embla-carousel-react": "^8.5.1",
"framer-motion": "^11.13.1",
"geist": "^1.3.0",
"lucide-react": "^0.468.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9836e27..b2a71cf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@hookform/resolvers':
specifier: ^3.9.1
version: 3.9.1(react-hook-form@7.54.0(react@18.3.1))
+ '@radix-ui/react-accordion':
+ specifier: ^1.2.2
+ version: 1.2.2(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-aspect-ratio':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -113,6 +116,9 @@ importers:
drizzle-orm:
specifier: ^0.33.0
version: 0.33.0(@types/react@18.3.14)(postgres@3.4.5)(react@18.3.1)
+ embla-carousel-react:
+ specifier: ^8.5.1
+ version: 8.5.1(react@18.3.1)
framer-motion:
specifier: ^11.13.1
version: 11.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -799,6 +805,19 @@ packages:
'@radix-ui/primitive@1.1.1':
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
+ '@radix-ui/react-accordion@1.2.2':
+ resolution: {integrity: sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-arrow@1.1.0':
resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==}
peerDependencies:
@@ -838,6 +857,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-collapsible@1.1.2':
+ resolution: {integrity: sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-collection@1.1.0':
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies:
@@ -2325,6 +2357,19 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ embla-carousel-react@8.5.1:
+ resolution: {integrity: sha512-z9Y0K84BJvhChXgqn2CFYbfEi6AwEr+FFVVKm/MqbTQ2zIzO1VQri6w67LcfpVF0AjbhwVMywDZqY4alYkjW5w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+ embla-carousel-reactive-utils@8.5.1:
+ resolution: {integrity: sha512-n7VSoGIiiDIc4MfXF3ZRTO59KDp820QDuyBDGlt5/65+lumPHxX2JLz0EZ23hZ4eg4vZGUXwMkYv02fw2JVo/A==}
+ peerDependencies:
+ embla-carousel: 8.5.1
+
+ embla-carousel@8.5.1:
+ resolution: {integrity: sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==}
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -4421,6 +4466,23 @@ snapshots:
'@radix-ui/primitive@1.1.1': {}
+ '@radix-ui/react-accordion@1.2.2(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-collapsible': 1.1.2(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-direction': 1.1.0(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.14)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.14
+ '@types/react-dom': 18.3.2
+
'@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -4448,6 +4510,22 @@ snapshots:
'@types/react': 18.3.14
'@types/react-dom': 18.3.2
+ '@radix-ui/react-collapsible@1.1.2(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-context': 1.1.1(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-id': 1.1.0(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.14)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.14)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.14
+ '@types/react-dom': 18.3.2
+
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.2)(@types/react@18.3.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.14)(react@18.3.1)
@@ -5993,6 +6071,18 @@ snapshots:
eastasianwidth@0.2.0: {}
+ embla-carousel-react@8.5.1(react@18.3.1):
+ dependencies:
+ embla-carousel: 8.5.1
+ embla-carousel-reactive-utils: 8.5.1(embla-carousel@8.5.1)
+ react: 18.3.1
+
+ embla-carousel-reactive-utils@8.5.1(embla-carousel@8.5.1):
+ dependencies:
+ embla-carousel: 8.5.1
+
+ embla-carousel@8.5.1: {}
+
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
diff --git a/public/editor.png b/public/editor.png
new file mode 100644
index 0000000..b973242
Binary files /dev/null and b/public/editor.png differ
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 6cf49f0..2fd7c1d 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,11 +1,23 @@
-import Features from "@/components/features/page";
import Hero from "@/components/landing/hero";
+import FAQ from "@/components/landing/faq";
+import HowToUse from "@/components/landing/how-to-use";
+import Features from "@/components/landing/features";
+import { Footer } from "@/components/landing/Footer";
export default function Home() {
return (
-
+
+ Quickstart
+
+
+
+
+
+
);
}
diff --git a/src/components/landing/Footer.tsx b/src/components/landing/Footer.tsx
new file mode 100644
index 0000000..99865b0
--- /dev/null
+++ b/src/components/landing/Footer.tsx
@@ -0,0 +1,44 @@
+import Link from 'next/link'
+
+
+export function Footer() {
+ return (
+
+ )
+}
+
diff --git a/src/components/landing/faq.tsx b/src/components/landing/faq.tsx
new file mode 100644
index 0000000..7958d35
--- /dev/null
+++ b/src/components/landing/faq.tsx
@@ -0,0 +1,71 @@
+"use client"
+
+import { useState } from 'react'
+import { ChevronDown } from 'lucide-react'
+
+// FAQ data
+const faqs = [
+ {
+ question: "What is zero knowledge app?",
+ answer: "A Zero-Knowledge (ZK) App leverages zero-knowledge proofs (ZKPs) to ensure data privacy and security while enabling verifiable interactions. We don't store any of your personal information or your password. We only encrypt text which never leaves your browser."
+ },
+ {
+ question: "How can I recover my site if I forget my password?",
+ answer: "Since this is a ZK app. There is no way for us to know which text belongs to what user. So unfortunately it is not possible to recover the notes without the password. Even if they are recovered it is not possible to decrypt them."
+ },
+ {
+ question: "How can I backup my notes?",
+ answer: "Currently you cannot backup your notes. I am working on that feature."
+ },
+ {
+ question: "How can I share my notes?",
+ answer: "As of now you will need to share both the name and password for your notepad. In future I am planning to allow users to create public notepads that can be shared without a password."
+ },
+ {
+ question: "How do you verify the password if it is never sent to the server?",
+ answer: "We use your password to encrypt your text on the client side. The encrypted text is then sent to the server. When you access your page, we retrieve the encrypted text from the server and decrypt it using your password. If the password is correct, the text is successfully decrypted; otherwise, it remains encrypted."
+ }
+]
+
+export default function FAQ() {
+ const [openItems, setOpenItems] = useState([])
+
+ const toggleItem = (value: string) => {
+ setOpenItems(prev =>
+ prev.includes(value)
+ ? prev.filter(item => item !== value)
+ : [...prev, value]
+ )
+ }
+
+ return (
+
+
+ FAQs
+
+
+ {faqs.map((faq, index) => (
+
+
toggleItem(`item-${index}`)}
+ >
+ {faq.question}
+
+
+ {openItems.includes(`item-${index}`) && (
+
+ )}
+
+ ))}
+
+
+ )
+}
+
diff --git a/src/components/landing/features.tsx b/src/components/landing/features.tsx
new file mode 100644
index 0000000..1646e81
--- /dev/null
+++ b/src/components/landing/features.tsx
@@ -0,0 +1,86 @@
+import { LockKeyhole, IdCard, Github, GlobeLock, type LucideIcon } from 'lucide-react'
+import { Button } from "@/components/ui/button"
+import Link from 'next/link';
+import { Card, CardContent } from "@/components/ui/card"
+
+type FeatureProps = {
+ icon: LucideIcon;
+ title: string;
+ description: string;
+};
+
+export default function Features() {
+ const features: FeatureProps[] = [
+ {
+ icon: LockKeyhole,
+ title: "Set a password for your notes",
+ description: "We never store your password. Instead, your password is used as a key to encrypt your notepad. The encrypted output is then stored in our database, and without the password, it is essentially just a random set of characters."
+ },
+ {
+ icon: GlobeLock,
+ title: "Hashed Site names",
+ description: "Your site or notepad names are hashed before being stored in our database. This ensures that even if your password is compromised, it is impossible for anyone to determine your site name."
+ },
+ {
+ icon: IdCard,
+ title: "No login" ,
+ description: "Since we only need a password to encrypt your notes, there's no need for you to log in or provide your email or any other personal information."
+ },
+ {
+ icon: Github,
+ title: "Fully open-source",
+ description: "We are fully open-source on Github. You can feel free to fork the repo and self-deploy or make some customized changes for yourself."
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
Encrypted Notepad
+
+ Protect your notes with a secure password.
+
+
+
+ {features.map((feature, index) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function Feature({ icon: Icon, title, description }: FeatureProps) {
+ return (
+
+
+
+
+
+
{title}
+
{description}
+
+
+ )
+}
diff --git a/src/components/landing/hero.tsx b/src/components/landing/hero.tsx
index 5ef2002..a620cac 100644
--- a/src/components/landing/hero.tsx
+++ b/src/components/landing/hero.tsx
@@ -22,7 +22,7 @@ export default function Hero() {
-
+
diff --git a/src/components/landing/how-to-use.tsx b/src/components/landing/how-to-use.tsx
new file mode 100644
index 0000000..98930b3
--- /dev/null
+++ b/src/components/landing/how-to-use.tsx
@@ -0,0 +1,35 @@
+import * as React from "react";
+
+import { Card, CardContent } from "@/components/ui/card";
+
+export default function HowToUse() {
+ const steps = [
+ {
+ title: "Step 1",
+ description: "Create a notepad with a unique name by simply hovering to sealnotes.com/your-name",
+ },
+ {
+ title: "Step 2",
+ description: "Set a password and start writing notes",
+ },
+ {
+ title: "Step 3",
+ description: "Save and close the tab once you are done!",
+ },
+ ];
+
+ return (
+
+
+ {steps.map((step, index) => (
+
+
+ {step.title}
+ {step.description}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..2f55a32
--- /dev/null
+++ b/src/components/ui/accordion.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..ec505d0
--- /dev/null
+++ b/src/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 967c37b..62f8d04 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -7,7 +7,10 @@ export default {
theme: {
extend: {
fontFamily: {
- sans: ["var(--font-geist-sans)", ...fontFamily.sans]
+ sans: [
+ 'var(--font-geist-sans)',
+ ...fontFamily.sans
+ ]
},
borderRadius: {
lg: 'var(--radius)',
@@ -55,6 +58,28 @@ export default {
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
+ },
+ 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'
}
}
},