├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .github ├── FUNDING.yml ├── workflows │ ├── tests.yml │ ├── cypress.yml │ └── lint.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── __mocks__ ├── remark-gfm.ts ├── remark-parse.ts ├── unified.ts ├── d3-selection.ts ├── next-router.ts ├── d3-drag.ts ├── ResizeObserver.ts ├── d3-zoom.ts ├── supabase.ts ├── d3-force.ts └── zustand │ └── vanilla.ts ├── supabase ├── .gitignore └── config.toml ├── worker └── index.js ├── types ├── remark-wiki-link.d.ts ├── utils.d.ts └── window.d.ts ├── vercel.json ├── .prettierrc ├── public ├── demo.mp4 ├── graph.mp4 ├── banner.png ├── favicon.ico ├── sidebar.png ├── graph-view.png ├── rich_text.mp4 ├── favicon-16x16.png ├── favicon-32x32.png ├── maskable-icon.png ├── page-stacking.png ├── apple-touch-icon.png ├── splash │ ├── ipad_splash.png │ ├── ipadpro1_splash.png │ ├── ipadpro2_splash.png │ ├── ipadpro3_splash.png │ ├── iphone5_splash.png │ ├── iphone6_splash.png │ ├── iphonex_splash.png │ ├── iphonexr_splash.png │ ├── iphoneplus_splash.png │ └── iphonexsmax_splash.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── manifest.json └── logo.svg ├── cypress ├── fixtures │ ├── user_new.json │ └── user.json ├── .eslintrc.js ├── tsconfig.json ├── support │ ├── e2e.js │ └── commands.d.ts ├── e2e │ ├── pages.cy.ts │ └── editor │ │ ├── blockMenu.cy.ts │ │ └── blockReference.cy.ts └── plugins │ └── index.js ├── postcss.config.js ├── .env.test.example ├── utils ├── date.ts ├── device.ts ├── usePrevious.ts ├── useIsPublish.ts ├── string.ts ├── useIsMounted.ts ├── useDebounce.ts ├── url.ts ├── useHotkeys.ts ├── useCurrentNote.tsx ├── useOnClickOutside.ts ├── useFeature.ts ├── getHighlightedPath.ts ├── useDeleteNote.ts ├── useOnClosePane.ts ├── useTagSearch.ts ├── useBlockSearch.ts ├── useNoteSearch.ts └── image-extensions.ts ├── jest.setup.js ├── constants ├── spring.ts └── strings.ts ├── __tests__ ├── .eslintrc.js ├── app │ ├── index.test.tsx │ └── graph.test.tsx └── components │ └── Editor.test.tsx ├── next-env.d.ts ├── components ├── Spinner.tsx ├── LogoWithText.tsx ├── PageLoading.tsx ├── editor │ ├── elements │ │ ├── ParagraphElement.tsx │ │ ├── ThematicBreakElement.tsx │ │ ├── ImageElement.tsx │ │ ├── EditorLeaf.tsx │ │ ├── ExternalLinkElement.tsx │ │ ├── withVerticalSpacing.tsx │ │ ├── NoteLinkElement.tsx │ │ ├── TagElement.tsx │ │ └── CheckListItemElement.tsx │ ├── backlinks │ │ ├── BacklinkReferenceBranch.tsx │ │ ├── BacklinkNoteBranch.tsx │ │ ├── BacklinkMatchLeaf.tsx │ │ └── BlockBacklinks.tsx │ ├── toolbar │ │ ├── FormatButton.tsx │ │ ├── LinkButton.tsx │ │ ├── ToolbarButton.tsx │ │ └── HoveringToolbar.tsx │ ├── blockmenu │ │ └── withBlockSideMenu.tsx │ ├── ReadOnlyEditor.tsx │ ├── Title.tsx │ └── NoteHeader.tsx ├── Logo.tsx ├── landing │ ├── LandingLayout.tsx │ ├── Navbar.tsx │ └── MobileMenu.tsx ├── publish │ ├── PublishTitle.tsx │ ├── PublishNoteHeader.tsx │ ├── PublishFooter.tsx │ ├── PublishEditor.tsx │ └── PublishNote.tsx ├── Tooltip.tsx ├── NotePermissionError.tsx ├── Portal.tsx ├── NoteMetadata.tsx ├── UpdateBanner.tsx ├── settings │ ├── Appearance.tsx │ └── EditorSettings.tsx ├── sidebar │ ├── OpenSidebarButton.tsx │ ├── SidebarItem.tsx │ ├── SidebarTab.tsx │ ├── DraggableSidebarNoteLink.tsx │ ├── SidebarContent.tsx │ └── SidebarNotesFooter.tsx ├── ServiceWorker.tsx ├── OfflineBanner.tsx ├── ErrorBoundary.tsx ├── FindOrCreateModal.tsx ├── UpgradeBanner.tsx ├── MoveToModal.tsx ├── UpgradeModal.tsx ├── Toggle.tsx ├── UpgradeButton.tsx ├── TreeNode.tsx ├── Tree.tsx ├── PublishLayout.tsx └── PricingPlan.tsx ├── editor ├── constants.ts ├── plugins │ ├── withTags.ts │ ├── withVoidElements.ts │ ├── withAutoMarkdown │ │ ├── handleBlockReference.ts │ │ ├── handleMark.ts │ │ ├── handleTag.ts │ │ ├── handleExternalLink.ts │ │ ├── handleNoteLink.ts │ │ ├── handleCustomNoteLink.ts │ │ └── index.ts │ ├── withBlockReferences.ts │ ├── withLinks.ts │ └── withCustomDeleteBackward.ts ├── serialization │ ├── types.d.ts │ └── remarkToSlate.ts ├── checks.ts ├── createEditor.ts ├── useHighlightedPath.ts ├── transforms.ts └── backlinks │ ├── deleteBacklinks.ts │ ├── updateBacklinks.ts │ ├── updateBlockBacklinks.ts │ ├── useBlockBacklinks.ts │ └── deleteBlockBacklinks.ts ├── lib ├── supabase.ts ├── api │ ├── deleteNote.ts │ ├── updateNote.ts │ └── upsertNote.ts ├── activeEditorsStore.ts ├── createUserSettingsSlice.ts └── storeUtils.ts ├── .prettierignore ├── .env.local.example ├── scripts ├── storage-setup.sql ├── seed.js └── seed.sql ├── pages ├── app │ └── index.tsx ├── _offline.tsx ├── pricing.tsx ├── api │ ├── email-list.ts │ ├── create-billing-portal-session.ts │ └── create-checkout-session.ts ├── signup.tsx ├── sponsors.tsx ├── login.tsx └── _app.tsx ├── cypress.config.ts ├── tsconfig.json ├── __fixtures__ └── notes.ts ├── sentry.client.config.js ├── sentry.server.config.js ├── .gitignore ├── jest.config.js ├── styles ├── styles.css └── nprogress.css ├── .eslintrc.js └── next.config.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: churichard 2 | -------------------------------------------------------------------------------- /__mocks__/remark-gfm.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /__mocks__/remark-parse.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /worker/index.js: -------------------------------------------------------------------------------- 1 | self.__WB_DISABLE_DEV_LOGS = true; 2 | -------------------------------------------------------------------------------- /types/remark-wiki-link.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'remark-wiki-link'; 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /public/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/demo.mp4 -------------------------------------------------------------------------------- /public/graph.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/graph.mp4 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/banner.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/sidebar.png -------------------------------------------------------------------------------- /cypress/fixtures/user_new.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "a@a.co", 3 | "password": "aaaaaa" 4 | } 5 | -------------------------------------------------------------------------------- /public/graph-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/graph-view.png -------------------------------------------------------------------------------- /public/rich_text.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/rich_text.mp4 -------------------------------------------------------------------------------- /cypress/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "hello@example.com", 3 | "password": "pass123" 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/maskable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/maskable-icon.png -------------------------------------------------------------------------------- /public/page-stacking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/page-stacking.png -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type PickPartial = Omit & 2 | Partial>; 3 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/splash/ipad_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/ipad_splash.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/splash/ipadpro1_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/ipadpro1_splash.png -------------------------------------------------------------------------------- /public/splash/ipadpro2_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/ipadpro2_splash.png -------------------------------------------------------------------------------- /public/splash/ipadpro3_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/ipadpro3_splash.png -------------------------------------------------------------------------------- /public/splash/iphone5_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/iphone5_splash.png -------------------------------------------------------------------------------- /public/splash/iphone6_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/iphone6_splash.png -------------------------------------------------------------------------------- /public/splash/iphonex_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/iphonex_splash.png -------------------------------------------------------------------------------- /public/splash/iphonexr_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/iphonexr_splash.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/splash/iphoneplus_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/iphoneplus_splash.png -------------------------------------------------------------------------------- /public/splash/iphonexsmax_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/churichard/notabase/HEAD/public/splash/iphonexsmax_splash.png -------------------------------------------------------------------------------- /.env.test.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL="http://localhost:54321" 2 | NEXT_PUBLIC_SUPABASE_KEY="" 3 | SUPABASE_SERVICE_KEY="" 4 | -------------------------------------------------------------------------------- /utils/date.ts: -------------------------------------------------------------------------------- 1 | export function dateCompare(d1: string, d2: string) { 2 | return new Date(d1).getTime() - new Date(d2).getTime(); 3 | } 4 | -------------------------------------------------------------------------------- /utils/device.ts: -------------------------------------------------------------------------------- 1 | const SM_BREAKPOINT = 640; 2 | 3 | export const isMobile = () => { 4 | return window.innerWidth <= SM_BREAKPOINT; 5 | }; 6 | -------------------------------------------------------------------------------- /cypress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['../.eslintrc.js', 'plugin:cypress/recommended'], 3 | plugins: ['cypress'], 4 | }; 5 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import '__mocks__/ResizeObserver'; 3 | import '__mocks__/zustand/vanilla'; 4 | -------------------------------------------------------------------------------- /__mocks__/unified.ts: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => ({ 2 | use: jest.fn().mockReturnThis(), 3 | processSync: jest.fn().mockReturnThis(), 4 | })); 5 | -------------------------------------------------------------------------------- /__mocks__/d3-selection.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | select: jest.fn(() => ({ 3 | call: jest.fn().mockReturnThis(), 4 | on: jest.fn().mockReturnThis(), 5 | })), 6 | }; 7 | 8 | export default {}; 9 | -------------------------------------------------------------------------------- /constants/spring.ts: -------------------------------------------------------------------------------- 1 | import { SpringConfig } from '@react-spring/web'; 2 | 3 | export const SPRING_CONFIG: SpringConfig = { 4 | mass: 1, 5 | tension: 170, 6 | friction: 10, 7 | clamp: true, 8 | } as const; 9 | -------------------------------------------------------------------------------- /constants/strings.ts: -------------------------------------------------------------------------------- 1 | const STRINGS = { 2 | error: { 3 | notePermissionError: 4 | "Oops! Either this page doesn't exist, or you don't have permission to view it.", 5 | }, 6 | }; 7 | 8 | export default STRINGS; 9 | -------------------------------------------------------------------------------- /__tests__/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '../.eslintrc.js', 4 | 'plugin:testing-library/react', 5 | 'plugin:jest-dom/recommended', 6 | ], 7 | plugins: ['testing-library', 'jest-dom'], 8 | }; 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /types/window.d.ts: -------------------------------------------------------------------------------- 1 | import { Workbox } from 'workbox-window'; 2 | 3 | declare global { 4 | interface Window { 5 | workbox: Workbox; 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | plausible: any; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__mocks__/next-router.ts: -------------------------------------------------------------------------------- 1 | const router = { 2 | useRouter: jest.fn(() => ({ 3 | pathname: '/app', 4 | query: {}, 5 | asPath: '', 6 | push: jest.fn(), 7 | })), 8 | }; 9 | 10 | module.exports = router; 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /utils/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | export default function usePrevious(value: T) { 4 | const ref = useRef(); 5 | 6 | useEffect(() => { 7 | ref.current = value; 8 | }, [value]); 9 | 10 | return ref.current; 11 | } 12 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "types": ["cypress", "@testing-library/cypress"] 6 | }, 7 | "include": ["../node_modules/cypress", "./**/*.ts"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /__mocks__/d3-drag.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | drag: jest.fn(() => ({ 3 | subject: jest.fn().mockReturnThis(), 4 | on: jest.fn().mockReturnThis(), 5 | })), 6 | subject: jest.fn().mockReturnThis(), 7 | on: jest.fn().mockReturnThis(), 8 | }; 9 | 10 | export default {}; 11 | -------------------------------------------------------------------------------- /components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | export default function Spinner() { 2 | return ( 3 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /__mocks__/ResizeObserver.ts: -------------------------------------------------------------------------------- 1 | class ResizeObserver { 2 | observe() { 3 | // do nothing 4 | } 5 | unobserve() { 6 | // do nothing 7 | } 8 | disconnect() { 9 | // do nothing 10 | } 11 | } 12 | 13 | window.ResizeObserver = ResizeObserver; 14 | export default ResizeObserver; 15 | -------------------------------------------------------------------------------- /editor/constants.ts: -------------------------------------------------------------------------------- 1 | import { Descendant } from 'slate'; 2 | import { ElementType } from 'types/slate'; 3 | import { createNodeId } from './plugins/withNodeId'; 4 | 5 | export const getDefaultEditorValue = (): Descendant[] => [ 6 | { id: createNodeId(), type: ElementType.Paragraph, children: [{ text: '' }] }, 7 | ]; 8 | -------------------------------------------------------------------------------- /lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | import { Database } from 'types/supabase'; 3 | 4 | const supabase = createClient( 5 | process.env.NEXT_PUBLIC_SUPABASE_URL ?? '', 6 | process.env.NEXT_PUBLIC_SUPABASE_KEY ?? '' 7 | ); 8 | 9 | export default supabase; 10 | -------------------------------------------------------------------------------- /utils/useIsPublish.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | 3 | /** 4 | * @returns Boolean specifying whether or not the user is currently on a public Publish page. 5 | */ 6 | export default function useIsPublish() { 7 | const router = useRouter(); 8 | return router.pathname.startsWith('/publish'); 9 | } 10 | -------------------------------------------------------------------------------- /__mocks__/d3-zoom.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | zoom: jest.fn(() => ({ 3 | scaleExtent: jest.fn().mockReturnThis(), 4 | extent: jest.fn().mockReturnThis(), 5 | on: jest.fn().mockReturnThis(), 6 | })), 7 | zoomIdentity: jest.fn().mockReturnThis(), 8 | zoomTransform: jest.fn().mockReturnThis(), 9 | }; 10 | 11 | export default {}; 12 | -------------------------------------------------------------------------------- /components/LogoWithText.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Logo from './Logo'; 3 | 4 | export default function LogoWithText() { 5 | return ( 6 | 7 | 8 | Notabase 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /utils/string.ts: -------------------------------------------------------------------------------- 1 | export function caseInsensitiveStringCompare(str1: string, str2: string) { 2 | return str1.localeCompare(str2, undefined, { 3 | sensitivity: 'base', 4 | numeric: true, 5 | }); 6 | } 7 | 8 | export function caseInsensitiveStringEqual(str1: string, str2: string) { 9 | return caseInsensitiveStringCompare(str1, str2) === 0; 10 | } 11 | -------------------------------------------------------------------------------- /components/PageLoading.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from './Spinner'; 2 | import Logo from './Logo'; 3 | 4 | export default function PageLoading() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /components/editor/elements/ParagraphElement.tsx: -------------------------------------------------------------------------------- 1 | import { EditorElementProps } from './EditorElement'; 2 | 3 | export default function ParagraphElement(props: EditorElementProps) { 4 | const { className, attributes, children } = props; 5 | return ( 6 |
7 | {children} 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /editor/plugins/withTags.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from 'slate'; 2 | import { ElementType } from 'types/slate'; 3 | 4 | const withTags = (editor: Editor) => { 5 | const { isInline } = editor; 6 | 7 | editor.isInline = (element) => { 8 | return element.type === ElementType.Tag ? true : isInline(element); 9 | }; 10 | 11 | return editor; 12 | }; 13 | 14 | export default withTags; 15 | -------------------------------------------------------------------------------- /components/editor/backlinks/BacklinkReferenceBranch.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | type BacklinkReferenceBranchProps = { 4 | title: string; 5 | }; 6 | 7 | const BacklinkReferenceBranch = (props: BacklinkReferenceBranchProps) => { 8 | const { title } = props; 9 | return

{title}

; 10 | }; 11 | 12 | export default memo(BacklinkReferenceBranch); 13 | -------------------------------------------------------------------------------- /utils/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | function useIsMounted() { 4 | const isMounted = useRef(false); 5 | 6 | useEffect(() => { 7 | isMounted.current = true; 8 | 9 | return () => { 10 | isMounted.current = false; 11 | }; 12 | }, []); 13 | 14 | return useCallback(() => isMounted.current, []); 15 | } 16 | 17 | export default useIsMounted; 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: unit tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 24.x 18 | cache: 'npm' 19 | 20 | - run: npm ci 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # local env files 25 | .env.local 26 | .env.development.local 27 | .env.test.local 28 | .env.production.local 29 | 30 | # vscode 31 | .vscode 32 | -------------------------------------------------------------------------------- /components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import logo from 'public/logo.svg'; 3 | 4 | type Props = { 5 | width: number; 6 | height: number; 7 | }; 8 | 9 | export default function Logo(props: Props) { 10 | const { width, height } = props; 11 | return ( 12 | Notabase logo 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /__mocks__/supabase.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | from: jest.fn().mockReturnThis(), 3 | channel: jest.fn().mockReturnThis(), 4 | on: jest.fn().mockReturnThis(), 5 | select: jest.fn().mockReturnThis(), 6 | eq: jest.fn().mockReturnThis(), 7 | order: jest.fn().mockReturnThis(), 8 | single: jest.fn().mockReturnThis(), 9 | maybeSingle: jest.fn().mockReturnThis(), 10 | subscribe: jest.fn().mockReturnThis(), 11 | unsubscribe: jest.fn().mockReturnThis(), 12 | }; 13 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co" 2 | NEXT_PUBLIC_SUPABASE_KEY="your-anon-key" 3 | SUPABASE_SERVICE_KEY="" 4 | 5 | // The following environment variables are optional and can be removed. 6 | NEXT_PUBLIC_SENTRY_DSN="" 7 | SENTRY_URL="https://sentry.io/" 8 | SENTRY_ORG="" 9 | SENTRY_PROJECT="" 10 | SENTRY_AUTH_TOKEN="" 11 | EMAILOCTOPUS_KEY="" 12 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="" 13 | STRIPE_SECRET_KEY="" 14 | STRIPE_WEBHOOK_SECRET="" 15 | -------------------------------------------------------------------------------- /scripts/storage-setup.sql: -------------------------------------------------------------------------------- 1 | -- Create storage bucket 2 | 3 | insert into storage.buckets (id, name) 4 | values ('user-assets', 'user-assets'); 5 | 6 | 7 | -- Add row-level-security policy for storage 8 | 9 | create policy "Access to user's file uploads" 10 | on storage.objects for all 11 | using ((bucket_id='user-assets'::text) AND ((auth.uid())::text=(storage.foldername(name))[1])) 12 | with check ((bucket_id='user-assets'::text) AND ((auth.uid())::text=(storage.foldername(name))[1])); 13 | 14 | -------------------------------------------------------------------------------- /lib/api/deleteNote.ts: -------------------------------------------------------------------------------- 1 | import { store } from 'lib/store'; 2 | import supabase from 'lib/supabase'; 3 | 4 | export default async function deleteNote(userId: string, noteId: string) { 5 | // Update note titles in sidebar 6 | store.getState().deleteNote(noteId); 7 | 8 | const response = await supabase.from('notes').delete().eq('id', noteId); 9 | 10 | await supabase 11 | .from('users') 12 | .update({ note_tree: store.getState().noteTree }) 13 | .eq('id', userId); 14 | 15 | return response; 16 | } 17 | -------------------------------------------------------------------------------- /editor/serialization/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface MdastNode { 2 | type?: string; 3 | ordered?: boolean; 4 | value?: string; 5 | text?: string; 6 | children?: Array; 7 | depth?: 1 | 2 | 3 | 4 | 5 | 6; 8 | url?: string; 9 | alt?: string; 10 | lang?: string; 11 | data?: { 12 | alias?: string; 13 | }; 14 | parentType?: string; // custom attribute specifying the parent type 15 | // mdast metadata 16 | position?: unknown; 17 | spread?: unknown; 18 | checked?: unknown; 19 | indent?: unknown; 20 | } 21 | -------------------------------------------------------------------------------- /components/landing/LandingLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import Footer from './Footer'; 3 | import Navbar from './Navbar'; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | showNavbar?: boolean; 8 | showFooter?: boolean; 9 | }; 10 | 11 | export default function LandingLayout(props: Props) { 12 | const { children, showNavbar = true, showFooter = true } = props; 13 | 14 | return ( 15 | <> 16 | {showNavbar ? : null} 17 | {children} 18 | {showFooter ?