├── .vercelignore ├── .eslintrc.json ├── public ├── empty.png ├── error.png ├── reading.png ├── documents.png ├── empty-dark.png ├── error-dark.png ├── reading-dark.png ├── documents-dark.png ├── logo.svg └── logo-dark.svg ├── .github ├── images │ ├── img1.png │ ├── img2.png │ ├── img3.png │ ├── img4.png │ ├── img_main.png │ └── stats.svg ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── postcss.config.js ├── lib ├── utils.ts └── edgestore.ts ├── convex ├── .env.example ├── auth.config.js ├── environment.d.ts ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── schema.ts ├── tsconfig.json ├── README.md └── documents.ts ├── app ├── (public) │ ├── layout.tsx │ └── (routes) │ │ └── preview │ │ └── [documentId] │ │ └── page.tsx ├── (marketing) │ ├── layout.tsx │ ├── page.tsx │ └── _components │ │ ├── footer.tsx │ │ ├── logo.tsx │ │ ├── heros.tsx │ │ ├── navbar.tsx │ │ └── heading.tsx ├── api │ └── edgestore │ │ └── [...edgestore] │ │ └── route.ts ├── error.tsx ├── not-found.tsx ├── (main) │ ├── layout.tsx │ ├── (routes) │ │ └── documents │ │ │ ├── page.tsx │ │ │ └── [documentId] │ │ │ └── page.tsx │ └── _components │ │ ├── navbar.tsx │ │ ├── banner.tsx │ │ ├── title.tsx │ │ ├── user-item.tsx │ │ ├── menu.tsx │ │ ├── document-list.tsx │ │ ├── trash-box.tsx │ │ ├── publish.tsx │ │ ├── item.tsx │ │ └── navigation.tsx ├── layout.tsx └── globals.css ├── next.config.js ├── components ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── dialog.tsx │ ├── alert-dialog.tsx │ ├── command.tsx │ └── dropdown-menu.tsx ├── providers │ ├── theme-provider.tsx │ ├── toaster-provider.tsx │ ├── modal-provider.tsx │ └── convex-provider.tsx ├── spinner.tsx ├── modals │ ├── settings-modal.tsx │ ├── confirm-modal.tsx │ └── cover-image-modal.tsx ├── icon-picker.tsx ├── mode-toggle.tsx ├── editor.tsx ├── cover.tsx ├── search-command.tsx ├── toolbar.tsx └── single-image-dropzone.tsx ├── hooks ├── use-settings.tsx ├── use-search.tsx ├── use-origin.tsx ├── use-cover-image.tsx └── use-scroll-top.tsx ├── components.json ├── environment.d.ts ├── .gitignore ├── .env.example ├── SECURITY.md ├── tsconfig.json ├── LICENSE ├── config └── index.ts ├── tailwind.config.ts ├── package.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── README.md /.vercelignore: -------------------------------------------------------------------------------- 1 | *.md 2 | .env.example 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/empty.png -------------------------------------------------------------------------------- /public/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/error.png -------------------------------------------------------------------------------- /public/reading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/reading.png -------------------------------------------------------------------------------- /public/documents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/documents.png -------------------------------------------------------------------------------- /.github/images/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/.github/images/img1.png -------------------------------------------------------------------------------- /.github/images/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/.github/images/img2.png -------------------------------------------------------------------------------- /.github/images/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/.github/images/img3.png -------------------------------------------------------------------------------- /.github/images/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/.github/images/img4.png -------------------------------------------------------------------------------- /public/empty-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/empty-dark.png -------------------------------------------------------------------------------- /public/error-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/error-dark.png -------------------------------------------------------------------------------- /public/reading-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/reading-dark.png -------------------------------------------------------------------------------- /public/documents-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/public/documents-dark.png -------------------------------------------------------------------------------- /.github/images/img_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/notion-clone/HEAD/.github/images/img_main.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sanidhyy] 4 | patreon: sanidhy 5 | custom: https://www.buymeacoffee.com/sanidhy 6 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /convex/.env.example: -------------------------------------------------------------------------------- 1 | # convex/.env.local 2 | 3 | # clerk issuer url (go to your clerk dashboard > JWT Templates > New template > Convex > Save and copy your Issuer URL) 4 | CLERK_ISSUER_URL= 5 | -------------------------------------------------------------------------------- /convex/auth.config.js: -------------------------------------------------------------------------------- 1 | const authConfig = { 2 | providers: [ 3 | { 4 | domain: process.env.CLERK_ISSUER_URL, 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | 10 | export default authConfig; 11 | -------------------------------------------------------------------------------- /app/(public)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from "react"; 2 | 3 | const PublicLayout = ({ children }: PropsWithChildren) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default PublicLayout; 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "files.edgestore.dev", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | module.exports = nextConfig; 14 | -------------------------------------------------------------------------------- /lib/edgestore.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { type EdgeStoreRouter } from "../app/api/edgestore/[...edgestore]/route"; 4 | import { createEdgeStoreProvider } from "@edgestore/react"; 5 | 6 | const { EdgeStoreProvider, useEdgeStore } = 7 | createEdgeStoreProvider(); 8 | 9 | export { EdgeStoreProvider, useEdgeStore }; 10 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /hooks/use-settings.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type SettingsStore = { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | }; 8 | 9 | export const useSettings = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }), 13 | })); 14 | -------------------------------------------------------------------------------- /convex/environment.d.ts: -------------------------------------------------------------------------------- 1 | // This file is needed to support autocomplete for process.env 2 | export {}; 3 | 4 | declare global { 5 | namespace NodeJS { 6 | interface ProcessEnv { 7 | // clerk issuer url (go to your clerk dashboard > JWT Templates > New template > Convex > Save and copy your Issuer URL) 8 | CLERK_ISSUER_URL: string; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from "react"; 2 | 3 | import { Navbar } from "./_components/navbar"; 4 | 5 | const MarketingLayout = ({ children }: PropsWithChildren) => { 6 | return ( 7 |
8 | 9 | 10 |
{children}
11 |
12 | ); 13 | }; 14 | 15 | export default MarketingLayout; 16 | -------------------------------------------------------------------------------- /hooks/use-search.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type SearchStore = { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | toggle: () => void; 8 | }; 9 | 10 | export const useSearch = create((set, get) => ({ 11 | isOpen: false, 12 | onOpen: () => set({ isOpen: true }), 13 | onClose: () => set({ isOpen: false }), 14 | toggle: () => set({ isOpen: !get().isOpen }), 15 | })); 16 | -------------------------------------------------------------------------------- /hooks/use-origin.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useOrigin = () => { 4 | const [isMounted, setIsMounted] = useState(false); 5 | const origin = 6 | typeof window !== "undefined" && window.location.origin 7 | ? window.location.origin 8 | : ""; 9 | 10 | useEffect(() => { 11 | setIsMounted(true); 12 | }, []); 13 | 14 | if (!isMounted) return ""; 15 | 16 | return origin; 17 | }; 18 | -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | // This file is needed to support autocomplete for process.env 2 | export {}; 3 | 4 | declare global { 5 | namespace NodeJS { 6 | interface ProcessEnv { 7 | // convex public url 8 | NEXT_PUBLIC_CONVEX_URL: string; 9 | 10 | // clerk auth keys 11 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: string; 12 | 13 | // edge store keys 14 | EDGE_STORE_ACCESS_KEY: string; 15 | EDGE_STORE_SECRET_KEY: string; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /components/providers/toaster-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster } from "sonner"; 5 | 6 | import { Spinner } from "../spinner"; 7 | 8 | export const ToasterProvider = () => { 9 | const { resolvedTheme } = useTheme(); 10 | 11 | return ( 12 | } 16 | richColors 17 | /> 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /app/api/edgestore/[...edgestore]/route.ts: -------------------------------------------------------------------------------- 1 | import { initEdgeStore } from "@edgestore/server"; 2 | import { createEdgeStoreNextHandler } from "@edgestore/server/adapters/next/app"; 3 | 4 | const es = initEdgeStore.create(); 5 | 6 | const edgeStoreRouter = es.router({ 7 | publicFiles: es.fileBucket().beforeDelete(() => true), 8 | }); 9 | 10 | const handler = createEdgeStoreNextHandler({ 11 | router: edgeStoreRouter, 12 | }); 13 | 14 | export { handler as GET, handler as POST }; 15 | 16 | export type EdgeStoreRouter = typeof edgeStoreRouter; 17 | -------------------------------------------------------------------------------- /hooks/use-cover-image.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type CoverImageStore = { 4 | isOpen: boolean; 5 | url?: string; 6 | onOpen: () => void; 7 | onClose: () => void; 8 | onReplace: (url: string) => void; 9 | }; 10 | 11 | export const useCoverImage = create((set) => ({ 12 | url: undefined, 13 | isOpen: false, 14 | onOpen: () => set({ isOpen: true, url: undefined }), 15 | onClose: () => set({ isOpen: false, url: undefined }), 16 | onReplace: (url: string) => set({ isOpen: true, url }), 17 | })); 18 | -------------------------------------------------------------------------------- /hooks/use-scroll-top.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useScrollTop = (threshold = 10) => { 4 | const [scrolled, setScrolled] = useState(false); 5 | 6 | useEffect(() => { 7 | const handleScroll = () => { 8 | if (window.scrollY > threshold) setScrolled(true); 9 | else setScrolled(false); 10 | }; 11 | 12 | window.addEventListener("scroll", handleScroll); 13 | 14 | return () => window.removeEventListener("scroll", handleScroll); 15 | }, [threshold]); 16 | 17 | return scrolled; 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.7.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "./_components/footer"; 2 | import { Heading } from "./_components/heading"; 3 | import { Heros } from "./_components/heros"; 4 | 5 | const MarketingPage = () => { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default MarketingPage; 19 | -------------------------------------------------------------------------------- /components/providers/modal-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | import { CoverImageModal } from "@/components/modals/cover-image-modal"; 6 | import { SettingsModal } from "@/components/modals/settings-modal"; 7 | 8 | export const ModalProvider = () => { 9 | const [isMounted, setIsMounted] = useState(false); 10 | 11 | useEffect(() => { 12 | setIsMounted(true); 13 | }, []); 14 | 15 | if (!isMounted) return null; 16 | 17 | return ( 18 | <> 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | documents: defineTable({ 6 | title: v.string(), 7 | userId: v.string(), 8 | isArchived: v.boolean(), 9 | parentDocument: v.optional(v.id("documents")), 10 | content: v.optional(v.string()), 11 | coverImage: v.optional(v.string()), 12 | icon: v.optional(v.string()), 13 | isPublished: v.boolean(), 14 | }) 15 | .index("by_user", ["userId"]) 16 | .index("by_user_parent", ["userId", "parentDocument"]), 17 | }); 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # .env.local 2 | 3 | # disabled next.js telemetry 4 | NEXT_TELEMETRY_DISABLED=1 5 | 6 | # deployment used by `npx convex dev` 7 | CONVEX_DEPLOYMENT=dev: # team: , project: 8 | 9 | # convex public url 10 | NEXT_PUBLIC_CONVEX_URL="" 11 | 12 | # clerk auth keys 13 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 14 | CLERK_SECRET_KEY=sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 15 | 16 | # edge store keys 17 | EDGE_STORE_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXX 18 | EDGE_STORE_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 19 | -------------------------------------------------------------------------------- /app/(marketing)/_components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | 3 | import { Logo } from "./logo"; 4 | 5 | export const Footer = () => { 6 | return ( 7 |
8 | 9 | 10 |
11 | 14 | 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /components/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import { Loader } from "lucide-react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const spinnerVariants = cva("text-muted-foreground animate-spin", { 7 | variants: { 8 | size: { 9 | default: "h-4 w-4", 10 | sm: "h-2 w-2", 11 | lg: "h-6 w-6", 12 | icon: "h-10 w-10", 13 | }, 14 | }, 15 | defaultVariants: { 16 | size: "default", 17 | }, 18 | }); 19 | 20 | type SpinnerProps = VariantProps; 21 | 22 | export const Spinner = ({ size }: SpinnerProps) => { 23 | return ; 24 | }; 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "skipLibCheck": true, 20 | "noEmit": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"] 24 | } 25 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | 8 | const Error = () => { 9 | return ( 10 |
11 | Error 18 | Error 25 | 26 |

Something went wrong.

27 | 30 |
31 | ); 32 | }; 33 | 34 | export default Error; 35 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | import { Button } from "@/components/ui/button"; 5 | 6 | const NotFound = () => { 7 | return ( 8 |
9 | Error 16 | Error 23 | 24 |

404

25 |

Page Not Found.

26 | 29 |
30 | ); 31 | }; 32 | 33 | export default NotFound; 34 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.7.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as documents from "../documents.js"; 18 | 19 | /** 20 | * A utility for referencing Convex functions in your app's API. 21 | * 22 | * Usage: 23 | * ```js 24 | * const myFunctionReference = api.myModule.myFunction; 25 | * ``` 26 | */ 27 | declare const fullApi: ApiFromModules<{ 28 | documents: typeof documents; 29 | }>; 30 | export declare const api: FilterApi< 31 | typeof fullApi, 32 | FunctionReference 33 | >; 34 | export declare const internal: FilterApi< 35 | typeof fullApi, 36 | FunctionReference 37 | >; 38 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /app/(marketing)/_components/logo.tsx: -------------------------------------------------------------------------------- 1 | import { Poppins } from "next/font/google"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const font = Poppins({ 8 | subsets: ["latin"], 9 | weight: ["400", "600"], 10 | }); 11 | 12 | export const Logo = () => { 13 | return ( 14 | 18 | Logo 26 | 27 | Logo 35 |

Jotion

36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /components/providers/convex-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ClerkProvider, useAuth } from "@clerk/clerk-react"; 4 | import { dark } from "@clerk/themes"; 5 | import { ConvexReactClient } from "convex/react"; 6 | import { ConvexProviderWithClerk } from "convex/react-clerk"; 7 | import { useTheme } from "next-themes"; 8 | import { type PropsWithChildren } from "react"; 9 | 10 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 11 | 12 | export const ConvexClientProvider = ({ children }: PropsWithChildren) => { 13 | const { resolvedTheme } = useTheme(); 14 | 15 | return ( 16 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useConvexAuth } from "convex/react"; 4 | import { redirect } from "next/navigation"; 5 | import { PropsWithChildren } from "react"; 6 | 7 | import { SearchCommand } from "@/components/search-command"; 8 | import { Spinner } from "@/components/spinner"; 9 | 10 | import { Navigation } from "./_components/navigation"; 11 | 12 | const MainLayout = ({ children }: PropsWithChildren) => { 13 | const { isAuthenticated, isLoading } = useConvexAuth(); 14 | 15 | if (isLoading) { 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | 23 | if (!isAuthenticated) return redirect("/"); 24 | 25 | return ( 26 |
27 | 28 |
29 | 30 | {children} 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default MainLayout; 37 | -------------------------------------------------------------------------------- /components/modals/settings-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ModeToggle } from "@/components/mode-toggle"; 4 | import { Dialog, DialogContent, DialogHeader } from "@/components/ui/dialog"; 5 | import { Label } from "@/components/ui/label"; 6 | import { useSettings } from "@/hooks/use-settings"; 7 | 8 | export const SettingsModal = () => { 9 | const settings = useSettings(); 10 | 11 | return ( 12 | 13 | 14 | 15 |

My settings

16 |
17 | 18 |
19 |
20 | 21 | 22 | Customize how Jotion looks on your device. 23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sanidhya Kumar Verma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/icon-picker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import EmojiPicker, { Theme } from "emoji-picker-react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { 7 | Popover, 8 | PopoverContent, 9 | PopoverTrigger, 10 | } from "@/components/ui/popover"; 11 | 12 | type IconPickerProps = { 13 | onChange: (icon: string) => void; 14 | children: React.ReactNode; 15 | asChild?: boolean; 16 | }; 17 | 18 | export const IconPicker = ({ 19 | onChange, 20 | children, 21 | asChild, 22 | }: IconPickerProps) => { 23 | const { resolvedTheme } = useTheme(); 24 | 25 | const currentTheme = (resolvedTheme || "light") as keyof typeof themeMap; 26 | 27 | const themeMap = { 28 | dark: Theme.DARK, 29 | light: Theme.LIGHT, 30 | }; 31 | 32 | const theme = themeMap[currentTheme]; 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | 39 | onChange(data.emoji)} 43 | /> 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Moon, Sun } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu"; 13 | 14 | export function ModeToggle() { 15 | const { setTheme } = useTheme(); 16 | 17 | return ( 18 | 19 | 20 | 25 | 26 | 27 | setTheme("light")}> 28 | Light 29 | 30 | setTheme("dark")}> 31 | Dark 32 | 33 | setTheme("system")}> 34 | System 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/(marketing)/_components/heros.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export const Heros = () => { 4 | return ( 5 |
6 |
7 |
8 | Documents 15 | 16 | Documents 23 |
24 | 25 |
26 | Reading 33 | Reading 40 |
41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /components/editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { PartialBlock, BlockNoteEditor } from "@blocknote/core"; 4 | import { BlockNoteView, useBlockNote } from "@blocknote/react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { useEdgeStore } from "@/lib/edgestore"; 8 | 9 | import "@blocknote/core/style.css"; 10 | 11 | type EditorProps = { 12 | onChange: (value: string) => void; 13 | initialContent?: string; 14 | editable?: boolean; 15 | }; 16 | 17 | const Editor = ({ onChange, initialContent, editable }: EditorProps) => { 18 | const { resolvedTheme } = useTheme(); 19 | const { edgestore } = useEdgeStore(); 20 | 21 | const handleUpload = async (file: File) => { 22 | const response = await edgestore.publicFiles.upload({ 23 | file, 24 | }); 25 | 26 | return response.url; 27 | }; 28 | 29 | const editor: BlockNoteEditor = useBlockNote({ 30 | editable, 31 | initialContent: initialContent 32 | ? (JSON.parse(initialContent) as PartialBlock[]) 33 | : undefined, 34 | onEditorContentChange: (editor) => { 35 | if (!editable) return; 36 | 37 | onChange(JSON.stringify(editor.topLevelBlocks, null, 2)); 38 | }, 39 | uploadFile: handleUpload, 40 | }); 41 | 42 | return ( 43 |
44 | 48 |
49 | ); 50 | }; 51 | 52 | export default Editor; 53 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | export const siteConfig: Metadata = { 4 | title: "Jotion", 5 | description: "The connected workspace where better, faster work happens.", 6 | icons: { 7 | icon: [ 8 | { 9 | media: "(prefers-color-scheme: light)", 10 | url: "/logo.svg", 11 | href: "/logo.svg", 12 | }, 13 | { 14 | media: "(prefers-color-scheme: dark)", 15 | url: "/logo-dark.svg", 16 | href: "/logo-dark.svg", 17 | }, 18 | ], 19 | }, 20 | keywords: [ 21 | "reactjs", 22 | "nextjs", 23 | "vercel", 24 | "react", 25 | "blocknote", 26 | "edgestore", 27 | "shadcn", 28 | "shadcn-ui", 29 | "radix-ui", 30 | "cn", 31 | "clsx", 32 | "notion-clone", 33 | "convex", 34 | "sonner", 35 | "zustand", 36 | "zod", 37 | "sql", 38 | "postgresql", 39 | "aiven", 40 | "lucide-react", 41 | "next-themes", 42 | "postcss", 43 | "prettier", 44 | "react-dom", 45 | "tailwindcss", 46 | "tailwindcss-animate", 47 | "ui/ux", 48 | "js", 49 | "javascript", 50 | "typescript", 51 | "eslint", 52 | "html", 53 | "css", 54 | ] as Array, 55 | authors: { 56 | name: "Sanidhya Kumar Verma", 57 | url: "https://github.com/sanidhyy", 58 | }, 59 | } as const; 60 | 61 | export const links = { 62 | sourceCode: "https://github.com/sanidhyy/notion-clone", 63 | } as const; 64 | -------------------------------------------------------------------------------- /components/modals/confirm-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | AlertDialog, 5 | AlertDialogAction, 6 | AlertDialogCancel, 7 | AlertDialogContent, 8 | AlertDialogDescription, 9 | AlertDialogFooter, 10 | AlertDialogHeader, 11 | AlertDialogTitle, 12 | AlertDialogTrigger, 13 | } from "@/components/ui/alert-dialog"; 14 | 15 | type ConfirmModalProps = { 16 | children: React.ReactNode; 17 | onConfirm: () => void; 18 | }; 19 | 20 | export const ConfirmModal = ({ children, onConfirm }: ConfirmModalProps) => { 21 | const handleConfirm = ( 22 | e: React.MouseEvent 23 | ) => { 24 | e.stopPropagation(); 25 | 26 | onConfirm(); 27 | }; 28 | return ( 29 | 30 | e.stopPropagation()} asChild> 31 | {children} 32 | 33 | 34 | 35 | Are you sure? 36 | 37 | This action cannot be undone. 38 | 39 | 40 | 41 | 42 | e.stopPropagation()}> 43 | Cancel 44 | 45 | Confirm 46 | 47 | 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { type PropsWithChildren } from "react"; 4 | 5 | import { ConvexClientProvider } from "@/components/providers/convex-provider"; 6 | import { ModalProvider } from "@/components/providers/modal-provider"; 7 | import { ThemeProvider } from "@/components/providers/theme-provider"; 8 | import { ToasterProvider } from "@/components/providers/toaster-provider"; 9 | import { siteConfig } from "@/config"; 10 | import { EdgeStoreProvider } from "@/lib/edgestore"; 11 | 12 | import "./globals.css"; 13 | 14 | const inter = Inter({ subsets: ["latin"] }); 15 | 16 | export const metadata: Metadata = siteConfig; 17 | 18 | export const viewport: Viewport = { 19 | themeColor: [ 20 | { media: "(prefers-color-scheme: dark)", color: "#1F1F1F" }, 21 | { media: "(prefers-color-scheme: light)", color: "#EFEFEF" }, 22 | ], 23 | }; 24 | 25 | const RootLayout = ({ children }: PropsWithChildren) => { 26 | return ( 27 | 28 | 29 | 36 | 37 | 38 | 39 | 40 | {children} 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | export default RootLayout; 49 | -------------------------------------------------------------------------------- /app/(main)/(routes)/documents/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useUser } from "@clerk/clerk-react"; 4 | import { useMutation } from "convex/react"; 5 | import { PlusCircle } from "lucide-react"; 6 | import Image from "next/image"; 7 | import { useRouter } from "next/navigation"; 8 | import { toast } from "sonner"; 9 | 10 | import { Button } from "@/components/ui/button"; 11 | import { api } from "@/convex/_generated/api"; 12 | 13 | const DocumentsPage = () => { 14 | const { user } = useUser(); 15 | const router = useRouter(); 16 | const create = useMutation(api.documents.create); 17 | 18 | const onCreate = () => { 19 | const promise = create({ title: "Untitled" }).then((documentId) => 20 | router.push(`/documents/${documentId}`) 21 | ); 22 | 23 | toast.promise(promise, { 24 | loading: "Creating a new note...", 25 | success: "New note created.", 26 | error: "Failed to create a new note.", 27 | }); 28 | }; 29 | 30 | return ( 31 |
32 | Empty 39 | Empty 46 | 47 |

48 | Welcome to {user?.firstName}'s Jotion. 49 |

50 | 54 |
55 | ); 56 | }; 57 | 58 | export default DocumentsPage; 59 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 0 0% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 0 0% 3.9%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 0 0% 3.9%; 21 | 22 | --primary: 0 0% 9%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 0 0% 96.1%; 26 | --secondary-foreground: 0 0% 9%; 27 | 28 | --muted: 0 0% 96.1%; 29 | --muted-foreground: 0 0% 45.1%; 30 | 31 | --accent: 0 0% 96.1%; 32 | --accent-foreground: 0 0% 9%; 33 | 34 | --destructive: 0 84.2% 60.2%; 35 | --destructive-foreground: 0 0% 98%; 36 | 37 | --border: 0 0% 89.8%; 38 | --input: 0 0% 89.8%; 39 | --ring: 0 0% 3.9%; 40 | 41 | --radius: 0.5rem; 42 | } 43 | 44 | .dark { 45 | --background: 0 0% 3.9%; 46 | --foreground: 0 0% 98%; 47 | 48 | --card: 0 0% 3.9%; 49 | --card-foreground: 0 0% 98%; 50 | 51 | --popover: 0 0% 3.9%; 52 | --popover-foreground: 0 0% 98%; 53 | 54 | --primary: 0 0% 98%; 55 | --primary-foreground: 0 0% 9%; 56 | 57 | --secondary: 0 0% 14.9%; 58 | --secondary-foreground: 0 0% 98%; 59 | 60 | --muted: 0 0% 14.9%; 61 | --muted-foreground: 0 0% 63.9%; 62 | 63 | --accent: 0 0% 14.9%; 64 | --accent-foreground: 0 0% 98%; 65 | 66 | --destructive: 0 62.8% 30.6%; 67 | --destructive-foreground: 0 0% 98%; 68 | 69 | --border: 0 0% 14.9%; 70 | --input: 0 0% 14.9%; 71 | --ring: 0 0% 83.1%; 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | body { 80 | @apply bg-background text-foreground; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/(marketing)/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignInButton, UserButton } from "@clerk/clerk-react"; 4 | import { useConvexAuth } from "convex/react"; 5 | import Link from "next/link"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { ModeToggle } from "@/components/mode-toggle"; 9 | import { Spinner } from "@/components/spinner"; 10 | import { useScrollTop } from "@/hooks/use-scroll-top"; 11 | import { cn } from "@/lib/utils"; 12 | 13 | import { Logo } from "./logo"; 14 | 15 | export const Navbar = () => { 16 | const { isAuthenticated, isLoading } = useConvexAuth(); 17 | const scrolled = useScrollTop(); 18 | 19 | return ( 20 |
26 | 27 | 28 |
29 | {isLoading && } 30 | 31 | {!isAuthenticated && !isLoading && ( 32 | <> 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | )} 44 | 45 | {isAuthenticated && !isLoading && ( 46 | <> 47 | 50 | 51 | 52 | 53 | )} 54 | 55 | 56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /app/(marketing)/_components/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignUpButton } from "@clerk/clerk-react"; 4 | import { useConvexAuth } from "convex/react"; 5 | import { ArrowRight, Github } from "lucide-react"; 6 | import Link from "next/link"; 7 | 8 | import { Spinner } from "@/components/spinner"; 9 | import { Button } from "@/components/ui/button"; 10 | import { links } from "@/config"; 11 | 12 | export const Heading = () => { 13 | const { isAuthenticated, isLoading } = useConvexAuth(); 14 | 15 | return ( 16 |
17 |

18 | Your Ideas, Documents, & Plans. Unified. Welcome to{" "} 19 | Jotion 20 |

21 | 22 |

23 | Jotion is the connected workspace where
24 | better, faster work happens. 25 |

26 |
27 | {isLoading && } 28 | 29 | {isAuthenticated && !isLoading && ( 30 | 36 | )} 37 | 38 | {!isAuthenticated && !isLoading && ( 39 | 40 | 43 | 44 | )} 45 | 46 | 56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /app/(main)/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useQuery } from "convex/react"; 4 | import { MenuIcon } from "lucide-react"; 5 | import { useParams } from "next/navigation"; 6 | 7 | import { api } from "@/convex/_generated/api"; 8 | import type { Id } from "@/convex/_generated/dataModel"; 9 | 10 | import { Banner } from "./banner"; 11 | import { Menu } from "./menu"; 12 | import { Publish } from "./publish"; 13 | import { Title } from "./title"; 14 | 15 | type NavbarProps = { 16 | isCollapsed: boolean; 17 | onResetWidth: () => void; 18 | }; 19 | 20 | export const Navbar = ({ isCollapsed, onResetWidth }: NavbarProps) => { 21 | const params = useParams(); 22 | 23 | const document = useQuery(api.documents.getById, { 24 | documentId: params.documentId as Id<"documents">, 25 | }); 26 | 27 | if (document === undefined) 28 | return ( 29 | 36 | ); 37 | 38 | if (document === null) return null; 39 | 40 | return ( 41 | <> 42 |