├── .eslintrc.json ├── public └── assets │ ├── 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 │ └── next.svg ├── postcss.config.js ├── convex ├── auth.config.js ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── schema.ts ├── tsconfig.json ├── README.md └── documents.ts ├── lib ├── utils.ts └── edgestore.ts ├── app ├── (public) │ ├── layout.tsx │ └── (routes) │ │ └── preview │ │ └── [documentId] │ │ └── page.tsx ├── not-found.tsx ├── (marketing) │ ├── _components │ │ ├── Footer.tsx │ │ ├── Logo.tsx │ │ ├── Hero.tsx │ │ ├── Heading.tsx │ │ └── Navbar.tsx │ ├── layout.tsx │ └── page.tsx ├── api │ └── edgestore │ │ └── [...edgestore] │ │ └── route.ts ├── error.tsx ├── (main) │ ├── layout.tsx │ ├── (routes) │ │ └── documents │ │ │ ├── page.tsx │ │ │ └── [documentId] │ │ │ └── page.tsx │ └── _components │ │ ├── Navbar.tsx │ │ ├── Menu.tsx │ │ ├── Banner.tsx │ │ ├── UserItem.tsx │ │ ├── Title.tsx │ │ ├── DocumentList.tsx │ │ ├── TrashBox.tsx │ │ ├── Publish.tsx │ │ ├── Item.tsx │ │ └── Navigation.tsx ├── layout.tsx └── globals.css ├── components ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── dialog.tsx │ ├── alert-dialog.tsx │ ├── command.tsx │ └── dropdown-menu.tsx ├── providers │ ├── ThemeProvider.tsx │ ├── ModalProvider.tsx │ └── ConvexProvider.tsx ├── Spinner.tsx ├── modals │ ├── SettingsModal.tsx │ ├── ConfirmModal.tsx │ └── CoverImageModal.tsx ├── IconPicker.tsx ├── ModeToggle.tsx ├── Editor │ ├── styles.css │ └── Editor.tsx ├── Cover.tsx ├── SearchCommand.tsx ├── Toolbar.tsx └── SingleImageDropzone.tsx ├── hooks ├── useSettings.tsx ├── useSearch.tsx ├── useOrigin.tsx └── useCoverImage.tsx ├── next.config.js ├── components.json ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/assets/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/empty.png -------------------------------------------------------------------------------- /public/assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/error.png -------------------------------------------------------------------------------- /public/assets/reading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/reading.png -------------------------------------------------------------------------------- /public/assets/documents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/documents.png -------------------------------------------------------------------------------- /public/assets/empty-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/empty-dark.png -------------------------------------------------------------------------------- /public/assets/error-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/error-dark.png -------------------------------------------------------------------------------- /public/assets/reading-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/reading-dark.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/assets/documents-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sougata-github/jotion/HEAD/public/assets/documents-dark.png -------------------------------------------------------------------------------- /convex/auth.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: "https://viable-fish-32.clerk.accounts.dev", 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/(public)/layout.tsx: -------------------------------------------------------------------------------- 1 | const PublicLayout = ({ children }: { children: React.ReactNode }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export default PublicLayout; 6 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 |
4 | Oops! The page you're looking for does not exist. 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /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 | export { EdgeStoreProvider, useEdgeStore }; 9 | -------------------------------------------------------------------------------- /app/(marketing)/_components/Footer.tsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 |
4 |

©Jotion | 2024

5 |
6 | ); 7 | }; 8 | 9 | export default Footer; 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/useSettings.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 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "http", 7 | hostname: "files.edgestore.dev", 8 | }, 9 | { 10 | protocol: "https", 11 | hostname: "files.edgestore.dev", 12 | }, 13 | ], 14 | }, 15 | }; 16 | 17 | module.exports = nextConfig; 18 | -------------------------------------------------------------------------------- /components/providers/ThemeProvider.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 | } -------------------------------------------------------------------------------- /hooks/useSearch.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/useOrigin.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useOrigin = () => { 4 | const [mounted, setMounted] = useState(false); 5 | const origin = 6 | typeof window !== "undefined" && window.location.origin 7 | ? window.location.origin 8 | : ""; 9 | 10 | useEffect(() => { 11 | setMounted(true); 12 | }, []); 13 | 14 | if (!mounted) { 15 | return ""; 16 | } 17 | 18 | return origin; 19 | }; 20 | -------------------------------------------------------------------------------- /app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "./_components/Navbar"; 2 | 3 | const MarketingLayout = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 |
6 | 7 |
8 | {children} 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default MarketingLayout; 15 | -------------------------------------------------------------------------------- /app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import Hero from "./_components/Hero"; 2 | import Footer from "./_components/Footer"; 3 | import Heading from "./_components/Heading"; 4 | 5 | const MarketingPage = () => { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default MarketingPage; 18 | -------------------------------------------------------------------------------- /hooks/useCoverImage.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type CoverImageStore = { 4 | url?: string; 5 | isOpen: boolean; 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 | -------------------------------------------------------------------------------- /.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.8.0. 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 | -------------------------------------------------------------------------------- /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(), //clerkId 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 | -------------------------------------------------------------------------------- /components/providers/ModalProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | import SettingsModal from "../modals/SettingsModal"; 6 | import CoverImageModal from "../modals/CoverImageModal"; 7 | 8 | export const ModalProvider = () => { 9 | const [isMounted, setIsMounted] = useState(false); 10 | 11 | //none of the modals will be rendered unless we are fully on the 12 | //client side. 13 | useEffect(() => { 14 | setIsMounted(true); 15 | }, []); 16 | 17 | if (!isMounted) return null; 18 | 19 | return ( 20 | <> 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /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": "node", 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 | -------------------------------------------------------------------------------- /components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { Loader } from "lucide-react"; 2 | 3 | import { cva, type VariantProps } from "class-variance-authority"; 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 | interface SpinnerProps extends VariantProps {} 21 | 22 | const Spinner = ({ size }: SpinnerProps) => { 23 | return ; 24 | }; 25 | 26 | export default Spinner; 27 | -------------------------------------------------------------------------------- /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 | /** 7 | * This is the main router for the Edge Store buckets. 8 | */ 9 | const edgeStoreRouter = es.router({ 10 | publicFiles: es.fileBucket().beforeDelete(() => { 11 | return true; // allow delete 12 | }), 13 | }); 14 | 15 | const handler = createEdgeStoreNextHandler({ 16 | router: edgeStoreRouter, 17 | }); 18 | export { handler as GET, handler as POST }; 19 | /** 20 | * This type is used to create the type-safe client for the frontend. 21 | */ 22 | export type EdgeStoreRouter = typeof edgeStoreRouter; 23 | -------------------------------------------------------------------------------- /components/providers/ConvexProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | 5 | import { ClerkProvider, useAuth } from "@clerk/clerk-react"; 6 | 7 | import { ConvexReactClient } from "convex/react"; 8 | import { ConvexProviderWithClerk } from "convex/react-clerk"; 9 | 10 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 11 | 12 | export const ConvexClientProvider = ({ children }: { children: ReactNode }) => { 13 | return ( 14 | 17 | 18 | {children} 19 | 20 | 21 | ); 22 | }; 23 | 24 | //convex as DB and Clerk as Auth Provider. 25 | -------------------------------------------------------------------------------- /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/(marketing)/_components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | import { Poppins } from "next/font/google"; 6 | 7 | const font = Poppins({ 8 | subsets: ["latin"], 9 | weight: ["400", "600"], 10 | }); 11 | 12 | const Logo = () => { 13 | return ( 14 |
15 | Logo 22 | Logo 29 |

Jotion

30 |
31 | ); 32 | }; 33 | 34 | export default Logo; 35 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | 5 | import Link from "next/link"; 6 | import Image from "next/image"; 7 | 8 | const Error = () => { 9 | return ( 10 |
11 | error 18 | error 25 |

Something went wrong!

26 | 29 |
30 | ); 31 | }; 32 | 33 | export default Error; 34 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |