├── .commitlintrc.json
├── .editorconfig
├── .env.example
├── .eslintrc.json
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .nvmrc
├── .prettierignore
├── .react-email
├── .eslintrc.json
├── .next
│ ├── app-build-manifest.json
│ ├── build-manifest.json
│ ├── cache
│ │ └── webpack
│ │ │ ├── client-development
│ │ │ ├── 0.pack.gz
│ │ │ ├── 1.pack.gz
│ │ │ ├── 2.pack.gz
│ │ │ ├── 3.pack.gz
│ │ │ ├── 4.pack.gz
│ │ │ ├── 5.pack.gz
│ │ │ ├── 6.pack.gz
│ │ │ ├── index.pack.gz
│ │ │ └── index.pack.gz.old
│ │ │ └── server-development
│ │ │ ├── 0.pack.gz
│ │ │ ├── 1.pack.gz
│ │ │ ├── 2.pack.gz
│ │ │ ├── 3.pack.gz
│ │ │ ├── 4.pack.gz
│ │ │ ├── 5.pack.gz
│ │ │ ├── index.pack.gz
│ │ │ └── index.pack.gz.old
│ ├── package.json
│ ├── react-loadable-manifest.json
│ ├── server
│ │ ├── app-paths-manifest.json
│ │ ├── client-reference-manifest.js
│ │ ├── client-reference-manifest.json
│ │ ├── middleware-build-manifest.js
│ │ ├── middleware-manifest.json
│ │ ├── middleware-react-loadable-manifest.js
│ │ ├── next-font-manifest.js
│ │ ├── next-font-manifest.json
│ │ ├── pages-manifest.json
│ │ ├── server-reference-manifest.js
│ │ └── server-reference-manifest.json
│ ├── static
│ │ ├── chunks
│ │ │ ├── polyfills.js
│ │ │ ├── react-refresh.js
│ │ │ └── webpack.js
│ │ └── development
│ │ │ ├── _buildManifest.js
│ │ │ └── _ssgManifest.js
│ ├── trace
│ └── types
│ │ └── package.json
├── .prettierignore
├── .prettierrc.js
├── emails
│ └── reminder-email.tsx
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── src
│ ├── app
│ │ ├── home.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── preview
│ │ │ └── [slug]
│ │ │ ├── page.tsx
│ │ │ └── preview.tsx
│ ├── components
│ │ ├── button.tsx
│ │ ├── code-container.tsx
│ │ ├── code.tsx
│ │ ├── heading.tsx
│ │ ├── icon-base.tsx
│ │ ├── icon-button.tsx
│ │ ├── icon-check.tsx
│ │ ├── icon-clipboard.tsx
│ │ ├── icon-download.tsx
│ │ ├── index.ts
│ │ ├── logo.tsx
│ │ ├── send.tsx
│ │ ├── shell.tsx
│ │ ├── sidebar.tsx
│ │ ├── text.tsx
│ │ ├── tooltip-content.tsx
│ │ ├── tooltip.tsx
│ │ └── topbar.tsx
│ ├── styles
│ │ └── globals.css
│ └── utils
│ │ ├── as.ts
│ │ ├── copy-text-to-clipboard.ts
│ │ ├── get-emails.ts
│ │ ├── index.ts
│ │ ├── language-map.ts
│ │ └── unreachable.ts
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LEARN.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── assets
└── fonts
│ ├── Inter-Bold.ttf
│ ├── Inter-Regular.ttf
│ ├── Satoshi-Black.ttf
│ ├── Satoshi-Bold.ttf
│ └── Satoshi-Variable.woff2
├── components.json
├── contentlayer.config.js
├── docker-compose.yml
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.js
├── prisma
├── migrations
│ ├── 20240408163644_neon_migration
│ │ └── migration.sql
│ ├── 20240607201413_docker
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── grid.svg
├── images
│ ├── auth-bg.webp
│ ├── features
│ │ ├── editor.webp
│ │ └── reminder.webp
│ ├── hero-dark.webp
│ └── testimonials
│ │ ├── alex.webp
│ │ ├── emily.webp
│ │ ├── john.webp
│ │ ├── lisa.webp
│ │ ├── mark.webp
│ │ └── sara.webp
├── lottie
│ └── GIRL STUDYING ON LAPTOP.json
├── next.svg
├── noise.webp
├── scribbly-logo.svg
├── site.webmanifest
└── window.svg
├── sentry.client.config.ts
├── sentry.edge.config.ts
├── sentry.server.config.ts
├── src
├── app
│ ├── (auth)
│ │ ├── layout.tsx
│ │ ├── sign-in
│ │ │ └── [[...sign-in]]
│ │ │ │ └── page.tsx
│ │ └── sign-up
│ │ │ └── [[...sign-up]]
│ │ │ └── page.tsx
│ ├── (editor)
│ │ └── editor
│ │ │ ├── [entryId]
│ │ │ ├── loading.tsx
│ │ │ ├── not-found.tsx
│ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ ├── (journal)
│ │ ├── _components
│ │ │ ├── billing
│ │ │ │ └── billing-form.tsx
│ │ │ ├── editor.tsx
│ │ │ ├── entry-list.tsx
│ │ │ ├── entry-operations.tsx
│ │ │ ├── journal-entry-card.tsx
│ │ │ ├── journal-entry-create-button.tsx
│ │ │ ├── journal-entry.tsx
│ │ │ └── settings
│ │ │ │ ├── appearance-form.tsx
│ │ │ │ ├── reminder-form.tsx
│ │ │ │ └── user-name-form.tsx
│ │ └── journal
│ │ │ ├── billing
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── settings
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── user-profile
│ │ │ └── [[...user-profile]]
│ │ │ └── page.tsx
│ ├── (legal)
│ │ ├── [...slug]
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (marketing)
│ │ ├── _components
│ │ │ ├── background.tsx
│ │ │ ├── features
│ │ │ │ ├── features.tsx
│ │ │ │ └── featuresSection.tsx
│ │ │ ├── hero
│ │ │ │ ├── heroImage.tsx
│ │ │ │ └── heroSection.tsx
│ │ │ ├── openSource
│ │ │ │ └── openSourceSection.tsx
│ │ │ ├── pricing
│ │ │ │ ├── pricing.tsx
│ │ │ │ └── pricingSection.tsx
│ │ │ ├── testimonials
│ │ │ │ ├── testimonials.tsx
│ │ │ │ └── testimonialsSection.tsx
│ │ │ └── wobble-card.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api
│ │ ├── cron
│ │ │ └── route.ts
│ │ ├── sentry-example-api
│ │ │ └── route.js
│ │ ├── uploadthing
│ │ │ ├── core.ts
│ │ │ └── route.ts
│ │ └── webhooks
│ │ │ ├── stripe
│ │ │ └── route.ts
│ │ │ └── user
│ │ │ └── route.ts
│ ├── global-error.tsx
│ ├── layout.tsx
│ ├── not-found.tsx
│ ├── opengraph-image.jpg
│ ├── robots.ts
│ └── sitemap.ts
├── components
│ ├── analytics-provider.tsx
│ ├── analytics.tsx
│ ├── animated-gradient-text.tsx
│ ├── border-beam.tsx
│ ├── callout.tsx
│ ├── card-skeleton.tsx
│ ├── copy-button.tsx
│ ├── emails
│ │ └── reminder-email.tsx
│ ├── empty-placeholder.tsx
│ ├── error-card.tsx
│ ├── fade-in.tsx
│ ├── header.tsx
│ ├── icons.tsx
│ ├── lottie-anim.tsx
│ ├── main-nav.tsx
│ ├── marquee.tsx
│ ├── mdx-card.tsx
│ ├── mdx-components.tsx
│ ├── mobile-nav.tsx
│ ├── nav.tsx
│ ├── providers.tsx
│ ├── shell.tsx
│ ├── site-footer.tsx
│ ├── sticky-scroll-reveal.tsx
│ ├── tailwind-indicator.tsx
│ ├── theme-provider.tsx
│ ├── tweet-card.tsx
│ ├── ui
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── radio-group.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── tooltip.tsx
│ │ └── use-toast.ts
│ ├── user-account-nav.tsx
│ └── user-avatar.tsx
├── config
│ ├── journal.ts
│ ├── marketing.ts
│ ├── site.ts
│ └── subscriptions.ts
├── content
│ └── pages
│ │ ├── privacy.mdx
│ │ └── terms.mdx
├── env.mjs
├── hooks
│ ├── use-lock-body.ts
│ ├── use-mounted.ts
│ └── use-scroll.ts
├── lib
│ ├── auth.ts
│ ├── db.ts
│ ├── events.ts
│ ├── exceptions.ts
│ ├── resend.ts
│ ├── stripe.ts
│ ├── uploadthing.ts
│ ├── utils.ts
│ ├── validations
│ │ ├── entry.ts
│ │ ├── reminder.ts
│ │ └── user.ts
│ └── verify-current-user-has-access-to-entry.ts
├── middleware.ts
├── server
│ ├── actions
│ │ ├── journal.ts
│ │ ├── reminder.ts
│ │ ├── stripe.ts
│ │ └── user.ts
│ └── queries
│ │ ├── editor.ts
│ │ └── reminder.ts
├── styles
│ ├── editor.css
│ └── globals.css
└── types
│ └── index.d.ts
├── tailwind.config.js
├── tsconfig.json
└── vercel.json
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # -----------------------------------------------------------------------------
2 | # App
3 | # -----------------------------------------------------------------------------
4 | NEXT_PUBLIC_APP_URL=http://localhost:3000
5 | NODE_ENV=development
6 |
7 | # -----------------------------------------------------------------------------
8 | # Analytics (PostHog)
9 | # -----------------------------------------------------------------------------
10 |
11 | NEXT_PUBLIC_POSTHOG_KEY=
12 | NEXT_PUBLIC_POSTHOG_HOST=
13 |
14 |
15 | # -----------------------------------------------------------------------------
16 | # Database (MySQL - PlanetScale)
17 | # -----------------------------------------------------------------------------
18 | DATABASE_URL=
19 |
20 |
21 |
22 | # -----------------------------------------------------------------------------
23 | # Authentication (clerk)
24 | # -----------------------------------------------------------------------------
25 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
26 | CLERK_SECRET_KEY=
27 | CLERK_WEBHOOK_SECRET=
28 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
29 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
30 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/journal
31 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/journal
32 |
33 | # -----------------------------------------------------------------------------
34 | # Editor Image Upload (Uplaodthing)
35 | # -----------------------------------------------------------------------------
36 | UPLOADTHING_SECRET=
37 | UPLOADTHING_APP_ID=
38 |
39 | # -----------------------------------------------------------------------------
40 | # Subscriptions (Stripe)
41 | # -----------------------------------------------------------------------------
42 | STRIPE_API_KEY=
43 | STRIPE_WEBHOOK_SECRET=
44 | STRIPE_PRO_MONTHLY_PLAN_ID=
45 |
46 |
47 | # -----------------------------------------------------------------------------
48 | # Reminder emails (Resend)
49 | # -----------------------------------------------------------------------------
50 | RESEND_API_KEY=
51 | # Register a domain at https://resend.com/domains
52 | EMAIL_FROM_ADDRESS="mail@[yourdomain]"
53 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "@next/next/no-html-link-for-pages": "off",
12 | "react/jsx-key": "off",
13 | "tailwindcss/no-custom-classname": "off",
14 | "tailwindcss/classnames-order": "error"
15 | },
16 | "settings": {
17 | "tailwindcss": {
18 | "callees": ["cn"],
19 | "config": "tailwind.config.js"
20 | }
21 | },
22 | "overrides": [
23 | {
24 | "files": ["*.ts", "*.tsx"],
25 | "parser": "@typescript-eslint/parser"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.react-email/node_modules
6 | /.pnp
7 | .pnp.js
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 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env*.local
31 | .env
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
40 | .vscode
41 |
42 | # vercel
43 | .vercel
44 |
45 | # Contentlayer
46 | *.tsbuildinfo
47 | .contentlayer
48 |
49 | # Sentry Config File
50 | .sentryclirc
51 |
52 | db-data
53 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx pretty-quick --staged
5 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.16.0
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .next
4 | build
5 | .contentlayer
--------------------------------------------------------------------------------
/.react-email/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "prettier"],
3 | "plugins": ["simple-import-sort", "unused-imports"],
4 | "rules": {
5 | "react/no-unescaped-entities": 0,
6 | "react-hooks/rules-of-hooks": 0,
7 | "no-unused-vars": "off",
8 | "simple-import-sort/imports": [
9 | "error",
10 | {
11 | // The default grouping, but with no blank lines.
12 | "groups": [["^\\u0000", "^@?\\w", "^", "^\\."]]
13 | }
14 | ],
15 | "simple-import-sort/exports": "error"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.react-email/.next/app-build-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": {}
3 | }
--------------------------------------------------------------------------------
/.react-email/.next/build-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "polyfillFiles": [
3 | "static/chunks/polyfills.js"
4 | ],
5 | "devFiles": [
6 | "static/chunks/webpack.js",
7 | "static/chunks/react-refresh.js"
8 | ],
9 | "ampDevFiles": [],
10 | "lowPriorityFiles": [
11 | "static/development/_buildManifest.js",
12 | "static/development/_ssgManifest.js"
13 | ],
14 | "rootMainFiles": [],
15 | "pages": {
16 | "/_app": []
17 | },
18 | "ampFirstPages": []
19 | }
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/0.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/0.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/1.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/1.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/2.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/2.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/3.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/3.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/4.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/4.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/5.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/5.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/6.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/6.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/index.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/index.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/client-development/index.pack.gz.old:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/client-development/index.pack.gz.old
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/0.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/0.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/1.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/1.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/2.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/2.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/3.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/3.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/4.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/4.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/5.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/5.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/index.pack.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/index.pack.gz
--------------------------------------------------------------------------------
/.react-email/.next/cache/webpack/server-development/index.pack.gz.old:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/.react-email/.next/cache/webpack/server-development/index.pack.gz.old
--------------------------------------------------------------------------------
/.react-email/.next/package.json:
--------------------------------------------------------------------------------
1 | {"type": "commonjs"}
--------------------------------------------------------------------------------
/.react-email/.next/react-loadable-manifest.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.react-email/.next/server/app-paths-manifest.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.react-email/.next/server/client-reference-manifest.js:
--------------------------------------------------------------------------------
1 | self.__RSC_MANIFEST="{\n \"ssrModuleMapping\": {},\n \"edgeSSRModuleMapping\": {},\n \"clientModules\": {},\n \"entryCSSFiles\": {}\n}"
--------------------------------------------------------------------------------
/.react-email/.next/server/client-reference-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "ssrModuleMapping": {},
3 | "edgeSSRModuleMapping": {},
4 | "clientModules": {},
5 | "entryCSSFiles": {}
6 | }
--------------------------------------------------------------------------------
/.react-email/.next/server/middleware-build-manifest.js:
--------------------------------------------------------------------------------
1 | self.__BUILD_MANIFEST={"polyfillFiles":["static/chunks/polyfills.js"],"devFiles":["static/chunks/webpack.js","static/chunks/react-refresh.js"],"ampDevFiles":[],"lowPriorityFiles":["static/development/_buildManifest.js","static/development/_ssgManifest.js"],"rootMainFiles":[],"pages":{"/_app":[]},"ampFirstPages":[]}
--------------------------------------------------------------------------------
/.react-email/.next/server/middleware-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "sortedMiddleware": [],
3 | "middleware": {},
4 | "functions": {},
5 | "version": 2
6 | }
--------------------------------------------------------------------------------
/.react-email/.next/server/middleware-react-loadable-manifest.js:
--------------------------------------------------------------------------------
1 | self.__REACT_LOADABLE_MANIFEST="{}"
--------------------------------------------------------------------------------
/.react-email/.next/server/next-font-manifest.js:
--------------------------------------------------------------------------------
1 | self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
--------------------------------------------------------------------------------
/.react-email/.next/server/next-font-manifest.json:
--------------------------------------------------------------------------------
1 | {"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
--------------------------------------------------------------------------------
/.react-email/.next/server/pages-manifest.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.react-email/.next/server/server-reference-manifest.js:
--------------------------------------------------------------------------------
1 | self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {}\n}"
--------------------------------------------------------------------------------
/.react-email/.next/server/server-reference-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "node": {},
3 | "edge": {}
4 | }
--------------------------------------------------------------------------------
/.react-email/.next/static/development/_buildManifest.js:
--------------------------------------------------------------------------------
1 | self.__BUILD_MANIFEST = {__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},sortedPages:["\u002F_app"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
--------------------------------------------------------------------------------
/.react-email/.next/static/development/_ssgManifest.js:
--------------------------------------------------------------------------------
1 | self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
--------------------------------------------------------------------------------
/.react-email/.next/types/package.json:
--------------------------------------------------------------------------------
1 | {"type": "module"}
--------------------------------------------------------------------------------
/.react-email/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | .next
3 | node_modules
--------------------------------------------------------------------------------
/.react-email/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | quoteProps: 'consistent',
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | printWidth: 80,
6 | useTabs: false,
7 | bracketSpacing: true,
8 | };
9 |
--------------------------------------------------------------------------------
/.react-email/emails/reminder-email.tsx:
--------------------------------------------------------------------------------
1 | import Mail from '../../src/components/emails/reminder-email.tsx';
2 | export default Mail;
3 |
--------------------------------------------------------------------------------
/.react-email/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | experimental: {
6 | appDir: true,
7 | externalDir: true, // compile files that are located next to the .react-email directory
8 | },
9 | };
10 |
11 | module.exports = nextConfig;
12 |
--------------------------------------------------------------------------------
/.react-email/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/.react-email/src/app/home.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import { Button, Heading, Text } from '../components';
5 | import { Shell } from '../components/shell';
6 |
7 | export default function Home({ navItems }) {
8 | return (
9 |
10 |
11 |
12 | Welcome to the React Email preview!
13 |
14 |
15 | To start developing your next email template, you can create a{' '}
16 | .jsx
or .tsx
file under the "emails" folder.
17 |
18 |
19 |
20 | Check the docs
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/.react-email/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css';
2 | import classnames from 'classnames';
3 | import { Inter } from 'next/font/google';
4 |
5 | export const inter = Inter({
6 | subsets: ['latin'],
7 | variable: '--font-inter',
8 | });
9 |
10 | export default function RootLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode;
14 | }) {
15 | return (
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/.react-email/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { getEmails } from '../utils/get-emails';
2 | import Home from './home';
3 |
4 | export default async function Index() {
5 | const { emails } = await getEmails();
6 | return ;
7 | }
8 |
9 | export const metadata = {
10 | title: 'React Email',
11 | };
12 |
--------------------------------------------------------------------------------
/.react-email/src/app/preview/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@react-email/render';
2 | import { promises as fs } from 'fs';
3 | import { dirname, join as pathJoin } from 'path';
4 | import { CONTENT_DIR, getEmails } from '../../../utils/get-emails';
5 | import Preview from './preview';
6 |
7 | export const dynamicParams = true;
8 |
9 | export async function generateStaticParams() {
10 | const { emails } = await getEmails();
11 |
12 | const paths = emails.map((email) => {
13 | return { slug: email };
14 | });
15 |
16 | return paths;
17 | }
18 |
19 | export default async function Page({ params }) {
20 | const { emails, filenames } = await getEmails();
21 | const template = filenames.filter((email) => {
22 | const [fileName] = email.split('.');
23 | return params.slug === fileName;
24 | });
25 |
26 | const Email = (await import(`../../../../emails/${params.slug}`)).default;
27 | const markup = render( , { pretty: true });
28 | const plainText = render( , { plainText: true });
29 | const basePath = pathJoin(process.cwd(), CONTENT_DIR);
30 | const path = pathJoin(basePath, template[0]);
31 |
32 | // the file is actually just re-exporting the default export of the original file. We need to resolve this first
33 | const exportTemplateFile: string = await fs.readFile(path, {
34 | encoding: 'utf-8',
35 | });
36 | const importPath = exportTemplateFile.match(/import Mail from '(.+)';/)![1];
37 | const originalFilePath = pathJoin(dirname(path), importPath);
38 |
39 | const reactMarkup: string = await fs.readFile(originalFilePath, {
40 | encoding: 'utf-8',
41 | });
42 |
43 | return (
44 |
51 | );
52 | }
53 |
54 | export async function generateMetadata({ params }) {
55 | return { title: `${params.slug} — React Email` };
56 | }
57 |
--------------------------------------------------------------------------------
/.react-email/src/app/preview/[slug]/preview.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4 | import React from 'react';
5 | import { CodeContainer } from '../../../components/code-container';
6 | import { Shell } from '../../../components/shell';
7 | import { Tooltip } from '../../../components/tooltip';
8 |
9 | export default function Preview({
10 | navItems,
11 | slug,
12 | markup,
13 | reactMarkup,
14 | plainText,
15 | }) {
16 | const router = useRouter();
17 | const pathname = usePathname();
18 | const searchParams = useSearchParams();
19 | const [activeView, setActiveView] = React.useState('desktop');
20 | const [activeLang, setActiveLang] = React.useState('jsx');
21 |
22 | React.useEffect(() => {
23 | const view = searchParams.get('view');
24 | const lang = searchParams.get('lang');
25 |
26 | if (view === 'source' || view === 'desktop') {
27 | setActiveView(view);
28 | }
29 |
30 | if (lang === 'jsx' || lang === 'markup' || lang === 'markdown') {
31 | setActiveLang(lang);
32 | }
33 | }, [searchParams]);
34 |
35 | const handleViewChange = (view: string) => {
36 | setActiveView(view);
37 | router.push(`${pathname}?view=${view}`);
38 | };
39 |
40 | const handleLangChange = (lang: string) => {
41 | setActiveLang(lang);
42 | router.push(`${pathname}?view=source&lang=${lang}`);
43 | };
44 |
45 | return (
46 |
53 | {activeView === 'desktop' ? (
54 |
55 | ) : (
56 |
57 |
58 |
67 |
68 |
69 | )}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/.react-email/src/components/button.tsx:
--------------------------------------------------------------------------------
1 | import * as SlotPrimitive from '@radix-ui/react-slot';
2 | import classnames from 'classnames';
3 | import * as React from 'react';
4 | import { unreachable } from '../utils';
5 |
6 | type ButtonElement = React.ElementRef<'button'>;
7 | type RootProps = React.ComponentPropsWithoutRef<'button'>;
8 |
9 | type Appearance = 'white' | 'gradient';
10 | type Size = '1' | '2' | '3' | '4';
11 |
12 | interface ButtonProps extends RootProps {
13 | asChild?: boolean;
14 | appearance?: Appearance;
15 | size?: Size;
16 | }
17 |
18 | export const Button = React.forwardRef>(
19 | (
20 | {
21 | asChild,
22 | appearance = 'white',
23 | className,
24 | children,
25 | size = '2',
26 | ...props
27 | },
28 | forwardedRef,
29 | ) => {
30 | const classNames = classnames(
31 | getSize(size),
32 | getAppearance(appearance),
33 | 'inline-flex items-center justify-center border font-medium',
34 | className,
35 | );
36 |
37 | return asChild ? (
38 |
39 | {children}
40 |
41 | ) : (
42 |
43 | {children}
44 |
45 | );
46 | },
47 | );
48 |
49 | Button.displayName = 'Button';
50 |
51 | const getAppearance = (appearance: Appearance | undefined) => {
52 | switch (appearance) {
53 | case undefined:
54 | case 'white':
55 | return [
56 | 'bg-white text-black',
57 | 'hover:bg-white/90',
58 | 'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-white/90',
59 | ];
60 | case 'gradient':
61 | return [
62 | 'bg-gradient backdrop-blur-[20px] border-[#34343A]',
63 | 'hover:bg-gradientHover',
64 | 'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-gradientHover',
65 | ];
66 | default:
67 | unreachable(appearance);
68 | }
69 | };
70 |
71 | const getSize = (size: Size | undefined) => {
72 | switch (size) {
73 | case '1':
74 | return '';
75 | case undefined:
76 | case '2':
77 | return 'text-[14px] h-8 px-3 rounded-md gap-2';
78 | case '3':
79 | return 'text-[14px] h-10 px-4 rounded-md gap-2';
80 | case '4':
81 | return 'text-base h-11 px-4 rounded-md gap-2';
82 | default:
83 | unreachable(size);
84 | }
85 | };
86 |
--------------------------------------------------------------------------------
/.react-email/src/components/heading.tsx:
--------------------------------------------------------------------------------
1 | import * as SlotPrimitive from '@radix-ui/react-slot';
2 | import classnames from 'classnames';
3 | import * as React from 'react';
4 | import { As, unreachable } from '../utils';
5 |
6 | export type HeadingSize =
7 | | '1'
8 | | '2'
9 | | '3'
10 | | '4'
11 | | '5'
12 | | '6'
13 | | '7'
14 | | '8'
15 | | '9'
16 | | '10';
17 | export type HeadingColor = 'white' | 'gray';
18 | export type HeadingWeight = 'medium' | 'bold';
19 |
20 | interface HeadingOwnProps {
21 | size?: HeadingSize;
22 | color?: HeadingColor;
23 | weight?: HeadingWeight;
24 | }
25 |
26 | type HeadingProps = As<'h1', 'h2', 'h3', 'h4', 'h5', 'h6'> & HeadingOwnProps;
27 |
28 | export const Heading = React.forwardRef<
29 | HTMLHeadingElement,
30 | Readonly
31 | >(
32 | (
33 | {
34 | as: Tag = 'h1',
35 | size = '3',
36 | className,
37 | color = 'white',
38 | children,
39 | weight = 'bold',
40 | ...props
41 | },
42 | forwardedRef,
43 | ) => (
44 |
54 | {children}
55 |
56 | ),
57 | );
58 |
59 | const getSizesClassNames = (size: HeadingSize | undefined) => {
60 | switch (size) {
61 | case '1':
62 | return 'text-xs';
63 | case '2':
64 | return 'text-sm';
65 | case undefined:
66 | case '3':
67 | return 'text-base';
68 | case '4':
69 | return 'text-lg';
70 | case '5':
71 | return 'text-xl tracking-[-0.16px]';
72 | case '6':
73 | return 'text-2xl tracking-[-0.288px]';
74 | case '7':
75 | return 'text-[28px] leading-[34px] tracking-[-0.416px]';
76 | case '8':
77 | return 'text-[35px] leading-[42px] tracking-[-0.64px]';
78 | case '9':
79 | return 'text-6xl leading-[73px] tracking-[-0.896px]';
80 | case '10':
81 | return [
82 | 'text-[38px] leading-[46px]',
83 | 'md:text-[70px] md:leading-[85px] tracking-[-1.024px;]',
84 | ];
85 | default:
86 | return unreachable(size);
87 | }
88 | };
89 |
90 | const getColorClassNames = (color: HeadingColor | undefined) => {
91 | switch (color) {
92 | case 'gray':
93 | return 'text-slate-11';
94 | case 'white':
95 | case undefined:
96 | return 'text-slate-12';
97 | default:
98 | return unreachable(color);
99 | }
100 | };
101 |
102 | const getWeightClassNames = (weight: HeadingWeight | undefined) => {
103 | switch (weight) {
104 | case 'medium':
105 | return 'font-medium';
106 | case 'bold':
107 | case undefined:
108 | return 'font-bold';
109 | default:
110 | return unreachable(weight);
111 | }
112 | };
113 |
114 | Heading.displayName = 'Heading';
115 |
--------------------------------------------------------------------------------
/.react-email/src/components/icon-base.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export interface IconProps {
4 | size?: number;
5 | }
6 |
7 | export type IconElement = React.ElementRef<'svg'>;
8 | export type RootProps = React.ComponentPropsWithoutRef<'svg'>;
9 |
10 | export interface IconProps extends RootProps {}
11 |
12 | export const IconBase = React.forwardRef>(
13 | ({ size = 20, ...props }, forwardedRef) => (
14 |
23 | ),
24 | );
25 |
26 | IconBase.displayName = 'IconBase';
27 |
--------------------------------------------------------------------------------
/.react-email/src/components/icon-button.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import * as React from 'react';
3 |
4 | export interface IconButtonProps
5 | extends React.ComponentPropsWithoutRef<'button'> {}
6 |
7 | export const IconButton = React.forwardRef<
8 | HTMLButtonElement,
9 | Readonly
10 | >(({ children, className, ...props }, forwardedRef) => (
11 |
19 | {children}
20 |
21 | ));
22 |
23 | IconButton.displayName = 'IconButton';
24 |
--------------------------------------------------------------------------------
/.react-email/src/components/icon-check.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { IconBase, IconElement, IconProps } from './icon-base';
3 |
4 | export const IconCheck = React.forwardRef>(
5 | ({ ...props }, forwardedRef) => (
6 |
7 |
14 |
15 | ),
16 | );
17 |
18 | IconCheck.displayName = 'IconCheck';
19 |
--------------------------------------------------------------------------------
/.react-email/src/components/icon-clipboard.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { IconBase, IconElement, IconProps } from './icon-base';
3 |
4 | export const IconClipboard = React.forwardRef>(
5 | ({ ...props }, forwardedRef) => (
6 |
7 |
14 |
21 |
28 |
35 |
36 | ),
37 | );
38 |
39 | IconClipboard.displayName = 'IconClipboard';
40 |
--------------------------------------------------------------------------------
/.react-email/src/components/icon-download.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { IconBase, IconElement, IconProps } from './icon-base';
3 |
4 | export const IconDownload = React.forwardRef>(
5 | ({ ...props }, forwardedRef) => (
6 |
7 |
14 |
15 | ),
16 | );
17 |
18 | IconDownload.displayName = 'IconDownload';
19 |
--------------------------------------------------------------------------------
/.react-email/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './button';
2 | export * from './code';
3 | export * from './heading';
4 | export * from './logo';
5 | export * from './sidebar';
6 | export * from './text';
7 | export * from './topbar';
8 |
--------------------------------------------------------------------------------
/.react-email/src/components/shell.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Sidebar } from './sidebar';
3 | import { Topbar } from './topbar';
4 |
5 | type ShellElement = React.ElementRef<'div'>;
6 | type RootProps = React.ComponentPropsWithoutRef<'div'>;
7 |
8 | interface ShellProps extends RootProps {
9 | navItems: string[];
10 | markup?: string;
11 | activeView?: string;
12 | setActiveView?: (view: string) => void;
13 | }
14 |
15 | export const Shell = React.forwardRef>(
16 | (
17 | { title, navItems, children, markup, activeView, setActiveView },
18 | forwardedRef,
19 | ) => {
20 | return (
21 |
22 |
23 |
24 | {title && (
25 |
31 | )}
32 |
35 |
36 |
37 | );
38 | },
39 | );
40 |
41 | Shell.displayName = 'Shell';
42 |
--------------------------------------------------------------------------------
/.react-email/src/components/text.tsx:
--------------------------------------------------------------------------------
1 | import * as SlotPrimitive from '@radix-ui/react-slot';
2 | import classnames from 'classnames';
3 | import * as React from 'react';
4 | import { As, unreachable } from '../utils';
5 |
6 | export type TextSize = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
7 | export type TextColor = 'gray' | 'white';
8 | export type TextTransform = 'uppercase' | 'lowercase' | 'capitalize';
9 | export type TextWeight = 'normal' | 'medium';
10 |
11 | interface TextOwnProps {
12 | size?: TextSize;
13 | color?: TextColor;
14 | transform?: TextTransform;
15 | weight?: TextWeight;
16 | }
17 |
18 | type TextProps = As<'span', 'div', 'p'> & TextOwnProps;
19 |
20 | export const Text = React.forwardRef>(
21 | (
22 | {
23 | as: Tag = 'span',
24 | size = '2',
25 | color = 'gray',
26 | transform,
27 | weight = 'normal',
28 | className,
29 | children,
30 | ...props
31 | },
32 | forwardedRef,
33 | ) => (
34 |
45 | {children}
46 |
47 | ),
48 | );
49 |
50 | const getSizesClassNames = (size: TextSize | undefined) => {
51 | switch (size) {
52 | case '1':
53 | return 'text-xs';
54 | case undefined:
55 | case '2':
56 | return 'text-sm';
57 | case '3':
58 | return 'text-base';
59 | case '4':
60 | return 'text-lg';
61 | case '5':
62 | return ['text-17px', 'md:text-xl tracking-[-0.16px]'];
63 | case '6':
64 | return 'text-2xl tracking-[-0.288px]';
65 | case '7':
66 | return 'text-[28px] leading-[34px] tracking-[-0.416px]';
67 | case '8':
68 | return 'text-[35px] leading-[42px] tracking-[-0.64px]';
69 | case '9':
70 | return 'text-6xl leading-[73px] tracking-[-0.896px]';
71 | default:
72 | return unreachable(size);
73 | }
74 | };
75 |
76 | const getColorClassNames = (color: TextColor | undefined) => {
77 | switch (color) {
78 | case 'white':
79 | return 'text-slate-12';
80 | case undefined:
81 | case 'gray':
82 | return 'text-slate-11';
83 | default:
84 | return unreachable(color);
85 | }
86 | };
87 |
88 | const getWeightClassNames = (weight: TextWeight | undefined) => {
89 | switch (weight) {
90 | case undefined:
91 | case 'normal':
92 | return 'font-normal';
93 | case 'medium':
94 | return 'font-medium';
95 | default:
96 | return unreachable(weight);
97 | }
98 | };
99 |
100 | Text.displayName = 'Text';
101 |
--------------------------------------------------------------------------------
/.react-email/src/components/tooltip-content.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2 | import classnames from 'classnames';
3 | import * as React from 'react';
4 | import { inter } from '../app/layout';
5 |
6 | type ContentElement = React.ElementRef;
7 | type ContentProps = React.ComponentPropsWithoutRef<
8 | typeof TooltipPrimitive.Content
9 | >;
10 |
11 | export interface TooltipProps extends ContentProps {}
12 |
13 | export const TooltipContent = React.forwardRef<
14 | ContentElement,
15 | Readonly
16 | >(({ sideOffset = 6, children, ...props }, forwardedRef) => (
17 |
18 |
27 | {children}
28 |
29 |
30 | ));
31 |
32 | TooltipContent.displayName = 'TooltipContent';
33 |
--------------------------------------------------------------------------------
/.react-email/src/components/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2 | import * as React from 'react';
3 | import { TooltipContent } from './tooltip-content';
4 |
5 | type RootProps = React.ComponentPropsWithoutRef;
6 |
7 | export interface TooltipProps extends RootProps {}
8 |
9 | export const TooltipRoot: React.FC> = ({
10 | children,
11 | ...props
12 | }) => {children} ;
13 |
14 | export const Tooltip = Object.assign(TooltipRoot, {
15 | Arrow: TooltipPrimitive.TooltipArrow,
16 | Provider: TooltipPrimitive.TooltipProvider,
17 | Content: TooltipContent,
18 | Trigger: TooltipPrimitive.TooltipTrigger,
19 | });
20 |
--------------------------------------------------------------------------------
/.react-email/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/.react-email/src/utils/as.ts:
--------------------------------------------------------------------------------
1 | export type As<
2 | DefaultTag extends React.ElementType,
3 | T1 extends React.ElementType,
4 | T2 extends React.ElementType = T1,
5 | T3 extends React.ElementType = T1,
6 | T4 extends React.ElementType = T1,
7 | T5 extends React.ElementType = T1,
8 | > =
9 | | (React.ComponentPropsWithRef & {
10 | as?: DefaultTag;
11 | })
12 | | (React.ComponentPropsWithRef & {
13 | as: T1;
14 | })
15 | | (React.ComponentPropsWithRef & {
16 | as: T2;
17 | })
18 | | (React.ComponentPropsWithRef & {
19 | as: T3;
20 | })
21 | | (React.ComponentPropsWithRef & {
22 | as: T4;
23 | })
24 | | (React.ComponentPropsWithRef & {
25 | as: T5;
26 | });
27 |
--------------------------------------------------------------------------------
/.react-email/src/utils/copy-text-to-clipboard.ts:
--------------------------------------------------------------------------------
1 | export const copyTextToClipboard = async (text: string) => {
2 | try {
3 | await navigator.clipboard.writeText(text);
4 | } catch {
5 | throw new Error('Not able to copy');
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/.react-email/src/utils/get-emails.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs';
2 | import path from 'path';
3 |
4 | export const CONTENT_DIR = 'emails';
5 |
6 | export const getEmails = async () => {
7 | const emailsDirectory = path.join(process.cwd(), CONTENT_DIR);
8 | const filenames = await fs.readdir(emailsDirectory);
9 | const emails = filenames
10 | .map((file) => file.replace(/\.(jsx|tsx)$/g, ''))
11 | .filter((file) => file !== 'components');
12 | return { emails, filenames };
13 | };
14 |
--------------------------------------------------------------------------------
/.react-email/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './as';
2 | export * from './copy-text-to-clipboard';
3 | export * from './unreachable';
4 |
--------------------------------------------------------------------------------
/.react-email/src/utils/language-map.ts:
--------------------------------------------------------------------------------
1 | const languageMap = {
2 | jsx: 'React',
3 | markup: 'HTML',
4 | markdown: 'Plain Text',
5 | };
6 |
7 | export default languageMap;
8 |
--------------------------------------------------------------------------------
/.react-email/src/utils/unreachable.ts:
--------------------------------------------------------------------------------
1 | export const unreachable = (
2 | condition: never,
3 | message = `Entered unreachable code. Received '${condition}'.`,
4 | ): never => {
5 | throw new TypeError(message);
6 | };
7 |
--------------------------------------------------------------------------------
/.react-email/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('@radix-ui/colors');
2 | const { fontFamily } = require('tailwindcss/defaultTheme');
3 | const plugin = require('tailwindcss/plugin');
4 |
5 | const iOsHeight = plugin(function ({ addUtilities }) {
6 | const supportsTouchRule = '@supports (-webkit-touch-callout: none)';
7 | const webkitFillAvailable = '-webkit-fill-available';
8 |
9 | const utilities = {
10 | '.min-h-screen-ios': {
11 | [supportsTouchRule]: {
12 | minHeight: webkitFillAvailable,
13 | },
14 | },
15 | '.h-screen-ios': {
16 | [supportsTouchRule]: {
17 | height: webkitFillAvailable,
18 | },
19 | },
20 | };
21 |
22 | addUtilities(utilities, ['responsive']);
23 | });
24 |
25 | /** @type {import('tailwindcss').Config} */
26 | module.exports = {
27 | content: [
28 | // app content
29 | `src/**/*.{js,ts,jsx,tsx}`,
30 | // include packages if not transpiling
31 | '../../packages/**/*.{js,ts,jsx,tsx}',
32 | '../../apps/**/*.{js,ts,jsx,tsx}',
33 | ],
34 | theme: {
35 | extend: {
36 | backgroundImage: {
37 | gradient:
38 | 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',
39 | gradientHover:
40 | 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.1) -8.75%, rgba(255, 255, 255, 0.057) 83.95%)',
41 | shine:
42 | 'linear-gradient(45deg, rgba(255,255,255,0) 45%,rgba(255,255,255,1) 50%,rgba(255,255,255,0) 55%,rgba(255,255,255,0) 100%)',
43 | },
44 | colors: {
45 | cyan: {
46 | 1: colors.cyanDarkA.cyanA1,
47 | 2: colors.cyanDarkA.cyanA2,
48 | 3: colors.cyanDarkA.cyanA3,
49 | 4: colors.cyanDarkA.cyanA4,
50 | 5: colors.cyanDarkA.cyanA5,
51 | 6: colors.cyanDarkA.cyanA6,
52 | 7: colors.cyanDarkA.cyanA7,
53 | 8: colors.cyanDarkA.cyanA8,
54 | 9: colors.cyanDarkA.cyanA9,
55 | 10: colors.cyanDarkA.cyanA10,
56 | 11: colors.cyanDarkA.cyanA11,
57 | 12: colors.cyanDarkA.cyanA12,
58 | },
59 | slate: {
60 | 1: colors.slateDarkA.slateA1,
61 | 2: colors.slateDarkA.slateA2,
62 | 3: colors.slateDarkA.slateA3,
63 | 4: colors.slateDarkA.slateA4,
64 | 5: colors.slateDarkA.slateA5,
65 | 6: colors.slateDarkA.slateA6,
66 | 7: colors.slateDarkA.slateA7,
67 | 8: colors.slateDarkA.slateA8,
68 | 9: colors.slateDarkA.slateA9,
69 | 10: colors.slateDarkA.slateA10,
70 | 11: colors.slateDarkA.slateA11,
71 | 12: colors.slateDarkA.slateA12,
72 | },
73 | },
74 | fontFamily: {
75 | sans: ['var(--font-inter)', ...fontFamily.sans],
76 | },
77 | keyframes: {
78 | shine: {
79 | '0%': { backgroundPosition: '-100%' },
80 | '100%': { backgroundPosition: '100%' },
81 | },
82 | dash: {
83 | '0%': { strokeDashoffset: 1000 },
84 | '100%': { strokeDashoffset: 0 },
85 | },
86 | },
87 | },
88 | },
89 | plugins: [iOsHeight],
90 | };
91 |
--------------------------------------------------------------------------------
/.react-email/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "incremental": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "strictNullChecks": true
23 | },
24 | "include": [
25 | "preview/next-env.d.ts",
26 | "preview/.next/types/**/*.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts"
30 | ],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------
/LEARN.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Subham Bharadwaz
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scribbly
2 |
3 | ## An open source Journal Web App
4 |
5 | 
6 |
7 | ## About this project
8 |
9 | Scribbly is a web application that provides a platform for users to create and manage their digital journal. With Scribbly, users can easily jot down their thoughts, experiences, and ideas, and organize them in a personal and customizable journal.
10 |
11 | - **Digital Journaling**: Users can create and store their journal entries online, eliminating the need for physical notebooks or papers.
12 | - **User-Friendly Interface**: Scribbly offers a clean and intuitive user interface, powered by Radix UI and Shadcn/UI components, making it easy for users to navigate and interact with their journal.
13 | - **Secure and Private**: Scribbly prioritizes user data security and privacy, ensuring that journal entries are kept confidential and protected.
14 | - **Subscription Plan**: Scribbly offers a monthly subscription plan that provides users with additional features and benefits.
15 | - **Reminder Feature**: The reminder feature is a valuable addition to Scribbly. By sending reminder emails to users who have activated this option every day at 9 pm, you help users stay consistent with their journaling habit and make it a part of their daily routine.
16 |
17 | ## Features
18 |
19 | - Next.js `/app` dir,
20 | - Routing, Layouts, Nested Layouts and Layout Groups
21 | - Data Fetching, Caching and Mutation using **TanStack Query**
22 | - Route handlers
23 | - Metadata files
24 | - Server and Client Components
25 | - API Routes and Middleware
26 | - Authentication using **Clerk**
27 | - Block-Style editor with **Editor.js**
28 | - ORM using **Prisma**
29 | - Database on **PlanetScale**
30 | - Creating and sending emails with **React Email** and **Resend**
31 | - UI Components built using **Radix UI** and **shadcn/ui**
32 | - Subscriptions using **Stripe**
33 | - Styled using **Tailwind CSS**
34 | - Analytics with **PostHog**
35 | - Error Handling in **Sentry**
36 | - Validations using **Zod**
37 | - Written in **TypeScript**
38 |
39 | ## Running Locally
40 |
41 | 1. Install dependencies using pnpm:
42 |
43 | ```sh
44 | pnpm install
45 | ```
46 |
47 | 2. Copy `.env.example` to `.env.local` and update the variables.
48 |
49 | ```sh
50 | cp .env.example .env.local
51 | ```
52 |
53 | 3. Start the development server:
54 |
55 | ```sh
56 | pnpm dev
57 | ```
58 |
59 | ## License
60 |
61 | Licensed under the [MIT license](https://github.com/subhamBharadwaz/scribbly/blob/main/LICENSE.md).
62 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 | ## Supported Version
3 |
4 | The security policy outlined below applies to version 0.1.0 of the project.
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 0.1.0 | :white_check_mark: |
8 |
9 |
10 | ## Reporting a Vulnerability
11 |
12 | If you discover a security vulnerability in this project, we appreciate your responsible disclosure. By working together, we can address the issue promptly and ensure the security of our users' data.
13 |
14 | To report a vulnerability, please follow these steps:
15 |
16 | 1. **Email**: Send an email to our security team at [:subhamsbharadwaz@gmail.com](mailto:subhamsbharadwaz@gmail.com).
17 | 2. **Subject**: Use a clear and descriptive subject line, such as "Security Vulnerability Report - [Scribbly]."
18 | 3. **Description**: Provide a detailed description of the vulnerability, including the steps to reproduce it and any relevant information that can help us understand and address the issue.
19 | 4. **Attach Proof of Concept**: If possible, provide a proof-of-concept or sample code that demonstrates the vulnerability. However, please refrain from performing any destructive actions or violating any privacy or security laws during your research.
20 | 5. **Encryption (Optional)**: If you prefer to encrypt your communication, please use our public PGP key, which can be found on our website or on public key servers.
21 | 6. **Responsiveness**: We strive to respond to vulnerability reports promptly. You can expect an initial response acknowledging your report within [specify time frame, e.g., 48 hours].
22 | 7. **Investigation and Disclosure**: Our security team will investigate the reported vulnerability and assess its impact on our system. We will keep you informed of our progress and any necessary actions.
23 | 8. **Responsible Disclosure**: We kindly request that you do not disclose the vulnerability publicly until we have addressed it and provided an official announcement. We will work together with you to determine an appropriate timeline for disclosure, considering the severity and complexity of the vulnerability.
24 |
25 | We value the contributions of security researchers and the broader community in improving the security of our project. As a token of our appreciation, we may acknowledge your contribution publicly, upon mutual agreement.
26 |
27 | Thank you for your commitment to keeping our project secure!
28 |
29 | Note: This security policy is subject to change without notice.
30 |
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/assets/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/assets/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/Satoshi-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/assets/fonts/Satoshi-Black.ttf
--------------------------------------------------------------------------------
/assets/fonts/Satoshi-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/assets/fonts/Satoshi-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Satoshi-Variable.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/assets/fonts/Satoshi-Variable.woff2
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tailwind": {
6 | "config": "tailwind.config.js",
7 | "css": "src/styles/globals.css",
8 | "baseColor": "slate",
9 | "cssVariables": true
10 | },
11 | "aliases": {
12 | "components": "@/components",
13 | "utils": "@/lib/utils"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/contentlayer.config.js:
--------------------------------------------------------------------------------
1 | import { defineDocumentType, makeSource } from "contentlayer/source-files";
2 |
3 | /** @type {import('contentlayer/source-files').ComputedFields} */
4 | const computedFields = {
5 | slug: {
6 | type: "string",
7 | resolve: (doc) => `/${doc._raw.flattenedPath}`,
8 | },
9 | slugAsParams: {
10 | type: "string",
11 | resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"),
12 | },
13 | };
14 |
15 | export const Page = defineDocumentType(() => ({
16 | name: "Page",
17 | filePathPattern: `pages/**/*.mdx`,
18 | contentType: "mdx",
19 | fields: {
20 | title: {
21 | type: "string",
22 | required: true,
23 | },
24 | description: {
25 | type: "string",
26 | },
27 | },
28 | computedFields,
29 | }));
30 |
31 | export default makeSource({
32 | contentDirPath: "src/content",
33 | documentTypes: [Page],
34 | });
35 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 | services:
3 | scribbly:
4 | image: postgres:16.2
5 | restart: always
6 | container_name: scribbly
7 | ports:
8 | - 5432:5432
9 | environment:
10 | POSTGRES_PASSWORD: example
11 | PGDATA: /data/postgres
12 | volumes:
13 | - ./db-data:/var/lib/postgresql/data
14 |
15 | volumes:
16 | postgres:
17 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { withSentryConfig } from "@sentry/nextjs"
2 | import { withContentlayer } from "next-contentlayer"
3 |
4 | /**
5 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
6 | * for Docker builds.
7 | */
8 | await import("./src/env.mjs")
9 |
10 | /** @type {import('next').NextConfig} */
11 | const nextConfig = {
12 | reactStrictMode: true,
13 | images: {
14 | domains: ["res.cloudinary.com", "images.unsplash.com", "uploadthing.com"],
15 | },
16 | async rewrites() {
17 | return [
18 | {
19 | source: "/ingest/static/:path*",
20 | destination: "https://us-assets.i.posthog.com/static/:path*",
21 | },
22 | {
23 | source: "/ingest/:path*",
24 | destination: "https://us.i.posthog.com/:path*",
25 | },
26 | ]
27 | },
28 | // This is required to support PostHog trailing slash API requests
29 | skipTrailingSlashRedirect: true,
30 | }
31 |
32 | export default withSentryConfig(
33 | withContentlayer(nextConfig),
34 | {
35 | // For all available options, see:
36 | // https://github.com/getsentry/sentry-webpack-plugin#options
37 |
38 | // Suppresses source map uploading logs during build
39 | silent: true,
40 | org: "subham-bharadwaz",
41 | project: "scribbly",
42 | },
43 | {
44 | // For all available options, see:
45 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
46 |
47 | // Upload a larger set of source maps for prettier stack traces (increases build time)
48 | widenClientFileUpload: true,
49 |
50 | // Transpiles SDK to be compatible with IE11 (increases bundle size)
51 | transpileClientSDK: true,
52 |
53 | // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
54 | // This can increase your server load as well as your hosting bill.
55 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
56 | // side errors will fail.
57 | // tunnelRoute: "/monitoring",
58 |
59 | // Hides source maps from generated client bundles
60 | hideSourceMaps: true,
61 |
62 | // Automatically tree-shake Sentry logger statements to reduce bundle size
63 | disableLogger: true,
64 |
65 | // Enables automatic instrumentation of Vercel Cron Monitors.
66 | // See the following for more information:
67 | // https://docs.sentry.io/product/crons/
68 | // https://vercel.com/docs/cron-jobs
69 | automaticVercelMonitors: true,
70 | }
71 | )
72 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | importOrder: [
9 | "^(react/(.*)$)|^(react$)",
10 | "^(next/(.*)$)|^(next$)",
11 | "",
12 | "",
13 | "^types$",
14 | "^@/env(.*)$",
15 | "^@/types/(.*)$",
16 | "^@/config/(.*)$",
17 | "^@/lib/(.*)$",
18 | "^@/hooks/(.*)$",
19 | "^@/components/ui/(.*)$",
20 | "^@/components/(.*)$",
21 | "^@/app/(.*)$",
22 | "",
23 | "^[./]",
24 | ],
25 | importOrderSeparation: false,
26 | importOrderSortSpecifiers: true,
27 | importOrderBuiltinModulesToTop: true,
28 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
29 | importOrderMergeDuplicateImports: true,
30 | importOrderCombineTypeAndValueImports: true,
31 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
32 | }
33 |
--------------------------------------------------------------------------------
/prisma/migrations/20240408163644_neon_migration/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "ReminderType" AS ENUM ('DAILY', 'WEEKLY');
3 |
4 | -- CreateTable
5 | CREATE TABLE "users" (
6 | "id" TEXT NOT NULL,
7 | "name" TEXT,
8 | "email" TEXT,
9 | "clerkId" TEXT NOT NULL,
10 | "image" TEXT,
11 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
12 | "updated_at" TIMESTAMP(3) NOT NULL,
13 | "stripe_customer_id" TEXT,
14 | "stripe_subscription_id" TEXT,
15 | "stripe_price_id" TEXT,
16 | "stripe_current_period_end" TIMESTAMP(3),
17 |
18 | CONSTRAINT "users_pkey" PRIMARY KEY ("id")
19 | );
20 |
21 | -- CreateTable
22 | CREATE TABLE "journal_entries" (
23 | "id" TEXT NOT NULL,
24 | "title" TEXT NOT NULL,
25 | "content" JSONB,
26 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
27 | "updated_at" TIMESTAMP(3) NOT NULL,
28 | "userId" TEXT NOT NULL,
29 |
30 | CONSTRAINT "journal_entries_pkey" PRIMARY KEY ("id")
31 | );
32 |
33 | -- CreateTable
34 | CREATE TABLE "reminders" (
35 | "id" TEXT NOT NULL,
36 | "frequency" "ReminderType" DEFAULT 'DAILY',
37 | "time" TIME(0) DEFAULT '09:00:00',
38 | "active" BOOLEAN DEFAULT false,
39 | "userId" TEXT NOT NULL,
40 |
41 | CONSTRAINT "reminders_pkey" PRIMARY KEY ("id")
42 | );
43 |
44 | -- CreateIndex
45 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
46 |
47 | -- CreateIndex
48 | CREATE UNIQUE INDEX "users_clerkId_key" ON "users"("clerkId");
49 |
50 | -- CreateIndex
51 | CREATE UNIQUE INDEX "users_stripe_customer_id_key" ON "users"("stripe_customer_id");
52 |
53 | -- CreateIndex
54 | CREATE UNIQUE INDEX "users_stripe_subscription_id_key" ON "users"("stripe_subscription_id");
55 |
56 | -- CreateIndex
57 | CREATE UNIQUE INDEX "journal_entries_userId_id_key" ON "journal_entries"("userId", "id");
58 |
59 | -- CreateIndex
60 | CREATE UNIQUE INDEX "reminders_userId_key" ON "reminders"("userId");
61 |
--------------------------------------------------------------------------------
/prisma/migrations/20240607201413_docker/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "journal_entries" ADD COLUMN "isBookmarked" BOOLEAN NOT NULL DEFAULT false;
3 |
4 | -- AlterTable
5 | ALTER TABLE "reminders" ALTER COLUMN "time" SET DEFAULT '09:00:00';
6 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "postgresql"
10 | url = env("DATABASE_URL")
11 | relationMode = "prisma"
12 | }
13 |
14 | model User {
15 | id String @id @default(cuid())
16 | name String?
17 | email String? @unique
18 | clerkId String @unique
19 | image String?
20 | createdAt DateTime @default(now()) @map(name: "created_at")
21 | updatedAt DateTime @updatedAt @map(name: "updated_at")
22 |
23 | entries JournalEntry[]
24 | reminder Reminder?
25 |
26 | stripeCustomerId String? @unique @map(name: "stripe_customer_id")
27 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
28 | stripePriceId String? @map(name: "stripe_price_id")
29 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
30 |
31 | @@map(name: "users")
32 | }
33 |
34 | model JournalEntry {
35 | id String @id @default(cuid())
36 | title String
37 | content Json?
38 | isBookmarked Boolean @default(false)
39 | createdAt DateTime @default(now()) @map(name: "created_at")
40 | updatedAt DateTime @updatedAt @map(name: "updated_at")
41 |
42 | userId String
43 | user User @relation(fields: [userId], references: [id])
44 |
45 | @@unique([userId, id])
46 | @@map(name: "journal_entries")
47 | }
48 |
49 | model Reminder {
50 | id String @id @default(cuid())
51 | frequency ReminderType? @default(DAILY)
52 | time DateTime? @default(dbgenerated("'09:00:00'")) @db.Time(0)
53 | active Boolean? @default(false)
54 |
55 | userId String
56 | user User @relation(fields: [userId], references: [id])
57 |
58 | @@unique([userId])
59 | @@map("reminders")
60 | }
61 |
62 | enum ReminderType {
63 | DAILY
64 | WEEKLY
65 | }
66 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/favicon.ico
--------------------------------------------------------------------------------
/public/grid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/auth-bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/auth-bg.webp
--------------------------------------------------------------------------------
/public/images/features/editor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/features/editor.webp
--------------------------------------------------------------------------------
/public/images/features/reminder.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/features/reminder.webp
--------------------------------------------------------------------------------
/public/images/hero-dark.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/hero-dark.webp
--------------------------------------------------------------------------------
/public/images/testimonials/alex.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/testimonials/alex.webp
--------------------------------------------------------------------------------
/public/images/testimonials/emily.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/testimonials/emily.webp
--------------------------------------------------------------------------------
/public/images/testimonials/john.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/testimonials/john.webp
--------------------------------------------------------------------------------
/public/images/testimonials/lisa.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/testimonials/lisa.webp
--------------------------------------------------------------------------------
/public/images/testimonials/mark.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/testimonials/mark.webp
--------------------------------------------------------------------------------
/public/images/testimonials/sara.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/images/testimonials/sara.webp
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/noise.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/public/noise.webp
--------------------------------------------------------------------------------
/public/scribbly-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Scribbly",
3 | "short_name": "Scribbly",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/sentry.client.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the client.
2 | // The config you add here will be used whenever a users loads a page in their browser.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs"
6 |
7 | Sentry.init({
8 | dsn: "https://11711c74e1808b7383eb56de412c500f@o859786.ingest.us.sentry.io/4507228697460736",
9 |
10 | // Adjust this value in production, or use tracesSampler for greater control
11 | tracesSampleRate: 1,
12 |
13 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
14 | debug: false,
15 |
16 | replaysOnErrorSampleRate: 1.0,
17 |
18 | // This sets the sample rate to be 10%. You may want this to be 100% while
19 | // in development and sample at a lower rate in production
20 | replaysSessionSampleRate: 0.1,
21 |
22 | // You can remove this option if you're not planning to use the Sentry Session Replay feature:
23 | integrations: [
24 | Sentry.replayIntegration({
25 | // Additional Replay configuration goes in here, for example:
26 | maskAllText: true,
27 | blockAllMedia: true,
28 | }),
29 | ],
30 | })
31 |
--------------------------------------------------------------------------------
/sentry.edge.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
2 | // The config you add here will be used whenever one of the edge features is loaded.
3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
5 |
6 | import * as Sentry from "@sentry/nextjs"
7 |
8 | Sentry.init({
9 | dsn: "https://11711c74e1808b7383eb56de412c500f@o859786.ingest.us.sentry.io/4507228697460736",
10 |
11 | // Adjust this value in production, or use tracesSampler for greater control
12 | tracesSampleRate: 1,
13 |
14 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
15 | debug: false,
16 | })
17 |
--------------------------------------------------------------------------------
/sentry.server.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the server.
2 | // The config you add here will be used whenever the server handles a request.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs"
6 |
7 | Sentry.init({
8 | dsn: "https://11711c74e1808b7383eb56de412c500f@o859786.ingest.us.sentry.io/4507228697460736",
9 |
10 | // Adjust this value in production, or use tracesSampler for greater control
11 | tracesSampleRate: 1,
12 |
13 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
14 | debug: false,
15 |
16 | // uncomment the line below to enable Spotlight (https://spotlightjs.com)
17 | // spotlight: process.env.NODE_ENV === 'development',
18 | })
19 |
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import Link from "next/link"
3 |
4 | import { siteConfig } from "@/config/site"
5 | import { AspectRatio } from "@/components/ui/aspect-ratio"
6 | import { Icons } from "@/components/icons"
7 |
8 | interface AuthLayoutProps {
9 | children: React.ReactNode
10 | }
11 |
12 | export default function AuthLayout({ children }: AuthLayoutProps) {
13 | return (
14 |
15 |
16 |
23 |
24 |
28 |
29 | {siteConfig.name}
30 |
31 |
49 |
50 |
51 | {children}
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next"
2 | import { SignIn } from "@clerk/nextjs"
3 |
4 | import { Shell } from "@/components/shell"
5 |
6 | export const metadata: Metadata = {
7 | title: "Sign in",
8 | description: "Sign in to your account",
9 | }
10 |
11 | export default function LoginPage() {
12 | return (
13 |
14 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { SignUp } from "@clerk/nextjs"
3 |
4 | import { Shell } from "@/components/shell"
5 |
6 | export const metadata = {
7 | title: "Create an account",
8 | description: "Create an account to get started.",
9 | }
10 |
11 | export default function RegisterPage() {
12 | return (
13 |
14 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/(editor)/editor/[entryId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton"
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(editor)/editor/[entryId]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { buttonVariants } from "@/components/ui/button"
4 | import { EmptyPlaceholder } from "@/components/empty-placeholder"
5 |
6 | export default function NotFound() {
7 | return (
8 |
9 |
10 | Uh oh! Not Found
11 |
12 | This entry could not be found. Please try again.
13 |
14 |
15 | Go to Journal
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/(editor)/editor/[entryId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound, redirect } from "next/navigation"
2 | import { getMyEntry } from "@/server/queries/editor"
3 |
4 | import { getUserByClerkId } from "@/lib/auth"
5 | import Editor from "@/app/(journal)/_components/editor"
6 |
7 | interface EditorPageProps {
8 | params: { entryId: string }
9 | }
10 |
11 | export default async function EditorPage({ params }: EditorPageProps) {
12 | const user = await getUserByClerkId()
13 |
14 | if (!user) {
15 | redirect("/sign-in")
16 | }
17 |
18 | const entry = await getMyEntry(params.entryId, user.id)
19 |
20 | if (!entry) {
21 | notFound()
22 | }
23 |
24 | return (
25 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/(editor)/editor/layout.tsx:
--------------------------------------------------------------------------------
1 | interface EditorProps {
2 | children?: React.ReactNode
3 | }
4 |
5 | export default function EditorLayout({ children }: EditorProps) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/(journal)/_components/billing/billing-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { stripeSubscription } from "@/server/actions/stripe"
5 | import { UserSubscriptionPlan } from "@/types"
6 |
7 | import { cn, formatDate } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 | import {
10 | Card,
11 | CardContent,
12 | CardDescription,
13 | CardFooter,
14 | CardHeader,
15 | CardTitle,
16 | } from "@/components/ui/card"
17 | import { toast } from "@/components/ui/use-toast"
18 | import { Icons } from "@/components/icons"
19 |
20 | interface BillingFormProps extends React.HTMLAttributes {
21 | subscriptionPlan: UserSubscriptionPlan & {
22 | isCanceled: boolean
23 | }
24 | }
25 |
26 | export function BillingForm({
27 | subscriptionPlan,
28 | className,
29 | ...props
30 | }: BillingFormProps) {
31 | const [isLoading, setIsLoading] = React.useState(false)
32 |
33 | async function onSubmit(event: React.FormEvent) {
34 | event.preventDefault()
35 | setIsLoading(!isLoading)
36 |
37 | // Get a Stripe session URL.
38 | const response = await stripeSubscription()
39 | console.log(response)
40 |
41 | if (response?.error) {
42 | return toast({
43 | title: "Something went wrong.",
44 | description: "Please refresh the page and try again.",
45 | variant: "destructive",
46 | })
47 | }
48 |
49 | // Redirect to the Stripe session.
50 | // This could be a checkout page for initial upgrade.
51 | // Or portal to manage existing subscription.
52 | if (response) {
53 | window.location.href = response?.url
54 | }
55 | }
56 |
57 | return (
58 |
90 | )
91 | }
92 |
--------------------------------------------------------------------------------
/src/app/(journal)/_components/journal-entry-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect } from "react"
4 | import Link from "next/link"
5 | import { JournalEntry } from "@prisma/client"
6 | import { AnimatePresence, motion, useAnimate, usePresence } from "framer-motion"
7 | import { BookmarkIcon } from "lucide-react"
8 |
9 | import { cn, formatDate } from "@/lib/utils"
10 |
11 | import { EntryOperations } from "./entry-operations"
12 |
13 | interface JournalEntryProps {
14 | entry: Pick<
15 | JournalEntry,
16 | "id" | "title" | "createdAt" | "isBookmarked" | "content"
17 | >
18 | isBookmarked: boolean
19 | className?: string
20 | }
21 |
22 | export function JournalEntryCard({
23 | entry,
24 | isBookmarked,
25 | className,
26 | }: JournalEntryProps) {
27 | const [isPresent, safeToRemove] = usePresence()
28 | const [scope, animate] = useAnimate()
29 |
30 | useEffect(() => {
31 | if (!isPresent) {
32 | const exitAnimation = async () => {
33 | await animate(
34 | scope.current,
35 | {
36 | opacity: 0,
37 | x: isBookmarked ? 24 : -24,
38 | },
39 | {
40 | duration: 0.125,
41 | ease: "easeIn",
42 | }
43 | )
44 | safeToRemove()
45 | }
46 | exitAnimation()
47 | }
48 | }, [isPresent])
49 | return (
50 |
64 |
65 |
69 | {" "}
70 | {entry.title}
71 |
72 |
73 | {/* @ts-ignore */}
74 | {entry.content?.blocks[0]?.data.text}
75 |
76 |
77 | {isBookmarked && (
78 |
79 |
80 |
81 | )}
82 |
83 |
84 |
85 | {formatDate(entry.createdAt?.toDateString())}
86 |
87 |
95 |
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/src/app/(journal)/_components/journal-entry-create-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useRouter } from "next/navigation"
5 | import { createJournalEntry } from "@/server/actions/journal"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { Button, ButtonProps } from "@/components/ui/button"
9 | import { toast } from "@/components/ui/use-toast"
10 | import { Icons } from "@/components/icons"
11 |
12 | interface JournalEntryCreateButtonProps extends ButtonProps {}
13 |
14 | const JournalEntryCreateButton: React.FC = ({
15 | className,
16 | variant,
17 | ...props
18 | }) => {
19 | const router = useRouter()
20 | const [isLoading, setIsLoading] = React.useState(false)
21 |
22 | async function onClick() {
23 | setIsLoading(true)
24 |
25 | const entry = await createJournalEntry({ title: "Untitled Entry" })
26 |
27 | setIsLoading(false)
28 |
29 | if ("error" in entry) {
30 | if (entry.code === 402) {
31 | return toast({
32 | title: "Limit of 3 entries reached.",
33 | description: "Please upgrade to the PRO plan.",
34 | variant: "destructive",
35 | })
36 | }
37 |
38 | return toast({
39 | title: "Something went wrong.",
40 | description: "Your entry was not created. Please try again.",
41 | variant: "destructive",
42 | })
43 | }
44 |
45 | if (entry) {
46 | router.push(`/editor/${entry.id}`)
47 | }
48 | }
49 |
50 | return (
51 |
62 | {isLoading ? (
63 |
64 | ) : (
65 |
66 | )}
67 | New Entry
68 |
69 | )
70 | }
71 |
72 | export default JournalEntryCreateButton
73 |
--------------------------------------------------------------------------------
/src/app/(journal)/_components/journal-entry.tsx:
--------------------------------------------------------------------------------
1 | import { JournalEntry } from "@prisma/client"
2 |
3 | import { Skeleton } from "@/components/ui/skeleton"
4 |
5 | import { JournalEntryCard } from "./journal-entry-card"
6 |
7 | interface JournalEntryProps {
8 | entry: Pick<
9 | JournalEntry,
10 | "id" | "title" | "createdAt" | "isBookmarked" | "content"
11 | >
12 | userBookmarks: string[]
13 | className?: string
14 | }
15 |
16 | export function JournalEntryItem({
17 | entry,
18 | userBookmarks,
19 | className,
20 | }: JournalEntryProps) {
21 | const isBookmarked = userBookmarks?.includes(entry?.id)
22 |
23 | return (
24 |
29 | )
30 | }
31 |
32 | JournalEntryItem.Skeleton = function JournalEntrySkeleton() {
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/(journal)/journal/billing/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/card-skeleton"
2 | import { Header } from "@/components/header"
3 | import { Shell } from "@/components/shell"
4 |
5 | export default function JournalBillingLoading() {
6 | return (
7 |
8 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(journal)/journal/error.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Error components must be Client Components
4 | import { useEffect } from "react"
5 |
6 | export default function Error({
7 | error,
8 | reset,
9 | }: {
10 | error: Error & { digest?: string }
11 | reset: () => void
12 | }) {
13 | useEffect(() => {
14 | // Log the error to an error reporting service
15 | console.error(error)
16 | }, [error])
17 |
18 | return (
19 |
20 |
Something went wrong!
21 | reset()
25 | }
26 | >
27 | Try again
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/(journal)/journal/layout.tsx:
--------------------------------------------------------------------------------
1 | import { journalConfig } from "@/config/journal"
2 | import { getUserByClerkId } from "@/lib/auth"
3 | import { MainNav } from "@/components/main-nav"
4 | import { JournalNav } from "@/components/nav"
5 | import SiteFooter from "@/components/site-footer"
6 | // import { SiteFooter } from "@/components/site-footer"
7 | import { UserAccountNav } from "@/components/user-account-nav"
8 |
9 | interface JournalLayoutProps {
10 | children?: React.ReactNode
11 | }
12 |
13 | export default async function JournalLayout({ children }: JournalLayoutProps) {
14 | const user = await getUserByClerkId()
15 |
16 | return (
17 |
18 |
30 |
31 |
36 |
37 | {children}
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/(journal)/journal/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Header } from "@/components/header"
2 | import { Shell } from "@/components/shell"
3 |
4 | import { JournalEntryItem } from "../_components/journal-entry"
5 | import JournalEntryCreateButton from "../_components/journal-entry-create-button"
6 |
7 | export default function JournalLoading() {
8 | return (
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/(journal)/journal/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | getMyBookmarkedJournalEntries,
3 | getMyJournalEntries,
4 | } from "@/server/actions/journal"
5 | import {
6 | dehydrate,
7 | HydrationBoundary,
8 | QueryClient,
9 | } from "@tanstack/react-query"
10 |
11 | import { Header } from "@/components/header"
12 | import { Shell } from "@/components/shell"
13 |
14 | import EntryList from "../_components/entry-list"
15 | import JournalEntryCreateButton from "../_components/journal-entry-create-button"
16 |
17 | export const metadata = {
18 | title: "Journal",
19 | }
20 |
21 | export default async function JournalPage() {
22 | const queryClient = new QueryClient()
23 |
24 | await queryClient.prefetchQuery({
25 | queryKey: ["entries"],
26 | queryFn: getMyJournalEntries,
27 | })
28 |
29 | await queryClient.prefetchQuery({
30 | queryKey: ["bookmarkedEntries"],
31 | queryFn: getMyBookmarkedJournalEntries,
32 | })
33 |
34 | return (
35 |
36 |
44 |
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/(journal)/journal/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/card-skeleton"
2 | import { Header } from "@/components/header"
3 | import { Shell } from "@/components/shell"
4 |
5 | export default function JournalBillingLoading() {
6 | return (
7 |
8 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(journal)/journal/settings/user-profile/[[...user-profile]]/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next"
2 | import { UserProfile } from "@clerk/nextjs"
3 |
4 | import { env } from "@/env.mjs"
5 | import { Header } from "@/components/header"
6 | import { Shell } from "@/components/shell"
7 |
8 | export const metadata: Metadata = {
9 | metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
10 | title: "Account",
11 | description: "Manage your account settings",
12 | }
13 |
14 | const UserProfilePage = () => (
15 |
16 |
17 |
18 |
34 |
35 |
36 | )
37 |
38 | export default UserProfilePage
39 |
--------------------------------------------------------------------------------
/src/app/(legal)/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next"
2 | import { notFound } from "next/navigation"
3 | import { allPages } from "contentlayer/generated"
4 |
5 | import { env } from "@/env.mjs"
6 | import { siteConfig } from "@/config/site"
7 | import { absoluteUrl } from "@/lib/utils"
8 | import { Mdx } from "@/components/mdx-components"
9 |
10 | interface PageProps {
11 | params: {
12 | slug: string[]
13 | }
14 | }
15 |
16 | async function getPageFromParams(params: any) {
17 | const slug = params?.slug?.join("/")
18 | const page = allPages.find((page) => page.slugAsParams === slug)
19 |
20 | if (!page) {
21 | null
22 | }
23 |
24 | return page
25 | }
26 |
27 | export async function generateMetadata({
28 | params,
29 | }: PageProps): Promise {
30 | const page = await getPageFromParams(params)
31 |
32 | if (!page) {
33 | return {}
34 | }
35 |
36 | const url = env.NEXT_PUBLIC_APP_URL
37 |
38 | const ogUrl = new URL(`${url}/api/og`)
39 | ogUrl.searchParams.set("heading", page.title)
40 | ogUrl.searchParams.set("type", siteConfig.name)
41 | ogUrl.searchParams.set("mode", "light")
42 |
43 | return {
44 | title: page.title,
45 | description: page.description,
46 | openGraph: {
47 | title: page.title,
48 | description: page.description,
49 | type: "article",
50 | url: absoluteUrl(page.slug),
51 | images: [
52 | {
53 | url: ogUrl.toString(),
54 | width: 1200,
55 | height: 630,
56 | alt: page.title,
57 | },
58 | ],
59 | },
60 | twitter: {
61 | card: "summary_large_image",
62 | title: page.title,
63 | description: page.description,
64 | images: [ogUrl.toString()],
65 | },
66 | }
67 | }
68 |
69 | // export async function generateStaticParams(): Promise {
70 | // return allPages.map((page) => ({
71 | // slug: page.slugAsParams.split("/"),
72 | // }));
73 | // }
74 |
75 | export default async function PagePage({ params }: PageProps) {
76 | const page = await getPageFromParams(params)
77 |
78 | if (!page) {
79 | notFound()
80 | }
81 |
82 | return (
83 |
84 |
85 |
86 | {page.title}
87 |
88 | {page.description && (
89 |
{page.description}
90 | )}
91 |
92 |
93 |
94 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/src/app/(legal)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react"
2 | import Link from "next/link"
3 |
4 | import { marketingConfig } from "@/config/marketing"
5 | import { getUserByClerkId } from "@/lib/auth"
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 | import { MainNav } from "@/components/main-nav"
9 | import SiteFooter from "@/components/site-footer"
10 |
11 | interface LegalLayoutProps {
12 | children: React.ReactNode
13 | }
14 |
15 | export default async function LegalLayout({ children }: LegalLayoutProps) {
16 | const user = await getUserByClerkId()
17 |
18 | return (
19 |
20 |
49 |
{children}
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/background.tsx:
--------------------------------------------------------------------------------
1 | export default function Background() {
2 | return (
3 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/features/featuresSection.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { FC, useEffect } from "react"
4 | import { stagger, useAnimate, useInView } from "framer-motion"
5 |
6 | import Features from "./features"
7 |
8 | const FeaturesSection: FC = () => {
9 | const [scope, animate] = useAnimate()
10 | const isInView = useInView(scope, { once: true })
11 |
12 | useEffect(() => {
13 | if (isInView) {
14 | animate(
15 | "#reveal-anim",
16 | { opacity: [0, 1], y: [20, 0] },
17 | { duration: 0.5, ease: "easeIn", delay: stagger(0.3) }
18 | )
19 | }
20 | }, [animate, isInView])
21 |
22 | return (
23 |
24 |
28 | An app where you'll find a{" "}
29 |
30 | peace of mind
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default FeaturesSection
42 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/hero/heroImage.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Image from "next/image"
4 | import { useInView } from "react-intersection-observer"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { BorderBeam } from "@/components/border-beam"
8 |
9 | import heroDarkImage from "../../../../../public/images/hero-dark.webp"
10 |
11 | export const HeroImage = () => {
12 | const { ref, inView } = useInView({ threshold: 0.4, triggerOnce: true })
13 |
14 | return (
15 |
16 |
24 |
34 |
35 | {inView &&
}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/openSource/openSourceSection.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | /* eslint-disable tailwindcss/no-contradicting-classname */
4 | import { FC, useEffect } from "react"
5 | import { stagger, useAnimate, useInView } from "framer-motion"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 | import { Icons } from "@/components/icons"
10 |
11 | const OpenSourceSection: FC = () => {
12 | const [scope, animate] = useAnimate()
13 | const isInView = useInView(scope, { once: true })
14 |
15 | useEffect(() => {
16 | if (isInView) {
17 | animate(
18 | "#reveal-anim",
19 | { opacity: [0, 1], y: [20, 0] },
20 | { duration: 0.5, ease: "easeIn", delay: stagger(0.3) }
21 | )
22 | }
23 | }, [animate, isInView])
24 | return (
25 |
26 |
27 |
31 | Proudly open source
32 |
33 |
37 | Our source code is available on GitHub - feel free to read, review, or
38 | contribute to it however you want!
39 |
40 |
50 | Github
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default OpenSourceSection
58 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/pricing/pricingSection.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { FC, useEffect } from "react"
4 | import { stagger, useAnimate, useInView } from "framer-motion"
5 |
6 | import Pricing from "./pricing"
7 |
8 | const PricingSection: FC = () => {
9 | const [scope, animate] = useAnimate()
10 | const isInView = useInView(scope, { once: true })
11 |
12 | useEffect(() => {
13 | if (isInView) {
14 | animate(
15 | "#reveal-anim",
16 | { opacity: [0, 1], y: [20, 0] },
17 | { duration: 0.5, ease: "easeIn", delay: stagger(0.3) }
18 | )
19 | }
20 | }, [animate, isInView])
21 |
22 | return (
23 |
24 |
28 | Ready to get{" "}
29 |
30 | started?
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default PricingSection
39 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/testimonials/testimonials.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 | import FadeIn from "@/components/fade-in"
5 | import Marquee from "@/components/marquee"
6 | import TweetCard from "@/components/tweet-card"
7 |
8 | const tweets = [
9 | "https://x.com/zsumair/status/1687064952702652416",
10 | "https://x.com/sanjay_4O4/status/1688527071616884736",
11 | "https://x.com/codewithmarcin/status/1687341476647403520",
12 | "https://x.com/CodeHagen/status/1687211559553224706",
13 | "https://x.com/pratikk_tiwari/status/1687471534934220801",
14 | "https://x.com/hemantwasthere/status/1687162068448018432",
15 | "https://x.com/johnkat_Mj/status/1687216212193824769",
16 | "https://x.com/xrehpicx/status/1687325756660301824",
17 | "https://x.com/arrogantcoder/status/1687080522617438208",
18 | "https://x.com/j14wei/status/1687087602573492224",
19 | "https://x.com/jotagep_dev/status/1687063370585841665",
20 | ].map((t) => t.split("/").slice(-1)[0])
21 |
22 | interface TestimonialsProps {
23 | className?: string
24 | }
25 |
26 | const Testimonials: FC = ({ className }) => {
27 | const firstRow = tweets.slice(0, tweets.length / 2)
28 | const secondRow = tweets.slice(tweets.length / 2)
29 |
30 | return (
31 |
32 |
33 | {firstRow.map((id, idx) => (
34 |
35 | ))}
36 |
37 |
38 | {secondRow.map((id, idx) => (
39 |
40 | ))}
41 |
42 | {/*
*/}
43 | {/*
*/}
44 |
45 | )
46 | }
47 |
48 | export default Testimonials
49 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/testimonials/testimonialsSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | import FadeIn from "@/components/fade-in"
4 |
5 | import Testimonials from "./testimonials"
6 |
7 | const TestimonialsSection: FC = () => {
8 | return (
9 |
10 |
11 |
12 | Stories from the{" "}
13 |
14 | heart
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default TestimonialsSection
25 |
--------------------------------------------------------------------------------
/src/app/(marketing)/_components/wobble-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { useState } from "react"
4 | import { motion } from "framer-motion"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | export const WobbleCard = ({
9 | children,
10 | containerClassName,
11 | className,
12 | id,
13 | }: {
14 | children: React.ReactNode
15 | containerClassName?: string
16 | className?: string
17 | id?: string
18 | }) => {
19 | const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
20 | const [isHovering, setIsHovering] = useState(false)
21 |
22 | const handleMouseMove = (event: React.MouseEvent) => {
23 | const { clientX, clientY } = event
24 | const rect = event.currentTarget.getBoundingClientRect()
25 | const x = (clientX - (rect.left + rect.width / 2)) / 20
26 | const y = (clientY - (rect.top + rect.height / 2)) / 20
27 | setMousePosition({ x, y })
28 | }
29 | return (
30 | setIsHovering(true)}
34 | onMouseLeave={() => {
35 | setIsHovering(false)
36 | setMousePosition({ x: 0, y: 0 })
37 | }}
38 | style={{
39 | transform: isHovering
40 | ? `translate3d(${mousePosition.x}px, ${mousePosition.y}px, 0) scale3d(1, 1, 1)`
41 | : "translate3d(0px, 0px, 0) scale3d(1, 1, 1)",
42 | transition: "transform 0.1s ease-out",
43 | }}
44 | className={cn(
45 | "relative mx-auto w-full overflow-hidden rounded-2xl bg-hero-gradient",
46 | containerClassName
47 | )}
48 | >
49 |
56 |
65 |
66 | {children}
67 |
68 |
69 |
70 | )
71 | }
72 |
73 | const Noise = () => {
74 | return (
75 |
82 | )
83 | }
84 |
--------------------------------------------------------------------------------
/src/app/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { marketingConfig } from "@/config/marketing"
4 | import { getUserByClerkId } from "@/lib/auth"
5 | import { cn } from "@/lib/utils"
6 | import { buttonVariants } from "@/components/ui/button"
7 | import { MainNav } from "@/components/main-nav"
8 | import SiteFooter from "@/components/site-footer"
9 |
10 | import Background from "./_components/background"
11 |
12 | interface MarketingLayoutProps {
13 | children: React.ReactNode
14 | }
15 |
16 | export default async function MarketingLayout({
17 | children,
18 | }: MarketingLayoutProps) {
19 | const user = await getUserByClerkId()
20 |
21 | return (
22 |
23 |
39 |
{children}
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/(marketing)/page.tsx:
--------------------------------------------------------------------------------
1 | import FeaturesSection from "./_components/features/featuresSection"
2 | import HeroSection from "./_components/hero/heroSection"
3 | import OpenSourceSection from "./_components/openSource/openSourceSection"
4 | import PricingSection from "./_components/pricing/pricingSection"
5 | import TestimonialsSection from "./_components/testimonials/testimonialsSection"
6 |
7 | export default function Home() {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
15 | >
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/api/cron/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server"
2 | import { type ErrorResponse } from "resend"
3 |
4 | import { env } from "@/env.mjs"
5 | import { db } from "@/lib/db"
6 | import { resend } from "@/lib/resend"
7 | import ReminderEmail from "@/components/emails/reminder-email"
8 |
9 | export async function GET(req: NextRequest) {
10 | try {
11 | const users = await db.user.findMany({
12 | where: {
13 | reminder: {
14 | active: true,
15 | },
16 | },
17 | })
18 |
19 | const subject =
20 | "📝 Daily Journal Reminder - Stay Committed to Your Journey! 🌟"
21 |
22 | // TODO: Send reminder based on frequency
23 |
24 | for (const user of users) {
25 | await resend.emails.send({
26 | from: env.EMAIL_FROM_ADDRESS,
27 | to: user.email,
28 | subject,
29 | react: ReminderEmail({
30 | firstName: user.name.split(" ")[0],
31 | fromEmail: env.EMAIL_FROM_ADDRESS,
32 | }),
33 | })
34 | }
35 | return NextResponse.json("Successfully sent", { status: 200 })
36 | } catch (error) {
37 | console.error(error)
38 |
39 | const resendError = error as ErrorResponse
40 |
41 | if (resendError?.error?.message) {
42 | return NextResponse.json(resendError.error.message, { status: 429 })
43 | }
44 |
45 | if (error instanceof Error) {
46 | return NextResponse.json(error.message, { status: 500 })
47 | }
48 |
49 | return NextResponse.json("Something went wrong", { status: 500 })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/api/sentry-example-api/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server"
2 |
3 | export const dynamic = "force-dynamic"
4 |
5 | // A faulty API route to test Sentry's error monitoring
6 | export function GET() {
7 | throw new Error("Sentry Example API Route Error")
8 | return NextResponse.json({ data: "Testing Sentry Error..." })
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/api/uploadthing/core.ts:
--------------------------------------------------------------------------------
1 | import { createUploadthing, type FileRouter } from "uploadthing/next"
2 |
3 | import { getUserByClerkId } from "@/lib/auth"
4 |
5 | const f = createUploadthing()
6 |
7 | export const ourFileRouter = {
8 | imageUploader: f({ image: { maxFileSize: "1MB" } })
9 | .middleware(async (req) => {
10 | const user = await getUserByClerkId()
11 |
12 | if (!user) throw new Error("Unauthorized")
13 |
14 | return { userId: user.id }
15 | })
16 | .onUploadComplete(async ({ metadata, file }) => {}),
17 | } satisfies FileRouter
18 |
19 | export type OurFileRouter = typeof ourFileRouter
20 |
--------------------------------------------------------------------------------
/src/app/api/uploadthing/route.ts:
--------------------------------------------------------------------------------
1 | import { createNextRouteHandler } from "uploadthing/next"
2 |
3 | import { ourFileRouter } from "./core"
4 |
5 | // Export routes for Next App Router
6 | export const { GET, POST } = createNextRouteHandler({
7 | router: ourFileRouter,
8 | })
9 |
--------------------------------------------------------------------------------
/src/app/api/webhooks/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers"
2 | import Stripe from "stripe"
3 |
4 | import { env } from "@/env.mjs"
5 | import { db } from "@/lib/db"
6 | import { stripe } from "@/lib/stripe"
7 |
8 | export async function POST(req: Request) {
9 | const body = await req.text()
10 | const signature = headers().get("Stripe-Signature") as string
11 |
12 | let event: Stripe.Event
13 |
14 | try {
15 | event = stripe.webhooks.constructEvent(
16 | body,
17 | signature,
18 | env.STRIPE_WEBHOOK_SECRET
19 | )
20 | } catch (error) {
21 | return new Response(`Webhook Error: ${error.message}`, { status: 400 })
22 | }
23 |
24 | const session = event.data.object as Stripe.Checkout.Session
25 |
26 | if (event.type === "checkout.session.completed") {
27 | // Retrieve the subscription details from Stripe.
28 | const subscription = await stripe.subscriptions.retrieve(
29 | session.subscription as string
30 | )
31 |
32 | // Update the user stripe into in our database.
33 | // Since this is the initial subscription, we need to update
34 | // the subscription id and customer id.
35 | await db.user.update({
36 | where: {
37 | id: session?.metadata?.userId,
38 | },
39 | data: {
40 | stripeSubscriptionId: subscription.id,
41 | stripeCustomerId: subscription.customer as string,
42 | stripePriceId: subscription.items.data[0].price.id,
43 | stripeCurrentPeriodEnd: new Date(
44 | subscription.current_period_end * 1000
45 | ),
46 | },
47 | })
48 | }
49 |
50 | if (event.type === "invoice.payment_succeeded") {
51 | // Retrieve the subscription details from Stripe.
52 | const subscription = await stripe.subscriptions.retrieve(
53 | session.subscription as string
54 | )
55 |
56 | // Update the price id and set the new period end.
57 | await db.user.update({
58 | where: {
59 | stripeSubscriptionId: subscription.id,
60 | },
61 | data: {
62 | stripePriceId: subscription.items.data[0].price.id,
63 | stripeCurrentPeriodEnd: new Date(
64 | subscription.current_period_end * 1000
65 | ),
66 | },
67 | })
68 | }
69 |
70 | return new Response(null, { status: 200 })
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect } from "react"
4 | import Error from "next/error"
5 | import * as Sentry from "@sentry/nextjs"
6 |
7 | export default function GlobalError(props: { error: unknown }) {
8 | useEffect(() => {
9 | Sentry.captureException(props.error)
10 | }, [props.error])
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css"
2 |
3 | import { Inter as FontSans } from "next/font/google"
4 | import localFont from "next/font/local"
5 | import { ClerkProvider } from "@clerk/nextjs"
6 |
7 | import { siteConfig } from "@/config/site"
8 | import { cn } from "@/lib/utils"
9 | import { Toaster } from "@/components/ui/toaster"
10 | import { Analytics } from "@/components/analytics"
11 | import { CSPostHogProvider } from "@/components/analytics-provider"
12 | import { Providers } from "@/components/providers"
13 | import { TailwindIndicator } from "@/components/tailwind-indicator"
14 |
15 | const fontSans = FontSans({
16 | subsets: ["latin"],
17 | variable: "--font-sans",
18 | })
19 |
20 | const fontHeading = localFont({
21 | src: "../../assets/fonts/Satoshi-Variable.woff2",
22 | variable: "--font-heading",
23 | weight: "700",
24 | display: "swap",
25 | style: "normal",
26 | })
27 |
28 | export const metadata = {
29 | title: {
30 | default: siteConfig.name,
31 | template: `%s | ${siteConfig.name}`,
32 | },
33 | description: siteConfig.description,
34 | keywords: [
35 | "Next.js",
36 | "React",
37 | "Tailwind CSS",
38 | "Server Components",
39 | "Radix UI",
40 | "Journal App",
41 | ],
42 | authors: [
43 | {
44 | name: "Subham Bharadwaz",
45 | url: "https://github.com/subhamBharadwaz",
46 | },
47 | ],
48 | creator: "Subham Bharadwaz",
49 | themeColor: [
50 | { media: "(prefers-color-scheme: light)", color: "white" },
51 | { media: "(prefers-color-scheme: dark)", color: "black" },
52 | ],
53 | openGraph: {
54 | type: "website",
55 | locale: "en_US",
56 | url: siteConfig.url,
57 | title: siteConfig.name,
58 | description: siteConfig.description,
59 | siteName: siteConfig.name,
60 | },
61 | twitter: {
62 | card: "summary_large_image",
63 | title: siteConfig.name,
64 | description: siteConfig.description,
65 | images: [`${siteConfig.url}/og.jpg`],
66 | creator: "@subh4mBharadwaz",
67 | },
68 | icons: {
69 | icon: "/favicon.ico",
70 | shortcut: "/favicon-16x16.png",
71 | apple: "/apple-touch-icon.png",
72 | },
73 | manifest: `${siteConfig.url}/site.webmanifest`,
74 | }
75 |
76 | export default function RootLayout({
77 | children,
78 | }: {
79 | children: React.ReactNode
80 | }) {
81 | return (
82 |
83 |
84 |
85 |
92 |
93 | {children}
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | )
103 | }
104 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorCard } from "@/components/error-card"
2 | import { Shell } from "@/components/shell"
3 |
4 | export default function PageNotFound() {
5 | return (
6 |
7 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/opengraph-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subhamBharadwaz/scribbly/3c3a0a21072bbc1382af729eef334e620400b351/src/app/opengraph-image.jpg
--------------------------------------------------------------------------------
/src/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next"
2 |
3 | export default function robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: "*",
7 | allow: "/",
8 | },
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next"
2 |
3 | export default function sitemap(): MetadataRoute.Sitemap {
4 | const baseUrl = "https://scribbly.subhambharadwaz.com"
5 | return [
6 | {
7 | url: baseUrl,
8 | lastModified: new Date(),
9 | },
10 | {
11 | url: `${baseUrl}/terms`,
12 | lastModified: new Date(),
13 | },
14 | {
15 | url: `${baseUrl}/privacy`,
16 | lastModified: new Date(),
17 | },
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/analytics-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect } from "react"
4 | import { useAuth, useUser } from "@clerk/nextjs"
5 | import posthog from "posthog-js"
6 | import { PostHogProvider } from "posthog-js/react"
7 |
8 | import { env } from "@/env.mjs"
9 |
10 | if (typeof window !== "undefined") {
11 | posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
12 | api_host: "/ingest",
13 | ui_host: "https://us.i.posthog.com",
14 | })
15 | }
16 | export function CSPostHogProvider({ children }) {
17 | return (
18 |
19 | {children}
20 |
21 | )
22 | }
23 |
24 | function PostHogAuthWrapper({ children }: { children: React.ReactNode }) {
25 | const auth = useAuth()
26 | const userInfo = useUser()
27 |
28 | useEffect(() => {
29 | if (userInfo.user) {
30 | posthog.identify(userInfo.user.id, {
31 | email: userInfo.user.emailAddresses[0].emailAddress,
32 | name: userInfo.user.fullName,
33 | })
34 | } else if (!auth.isSignedIn) {
35 | posthog.reset()
36 | }
37 | }, [auth, userInfo])
38 | return children
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react"
4 |
5 | export function Analytics() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/animated-gradient-text.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export default function AnimatedGradientText({
6 | children,
7 | className,
8 | }: {
9 | children: ReactNode
10 | className?: string
11 | }) {
12 | return (
13 |
19 |
22 |
23 | {children}
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/border-beam.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable tailwindcss/no-contradicting-classname */
2 | import { cn } from "@/lib/utils"
3 |
4 | interface BorderBeamProps {
5 | className?: string
6 | size?: number
7 | duration?: number
8 | borderWidth?: number
9 | anchor?: number
10 | colorFrom?: string
11 | colorTo?: string
12 | delay?: number
13 | }
14 |
15 | export const BorderBeam = ({
16 | className,
17 | size = 200,
18 | duration = 15,
19 | anchor = 90,
20 | borderWidth = 1.5,
21 | colorFrom = "#ff99d7",
22 | colorTo = "#9c40ff",
23 | delay = 0,
24 | }: BorderBeamProps) => {
25 | return (
26 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/callout.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | interface CalloutProps {
4 | icon?: string;
5 | children?: React.ReactNode;
6 | type?: "default" | "warning" | "danger";
7 | }
8 |
9 | export function Callout({
10 | children,
11 | icon,
12 | type = "default",
13 | ...props
14 | }: CalloutProps) {
15 | return (
16 |
23 | {icon &&
{icon} }
24 |
{children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/card-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
2 | import { Skeleton } from "@/components/ui/skeleton"
3 |
4 | export function CardSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/copy-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { CheckIcon, CopyIcon } from "@radix-ui/react-icons"
5 |
6 | import { Event, trackEvent } from "@/lib/events"
7 | import { cn } from "@/lib/utils"
8 | import { Button } from "@/components/ui/button"
9 |
10 | interface CopyButtonProps extends React.HTMLAttributes {
11 | value: string
12 | src?: string
13 | event?: Event["name"]
14 | }
15 |
16 | async function copyToClipboardWithMeta(value: string, event?: Event) {
17 | navigator.clipboard.writeText(value)
18 | if (event) {
19 | trackEvent(event)
20 | }
21 | }
22 |
23 | export function CopyButton({
24 | value,
25 | className,
26 | src,
27 | event,
28 | ...props
29 | }: CopyButtonProps) {
30 | const [hasCopied, setHasCopied] = React.useState(false)
31 |
32 | React.useEffect(() => {
33 | setTimeout(() => {
34 | setHasCopied(false)
35 | }, 2000)
36 | }, [hasCopied])
37 |
38 | return (
39 | {
47 | copyToClipboardWithMeta(
48 | value,
49 | event
50 | ? {
51 | name: event,
52 | properties: {
53 | code: value,
54 | },
55 | }
56 | : undefined
57 | )
58 | setHasCopied(true)
59 | }}
60 | {...props}
61 | >
62 | Copy
63 | {hasCopied ? (
64 |
65 | ) : (
66 |
67 | )}
68 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/empty-placeholder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { Icons } from "@/components/icons"
5 |
6 | interface EmptyPlaceholderProps extends React.HTMLAttributes {}
7 |
8 | export function EmptyPlaceholder({
9 | className,
10 | children,
11 | ...props
12 | }: EmptyPlaceholderProps) {
13 | return (
14 |
21 |
22 | {children}
23 |
24 |
25 | )
26 | }
27 |
28 | interface EmptyPlaceholderIconProps
29 | extends Partial> {
30 | name: keyof typeof Icons
31 | }
32 |
33 | EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({
34 | name,
35 | className,
36 | ...props
37 | }: EmptyPlaceholderIconProps) {
38 | const Icon = Icons[name]
39 |
40 | if (!Icon) {
41 | return null
42 | }
43 |
44 | return (
45 |
46 | {/* @ts-ignore */}
47 |
48 |
49 | )
50 | }
51 |
52 | interface EmptyPlacholderTitleProps
53 | extends React.HTMLAttributes {}
54 |
55 | EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
56 | className,
57 | ...props
58 | }: EmptyPlacholderTitleProps) {
59 | return (
60 |
61 | )
62 | }
63 |
64 | interface EmptyPlacholderDescriptionProps
65 | extends React.HTMLAttributes {}
66 |
67 | EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
68 | className,
69 | ...props
70 | }: EmptyPlacholderDescriptionProps) {
71 | return (
72 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/error-card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import Link from "next/link"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardFooter,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card"
14 | import { Icons } from "@/components/icons"
15 |
16 | interface ErrorCardProps extends React.ComponentPropsWithoutRef {
17 | icon?: keyof typeof Icons
18 | title: string
19 | description: string
20 | retryLink?: string
21 | retryLinkText?: string
22 | }
23 |
24 | export function ErrorCard({
25 | icon,
26 | title,
27 | description,
28 | retryLink,
29 | retryLinkText = "Go back",
30 | className,
31 | ...props
32 | }: ErrorCardProps) {
33 | const Icon = Icons[icon ?? "warning"]
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {title}
44 | {description}
45 |
46 | {retryLink ? (
47 |
48 |
49 |
56 | {retryLinkText}
57 | {retryLinkText}
58 |
59 |
60 |
61 | ) : null}
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/fade-in.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useRef } from "react"
4 | import { motion, useInView } from "framer-motion"
5 |
6 | export default function FadeIn({
7 | children = null,
8 | className,
9 | noVertical,
10 | delay,
11 | viewTriggerOffset,
12 | }: {
13 | children?: React.ReactNode
14 | className?: string
15 | noVertical?: boolean
16 | delay?: number
17 | viewTriggerOffset?: boolean
18 | }) {
19 | const ref = useRef(null)
20 | const inView = useInView(ref, {
21 | once: true,
22 | margin: viewTriggerOffset ? "-128px" : "0px",
23 | })
24 |
25 | const fadeUpVariants = {
26 | initial: {
27 | opacity: 0,
28 | y: noVertical ? 0 : 24,
29 | },
30 | animate: {
31 | opacity: 1,
32 | y: 0,
33 | },
34 | }
35 |
36 | if (!children) {
37 | return null
38 | }
39 |
40 | return (
41 |
54 | {children}
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface HeaderProps extends React.HTMLAttributes {
4 | title: string
5 | description?: string | null
6 | size?: "default" | "sm"
7 | children?: React.ReactNode
8 | }
9 | export function Header({
10 | title,
11 | description,
12 | size = "default",
13 | className,
14 | children,
15 | ...props
16 | }: HeaderProps) {
17 | return (
18 |
19 |
20 |
26 | {title}
27 |
28 | {description ? (
29 |
35 | {description}
36 |
37 | ) : null}
38 |
39 | {children}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/lottie-anim.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { FC } from "react"
4 | import { useLottie } from "lottie-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | import girlWritingAnim from "../../public/lottie/GIRL STUDYING ON LAPTOP.json"
9 |
10 | interface LottieAnimProps {
11 | className?: string
12 | }
13 |
14 | const LottieAnim: FC = ({ className }) => {
15 | const options = {
16 | loop: true,
17 | autoplay: true,
18 | animationData: girlWritingAnim,
19 | }
20 |
21 | const { View } = useLottie(options)
22 | return {View}
23 | }
24 |
25 | export default LottieAnim
26 |
--------------------------------------------------------------------------------
/src/components/main-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import Link from "next/link"
5 | import { useSelectedLayoutSegment } from "next/navigation"
6 | import { MainNavItem } from "@/types"
7 |
8 | import { siteConfig } from "@/config/site"
9 | import { cn } from "@/lib/utils"
10 | import { Icons } from "@/components/icons"
11 | import { MobileNav } from "@/components/mobile-nav"
12 |
13 | interface MainNavProps {
14 | items?: MainNavItem[]
15 | children?: React.ReactNode
16 | }
17 |
18 | export function MainNav({ items, children }: MainNavProps) {
19 | const segment = useSelectedLayoutSegment()
20 |
21 | const [showMobileMenu, setShowMobileMenu] = React.useState(false)
22 |
23 | return (
24 |
25 |
26 |
27 | {siteConfig.name}
28 |
29 |
30 |
31 | {items?.map((item, index) => (
32 |
43 | {item.title}
44 |
45 | ))}
46 |
47 |
48 | setShowMobileMenu(!showMobileMenu)}
51 | >
52 | {showMobileMenu ? : }
53 |
54 | {showMobileMenu && items && (
55 | {children}
56 | )}
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/marquee.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface MarqueeProps {
4 | className?: string
5 | reverse?: boolean
6 | pauseOnHover?: boolean
7 | children?: React.ReactNode
8 | vertical?: boolean
9 | repeat?: number
10 | [key: string]: any
11 | }
12 |
13 | export default function Marquee({
14 | className,
15 | reverse,
16 | pauseOnHover = false,
17 | children,
18 | vertical = false,
19 | repeat = 4,
20 | ...props
21 | }: MarqueeProps) {
22 | return (
23 |
34 | {Array(repeat)
35 | .fill(0)
36 | .map((_, i) => (
37 |
46 | {children}
47 |
48 | ))}
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/mdx-card.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | interface CardProps extends React.HTMLAttributes {
6 | href?: string
7 | disabled?: boolean
8 | }
9 |
10 | export function MdxCard({
11 | href,
12 | className,
13 | children,
14 | disabled,
15 | ...props
16 | }: CardProps) {
17 | return (
18 |
26 |
27 |
28 | {children}
29 |
30 |
31 | {href && (
32 |
33 |
View
34 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import Link from "next/link"
3 | import { MainNavItem } from "@/types"
4 |
5 | import { siteConfig } from "@/config/site"
6 | import { cn } from "@/lib/utils"
7 | import { useLockBody } from "@/hooks/use-lock-body"
8 | import { Icons } from "@/components/icons"
9 |
10 | interface MobileNavProps {
11 | items: MainNavItem[]
12 | children?: React.ReactNode
13 | }
14 |
15 | export function MobileNav({ items, children }: MobileNavProps) {
16 | useLockBody()
17 |
18 | return (
19 |
24 |
25 |
26 |
27 | {siteConfig.name}
28 |
29 |
30 | {items.map((item, index) => (
31 |
39 | {item.title}
40 |
41 | ))}
42 |
43 | {children}
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { usePathname } from "next/navigation"
5 | import { SidebarNavItem } from "@/types"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { Icons } from "@/components/icons"
9 |
10 | interface JournalNavProps {
11 | items: SidebarNavItem[]
12 | }
13 |
14 | export function JournalNav({ items }: JournalNavProps) {
15 | const path = usePathname()
16 |
17 | if (!items?.length) {
18 | return null
19 | }
20 |
21 | return (
22 |
23 | {items.map((item, index) => {
24 | const Icon = Icons[item.icon || "arrowRight"]
25 | return (
26 | item.href && (
27 |
28 |
35 |
36 | {item.title}
37 |
38 |
39 | )
40 | )
41 | })}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { ReactNode, useState } from "react"
4 | import { usePathname } from "next/navigation"
5 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
6 |
7 | import { ThemeProvider } from "@/components/theme-provider"
8 |
9 | import { TooltipProvider } from "./ui/tooltip"
10 |
11 | interface ProviderProps {
12 | children: ReactNode
13 | }
14 |
15 | function makeQueryClient() {
16 | return new QueryClient({
17 | defaultOptions: {
18 | queries: {
19 | // With SSR, we usually want to set some default staleTime
20 | // above 0 to avoid refetching immediately on the client
21 | staleTime: 60 * 1000,
22 | },
23 | },
24 | })
25 | }
26 |
27 | let browserQueryClient: QueryClient | undefined = undefined
28 |
29 | function getQueryClient() {
30 | if (typeof window === "undefined") {
31 | // Server: always make a new query client
32 | return makeQueryClient()
33 | } else {
34 | // Browser: make a new query client if we don't already have one
35 | // This is very important, so we don't re-make a new client if React
36 | // suspends during the initial render. This may not be needed if we
37 | // have a suspense boundary BELOW the creation of the query client
38 | if (!browserQueryClient) browserQueryClient = makeQueryClient()
39 | return browserQueryClient
40 | }
41 | }
42 |
43 | export function Providers({ children }: ProviderProps) {
44 | const pathname = usePathname()
45 | const queryClient = getQueryClient()
46 | return (
47 |
53 |
54 | {children}
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/shell.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const shellVariants = cva("grid items-center gap-8 pb-8 pt-6 md:py-8", {
7 | variants: {
8 | variant: {
9 | default: "container",
10 | sidebar: "",
11 | centered: "mx-auto mb-16 mt-20 max-w-md justify-center",
12 | markdown: "container max-w-3xl gap-0 py-8 md:py-10 lg:py-10",
13 | },
14 | },
15 | defaultVariants: {
16 | variant: "default",
17 | },
18 | })
19 |
20 | interface ShellProps
21 | extends React.HTMLAttributes,
22 | VariantProps {
23 | as?: React.ElementType
24 | }
25 |
26 | function Shell({
27 | as: Comp = "section",
28 | className,
29 | variant,
30 | ...props
31 | }: ShellProps) {
32 | return (
33 |
34 | )
35 | }
36 |
37 | export { Shell, shellVariants }
38 |
--------------------------------------------------------------------------------
/src/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | import { env } from "@/env.mjs"
2 |
3 | export function TailwindIndicator() {
4 | if (env.NODE_ENV === "production") return null
5 |
6 | return (
7 |
8 |
xs
9 |
10 | sm
11 |
12 |
md
13 |
lg
14 |
xl
15 |
2xl
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 | import { ThemeProviderProps } from "next-themes/dist/types"
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children}
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | cta: [
21 | "bg-primary-gradient rounded-full hover:bg-primary-gradient-2 hover:shadow-[0px_4px_30px] hover:shadow-[rgb(247_176_253_/_50%)] transition-shadow duration-300",
22 | "[&_.highlight]:ml-2",
23 | ],
24 | ghost: "hover:bg-accent hover:text-accent-foreground",
25 | link: "text-primary underline-offset-4 hover:underline",
26 | },
27 | size: {
28 | default: "h-9 px-4 py-2",
29 | sm: "h-8 rounded-md px-3 text-xs",
30 | lg: "h-10 rounded-md px-8",
31 | icon: "h-9 w-9",
32 | },
33 | },
34 | defaultVariants: {
35 | variant: "default",
36 | size: "default",
37 | },
38 | }
39 | )
40 |
41 | export interface ButtonProps
42 | extends React.ButtonHTMLAttributes,
43 | VariantProps {
44 | asChild?: boolean
45 | }
46 |
47 | const Button = React.forwardRef(
48 | ({ className, variant, size, asChild = false, ...props }, ref) => {
49 | const Comp = asChild ? Slot : "button"
50 | return (
51 |
56 | )
57 | }
58 | )
59 | Button.displayName = "Button"
60 |
61 | export { Button, buttonVariants }
62 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { CheckIcon } from "@radix-ui/react-icons"
5 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/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 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/components/user-account-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { SignOutButton } from "@clerk/nextjs"
5 |
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuSeparator,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu"
13 | import { UserAvatar } from "@/components/user-avatar"
14 |
15 | import { Icons } from "./icons"
16 | import { buttonVariants } from "./ui/button"
17 |
18 | interface UserAccountNavProps extends React.HTMLAttributes {
19 | user: {
20 | name: string
21 | email: string
22 | image: string
23 | }
24 | }
25 |
26 | export function UserAccountNav({ user }: UserAccountNavProps) {
27 | return (
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 | {user.name &&
{user.name}
}
39 | {user.email && (
40 |
41 | {user.email}
42 |
43 | )}
44 |
45 |
46 |
47 |
48 |
52 |
53 | Account
54 |
55 |
56 |
57 |
58 |
59 | Journal
60 |
61 |
62 |
63 |
64 |
65 | Settings
66 |
67 |
68 |
69 |
70 |
71 |
72 | Sign Out
73 |
74 |
75 |
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client";
2 | import { AvatarProps } from "@radix-ui/react-avatar";
3 |
4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5 | import { Icons } from "@/components/icons";
6 |
7 | interface UserAvatarProps extends AvatarProps {
8 | user: Pick;
9 | }
10 |
11 | export function UserAvatar({ user, ...props }: UserAvatarProps) {
12 | return (
13 |
14 | {user.image ? (
15 |
16 | ) : (
17 |
18 | {user.name}
19 |
20 |
21 | )}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/config/journal.ts:
--------------------------------------------------------------------------------
1 | import { JournalConfig } from "@/types"
2 |
3 | export const journalConfig: JournalConfig = {
4 | mainNav: [
5 | {
6 | title: "Entries",
7 | href: "/journal",
8 | },
9 | {
10 | title: "Billing",
11 | href: "/journal/billing",
12 | },
13 | {
14 | title: "Settings",
15 | href: "/journal/settings",
16 | },
17 | ],
18 | sidebarNav: [
19 | {
20 | title: "Entries",
21 | href: "/journal",
22 | icon: "post",
23 | },
24 | {
25 | title: "Billing",
26 | href: "/journal/billing",
27 | icon: "billing",
28 | },
29 | {
30 | title: "Settings",
31 | href: "/journal/settings",
32 | icon: "settings",
33 | },
34 | ],
35 | }
36 |
--------------------------------------------------------------------------------
/src/config/marketing.ts:
--------------------------------------------------------------------------------
1 | import { MarketingConfig } from "@/types"
2 |
3 | export const marketingConfig: MarketingConfig = {
4 | mainNav: [
5 | {
6 | title: "Features",
7 | href: "/#features",
8 | },
9 | {
10 | title: "Pricing",
11 | href: "/#pricing",
12 | },
13 | {
14 | title: "Journal",
15 | href: "/journal",
16 | },
17 | ],
18 | }
19 |
--------------------------------------------------------------------------------
/src/config/site.ts:
--------------------------------------------------------------------------------
1 | import { SiteConfig } from "@/types"
2 |
3 | export const siteConfig: SiteConfig = {
4 | name: "Scribbly",
5 | description:
6 | "Scribbly is a web application that provides a platform for users to create and manage their digital journal. With Scribbly, users can easily jot down their thoughts, experiences, and ideas, and organize them in a personal and customizable journal.",
7 | url: "https://scribbly.subhambharadwaz.com",
8 | ogImage: "https://scribbly.subhambharadwaz.com/og.jpg",
9 | links: {
10 | twitter: "https://twitter.com/webhashira",
11 | github: "https://github.com/subhamBharadwaz",
12 | linkedin: "https://www.linkedin.com/in/subham-bharadwaz-5a9792197/",
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/src/config/subscriptions.ts:
--------------------------------------------------------------------------------
1 | import { SubscriptionPlan } from "@/types"
2 |
3 | import { env } from "@/env.mjs"
4 |
5 | export const freePlan: SubscriptionPlan = {
6 | name: "Free",
7 | description:
8 | "The free plan is limited to 3 entries. Upgrade to the PRO plan for unlimited entries.",
9 | stripePriceId: "",
10 | }
11 |
12 | export const proPlan: SubscriptionPlan = {
13 | name: "PRO",
14 | description: "The PRO plan has unlimited entries.",
15 | stripePriceId: env.STRIPE_PRO_MONTHLY_PLAN_ID || "",
16 | }
17 |
--------------------------------------------------------------------------------
/src/content/pages/terms.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Terms & Conditions
3 | description: Read our terms and conditions.
4 | ---
5 |
6 | Effective Date: July 20, 2023
7 |
8 | Welcome to Scribbly! These terms of service ("Terms") govern your use of the Scribbly digital journal app ("App") and all related services provided by Scribbly, Inc. ("we," "our," or "us"). By accessing or using the App, you agree to be bound by these Terms. If you do not agree to these Terms, please do not use the App.
9 |
10 | ## License to Use the App
11 |
12 | Subject to your compliance with these Terms, we grant you a limited, non-exclusive, non-transferable, and revocable license to use the App for personal, non-commercial purposes. You may not modify, distribute, or create derivative works based on the App or any part thereof.
13 |
14 | ## Your Content
15 |
16 | You retain ownership of any content you submit, post, or display on the App ("Your Content"). By submitting Your Content, you grant us a worldwide, royalty-free, sublicensable, and transferable license to use, reproduce, distribute, and display Your Content in connection with the operation and improvement of the App.
17 |
18 | ## Privacy
19 |
20 | We value your privacy and are committed to protecting your personal information. Please review our [Privacy Policy](https://scribbly.subhambharadwaz.com/privacy) to understand how we collect, use, and disclose information about you.
21 |
22 | ## Prohibited Conduct
23 |
24 | While using the App, you agree not to:
25 |
26 | - Violate any applicable laws, regulations, or third-party rights.
27 | - Use the App to engage in any harmful, harassing, or abusive behavior.
28 | - Interfere with or disrupt the integrity or performance of the App.
29 | - Attempt to gain unauthorized access to the App or its related systems.
30 | - Use the App for any commercial purposes without our prior written consent.
31 |
32 | ## Termination
33 |
34 | We reserve the right to suspend or terminate your access to the App, in whole or in part, at any time and for any reason without prior notice. Upon termination, all licenses and rights granted to you will immediately cease.
35 |
36 | ## Disclaimer of Warranties
37 |
38 | The App is provided on an "as is" and "as available" basis. We make no warranties or representations of any kind, whether express or implied, regarding the App's performance, reliability, or suitability for your intended use.
39 |
40 | ## Changes to the Terms
41 |
42 | We may update these Terms from time to time. Any changes will be effective upon posting the revised Terms on the App. Your continued use of the App after the changes will signify your acceptance of the updated Terms.
43 |
44 | ## Governing Law
45 |
46 | These Terms shall be governed by and construed in accordance with the laws of India, without regard to its conflicts of law principles.
47 |
48 | ## Contact Us
49 |
50 | If you have any questions or concerns regarding these Terms or the App, please contact us at [subhamsbharadwaz@gmail.com](mailto:subhamsbharadwaz@gmail.com).
51 |
52 | Thank you for using Scribbly! Happy journaling!
53 |
--------------------------------------------------------------------------------
/src/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs"
2 | import { z } from "zod"
3 |
4 | export const env = createEnv({
5 | server: {
6 | DATABASE_URL: z.string().min(1),
7 | NODE_ENV: z.enum(["development", "test", "production"]),
8 | STRIPE_API_KEY: z.string().min(1),
9 | CLERK_SECRET_KEY: z.string(),
10 | UPLOADTHING_SECRET: z.string(),
11 | UPLOADTHING_APP_ID: z.string(),
12 | RESEND_API_KEY: z.string(),
13 | EMAIL_FROM_ADDRESS: z.string().email(),
14 | STRIPE_API_KEY: z.string(),
15 | STRIPE_WEBHOOK_SECRET: z.string().min(1),
16 | CLERK_WEBHOOK_SECRET: z.string().min(1),
17 | STRIPE_PRO_MONTHLY_PLAN_ID: z.string().min(1),
18 | },
19 | /**
20 | * Specify your client-side environment variables schema here. This way you can ensure the app
21 | * isn't built with invalid env vars. To expose them to the client, prefix them with
22 | * `NEXT_PUBLIC_`.
23 | */
24 | client: {
25 | NEXT_PUBLIC_APP_URL: z.string().min(1),
26 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(),
27 | NEXT_PUBLIC_POSTHOG_KEY: z.string(),
28 | NEXT_PUBLIC_POSTHOG_HOST: z.string(),
29 | },
30 |
31 | /**
32 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
33 | * middlewares) or client-side so we need to destruct manually.
34 | */
35 | runtimeEnv: {
36 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
37 | DATABASE_URL: process.env.DATABASE_URL,
38 | NODE_ENV: process.env.NODE_ENV,
39 | CLERK_WEBHOOK_SECRET: process.env.CLERK_WEBHOOK_SECRET,
40 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
41 | process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
42 | CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
43 | UPLOADTHING_SECRET: process.env.UPLOADTHING_SECRET,
44 | UPLOADTHING_APP_ID: process.env.UPLOADTHING_APP_ID,
45 | RESEND_API_KEY: process.env.RESEND_API_KEY,
46 | EMAIL_FROM_ADDRESS: process.env.EMAIL_FROM_ADDRESS,
47 | NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
48 | NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
49 | STRIPE_API_KEY: process.env.STRIPE_API_KEY,
50 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
51 | STRIPE_PRO_MONTHLY_PLAN_ID: process.env.STRIPE_PRO_MONTHLY_PLAN_ID,
52 | },
53 | /**
54 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
55 | * This is especially useful for Docker builds.
56 | */
57 | skipValidation: !!process.env.SKIP_ENV_VALIDATION,
58 | })
59 |
--------------------------------------------------------------------------------
/src/hooks/use-lock-body.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | // @see https://usehooks.com/useLockBodyScroll.
4 | export function useLockBody() {
5 | React.useLayoutEffect((): (() => void) => {
6 | const originalStyle: string = window.getComputedStyle(
7 | document.body
8 | ).overflow;
9 | document.body.style.overflow = "hidden";
10 | return () => (document.body.style.overflow = originalStyle);
11 | }, []);
12 | }
13 |
--------------------------------------------------------------------------------
/src/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | setMounted(true);
8 | }, []);
9 |
10 | return mounted;
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/use-scroll.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react"
2 |
3 | export default function useScroll(threshold: number) {
4 | const [scrolled, setScrolled] = useState(false)
5 |
6 | const onScroll = useCallback(() => {
7 | setScrolled(window.pageYOffset > threshold)
8 | }, [threshold])
9 |
10 | useEffect(() => {
11 | window.addEventListener("scroll", onScroll)
12 | return () => window.removeEventListener("scroll", onScroll)
13 | }, [onScroll])
14 |
15 | return scrolled
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@clerk/nextjs/server"
2 |
3 | import { db } from "./db"
4 |
5 | export const getUserByClerkId = async () => {
6 | const { userId } = auth()
7 |
8 | if (userId) {
9 | try {
10 | const user = await db.user.findUniqueOrThrow({
11 | where: {
12 | clerkId: userId as string,
13 | },
14 | })
15 |
16 | return user
17 | } catch (error) {
18 | console.error(error)
19 | if (error.code === "P2025") {
20 | // Network error, retry the function call
21 | return await getUserByClerkId()
22 | } else if (error.code === "P2016") {
23 | // Record not found, wait and retry the function call
24 | await new Promise((resolve) => setTimeout(resolve, 1000))
25 | return await getUserByClerkId()
26 | } else {
27 | throw new Error("User not found")
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client"
2 |
3 | declare global {
4 | // eslint-disable-next-line no-var
5 | var cachedPrisma: PrismaClient
6 | }
7 |
8 | let prisma: PrismaClient
9 | if (process.env.NODE_ENV === "production") {
10 | prisma = new PrismaClient()
11 | } else {
12 | if (!global.cachedPrisma) {
13 | global.cachedPrisma = new PrismaClient()
14 | }
15 | prisma = global.cachedPrisma
16 | }
17 |
18 | export const db = prisma
19 |
--------------------------------------------------------------------------------
/src/lib/events.ts:
--------------------------------------------------------------------------------
1 | import va from "@vercel/analytics"
2 | import { z } from "zod"
3 |
4 | const eventSchema = z.object({
5 | name: z.enum(["copy_card_number"]),
6 | // declare type AllowedPropertyValues = string | number | boolean | null
7 | properties: z
8 | .record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
9 | .optional(),
10 | })
11 |
12 | export type Event = z.infer
13 |
14 | export function trackEvent(input: Event): void {
15 | const event = eventSchema.parse(input)
16 | if (event) {
17 | va.track(event.name, event.properties)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/exceptions.ts:
--------------------------------------------------------------------------------
1 | export class RequiresProPlanError extends Error {
2 | constructor(message = "This action requires a pro plan") {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/resend.ts:
--------------------------------------------------------------------------------
1 | import { Resend } from "resend"
2 |
3 | import { env } from "@/env.mjs"
4 |
5 | export const resend = new Resend(env.RESEND_API_KEY)
6 |
--------------------------------------------------------------------------------
/src/lib/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe"
2 |
3 | import { env } from "@/env.mjs"
4 |
5 | export const stripe = new Stripe(env.STRIPE_API_KEY, {
6 | apiVersion: "2022-11-15",
7 | typescript: true,
8 | })
9 |
--------------------------------------------------------------------------------
/src/lib/uploadthing.ts:
--------------------------------------------------------------------------------
1 | import { generateReactHelpers } from "@uploadthing/react/hooks"
2 |
3 | import type { OurFileRouter } from "@/app/api/uploadthing/core"
4 |
5 | export const { uploadFiles } = generateReactHelpers()
6 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | import { env } from "@/env.mjs"
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs))
8 | }
9 |
10 | export function formatDate(input: string | number): string {
11 | const date = new Date(input)
12 | return date.toLocaleDateString("en-US", {
13 | month: "long",
14 | day: "numeric",
15 | year: "numeric",
16 | })
17 | }
18 |
19 | export function absoluteUrl(path: string) {
20 | return `${env.NEXT_PUBLIC_APP_URL}${path}`
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/validations/entry.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const entryPatchSchema = z.object({
4 | title: z.string().min(3).max(128).optional(),
5 |
6 | // TODO: Type this properly from editorjs block types?
7 | content: z.any().optional(),
8 | })
9 |
--------------------------------------------------------------------------------
/src/lib/validations/reminder.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const reminderFormSchema = z.object({
4 | frequency: z.enum(["DAILY", "WEEKLY"], {
5 | required_error: "Please select a reminder frequency.",
6 | }),
7 | active: z.boolean().default(false).optional(),
8 | })
9 |
10 | export type ReminderFormValues = z.infer
11 |
--------------------------------------------------------------------------------
/src/lib/validations/user.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userNameSchema = z.object({
4 | name: z.string().min(3).max(32),
5 | })
6 |
--------------------------------------------------------------------------------
/src/lib/verify-current-user-has-access-to-entry.ts:
--------------------------------------------------------------------------------
1 | import { getUserByClerkId } from "@/lib/auth"
2 | import { db } from "@/lib/db"
3 |
4 | export async function verifyCurrentUserHasAccessToEntry(entryId: string) {
5 | const user = await getUserByClerkId()
6 | const count = await db.journalEntry.count({
7 | where: {
8 | id: entryId,
9 | userId: user?.id,
10 | },
11 | })
12 |
13 | return count > 0
14 | }
15 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
2 |
3 | const isProtectedRoute = createRouteMatcher(["/editor(.*)", "/journal(.*)"])
4 |
5 | export default clerkMiddleware((auth, req) => {
6 | if (!auth().userId && isProtectedRoute(req)) {
7 | // Add custom logic to run before redirecting
8 |
9 | return auth().redirectToSignIn()
10 | }
11 | })
12 |
13 | export const config = {
14 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
15 | }
16 |
--------------------------------------------------------------------------------
/src/server/actions/reminder.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { revalidatePath } from "next/cache"
4 | import { getUserSubscriptionPlan } from "@/server/actions/stripe"
5 | import { z } from "zod"
6 |
7 | import { getUserByClerkId } from "@/lib/auth"
8 | import { db } from "@/lib/db"
9 | import { RequiresProPlanError } from "@/lib/exceptions"
10 | import { reminderFormSchema } from "@/lib/validations/reminder"
11 |
12 | export async function updateReminder(
13 | rawInput: z.infer
14 | ) {
15 | try {
16 | const user = await getUserByClerkId()
17 | if (!user) {
18 | throw new Error("Unauthorized")
19 | }
20 |
21 | const subscriptionPlan = await getUserSubscriptionPlan(user?.id)
22 | if (!subscriptionPlan?.isPro) {
23 | throw new RequiresProPlanError()
24 | }
25 |
26 | const payload = reminderFormSchema.parse(rawInput)
27 |
28 | const reminder = await db.reminder.upsert({
29 | where: {
30 | userId: user.id,
31 | },
32 | update: {
33 | frequency: payload.frequency,
34 | active: payload.active,
35 | },
36 | create: {
37 | frequency: payload.frequency,
38 | active: payload.active,
39 | userId: user?.id,
40 | },
41 | })
42 |
43 | revalidatePath("/journal/settings")
44 | return reminder
45 | } catch (error: unknown) {
46 | if (error instanceof z.ZodError) {
47 | return {
48 | error: error.issues,
49 | code: 422,
50 | }
51 | }
52 | if (error instanceof RequiresProPlanError) {
53 | return {
54 | error: error.message,
55 | code: 402,
56 | }
57 | }
58 | return {
59 | error,
60 | code: 500,
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/server/actions/stripe.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { UserSubscriptionPlan } from "@/types"
4 | import { z } from "zod"
5 |
6 | import { freePlan, proPlan } from "@/config/subscriptions"
7 | import { getUserByClerkId } from "@/lib/auth"
8 | import { db } from "@/lib/db"
9 | import { stripe } from "@/lib/stripe"
10 | import { absoluteUrl } from "@/lib/utils"
11 |
12 | const billingUrl = absoluteUrl("/journal/billing")
13 |
14 | export async function getUserSubscriptionPlan(
15 | userId: string
16 | ): Promise {
17 | const user = await db.user.findFirst({
18 | where: {
19 | id: userId,
20 | },
21 | select: {
22 | stripeSubscriptionId: true,
23 | stripeCurrentPeriodEnd: true,
24 | stripeCustomerId: true,
25 | stripePriceId: true,
26 | },
27 | })
28 |
29 | if (!user) {
30 | throw new Error("User not found")
31 | }
32 |
33 | // Check if user is on a pro plan.
34 | const isPro =
35 | user.stripePriceId &&
36 | user.stripeCurrentPeriodEnd?.getTime() + 86_400_000 > Date.now()
37 |
38 | const plan = isPro ? proPlan : freePlan
39 |
40 | return {
41 | ...plan,
42 | ...user,
43 | stripeCurrentPeriodEnd: user.stripeCurrentPeriodEnd?.getTime(),
44 | isPro,
45 | }
46 | }
47 |
48 | export async function stripeSubscription() {
49 | try {
50 | const user = await getUserByClerkId()
51 | if (!user || !user.email) {
52 | throw new Error("Unauthorized")
53 | }
54 |
55 | const subscriptionPlan = await getUserSubscriptionPlan(user.id)
56 |
57 | // The user is on the pro plan.
58 | // Create a portal session to manage subscription.
59 | if (subscriptionPlan.isPro && subscriptionPlan.stripeCustomerId) {
60 | const stripeSession = await stripe.billingPortal.sessions.create({
61 | customer: subscriptionPlan.stripeCustomerId,
62 | return_url: billingUrl,
63 | })
64 | return {
65 | url: stripeSession.url,
66 | }
67 | }
68 |
69 | // The user is on the free plan.
70 | // Create a checkout session to upgrade.
71 | const stripeSession = await stripe.checkout.sessions.create({
72 | success_url: billingUrl,
73 | cancel_url: billingUrl,
74 | payment_method_types: ["card"],
75 | mode: "subscription",
76 | billing_address_collection: "auto",
77 | customer_email: user.email,
78 | line_items: [
79 | {
80 | price: proPlan.stripePriceId,
81 | quantity: 1,
82 | },
83 | ],
84 | metadata: {
85 | userId: user.id,
86 | },
87 | })
88 |
89 | return {
90 | url: stripeSession.url,
91 | }
92 | } catch (error: unknown) {
93 | if (error instanceof z.ZodError) {
94 | return {
95 | error: error.issues,
96 | code: 422,
97 | }
98 | }
99 | return {
100 | error,
101 | code: 500,
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/server/actions/user.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { revalidatePath } from "next/cache"
4 | import { z } from "zod"
5 |
6 | import { getUserByClerkId } from "@/lib/auth"
7 | import { db } from "@/lib/db"
8 | import { userNameSchema } from "@/lib/validations/user"
9 |
10 | const routeContextSchema = z.object({
11 | params: z.object({
12 | userId: z.string(),
13 | }),
14 | })
15 |
16 | export async function updateUserName(
17 | rawInput: z.infer,
18 | input: z.infer
19 | ) {
20 | try {
21 | const { params } = routeContextSchema.parse(rawInput)
22 |
23 | const user = await getUserByClerkId()
24 | if (!user || params.userId !== user.id) {
25 | throw new Error("Unauthorized")
26 | }
27 |
28 | const payload = userNameSchema.parse(input)
29 |
30 | await db.user.update({
31 | where: {
32 | id: user.id,
33 | },
34 | data: {
35 | name: payload.name,
36 | },
37 | })
38 |
39 | revalidatePath("/journal/settings")
40 | } catch (error: unknown) {
41 | if (error instanceof z.ZodError) {
42 | return {
43 | error: error.issues,
44 | code: 422,
45 | }
46 | }
47 | return {
48 | error,
49 | code: 500,
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/server/queries/editor.ts:
--------------------------------------------------------------------------------
1 | import "server-only"
2 |
3 | import { JournalEntry, User } from "@prisma/client"
4 |
5 | import { db } from "@/lib/db"
6 |
7 | export async function getMyEntry(
8 | entryId: JournalEntry["id"],
9 | userId: User["id"]
10 | ) {
11 | return await db.journalEntry.findFirst({
12 | where: {
13 | id: entryId,
14 | userId: userId,
15 | },
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/src/server/queries/reminder.ts:
--------------------------------------------------------------------------------
1 | import "server-only"
2 |
3 | import { z } from "zod"
4 |
5 | import { getUserByClerkId } from "@/lib/auth"
6 | import { db } from "@/lib/db"
7 |
8 | export async function getMyReminderSettings() {
9 | try {
10 | const user = await getUserByClerkId()
11 |
12 | if (!user) {
13 | throw new Error("Unauthorized")
14 | }
15 |
16 | const reminder = await db.reminder.findFirst({
17 | select: {
18 | id: true,
19 | frequency: true,
20 | time: true,
21 | active: true,
22 | },
23 | where: {
24 | userId: user.id,
25 | },
26 | })
27 | return reminder
28 | } catch (error: unknown) {
29 | if (error instanceof z.ZodError) {
30 | return {
31 | error: error.issues,
32 | code: 422,
33 | }
34 | }
35 | throw new Error("Server error", error)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/styles/editor.css:
--------------------------------------------------------------------------------
1 | .dark .ce-block--selected .ce-block__content,
2 | .dark .ce-inline-toolbar,
3 | .dark .codex-editor--narrow .ce-toolbox,
4 | .dark .ce-conversion-toolbar,
5 | .dark .ce-settings,
6 | .dark .ce-settings__button,
7 | .dark .ce-toolbar__settings-btn,
8 | .dark .cdx-button,
9 | .dark .ce-popover,
10 | .dark .ce-toolbar__plus:hover {
11 | background: theme("colors.popover.DEFAULT");
12 | color: inherit;
13 | border-color: theme("colors.border");
14 | }
15 |
16 | .dark .ce-inline-tool,
17 | .dark .ce-conversion-toolbar__label,
18 | .dark .ce-toolbox__button,
19 | .dark .cdx-settings-button,
20 | .dark .ce-toolbar__plus {
21 | color: inherit;
22 | }
23 |
24 | .dark .ce-popover-item__icon,
25 | .dark .ce-conversion-tool__icon {
26 | background-color: theme("colors.popover.DEFAULT");
27 | color: theme("colors.muted.foreground");
28 | box-shadow: none;
29 | }
30 |
31 | .dark .ce-popover-item__title {
32 | color: theme("colors.muted.foreground");
33 | }
34 |
35 | .dark .cdx-search-field {
36 | border-color: theme("colors.border");
37 | background: theme("colors.input");
38 | color: inherit;
39 | }
40 |
41 | .dark ::selection {
42 | background: theme("colors.accent.DEFAULT");
43 | }
44 |
45 | .dark .cdx-settings-button:hover,
46 | .dark .ce-settings__button:hover,
47 | .dark .ce-toolbox__button--active,
48 | .dark .ce-toolbox__button:hover,
49 | .dark .cdx-button:hover,
50 | .dark .ce-popover-item__icon:hover,
51 | .dark .ce-popover-item__title:hover,
52 | .dark .ce-inline-toolbar__dropdown:hover,
53 | .dark .ce-inline-tool:hover,
54 | .dark .ce-popover-item:hover,
55 | .dark .ce-conversion-tool:hover,
56 | .dark .ce-toolbar__settings-btn:hover {
57 | background-color: theme("colors.accent.DEFAULT");
58 | color: theme("colors.accent.foreground");
59 | }
60 |
61 | .dark .cdx-notify--error {
62 | background: theme("colors.destructive.DEFAULT") !important;
63 | }
64 |
65 | .dark .cdx-notify__cross::after,
66 | .dark .cdx-notify__cross::before {
67 | background: white;
68 | }
69 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --muted: 210 40% 96.1%;
11 | --muted-foreground: 215.4 16.3% 46.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 84% 4.9%;
18 |
19 | --border: 214.3 31.8% 91.4%;
20 | --input: 214.3 31.8% 91.4%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --accent: 210 40% 96.1%;
29 | --accent-foreground: 222.2 47.4% 11.2%;
30 |
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 210 40% 98%;
33 |
34 | --ring: 215 20.2% 65.1%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | .dark {
40 | --background: 222.2 84% 4.9%;
41 | --foreground: 210 40% 98%;
42 |
43 | --muted: 217.2 32.6% 17.5%;
44 | --muted-foreground: 215 20.2% 65.1%;
45 |
46 | --popover: 222.2 84% 4.9%;
47 | --popover-foreground: 210 40% 98%;
48 |
49 | --card: 222.2 84% 4.9%;
50 | --card-foreground: 210 40% 98%;
51 |
52 | --border: 217.2 32.6% 17.5%;
53 | --input: 217.2 32.6% 17.5%;
54 |
55 | --primary: 210 40% 98%;
56 | --primary-foreground: 222.2 47.4% 11.2%;
57 |
58 | --secondary: 217.2 32.6% 17.5%;
59 | --secondary-foreground: 210 40% 98%;
60 |
61 | --accent: 217.2 32.6% 17.5%;
62 | --accent-foreground: 210 40% 98%;
63 |
64 | --destructive: 0 62.8% 30.6%;
65 | --destructive-foreground: 0 85.7% 97.3%;
66 |
67 | --ring: 217.2 32.6% 17.5%;
68 | }
69 | }
70 |
71 | @layer base {
72 | * {
73 | @apply border-border;
74 | }
75 | html {
76 | scroll-behavior: smooth;
77 | }
78 | body {
79 | @apply bg-background text-foreground;
80 | font-feature-settings: "rlig" 1, "calt" 1;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { Icon } from "lucide-react"
2 |
3 | import { Icons } from "@/components/icons"
4 |
5 | export type NavItem = {
6 | title: string
7 | href: string
8 | disabled?: boolean
9 | }
10 |
11 | export type MainNavItem = NavItem
12 |
13 | export type SidebarNavItem = {
14 | title: string
15 | disabled?: boolean
16 | external?: boolean
17 | icon?: keyof typeof Icons
18 | } & (
19 | | {
20 | href: string
21 | items?: never
22 | }
23 | | {
24 | href?: string
25 | items: NavLink[]
26 | }
27 | )
28 |
29 | export type SiteConfig = {
30 | name: string
31 | description: string
32 | url: string
33 | ogImage: string
34 | links: {
35 | twitter: string
36 | github: string
37 | linkedin: string
38 | }
39 | }
40 |
41 | export type MarketingConfig = {
42 | mainNav: MainNavItem[]
43 | }
44 |
45 | export type SubscriptionPlan = {
46 | name: string
47 | description: string
48 | stripePriceId: string
49 | }
50 |
51 | export type JournalConfig = {
52 | mainNav?: MainNavItem[]
53 | sidebarNav: SidebarNavItem[]
54 | }
55 |
56 | export type UserSubscriptionPlan = SubscriptionPlan &
57 | Pick & {
58 | stripeCurrentPeriodEnd: number
59 | isPro: boolean
60 | }
61 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": ["./src/*"],
25 | "contentlayer/generated": ["./.contentlayer/generated"]
26 | }
27 | },
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts",
33 | ".contentlayer/generated"
34 | ],
35 | "exclude": ["node_modules"]
36 | }
37 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "crons": [
3 | {
4 | "path": "/api/cron",
5 | "schedule": "0 21 * * *"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------