├── .dockerignore
├── .eslintrc.json
├── .firebase
├── hosting.Lm5leHQvc3RhdGlj.cache
└── hosting.b3V0.cache
├── .firebaserc
├── .github
├── ISSUE_TEMPLATE
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── bug.md
│ └── feature_request.md
└── workflows
│ ├── firebase-hosting-merge.yml
│ └── firebase-hosting-pull-request.yml
├── .gitignore
├── .vercelignore
├── .vscode
└── settings.json
├── Changelog.md
├── Contributing.md
├── Dockerfile
├── LICENSE
├── README.md
├── components.json
├── firebase.json
├── functions
├── .eslintrc.js
├── .gitignore
├── package-lock.json
├── package.json
├── src
│ └── index.ts
├── tsconfig.dev.json
└── tsconfig.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── favicon
│ ├── apple-touch-icon.png
│ ├── favicon-48x48.png
│ ├── favicon.ico
│ ├── favicon.svg
│ ├── icon-16x16.png
│ ├── icon-180x180.png
│ ├── icon-192x192.png
│ ├── icon-48x48.png
│ └── icon-512x512.png
├── images
│ ├── cover-dark.png
│ ├── cover.png
│ ├── frame.png
│ ├── gemini.png
│ ├── local.png
│ ├── logo.png
│ └── preview.png
├── manifest.json
├── next.svg
├── sw.js
├── vercel.svg
├── workbox-7144475a.js
└── workbox-7144475a.js.map
├── src
├── app
│ ├── (main)
│ │ ├── _components
│ │ │ ├── CodeHighlight.tsx
│ │ │ ├── CopyIcon.tsx
│ │ │ ├── DatePicker.tsx
│ │ │ ├── Dialog
│ │ │ │ ├── Changelog.tsx
│ │ │ │ ├── Delete.tsx
│ │ │ │ ├── GeminiDialog.tsx
│ │ │ │ ├── Import.tsx
│ │ │ │ ├── SearchDialog.tsx
│ │ │ │ └── Share.tsx
│ │ │ ├── Dropdown.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── GeminiIcon.tsx
│ │ │ ├── GradientText.tsx
│ │ │ ├── IconButton.tsx
│ │ │ ├── LoginMenu.tsx
│ │ │ ├── MDPreview.tsx
│ │ │ ├── Navigation.tsx
│ │ │ ├── PSAccordian.tsx
│ │ │ ├── PSBanner.tsx
│ │ │ ├── PSInput.tsx
│ │ │ ├── PSNavbar.tsx
│ │ │ ├── Pastelog.tsx
│ │ │ ├── PreviewAction.tsx
│ │ │ ├── PreviewPage.tsx
│ │ │ ├── RouteClient.tsx
│ │ │ ├── ShortCutWrapper.tsx
│ │ │ ├── ShortcutsGuide.tsx
│ │ │ ├── SideBarItem.tsx
│ │ │ ├── Sidebar.tsx
│ │ │ ├── TermsAndPrivacy.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ ├── ToastProvider.tsx
│ │ │ ├── Welcome.tsx
│ │ │ ├── accordion.tsx
│ │ │ ├── button.tsx
│ │ │ ├── calendar.tsx
│ │ │ ├── completion.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── features
│ │ │ │ ├── BeautifulMarkdown.tsx
│ │ │ │ ├── CreateAndShare.tsx
│ │ │ │ ├── DarkMode.tsx
│ │ │ │ ├── GeminiPowered.tsx
│ │ │ │ ├── Introduction.tsx
│ │ │ │ ├── KeyboardShortcuts.tsx
│ │ │ │ └── SaveLocally.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── popover.tsx
│ │ │ └── select.tsx
│ │ ├── _hooks
│ │ │ ├── outsideclick.ts
│ │ │ ├── useSettings.ts
│ │ │ ├── useSidebar.ts
│ │ │ └── useSmallScreen.ts
│ │ ├── _services
│ │ │ ├── Analytics.ts
│ │ │ ├── AuthService.ts
│ │ │ ├── BannerState.ts
│ │ │ ├── EditorState.ts
│ │ │ ├── MDFormatter.ts
│ │ │ ├── UserService.ts
│ │ │ ├── feature.ts
│ │ │ └── logService.ts
│ │ └── logs
│ │ │ ├── [id]
│ │ │ └── page.tsx
│ │ │ ├── app_layout.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ ├── (policies)
│ │ └── policies
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ ├── (publish)
│ │ ├── layout.tsx
│ │ └── logs
│ │ │ └── publish
│ │ │ └── [id]
│ │ │ └── page.tsx
│ ├── config.js
│ ├── constants.ts
│ ├── globals.css
│ ├── icon.ico
│ ├── layout.tsx
│ ├── markdown.css
│ ├── page.tsx
│ ├── providers.tsx
│ └── style.css
├── lib
│ ├── Context
│ │ └── PSNavbarProvider.tsx
│ ├── features
│ │ └── menus
│ │ │ ├── authSlice.ts
│ │ │ ├── editorSlice.ts
│ │ │ ├── menuSlice.ts
│ │ │ └── sidebarSlice.ts
│ ├── store.ts
│ └── utils.ts
└── utils
│ ├── DateUtils.ts
│ ├── firebase.ts
│ ├── toast_utils.tsx
│ └── utils.ts
├── tailwind.config.ts
├── tree.md
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | .env
2 | .env.local
3 | node_modules
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.firebase/hosting.Lm5leHQvc3RhdGlj.cache:
--------------------------------------------------------------------------------
1 | EY8konJMwavGWJ4iIv4QF/_ssgManifest.js,1718806270868,8c469cd29c6a706334d3b9ec2e526bdbdac460bef3b3d364fdc528bcd7d19e67
2 | EY8konJMwavGWJ4iIv4QF/_buildManifest.js,1718806231253,0474c56df53665d9f0c84e3ed67dea5b38f6e7ff31a982c3bbb5904e4771ee4b
3 | chunks/296-816d237730189113.js,1718806231231,5a5ee862caddf24820030d5069fbbeb5f542d916bd70bb5a3b4133ff1b3ebb45
4 | chunks/231-f6dae81872c7d78b.js,1718806231243,e53a101025c5e29c2f792e2d421777b817a833f7830ca05a2e001bf4dd4a2509
5 | chunks/159-3903c4f71d267b37.js,1718806231201,6318c1ddc3ed2e0fb7ac3306152f5d365bbaa59b1368f3ec86f8829735eddca6
6 | chunks/743-b35eb48c8eda0aac.js,1718806231245,113b5d8c92e6912268722c37a82b883d7085df2582cfc16afbfbd0acccadc446
7 | chunks/763-4b36ebca252e1b62.js,1718806231246,ad9e8385afbe6406c0f7713c66f20fa3f0ce943f011c7de2e1373c2eab10ace0
8 | chunks/956-de831295c52cc99e.js,1718806231241,23cdbb898ed477f8e3c166630bb39dd6319f52375331dcde705a4a10f1555e17
9 | chunks/main-app-f4cb664fb5ef45a9.js,1718806231176,444f3726d23518f83e426e713d90ecb74177af13b53abfa809d63c95ac2ab195
10 | chunks/app/layout-cb8df515adb5d8a5.js,1718806231175,a58fad8eab75a8a00018becc5ce3bfebdbacaead5da60b3576e318e01ceecb26
11 | chunks/app/page-7acb3d503e4e035b.js,1718806231174,44cf23a1f453bd5296d00d6a47df8982f5f898a51ae5f0015f9dc6e78cab238f
12 | chunks/app/(main)/logs/[id]/page-c09c015404a75874.js,1718806231203,6d3f9dd1843e8b972b78470017cac897b0c3a4c1dc7c21a95291a7828c575ccd
13 | chunks/app/(publish)/layout-aad6176b770bad03.js,1718806231198,eb0aed9db09a3fa95d035bc58aabcec1ad67a78d0dcd048b09076fbb4b8fdc3f
14 | chunks/app/(policies)/policies/page-158323feb7a169fc.js,1718806231180,0b66ead6527b8353a68a5fca95d776790a8519ad88b1a58a3186b1151cbfbe06
15 | chunks/app/(main)/logs/page-c6b685aadee43392.js,1718806231200,467b5b7cb924df36a08bfcf7afacea89cf3ae1dfad836ef1a14e8809bfdd58fb
16 | chunks/app/_not-found/page-c37604c0fee25ed4.js,1718806231183,9ca424689608f02ed6af05d422b7d999ef39e4d4b12d658b81967e5d47b15379
17 | chunks/app/(publish)/logs/publish/[id]/page-80583e0b57a961de.js,1718806231216,d5312a1b696d66589a6eeeef15f3244e2058b1bb500990d0904d4fffbb066541
18 | chunks/pages/_error-6ae619510b1539d6.js,1718806231173,e73729a247fbd5cf1acddc0c6f67271a511c97ca0b0068e42447d5f5e996ecf7
19 | chunks/pages/_app-037b5d058bd9a820.js,1718806231174,06e750a0ef670f4de0c1bd480b5011b19558d8742f9b03ec7109bdceae9c7931
20 | css/2f71e0d51b6954c9.css,1718806231261,dfc9deaaf139e09d08338e38db4e3a51eb2bd33a06eaaa9ff82df253e8e7153c
21 | media/05a31a2ca4975f99-s.woff2,1718806231172,6efe3c10c8cf8d8db389af700f8e6dd56ef3734c59159ae26bea5e9f3b05d919
22 | chunks/webpack-495241f564b871a1.js,1718806231192,e883eda1102d858623f1bc77c7c87b0205b3a60729fd2e03dcc09f4df2b9cd0c
23 | css/ba45e92c03b1843b.css,1718806231261,586b688c280cff086052c930645ca3e4766a47c7f3edb67f317c3c0648751205
24 | chunks/app/(main)/logs/layout-503a0fefe97f90ae.js,1718806231199,159837e4ef1712cedf9eadf280f89d095bbc0880c2ff752bae04be9a7248be1f
25 | media/fd4db3eb5472fc27-s.woff2,1718806231174,b62a5c48aa2a5c1f795d9f0e040eb59387eb7527523ebf1a2a37a0b80ed8afda
26 | media/513657b02c5c193f-s.woff2,1718806231178,12fead7b73c8d1e6d927c1558658f895e2bc585abb9fa0787fd3a18ab6de0229
27 | media/ec159349637c90ad-s.woff2,1718806231174,72adc3dde09d31fedb2a4472cd9ac9ae14108ee045cbe7de2afc351c6761e047
28 | media/51ed15f9841b9f9d-s.woff2,1718806231175,1858da22f630427753e92d04720084a47e27d78af30e9bcfe21f1514a014b67f
29 | chunks/152-9ec55e4056236625.js,1718806231201,1577936183300f13d67c9ae56b00fd4f63fa25759ee8f1d8f4e384bb62524308
30 | chunks/523-e3c888c0f967560a.js,1718806231234,d1db6adbeb6ba1a727d4cb1f5ea0f1ea9d45137029760fe78b4a440cdfc074db
31 | chunks/387-e2f45577f77adde7.js,1718806231200,5662c49b44374109e1e5d3eb9783645569fd5e276bb6268d173564ad3f97018e
32 | chunks/23-1808afd62972ec90.js,1718806231245,f79d32ad86c7bc93b615355c2947bce161f7ebdd9c040185b54f583f57198425
33 | chunks/f8e4659f-fbca1f7b2b1ed97c.js,1718806231193,4cc0c82692f75f39397e3fef12c5b3c8c03400b382ec6a3e0253fd033c09e422
34 | chunks/main-03244eb4fff4e901.js,1718806231173,47ecf915042407e600b846c38f5328fbbd1b469369510a8a692d0b1c5a8fb475
35 | media/c9a5bc6a7c948fb0-s.p.woff2,1718806231174,37d0358a532a69cb27ae70525b099439dc7139875db00f29a2a103bf24e7f54f
36 | chunks/polyfills-78c92fac7aa8fdd8.js,1718806231245,a5b39edcc77b6069d442b5dbeb8091cf27ac47ac7a1a99cfc325213bdaae2fb5
37 | chunks/131-b236aadf3b763336.js,1718806231231,9cb1b69c149b534dd17e1a47c2bd03a9d149db30e250cd0ed0d3e4b0e0991337
38 | chunks/669.4db2b53eca70cd2d.js,1718806231260,1d668bd5f1d53748693aa182015ebb2ef29de6de76a2989201af511b20ef9958
39 | chunks/781-df2db01f32f9186c.js,1718806231199,2d5c5094f45cafda68353b3b2e4109b3f80afa1c5a90a84bcbd951a3c5e4fdf5
40 | chunks/framework-00a8ba1a63cfdc9e.js,1718806231192,c2012692d29b99c17d95c52f9e0f321c0aa21c7e8bc5db5fae470ad7eda00549
41 | css/1fe258efd1e443f0.css,1718806231261,b24588de3897028db953fbddb658915ea6ab570218519006012f22fd0d3a61fb
42 | media/d6b16ce4a6175f26-s.woff2,1718806231173,b4d117b2e85e547b1698898d382cc41f3f9b9f2549a7590657b4b4ad8071987b
43 | chunks/fd9d1056-19d730fc2f8f727f.js,1718806231195,25f5dce45252959ff01f83f2f702c7bc14ffac84dcf3e1efbb080e0a12474250
44 | chunks/bc9e92e6-133eb1bf9a3a2089.js,1718806231192,639a88dcf9c6bda9ebfe6449fbdb39de11f0b117907bf6925b0f324bc2bc3e1b
45 | chunks/627-eaafaafe15b52f68.js,1718806231244,a3b08bd0ea52d337671d978b76cfd280266f0e984773cf5eb5edb916b60f1ff3
46 | chunks/ad2866b8-abe348347acfde4a.js,1718806231192,5188ffc434157965e6b1db58cfc463c20a0e656764cc778c7971dfd95742cfa5
47 | chunks/465-5326a950a770e88d.js,1718806231233,6f2a8ee24c9df75d2fc6f62fd98e80d3093564b2f16124965a5afdae1b501a3e
48 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "pastelog-id"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | _Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots._
2 |
3 | _List which issues are fixed by this PR. You must list at least one issue._
4 |
5 | ## Pre-launch Checklist
6 |
7 | - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
8 | - [ ] I listed at least one issue that this PR fixes in the description above.
9 | - [ ] I updated/added relevant documentation (doc comments with `///`).
10 |
11 | If you need help, consider pinging the maintainer @maheshmnj
12 |
13 |
14 |
15 | [Contributor Guide]: https://github.com/maheshmnj/searchfield/blob/master/CONTRIBUTING.md
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Found a bug? Report and help us fix it.
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
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 | **Actual behavior**
24 | What you actually saw instead of the expected behavior.
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: "Have an Idea ? Propose your request "
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the feature is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-merge.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Firebase Hosting on merge
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | build_and_deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 |
13 | # Generate the .env file with required environment variables
14 | - name: Create .env file
15 | run: echo "${{ secrets.PASTELOG_ENV }}" > .env
16 |
17 | # Install dependencies and build the project
18 | - run: npm ci && npm run build
19 |
20 | # # Deploy to Firebase Hosting
21 | # - uses: FirebaseExtended/action-hosting-deploy@v0
22 | # with:
23 | # repoToken: "${{ secrets.GITHUB_TOKEN }}"
24 | # firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PASTELOG_ID }}"
25 | # channelId: live
26 | # projectId: pastelog-id
27 | # env:
28 | # FIREBASE_CLI_PREVIEWS: hostingchannels
29 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-pull-request.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Build on PR
5 | "on": pull_request
6 | jobs:
7 | build_and_preview:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 |
12 | # Generate the .env file with required environment variables
13 | - name: Create .env file
14 | run: echo "${{ secrets.PASTELOG_ENV }}" > .env
15 |
16 | # Install dependencies and build the project
17 | - run: npm ci && npm run build
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | public/sw.js.map
20 | # misc
21 | .DS_Store
22 | *.pem
23 | .env
24 |
25 | # debug
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # local env files
31 | .env*.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 | build.md
40 | react-ghost-text/
--------------------------------------------------------------------------------
/.vercelignore:
--------------------------------------------------------------------------------
1 | functions
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.autoSaveDelay": 3000,
3 | "editor.formatOnSave": true,
4 | "dart.flutterSdkPath": "/Users/mahesh/Documents/flutter",
5 | "dart.debugExternalLibraries": true,
6 | "editor.codeActionsOnSave": {
7 | "source.organizeImports": "always",
8 | "source.fixAll": "explicit",
9 | "source.addMissingImports.ts": "explicit"
10 | },
11 | "editor.defaultFormatter": "esbenp.prettier-vscode",
12 | "eslint.validate": [
13 | "javascript",
14 | "typescript"
15 | ],
16 | // shows error in entire project
17 | "typescript.tsserver.experimental.enableProjectDiagnostics": true,
18 | "typescript.tsdk": "functions/node_modules/typescript/lib",
19 | "eslint.workingDirectories": [
20 | {
21 | "mode": "auto"
22 | },
23 | {
24 | "pattern": "functions/"
25 | }
26 | ],
27 | "java.configuration.updateBuildConfiguration": "interactive"
28 | }
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | ### [0.6.3] - Apr 26, 2025
2 |
3 | - Sort by last modified date [Issue #43](https://github.com/maheshj01/pastelog/issues/43)
4 | - Load log from local storage first to improve loading time
5 | - Add copy icon to tooltip for links
6 |
7 | ### [0.6.2] - Mar 07, 2025
8 |
9 | - Improved app loading time by 50%
10 | - Eliminate Context API and Migrate to Redux
11 |
12 | ### [0.6.1] - October 06, 2024
13 |
14 | - Show title in Navbar on overscroll
15 | - reset scroll when a new slog is selected
16 |
17 | ### [0.6.0] - October 06, 2024
18 |
19 | - Add Google Sign In Authentication
20 | - Save guest notes in local storage
21 | - Add more options for notes
22 | - Integrate Gemini to summarize and rename the log
23 | - Add PWA Support
24 | - Improve Responsiveness of the app
25 | - And lots of minor fixes and improvements
26 |
27 | ### [0.5.4] - July 31, 2024
28 |
29 | - Add Edit feature
30 |
31 | ### [0.5.3] - July 13, 2024
32 |
33 | - Adds Authentication [Issue #37](https://github.com/maheshmnj/pastelog/issues/37) With Cloud Sync
34 | - Add `new log` and `toggle theme` keyboard shortcut
35 |
36 | ### [0.5.2] - July 12, 2024
37 |
38 | Adds Following Shortcuts
39 |
40 | - Shift + Tab in a code block for selected text should remove tabs
41 | - Return Key continues ordered/unordered list from previous line
42 | - Toggle Sidebar ctrl + m
43 | - Add Keyboard Shortcuts Guide in Sidebar
44 |
45 | ### [0.5.1] - July 02, 2024
46 |
47 | - Apply text formatting to trimmed text on keyboard shortcuts
48 | - add tooltip to gemini icon
49 | - fix share dialog UI
50 | - add import icon
51 | - fix policies text color
52 | - Summary is local Only
53 | - fix: horizontal line was not visible
54 | - update summary prompt
55 | - Fix undo redo of the Editor
56 |
57 | ### [0.5.0] - Jun 29, 2024
58 |
59 | - Summarize log using Gemini
60 |
61 | ### [0.4.1] - Jun 27, 2024
62 |
63 | - Fix line breaks in the log
64 | - Improve the landingpage UI
65 | - Added a new Logo
66 |
67 | ### [0.4.0] - Jun 26, 2024
68 |
69 | - Feat: Import log from a link [Issue #20](https://github.com/maheshmnj/pastelog/issues/20)
70 | - Feat: Integrate Firebase Analytics [Issue #18](https://github.com/maheshmnj/pastelog/issues/18)
71 |
72 | ### [0.3.0] - Jun 25, 2024
73 |
74 | - Adds Keyboard Shortcuts
75 | - Add a realtime banner to show info/warning/success
76 | - Add a toast for success/error
77 | - Fix inline code showing single backtick
78 | - Add a option to Republish
79 |
80 | ### [0.2.0] - Jun 22, 2024
81 |
82 | - Remove Delete from firestore
83 | - Add a delete confirmation Dialog
84 | - improve the DatePicker UI
85 | - Stick the navbar at top
86 |
87 | ### [0.1.0] - Jun 20, 2024
88 |
89 | - Initial release ported to NextJS 14
90 |
--------------------------------------------------------------------------------
/Contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ### Contributor
4 |
5 | Thanks for taking you time to contribute to this repo. Before you start contributing please go through the following guidelines which we consider are important to maintain this repository and can help new contributers to open source.
6 |
7 | - Before you submit a Pull request ensure a issue exist describing the issue / feature request. If it doesn't please file an issue so that we could discuss about the issue before the actual PR is submitted.
8 |
9 | - The issue should be sufficient enough to explain the bug/feature request and a possible solution/ proposal
10 |
11 | - We appreciate any contribution, from fixing a grammar mistake in a comment, improving code snippets to fixing a bug or making a feature requests and writing proper tests are also highly welcome.
12 |
13 | - Follow the best practices to maintain the quality of code.
14 |
15 | (Optional)
16 |
17 | - Additional changes for publishing a release. Update the version in readme, pubspec.yaml, and update the changelog. Make sure the documentation is updated as per the changes. Make sure existing and new tests are passing. Make sure the code is well formatted. Ensure the linter warnigs are zero.
18 |
19 | ### Contributing
20 |
21 | 1. Fork This repo
22 | 2. Create a new branch
23 | 3. Commit a Fix
24 | 4. Add appropriate tests(recommended)
25 | 5. Submit a PR referencing the issue
26 | 6. Request a review
27 | 7. wait for LGTM 🚀 comment
28 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the official Node.js 18 image as a parent image
2 | FROM node:18-alpine
3 |
4 | # Set the working directory
5 | WORKDIR /app
6 |
7 | # Copy package.json and package-lock.json
8 | COPY package*.json ./
9 |
10 | # Install dependencies
11 | RUN npm ci
12 |
13 | # Copy the rest of your app's source code
14 | COPY . .
15 |
16 | # Build your Next.js app
17 | RUN npm run build
18 |
19 | # Expose the port your app runs on
20 | EXPOSE 3000
21 |
22 | # Start the app
23 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mahesh Jamdade
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 | 
2 |
3 | # Pastelog
4 |
5 | Create Stunning AI Powered Rich notes with markdown Support and Code Highlighting and share it with a unique URL. The logs are publicly accessible and auto expire after the specified date. Powered By Next.js, Firebase and Gemini API.
6 |
7 |
8 |
9 |
10 |
11 |
12 | ### Features
13 |
14 |
15 | - The logs are publicly accessible, no SignIn required
16 | - The logs auto expire after the specified date
17 | - Stores logs locally for quick access
18 | - Supports rich text content with github falvoured markdown and code highlighting
19 | - Export logs as image and plain text
20 | - Import logs from Github gist or from Pastelog Url
21 | - Intelligent editor with Markdown Keyboard shortcuts support to help you write faster
22 | - Supports Darkmode for better readability
23 | - Share logs with a unique URL
24 | - Summarize logs using the Google Gemini API (Uses Ephemeral API key storage)
25 |
26 | ### Building the project
27 |
28 | 1. Clone the repository
29 |
30 | ```bash
31 | git clone
32 | ```
33 |
34 | 2. Install the dependencies
35 |
36 | ```bash
37 |
38 | npm install
39 | ```
40 |
41 | 3. Add the .env in the root with the following keys
42 |
43 | ```bash
44 | NEXT_PUBLIC_FIREBASE_API_KEY=
45 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
46 | NEXT_PUBLIC_FIREBASE_PROJECT_ID=
47 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
48 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
49 | NEXT_PUBLIC_FIREBASE_APP_ID=
50 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
51 | NEXT_PUBLIC_FIREBASE_COLLECTION=
52 | NEXT_PUBLIC_FIREBASE_CONFIG_COLLECTION=
53 | NEXT_PUBLIC_FIREBASE_FEATURE_BANNER=
54 | NEXT_PUBLIC_NEW_USER_VISITED=
55 | NEXT_PUBLIC_CONTACT_EMAIL=
56 | NEXT_PUBLIC_GITHUB_REPO=https://github.com/maheshmnj/pastelog
57 | NEXT_PUBLIC_PRIVACY_POLICY=/logs/publish/1R5Kx9fQRBHe85SUOG89
58 | NEXT_PUBLIC_BASE_URL=https://pastelog.vercel.app
59 | NEXT_PUBLIC_GITHUB_LOGO=https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/GitHub_Invertocat_Logo.svg/1200px-GitHub_Invertocat_Logo.svg.png
60 | NEXT_PUBLIC_GITHUB_GIST_API=https://api.github.com/gists
61 | ```
62 |
63 | 3. Run the project
64 |
65 | ```bash
66 | npm run dev
67 | ```
68 |
69 | ### Folder Structure
70 |
71 |
72 |
73 | ```
74 | root /
75 | ├──src
76 | │ ├── app /
77 | │ │ ├── (main)/
78 | │ │ │ │ ├── Log.ts
79 | │ │ │ ├── \_services/
80 | │ │ │ │ ├── LogService.ts
81 | │ │ │ ├── \_components/
82 | │ │ │ ├ ├──├── \_Dialog/
83 | │ │ │ ├ |──── └── SearchDialog.tsx
84 | │ │ │ │ ├── Sidebar.tsx
85 | │ │ │ │ ├── Navbar.tsx
86 | │ │ │ │ ├── MainContent.tsx
87 | │ │ │ │ │
88 | │ │ │ ├── logs /
89 | │ │ │ │ ├──[id]
90 | │ │ │ │ │ └── page.tsx
91 | │ │ │ │ └── layout.tsx
92 | │ │ │ │ └── app_layout.tsx
93 | │ │ │ │ └── page.tsx
94 | │ │ ├── (publish)/
95 | │ │ │ ├── logs /
96 | │ │ │ │ ├── publish /
97 | │ │ │ │ │ ├──[id]/
98 | │ │ │ │ │ └── page.tsx
99 | │ │ │ └── layout.tsx
100 | │ │ │
101 | │ │ └── layout.tsx
102 | │ │ └── global.css
103 | │ │ └── page.tsx
104 | ```
105 |
106 | ### Summarize Logs with Gemini
107 |
108 |
109 |
110 | ### Demo
111 |
112 |
113 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "src/app/(main)/_components",
15 | "utils": "src/lib/utils",
16 | "ui": "src/app/(main)/_components"
17 | }
18 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "ignore": [
7 | "node_modules",
8 | ".git",
9 | "firebase-debug.log",
10 | "firebase-debug.*.log",
11 | "*.local"
12 | ],
13 | "predeploy": [
14 | "npm --prefix \"$RESOURCE_DIR\" run lint",
15 | "npm --prefix \"$RESOURCE_DIR\" run build"
16 | ]
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/functions/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | es6: true,
5 | node: true,
6 | },
7 | extends: [
8 | "eslint:recommended",
9 | "plugin:import/errors",
10 | "plugin:import/warnings",
11 | "plugin:import/typescript",
12 | "google",
13 | "plugin:@typescript-eslint/recommended",
14 | ],
15 | parser: "@typescript-eslint/parser",
16 | parserOptions: {
17 | project: ["tsconfig.json", "tsconfig.dev.json"],
18 | sourceType: "module",
19 | },
20 | ignorePatterns: [
21 | "/lib/**/*", // Ignore built files.
22 | "/generated/**/*", // Ignore generated files.
23 | ],
24 | plugins: ["@typescript-eslint", "import"],
25 | rules: {
26 | "quotes": ["error", "double"],
27 | "import/no-unresolved": 0,
28 | // off max line length
29 | "max-len": "off",
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled JavaScript files
2 | lib/**/*.js
3 | lib/**/*.js.map
4 |
5 | # TypeScript v1 declaration files
6 | typings/
7 |
8 | # Node.js dependency directory
9 | node_modules/
10 | *.local
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "scripts": {
4 | "lint": "eslint --ext .js,.ts .",
5 | "build": "tsc",
6 | "build:watch": "tsc --watch",
7 | "serve": "npm run build && firebase emulators:start --only functions",
8 | "shell": "npm run build && firebase functions:shell",
9 | "start": "npm run shell",
10 | "deploy": "firebase deploy --only functions",
11 | "logs": "firebase functions:log"
12 | },
13 | "engines": {
14 | "node": "22"
15 | },
16 | "main": "lib/index.js",
17 | "dependencies": {
18 | "firebase-admin": "^12.7.0",
19 | "firebase-functions": "^6.3.2"
20 | },
21 | "devDependencies": {
22 | "@typescript-eslint/eslint-plugin": "^5.12.0",
23 | "@typescript-eslint/parser": "^5.12.0",
24 | "eslint": "^8.9.0",
25 | "eslint-config-google": "^0.14.0",
26 | "eslint-plugin-import": "^2.25.4",
27 | "firebase-functions-test": "^3.1.0",
28 | "typescript": "^5.8.2"
29 | },
30 | "private": true
31 | }
32 |
--------------------------------------------------------------------------------
/functions/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import * as admin from "firebase-admin";
3 | import {Timestamp} from "firebase-admin/firestore";
4 | import {defineSecret} from "firebase-functions/params";
5 | import * as functions from "firebase-functions/v2";
6 |
7 | const app = admin.initializeApp({
8 | credential: admin.credential.applicationDefault(),
9 | });
10 | const db = app.firestore();
11 |
12 | const logCollection = "logs";
13 |
14 | export const updateLogs = functions.scheduler.onSchedule(
15 | {
16 | schedule: "0 0 * * *", // Runs at midnight UTC every day
17 | timeZone: "Etc/UTC",
18 | retryCount: 3,
19 | memory: "256MiB", // Optional: Customize memory allocation
20 | timeoutSeconds: 60, // Optional: Set timeout for the function
21 | },
22 | async (event: any) => {
23 | try {
24 | const logsCollection = db.collection("logs");
25 | const logsSnapshot = await logsCollection.get();
26 | console.log(
27 | `Checking for expired logs... in ${logCollection} against ${logsSnapshot.size} documents`
28 | );
29 | if (logsSnapshot.empty) {
30 | console.log("Not found in logs collection....");
31 | return; // Return nothing instead of null
32 | }
33 |
34 | const currentDate = new Date();
35 | const batch = db.batch();
36 |
37 | logsSnapshot.forEach((doc) => {
38 | const logData = doc.data();
39 | const expiryDateString = logData.expiryDate;
40 |
41 | // Ensure expiryDate is correctly parsed
42 | const expiryDate = expiryDateString ?
43 | new Date(expiryDateString) :
44 | null;
45 |
46 | // Convert both dates to UTC to ensure correct comparison
47 | const isExpired = expiryDate ?
48 | expiryDate.getTime() <= currentDate.getTime() :
49 | false;
50 |
51 | batch.update(doc.ref, {isExpired});
52 | });
53 |
54 | await batch.commit();
55 | console.log("Expiry check and update completed.");
56 | return;
57 | } catch (error) {
58 | console.error("Error checking expiry:", error);
59 | }
60 | }
61 | );
62 |
63 | // move collections with `isExpired` to "expiredNotes"
64 | export const moveExpiredLogs = functions.scheduler.onSchedule(
65 | {
66 | // Run once every week at midnight UTC on Sunday
67 | schedule: "0 0 * * 0",
68 | timeZone: "Etc/UTC",
69 | retryCount: 3,
70 | memory: "256MiB",
71 | timeoutSeconds: 60,
72 | secrets: ["EXPIRED_COLLECTION", "NOTES_COLLECTION"],
73 | },
74 | async () => {
75 | try {
76 | const expiredCollection = defineSecret("EXPIRED_COLLECTION");
77 | const notesCollection = defineSecret("NOTES_COLLECTION");
78 | const logsCollection = db.collection(notesCollection.value());
79 | const expiredNotesCollection = db.collection(
80 | expiredCollection.value()
81 | );
82 | const now = Timestamp.now(); // Current timestamp
83 |
84 | const logsSnapshot = await logsCollection.get();
85 |
86 | console.log(`Total logs fetched: ${logsSnapshot.size}`);
87 |
88 | if (logsSnapshot.empty) {
89 | console.log("No logs found.");
90 | return;
91 | }
92 |
93 | const batch = db.batch();
94 |
95 | let expiredMovedCount = 0;
96 | let skippedNoExpiryCount = 0;
97 | let invalidFormatCount = 0;
98 |
99 | logsSnapshot.forEach((doc) => {
100 | const docData = doc.data();
101 | const expiryDate = docData.expiryDate;
102 |
103 | if (!expiryDate) {
104 | skippedNoExpiryCount++;
105 | return;
106 | }
107 |
108 | if (expiryDate instanceof Timestamp) {
109 | if (expiryDate.toMillis() <= now.toMillis()) {
110 | const docRef = logsCollection.doc(doc.id);
111 | batch.update(docRef, {isExpired: true});
112 |
113 | const expiredDocRef = expiredNotesCollection.doc(
114 | doc.id
115 | );
116 | batch.set(expiredDocRef, {
117 | ...docData,
118 | isExpired: true,
119 | });
120 |
121 | batch.delete(docRef);
122 |
123 | expiredMovedCount++;
124 | }
125 | } else {
126 | invalidFormatCount++;
127 | }
128 | });
129 |
130 | await batch.commit();
131 |
132 | console.log("✅ Migration completed!");
133 | console.log(`- Expired logs moved: ${expiredMovedCount}`);
134 | console.log(`- Logs without expiryDate: ${skippedNoExpiryCount}`);
135 | console.log(
136 | `- Logs with invalid expiryDate format: ${invalidFormatCount}`
137 | );
138 | } catch (error) {
139 | console.error("Error moving expired logs:", error);
140 | }
141 | }
142 | );
143 |
--------------------------------------------------------------------------------
/functions/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | ".eslintrc.js"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "NodeNext",
4 | "esModuleInterop": true,
5 | "moduleResolution": "nodenext",
6 | "noImplicitReturns": true,
7 | "noUnusedLocals": true,
8 | "outDir": "lib",
9 | "sourceMap": true,
10 | "strict": true,
11 | "target": "es2017"
12 | },
13 | "compileOnSave": true,
14 | "include": [
15 | "src"
16 | ]
17 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | import withPWAInit from "@ducanh2912/next-pwa";
3 |
4 | const withPWA = withPWAInit({
5 | dest: "public",
6 | });
7 |
8 | export default withPWA({
9 | trailingSlash: true,
10 | images: {
11 | unoptimized: true
12 | },
13 | reactStrictMode: true,
14 | });
15 | // const nextConfig = {
16 | // // output: 'export',
17 | // trailingSlash: true,
18 | // images: {
19 | // unoptimized: true
20 | // }
21 | // };
22 |
23 | // export default nextConfig;
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pastelog",
3 | "version": "0.6.3",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ducanh2912/next-pwa": "^10.2.9",
13 | "@emotion/react": "^11.11.4",
14 | "@emotion/styled": "^11.11.5",
15 | "@google/generative-ai": "^0.14.0",
16 | "@heroicons/react": "^2.1.3",
17 | "@mui/icons-material": "^5.15.19",
18 | "@mui/material": "^5.15.19",
19 | "@nextui-org/react": "^2.4.0",
20 | "@radix-ui/react-accordion": "^1.2.2",
21 | "@radix-ui/react-dialog": "^1.1.6",
22 | "@radix-ui/react-dropdown-menu": "^2.1.2",
23 | "@radix-ui/react-hover-card": "^1.1.1",
24 | "@radix-ui/react-icons": "^1.3.0",
25 | "@radix-ui/react-navigation-menu": "^1.1.4",
26 | "@radix-ui/react-popover": "^1.1.1",
27 | "@radix-ui/react-select": "^2.1.1",
28 | "@radix-ui/react-slot": "^1.1.0",
29 | "@reduxjs/toolkit": "^2.5.0",
30 | "axios": "^1.7.2",
31 | "class-variance-authority": "^0.7.0",
32 | "clsx": "^2.1.1",
33 | "date-fns": "^3.6.0",
34 | "firebase": "^10.14.0",
35 | "framer-motion": "^11.15.0",
36 | "html2canvas": "^1.4.1",
37 | "next": "^14.2.3",
38 | "next-themes": "^0.3.0",
39 | "react": "^18",
40 | "react-day-picker": "^8.10.1",
41 | "react-dom": "^18",
42 | "react-icons": "^5.2.1",
43 | "react-markdown": "^9.0.1",
44 | "react-redux": "^9.2.0",
45 | "react-syntax-highlighter": "^15.5.0",
46 | "react-toastify": "^10.0.5",
47 | "rehype-highlight": "^7.0.0",
48 | "remark-gfm": "^4.0.0",
49 | "tailwind-merge": "^2.3.0",
50 | "tailwindcss-animate": "^1.0.7",
51 | "uuid": "^9.0.1"
52 | },
53 | "devDependencies": {
54 | "@tailwindcss/typography": "^0.5.13",
55 | "@types/node": "^20",
56 | "@types/react": "^18",
57 | "@types/react-dom": "^18",
58 | "@types/react-syntax-highlighter": "^15.5.13",
59 | "eslint": "^8",
60 | "eslint-config-next": "14.2.3",
61 | "postcss": "^8",
62 | "tailwindcss": "^3.4.1",
63 | "typescript": "^5",
64 | "webpack": "^5.95.0"
65 | }
66 | }
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon/favicon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/favicon-48x48.png
--------------------------------------------------------------------------------
/public/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/favicon.ico
--------------------------------------------------------------------------------
/public/favicon/icon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/icon-16x16.png
--------------------------------------------------------------------------------
/public/favicon/icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/icon-180x180.png
--------------------------------------------------------------------------------
/public/favicon/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/icon-192x192.png
--------------------------------------------------------------------------------
/public/favicon/icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/icon-48x48.png
--------------------------------------------------------------------------------
/public/favicon/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/favicon/icon-512x512.png
--------------------------------------------------------------------------------
/public/images/cover-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/images/cover-dark.png
--------------------------------------------------------------------------------
/public/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/images/cover.png
--------------------------------------------------------------------------------
/public/images/frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/images/frame.png
--------------------------------------------------------------------------------
/public/images/gemini.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/images/gemini.png
--------------------------------------------------------------------------------
/public/images/local.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/images/local.png
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/public/images/preview.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pastelog",
3 | "short_name": "Pastelog",
4 | "description": "Create Stunning AI Powered Rich notes with markdown Support",
5 | "start_url": "/",
6 | "display": "standalone",
7 | "background_color": "#ffffff",
8 | "theme_color": "#000000",
9 | "icons": [
10 | {
11 | "src": "/favicon/icon-16x16.png",
12 | "sizes": "16x16",
13 | "type": "image/png",
14 | "purpose": "any"
15 | },
16 | {
17 | "src": "/favicon/icon-48x48.png",
18 | "sizes": "48x48",
19 | "type": "image/png",
20 | "purpose": "any"
21 | },
22 | {
23 | "src": "/favicon/icon-180x180.png",
24 | "sizes": "180x180",
25 | "type": "image/png",
26 | "purpose": "any"
27 | },
28 | {
29 | "src": "/favicon/icon-192x192.png",
30 | "sizes": "192x192",
31 | "type": "image/png",
32 | "purpose": "any"
33 | },
34 | {
35 | "src": "/favicon/icon-512x512.png",
36 | "sizes": "512x512",
37 | "type": "image/png",
38 | "purpose": "any"
39 | }
40 | ],
41 | "screenshots": [
42 | {
43 | "src": "/images/cover.png",
44 | "sizes": "3104x1974",
45 | "form_factor": "wide",
46 | "type": "image/png",
47 | "label": "Preview of Pastelog notes"
48 | },
49 | {
50 | "src": "/images/preview.png",
51 | "sizes": "1828x1974",
52 | "type": "image/png",
53 | "label": "Preview of Pastelog notes"
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 |
14 | // If the loader is already loaded, just stop.
15 | if (!self.define) {
16 | let registry = {};
17 |
18 | // Used for `eval` and `importScripts` where we can't get script URL by other means.
19 | // In both cases, it's safe to use a global var because those functions are synchronous.
20 | let nextDefineUri;
21 |
22 | const singleRequire = (uri, parentUri) => {
23 | uri = new URL(uri + ".js", parentUri).href;
24 | return registry[uri] || (
25 |
26 | new Promise(resolve => {
27 | if ("document" in self) {
28 | const script = document.createElement("script");
29 | script.src = uri;
30 | script.onload = resolve;
31 | document.head.appendChild(script);
32 | } else {
33 | nextDefineUri = uri;
34 | importScripts(uri);
35 | resolve();
36 | }
37 | })
38 |
39 | .then(() => {
40 | let promise = registry[uri];
41 | if (!promise) {
42 | throw new Error(`Module ${uri} didn’t register its module`);
43 | }
44 | return promise;
45 | })
46 | );
47 | };
48 |
49 | self.define = (depsNames, factory) => {
50 | const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
51 | if (registry[uri]) {
52 | // Module is already loading or loaded.
53 | return;
54 | }
55 | let exports = {};
56 | const require = depUri => singleRequire(depUri, uri);
57 | const specialDeps = {
58 | module: { uri },
59 | exports,
60 | require
61 | };
62 | registry[uri] = Promise.all(depsNames.map(
63 | depName => specialDeps[depName] || require(depName)
64 | )).then(deps => {
65 | factory(...deps);
66 | return exports;
67 | });
68 | };
69 | }
70 | define(['./workbox-7144475a'], (function (workbox) { 'use strict';
71 |
72 | importScripts();
73 | self.skipWaiting();
74 | workbox.clientsClaim();
75 | workbox.registerRoute("/", new workbox.NetworkFirst({
76 | "cacheName": "start-url",
77 | plugins: [{
78 | cacheWillUpdate: async ({
79 | response: e
80 | }) => e && "opaqueredirect" === e.type ? new Response(e.body, {
81 | status: 200,
82 | statusText: "OK",
83 | headers: e.headers
84 | }) : e
85 | }]
86 | }), 'GET');
87 | workbox.registerRoute(/.*/i, new workbox.NetworkOnly({
88 | "cacheName": "dev",
89 | plugins: []
90 | }), 'GET');
91 |
92 | }));
93 | //# sourceMappingURL=sw.js.map
94 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/CodeHighlight.tsx:
--------------------------------------------------------------------------------
1 | import ContentCopyIcon from '@mui/icons-material/ContentCopy';
2 | import DoneIcon from '@mui/icons-material/Done';
3 | import { useTheme } from 'next-themes';
4 | import { useState } from 'react';
5 | import SyntaxHighlighter from 'react-syntax-highlighter';
6 | import { docco } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
7 | import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/hljs';
8 | import IconButton from './IconButton';
9 | import { Theme } from './ThemeSwitcher';
10 | interface CodeBlockProps {
11 | language: string; // The programming language of the code block (e.g., 'javascript', 'python')
12 | children: string; // The code to be highlighted
13 | }
14 | /**
15 | *
16 | *
17 | * @param {*} { language, children, ...rest }
18 | * @return {*}
19 | */
20 | const CodeBlock: React.FC = ({ language, children, ...rest }) => {
21 | const { theme } = useTheme();
22 | const dark = theme == Theme.DARK;
23 | const [copied, setCopied] = useState(false);
24 | const handleCopy = () => {
25 | navigator.clipboard.writeText(children);
26 | setCopied(true);
27 | setTimeout(() => {
28 | setCopied(false);
29 | }, 2000);
30 | }
31 | return (
32 |
33 | {!copied ?
38 | ( )
41 | :
42 | ( )
45 | }
46 |
47 |
62 | {children}
63 |
64 |
65 | );
66 | };
67 |
68 | export default CodeBlock;
--------------------------------------------------------------------------------
/src/app/(main)/_components/CopyIcon.tsx:
--------------------------------------------------------------------------------
1 | import { showToast } from "@/utils/toast_utils";
2 | import React, { useState } from "react";
3 | import { toast } from "react-toastify";
4 | import IconButton from './IconButton';
5 |
6 | interface CopyIconProps {
7 | className?: string
8 | icon?: React.ReactNode
9 | copiedIcon?: React.ReactNode
10 | onClick?: () => void
11 | notifyCopy?: boolean
12 | tooltip?: string
13 | message: string
14 | data: string
15 | id: string
16 | size?: "sm" | "md" | "lg"
17 | }
18 | export default function CopyIcon({ id, className, icon, message, copiedIcon, onClick, data, tooltip, size = 'md', notifyCopy = true }: CopyIconProps) {
19 | const [copied, setCopied] = useState(false);
20 |
21 |
22 | function handleIconClick() {
23 | onClick && onClick();
24 | navigator.clipboard.writeText(data)
25 | .then(() => {
26 | setCopied(true);
27 | notifyCopy && notify(message);
28 | })
29 | .catch(() => {
30 | notifyCopy && notify("Failed to copy!");
31 | });
32 | setTimeout(() => {
33 | setCopied(false);
34 | }, 2000);
35 | }
36 |
37 |
38 | const toastId = React.useRef(id);
39 |
40 | const notify = (message: string) => {
41 | if (!toast.isActive(toastId.current)) {
42 | showToast(
43 | "success",
44 | {message}
,
45 | {
46 | toastId: toastId.current,
47 | autoClose: 1000
48 | },
49 | );
50 | }
51 | };
52 |
53 | return (
54 |
58 | {!copied ? icon : copiedIcon}
59 |
60 | )
61 |
62 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/DatePicker.tsx:
--------------------------------------------------------------------------------
1 | import { RootState } from "@/lib/store";
2 | import DateUtils from "@/utils/DateUtils";
3 | import { cn } from "@nextui-org/react";
4 | import { CalendarIcon } from "@radix-ui/react-icons";
5 | import React from "react";
6 | import { useSelector } from "react-redux";
7 | import { Button } from "./button";
8 | import { Calendar } from "./calendar";
9 | import { Popover, PopoverContent, PopoverTrigger } from "./popover";
10 |
11 | interface DatePickerProps {
12 | selected?: Date;
13 | onDateSelect: (date: string) => void;
14 | label?: string;
15 | }
16 |
17 | export function DatePicker({ selected, onDateSelect, label }: DatePickerProps) {
18 | const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
19 | const today = new Date();
20 | // const isSmall = useSmallScreen();
21 | const user = useSelector((state: RootState) => state.auth.user);
22 | return (
23 |
27 |
28 |
29 |
33 |
34 | {selected ? {DateUtils.formatDateMMMMDDYYYY(selected)} : Pick a date }
35 |
36 | {(label &&
{label}
)}
37 |
38 |
39 |
40 | {
48 | if (onDateSelect) {
49 | onDateSelect(e!.toDateString());
50 | }
51 | setIsPopoverOpen(false);
52 | }}
53 | initialFocus
54 | />
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Dialog/Changelog.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/react";
2 | import { EyeOpenIcon } from "@radix-ui/react-icons";
3 | import React from "react";
4 | import { Button } from "../button";
5 | import MDPreview from "../MDPreview";
6 | interface ChangelogDialogProps {
7 | isOpen: boolean;
8 | onClose: () => void;
9 | onShare: () => void; // Function to handle share action
10 | title: string; // Title for the modal header
11 | content: string; // Content to display in the modal body
12 | }
13 |
14 | const ChangelogDialog: React.FC = ({ isOpen, onClose, onShare, title, content }) => {
15 | return (
16 |
17 |
18 | {title}
19 |
20 |
22 |
23 |
24 |
27 | Close
28 |
29 |
32 | Preview
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default ChangelogDialog;
41 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Dialog/Delete.tsx:
--------------------------------------------------------------------------------
1 | import { RootState } from "@/lib/store";
2 | import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/react";
3 | import React from "react";
4 | import { useSelector } from "react-redux";
5 |
6 | interface DeleteDialogProps {
7 | isOpen: boolean;
8 | onClose: () => void;
9 | onDelete: (deleteLocal: boolean) => void; // Function to handle delete action
10 | title: string; // Title for the modal header
11 | content: string; // Content to display in the modal body
12 | }
13 |
14 | const DeleteDialog: React.FC = ({ isOpen, onClose, onDelete, title, content }) => {
15 | const [deleteLocal, setDeleteLocal] = React.useState(true);
16 | const user = useSelector((state: RootState) => state.auth.user);
17 | return (
18 | {
24 | e.stopPropagation();
25 | }
26 | }
27 | >
28 |
29 | {(onClose) => (
30 | <>
31 | {title}
32 |
33 | {content}
34 |
35 | {/*
setDeleteLocal(!deleteLocal)}
39 | classNames={{
40 | label: "text-small",
41 | }}
42 | >
43 | */}
44 | {!user && (
45 | Note that this log will still be available at its unique link until it expires.
46 |
47 | )}
48 |
49 |
50 |
51 |
52 | Cancel
53 |
54 | { onDelete(deleteLocal); onClose(); }}>
56 | Delete
57 |
58 |
59 | >
60 | )}
61 |
62 |
63 | );
64 | };
65 |
66 | export default DeleteDialog;
67 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Dialog/GeminiDialog.tsx:
--------------------------------------------------------------------------------
1 | import { showToast } from "@/utils/toast_utils";
2 | import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/react";
3 | import Image from "next/image";
4 | import React from "react";
5 | import { toast } from "react-toastify";
6 | import Analytics from "../../_services/Analytics";
7 | import GradientText from "../GradientText";
8 | import PSInput from "../PSInput";
9 | import { Button } from "../button";
10 | interface GeminiDialogProps {
11 | isOpen: boolean;
12 | onClose: () => void;
13 | onSave: (api: string) => void;
14 | title: string;
15 | content: string;
16 | }
17 |
18 | const GeminiDialog: React.FC = ({ isOpen, onClose, onSave, title, content }) => {
19 | const toastId = React.useRef('stored-toast');
20 |
21 | const [value, setValue] = React.useState('');
22 | const notify = (message: string) => {
23 | if (!toast.isActive(toastId.current!)) {
24 | showToast("success", {message}
,
25 | {
26 | toastId: 'stored-toast',
27 | }
28 | );
29 | }
30 | Analytics.logEvent('API key Stored');
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
45 |
46 |
47 | {content}
48 |
49 |
50 |
51 |
setValue(e.target.value)}
54 | placeHolder="Enter Your API key"
55 | className="mt-1 w-full" />
56 | Note: The API key is stored in memory only for the time this app runs.
57 | How to get the API key?
58 | 1. Go to ai.google.dev/gemini-api and login.
59 | {"2. Click -> Get API key in Google AI Studio"}
60 | 3. Create API key
61 |
62 |
63 |
64 |
67 | Close
68 |
69 | {
71 | if (value) {
72 | notify('API Key Stored in Ephemeral Storage');
73 | onSave(value);
74 | onClose();
75 | }
76 | }}
77 | className={`bg-gradient-to-r from-gray-700 to-gray-800`}>
78 | Store API key
79 |
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default GeminiDialog;
87 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Dialog/Import.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/react";
2 | import { UploadIcon } from "@radix-ui/react-icons";
3 | import React, { useEffect, useState } from "react";
4 | import PSInput from "../PSInput";
5 | import { Button } from "../button";
6 | interface ImportDialogProps {
7 | isOpen: boolean;
8 | onClose: () => void;
9 | onImport: (url: string) => void;
10 | title: string;
11 | content: string;
12 | importLoading: boolean;
13 | }
14 |
15 | const ImportDialog: React.FC = ({ isOpen, onClose, onImport, title, content, importLoading }) => {
16 | const [url, setUrl] = useState('');
17 | useEffect(() => {
18 | setUrl('');
19 | }, []);
20 | return (
21 |
22 |
23 | {title}
24 |
25 | {content}
26 | setUrl(e.target.value)}
30 | placeHolder="Enter the URL"
31 | />
32 |
33 |
34 |
37 | Close
38 |
39 | {
41 | onImport(url);
42 | }}
43 | className={`bg-gradient-to-r from-gray-700 to-gray-800`}>
44 |
45 | {importLoading ? (
46 |
47 | ) : "Import"}
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default ImportDialog;
57 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Dialog/SearchDialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Constants } from "@/app/constants";
3 | import { setId, setNavbarTitle, setSelected, setShowSideBar } from "@/lib/features/menus/sidebarSlice";
4 | import { AppDispatch, RootState } from "@/lib/store";
5 | import SearchIcon from '@heroicons/react/24/solid/MagnifyingGlassIcon';
6 | import PencilSquareIcon from '@heroicons/react/24/solid/PencilSquareIcon';
7 | import { Divider, Input } from "@nextui-org/react";
8 | import { useRouter } from "next/navigation";
9 | import { useCallback, useEffect, useState } from "react";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import Analytics from "../../_services/Analytics";
12 | import { Dialog, DialogContent, DialogTrigger } from "../dialog";
13 | import IconButton from "../IconButton";
14 |
15 | export function SearchDialog() {
16 | const logs = useSelector((state: RootState) => state.sidebar.logs);
17 | const dispatch = useDispatch();
18 | const router = useRouter();
19 | const [open, setOpen] = useState(false)
20 | const [inputValue, setInputValue] = useState('');
21 |
22 |
23 | function defaultList() {
24 | return logs;
25 | }
26 |
27 | const [filteredLogs, setFilteredLogs] = useState(defaultList());
28 | useEffect(() => {
29 | setFilteredLogs(logs);
30 | }, [logs])
31 |
32 | const onLogClick = useCallback((log: any | null) => {
33 | if (log) {
34 | dispatch(setSelected(log));
35 | setNavbarTitle('');
36 | dispatch(setId(log.id!));
37 | router.push(`/logs/${log.id}`);
38 | Analytics.logEvent('change_log', { id: log.id, action: 'click' });
39 | if (window.innerWidth <= 640) {
40 | dispatch(setShowSideBar(false));
41 | }
42 | } else {
43 | dispatch(setSelected(null));
44 | dispatch(setId(null));
45 | router.push(`/logs`);
46 | Analytics.logEvent('new_log');
47 | }
48 | setOpen(false)
49 | }, []);
50 |
51 |
52 | function documentIcon() {
53 | return (
54 |
55 |
56 |
57 | )
58 | }
59 |
60 | const handleInputChange = (e: React.ChangeEvent) => {
61 | setInputValue(e.target.value);
62 | if (e.target.value.length > 0) {
63 | const filteredLogs = logs.filter((log) => log.title.toLowerCase().includes(e.target.value.toLowerCase()));
64 | setFilteredLogs(filteredLogs);
65 | } else {
66 | setFilteredLogs(logs);
67 | }
68 | }
69 |
70 |
71 | function SearchItem(log: any) {
72 | return (
73 |
74 | {documentIcon()}
75 |
onLogClick(log)
78 | }
79 | key={log.id}>
80 |
{log.title}
81 | {/*
82 | {log.data && typeof log.data === 'string'
83 | ? log.data.substring(0, 100)
84 | : ""}
85 |
*/}
86 |
87 |
88 | )
89 | }
90 |
91 |
92 | return (
93 |
94 |
95 | { }}
97 | ariaLabel="Search">
98 |
99 |
100 |
101 |
102 |
103 |
handleInputChange(e)}
106 | placeholder="Search"
107 | className="w-full rounded-t-lg"
108 | classNames={{
109 | inputWrapper: "rounded-t-lg rounded-b-none",
110 | }}
111 | />
112 |
113 |
114 |
onLogClick(null)
117 | }>
118 |
{"New Note"}
119 |
120 |
121 |
122 | {filteredLogs && filteredLogs.length > 0 ? (
123 |
124 | {filteredLogs.map((log) => SearchItem(log))}
125 |
126 | ) : (
127 |
128 |
Notes not found
129 |
130 | )}
131 |
132 |
133 |
134 | )
135 | }
136 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Dialog/Share.tsx:
--------------------------------------------------------------------------------
1 | import { Constants } from "@/app/constants";
2 | import { showToast } from "@/utils/toast_utils";
3 | import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/react";
4 | import { ClipboardCopyIcon, EyeOpenIcon } from "@radix-ui/react-icons";
5 | import React from "react";
6 | import { toast } from "react-toastify";
7 | import IconButton from "../IconButton";
8 | import { Button } from "../button";
9 | interface ShareDialogProps {
10 | isOpen: boolean;
11 | onClose: () => void;
12 | onShare: () => void; // Function to handle share action
13 | title: string; // Title for the modal header
14 | content: string; // Content to display in the modal body
15 | }
16 |
17 | const ShareDialog: React.FC = ({ isOpen, onClose, onShare, title, content }) => {
18 | const toastId = React.useRef('copied-toast');
19 | const notify = () => {
20 | if (!toast.isActive(toastId.current!)) {
21 | showToast("success", Link copied!
,
22 | {
23 | toastId: 'copied-toast',
24 | style: {
25 | backgroundColor: 'rgba(0, 0, 0, 0.8)',
26 | color: 'white',
27 | }
28 | }
29 | );
30 | }
31 | }
32 | return (
33 | {
36 | e.stopPropagation();
37 | }
38 | }
39 | size="md" isOpen={isOpen} onClose={onClose} isDismissable={true}>
40 |
41 | {title}
42 |
43 |
44 |
45 |
46 | {content}
47 |
48 |
49 |
50 | {
51 | navigator.clipboard.writeText(content);
52 | notify();
53 | }}>
54 |
55 |
56 |
57 |
58 | Note: This log will be available to anyone with the link.
59 |
60 |
61 |
64 | Close
65 |
66 |
69 | Preview
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default ShareDialog;
78 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from '@nextui-org/react';
2 | import { Key, ReactNode } from 'react';
3 |
4 | interface PSDropdownProps {
5 | options: string[];
6 | onClick: (key: Key) => void;
7 | children: ReactNode;
8 | className?: string;
9 | placement?: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end';
10 | }
11 |
12 | const PSDropdown: React.FC = ({ options, onClick, children, className, placement }) => {
13 | return (
14 |
19 |
20 | {children}
21 |
22 |
26 | {options.map((option, index) => (
27 | {option}
28 | ))}
29 |
30 |
31 | );
32 | };
33 |
34 | export default PSDropdown;
35 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/Editor.tsx:
--------------------------------------------------------------------------------
1 | // src/_components/PSContent.tsx
2 | import dynamic from "next/dynamic";
3 | import { usePathname } from "next/navigation";
4 | import React, { ChangeEvent, useEffect, useRef } from "react";
5 | import { EditorHistoryState } from "../_services/EditorState";
6 | import MDPreview from "./MDPreview";
7 | import TextCompletionInput from "./completion";
8 | // import ReactMarkdown from 'react-markdown';
9 | const ReactMarkdown = dynamic(() => import("react-markdown"), { ssr: false });
10 | interface PSEditorProps {
11 | className?: string;
12 | placeHolder?: string;
13 | value?: string;
14 | preview?: boolean;
15 | disabled?: boolean;
16 | isRepublish?: boolean;
17 | onChange?: (e: ChangeEvent) => void;
18 | }
19 |
20 | const Editor: React.FC = ({ value, onChange, placeHolder, preview, disabled, className, isRepublish }) => {
21 | const editorStateRef = useRef(new EditorHistoryState(value || ''));
22 |
23 | useEffect(() => {
24 | editorStateRef.current.setCurrentValue(value || '');
25 | }, [value]);
26 |
27 |
28 | const handleChange = (event: React.ChangeEvent) => {
29 | const newValue = event.target.value;
30 | editorStateRef.current.updateValue(newValue);
31 |
32 | if (onChange) {
33 | onChange(event);
34 | }
35 | };
36 |
37 | const handleUndo = () => {
38 | const previousValue = editorStateRef.current.undo();
39 | if (previousValue !== null) {
40 | updateEditorState(previousValue);
41 | }
42 | };
43 |
44 | const handleRedo = () => {
45 | const nextValue = editorStateRef.current.redo();
46 | if (nextValue !== null) {
47 | updateEditorState(nextValue);
48 | }
49 | };
50 |
51 | const updateEditorState = (newValue: string) => {
52 | if (onChange) {
53 | const syntheticEvent = {
54 | target: { value: newValue }
55 | } as React.ChangeEvent;
56 | onChange(syntheticEvent);
57 | }
58 | };
59 |
60 | const placeholder: string =
61 | `Start typing here...
62 | \nPublish your logs to the cloud and access them from anywhere via a unique link.
63 | \nNo Sign up required.
64 | \n
65 | \nNote: Do not publish sensitive information here, these logs are public and can be accessed by anyone with the link.
66 | `;
67 | const pathName = usePathname();
68 | const isPublishRoute = pathName.includes('/logs/publish');
69 | const customClass = `px-2 py-2 rounded-b-lg border-surface focus:ring-secondary focus:outline-none focus:ring-2 focus:ring-2 resize-y min-h-80 w-full ${className}`;
70 |
71 | var previewClass = '';
72 | if (preview) {
73 | previewClass = 'fade-in-animation';
74 | } else {
75 |
76 | previewClass = 'fade-out-animation'
77 | };
78 | return (
79 |
80 | {preview ? (
81 | // if value is empty, show placeholder
82 | value ? (
83 |
87 | ) : (!isPublishRoute &&
88 |
89 |
Nothing to Preview
90 |
)
91 | ) : (
92 |
105 | )}
106 |
107 | )
108 |
109 | };
110 |
111 | export default Editor;
112 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/GeminiIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip, useDisclosure } from '@nextui-org/react';
2 | import React from 'react';
3 | import { useSidebar } from '../_hooks/useSidebar';
4 | import GeminiDialog from './Dialog/GeminiDialog';
5 |
6 | interface GeminiProps {
7 | onGeminiTrigger: () => void;
8 | className?: string;
9 | children?: React.ReactNode;
10 | toolTip?: string;
11 | }
12 |
13 | const GeminiIcon: React.FC = ({ onGeminiTrigger, className, children, toolTip }) => {
14 | const geminiContent = {
15 | title: "Gemini",
16 | content: 'With the power of Gemini, you can summarize long notes content. Enter your API key to get started.',
17 | };
18 | const { isOpen: geminiOpen, onOpen: onGeminiOpen, onClose: onGeminiClose } = useDisclosure();
19 | const { apiKey, setApiKey } = useSidebar();
20 | const onGeminiApiSave = (key: string) => {
21 | if (key) {
22 | setApiKey(key);
23 | }
24 | };
25 |
26 | return (
27 |
28 |
31 | {
32 | if (apiKey === undefined || apiKey === null || apiKey === '') {
33 | onGeminiOpen();
34 | } else {
35 | onGeminiTrigger();
36 | }
37 | }}>
38 | {children}
39 |
40 |
41 |
48 |
49 | );
50 | };
51 |
52 | export default GeminiIcon;
53 |
54 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/GradientText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface GradientTextProps {
4 | text: string;
5 | gradientColors: string[];
6 | fontSize?: string;
7 | fontWeight?: string | number;
8 | fontFamily?: string;
9 | className?: string;
10 | }
11 |
12 | const GradientText: React.FC = ({
13 | text,
14 | gradientColors,
15 | fontSize = '3rem',
16 | fontWeight = 'bold',
17 | fontFamily = 'Arial, sans-serif',
18 | className = '',
19 | }) => {
20 | const gradientStyle = {
21 | background: `linear-gradient(to right, ${gradientColors.join(', ')})`,
22 | WebkitBackgroundClip: 'text',
23 | WebkitTextFillColor: 'transparent',
24 | backgroundClip: 'text',
25 | color: 'transparent',
26 | fontSize,
27 | fontWeight,
28 | fontFamily,
29 | };
30 |
31 | return (
32 |
33 | {text}
34 |
35 | );
36 | };
37 |
38 | export default GradientText;
--------------------------------------------------------------------------------
/src/app/(main)/_components/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@nextui-org/button';
2 | import { Tooltip } from "@nextui-org/tooltip";
3 | type IconButtonProps = {
4 | onClick?: () => void;
5 | children: React.ReactNode;
6 | ariaLabel?: string;
7 | className?: string;
8 | size?: "sm" | "md" | "lg";
9 | tooltipPlacement?: "top-start" | "top" | "top-end" | "right-start" | "right" | "right-end" | "bottom-start" | "bottom" | "bottom-end" | "left-start" | "left" | "left-end";
10 | };
11 |
12 | const IconButton: React.FC = ({ onClick, children, ariaLabel, className, tooltipPlacement, size }) => {
13 |
14 | if (!ariaLabel) {
15 | return (
16 |
22 | {children}
23 |
24 | );
25 | }
26 |
27 | return (
28 |
33 |
39 | {children}
40 |
41 |
42 | );
43 | };
44 |
45 | export default IconButton;
--------------------------------------------------------------------------------
/src/app/(main)/_components/MDPreview.tsx:
--------------------------------------------------------------------------------
1 | // src/_components/PSContent.tsx
2 | import { Constants } from "@/app/constants";
3 | import ContentCopyIcon from '@mui/icons-material/ContentCopy';
4 | import DoneIcon from '@mui/icons-material/Done';
5 | import { Tooltip } from "@nextui-org/react";
6 | import dynamic from "next/dynamic";
7 | import remarkGfm from "remark-gfm";
8 | import CodeBlock from "./CodeHighlight";
9 | import CopyIcon from "./CopyIcon";
10 | const ReactMarkdown = dynamic(() => import("react-markdown"), { ssr: false });
11 |
12 | interface MDPreviewProps {
13 | value?: string;
14 | className?: string;
15 | }
16 |
17 | const MDPreview = ({ value, className }: MDPreviewProps) => {
18 | const customClass = `px-2 py-2 rounded-b-lg border-surface focus:ring-secondary focus:outline-none focus:ring-2 resize-y min-h-60 w-full reactMarkDown ${className}`;
19 |
20 | function LinkRenderer(props: any) {
21 | return (
22 | window.open(props.href, '_blank')}
25 | content={
26 |
27 | {props.href.length > 40 ? props.href.substring(0, 40) + '...' : props.href}
28 | }
33 | icon={ }
34 | size='sm'
35 | notifyCopy={false}
36 | />
37 |
38 | }
39 | placement='top-start'>
40 |
41 | {props.children}
42 |
43 |
44 | );
45 | }
46 | return
52 |
61 | {String(children).replace(/\n$/, '')}
62 |
63 | ) : (
64 |
65 | {children}
66 |
67 | )
68 | }
69 | }}
70 | remarkPlugins={[remarkGfm]}
71 | // rehypePlugins={[rehypeHighlight]}
72 | // eslint-disable-next-line react/no-children-prop
73 | children={value}
74 | />
75 |
76 | }
77 |
78 | export default MDPreview;
--------------------------------------------------------------------------------
/src/app/(main)/_components/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | interface NavigationProps {
4 | darkTheme: boolean;
5 | index: number | null;
6 | scrollToSection: (index: number) => void;
7 | }
8 |
9 | export function Navigation({ darkTheme, index, scrollToSection }: NavigationProps) {
10 | const sectionTitles = [
11 | "Introduction",
12 | "Beautiful Markdown",
13 | "Powered By Gemini",
14 | "Keyboard Shortcuts",
15 | "Create and Share",
16 | "Dark Mode",
17 | "Save Locally"
18 | ];
19 |
20 | return (
21 |
22 |
23 |
30 |
31 |
32 | {sectionTitles.slice(1).map((title, secIndex) => (
33 |
scrollToSection(secIndex + 1)}
37 | >
38 | {title}
39 |
40 | ))}
41 |
42 |
43 | );
44 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/PSAccordian.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from './accordion';
7 |
8 | interface PSAccordionProps {
9 | id: string;
10 | title: string;
11 | className?: string;
12 | children: React.ReactNode;
13 | }
14 |
15 | export default function PSAccordion({ id, title, children, className }: PSAccordionProps) {
16 | return (
17 |
20 |
21 | {title}
22 |
23 | {children}
24 |
25 |
26 |
27 | );
28 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/PSBanner.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import useBannerState from '../_services/BannerState';
3 |
4 | type BannerProps = {
5 | children?: React.ReactNode;
6 | message: string;
7 | className?: string;
8 | show: boolean;
9 | };
10 |
11 | const PSBanner: React.FC = ({ children, className, show }) => {
12 | const [height, setHeight] = useState(undefined);
13 | const contentRef = useRef(null);
14 | const { show: showRemote, message } = useBannerState();
15 | useEffect(() => {
16 | if (contentRef.current) {
17 | setHeight(contentRef.current.scrollHeight);
18 | }
19 | }, [message, children]);
20 |
21 | useEffect(() => {
22 | }, [show, showRemote, message]);
23 |
24 | return (
25 |
33 |
34 |
35 | {children}
36 |
37 |
38 | );
39 | };
40 |
41 | export default PSBanner;
--------------------------------------------------------------------------------
/src/app/(main)/_components/PSInput.tsx:
--------------------------------------------------------------------------------
1 | // src/_components/PSInput.tsx
2 | import React, { ChangeEvent } from "react";
3 |
4 | interface DescriptionInputProps {
5 | className?: string;
6 | placeHolder?: string;
7 | value?: string;
8 | disabled?: boolean,
9 | onChange?: (e: ChangeEvent) => void;
10 | }
11 |
12 | const PSInput: React.FC = ({ value, onChange, disabled, placeHolder, className }) => {
13 | return (
14 |
22 | );
23 | };
24 |
25 | export default PSInput;
26 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/PSNavbar.tsx:
--------------------------------------------------------------------------------
1 | import { useNavbar } from '@/lib/Context/PSNavbarProvider';
2 | import { setId, setSelected, setShowSideBar } from '@/lib/features/menus/sidebarSlice';
3 | import { AppDispatch, RootState } from '@/lib/store';
4 | import { AnimatePresence, motion } from 'framer-motion';
5 | import Image from 'next/image';
6 | import { usePathname, useRouter } from "next/navigation";
7 | import { FiSidebar } from 'react-icons/fi';
8 | import { useDispatch, useSelector } from 'react-redux';
9 | import IconButton from './IconButton';
10 | import { ThemeSwitcher } from "./ThemeSwitcher";
11 |
12 | interface PSNavbarProps {
13 | className?: string;
14 | sideBarIcon?: boolean;
15 | onToggleSidebar?: () => void;
16 | }
17 | const PSNavbar: React.FC = ({ sideBarIcon, className }) => {
18 | const router = useRouter();
19 | const pathName = usePathname();
20 | const isPublishRoute = pathName.includes('/logs/publish');
21 | const { navbarTitle } = useNavbar();
22 | const showSideBar = useSelector((state: RootState) => state.sidebar.showSideBar);
23 | const dispatch = useDispatch();
24 | return (
25 |
26 |
27 |
28 | {
29 | sideBarIcon && (
30 |
{
33 | dispatch(setShowSideBar(!showSideBar));
34 | }}
35 | ariaLabel="Open Sidebar">
36 |
37 |
38 | )}
39 |
{
41 | dispatch(setId(null))
42 | dispatch(setSelected(null));
43 | router.push('/logs');
44 | }}>
45 |
52 |
53 |
54 | {/* appbar content */}
55 |
56 |
57 | {navbarTitle && (
58 |
66 | {navbarTitle}
67 |
68 | )}
69 |
70 |
71 |
72 |
73 | {isPublishRoute && }
74 |
75 |
76 | )
77 | }
78 | export default PSNavbar;
--------------------------------------------------------------------------------
/src/app/(main)/_components/RouteClient.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { RootState } from '@/lib/store';
3 | import { usePathname, useSearchParams } from 'next/navigation';
4 | import { useEffect } from 'react';
5 | import { useSelector } from 'react-redux';
6 | import Analytics from '../_services/Analytics';
7 |
8 | export default function RouteClient() {
9 | const searchParams = useSearchParams();
10 | const pathName = usePathname();
11 | const id = useSelector((state: RootState) => state.sidebar.id);
12 |
13 | const handleRouteChange = (url: string) => {
14 | switch (url) {
15 | case '/':
16 | Analytics.logPageView(url, 'Welcome');
17 | break;
18 | case '/logs':
19 | Analytics.logPageView(url, 'New Log');
20 | break;
21 | case `/logs/${id}`:
22 | Analytics.logPageView(url, 'individual log');
23 | break;
24 | default:
25 | break;
26 | }
27 | };
28 | useEffect(() => {
29 | handleRouteChange(pathName)
30 | }, [pathName, searchParams]);
31 |
32 | return null;
33 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/ShortCutWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from 'react';
2 |
3 | interface ShortcutWrapperProps {
4 | // callback function to handle shortcut key press with key as argument
5 | onShortCutClick?: (key: string) => void;
6 | children: React.ReactNode;
7 | }
8 |
9 | const ShortcutWrapper: React.FC = ({ onShortCutClick, children }) => {
10 | const handleKeyPress = useCallback((event: KeyboardEvent) => {
11 | if ((event.ctrlKey || event.metaKey) && (event.key == 'p' || event.key == 'n' || event.key == 's' || event.key == 'd') && (!event.shiftKey && !event.altKey)) {
12 | event.preventDefault();
13 | if (onShortCutClick) {
14 | onShortCutClick(event.key);
15 | }
16 | }
17 | }, [onShortCutClick]);
18 |
19 | useEffect(() => {
20 | window.addEventListener('keydown', handleKeyPress);
21 | return () => {
22 | window.removeEventListener('keydown', handleKeyPress);
23 | };
24 | }, [handleKeyPress]);
25 |
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | export default ShortcutWrapper;
34 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/ShortcutsGuide.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { Constants } from "@/app/constants";
3 | import {
4 | HoverCard,
5 | HoverCardContent,
6 | HoverCardTrigger,
7 | } from "@radix-ui/react-hover-card";
8 | import { MdOutlineKeyboardCommandKey } from "react-icons/md";
9 |
10 |
11 | export default function ShortCutsGuide() {
12 |
13 | const ShortcutsMap = [
14 | { keys: '+ P', description: 'Toggle Preview' },
15 | { keys: '+ S', description: 'Toggle Sidebar' },
16 | { keys: '+ D', description: 'Toggle DarkMode' },
17 | { keys: '+ N', description: 'Add a New Note' },
18 | { keys: '+ B', description: 'Bold' },
19 | { keys: '+ I', description: 'Italic' },
20 | { keys: '+ Shift + X', description: 'Strikethrough' },
21 | { keys: '+ Shift + [1-6]', description: 'Heading' },
22 | { keys: '+ K', description: 'Link' },
23 | { keys: '+ E', description: 'Code' },
24 | { keys: '+ Shift + C', description: 'Code Block' },
25 | { keys: '+ U', description: 'Unordered List' },
26 | { keys: '+ Shift + O', description: 'Ordered List' },
27 | { keys: '+ Shift + .', description: 'Blockquote' },
28 | { keys: '+ Shift + -', description: 'Horizontal Rule' },
29 | { keys: 'Tab / Shift + Tab', description: 'Indent/Unindent Code Block' },
30 | ]
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
Keyboard Shortcuts
41 |
42 |
43 | {[...Array(ShortcutsMap.length)].map((_, index) => {
44 | const shortcut: any = ShortcutsMap[index];
45 | return (
46 |
47 | {index === 4 &&
Markdown Shortcuts
}
48 |
49 |
50 |
51 | {/* */}
52 | {shortcut.keys}
53 |
54 |
{shortcut.description}
55 |
56 |
57 | );
58 | })}
59 |
60 |
61 |
62 |
63 | );
64 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/TermsAndPrivacy.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | const TermsAndPrivacy = () => {
4 | return (
5 |
6 |
7 |
Terms of Use and Privacy Policy
8 |
9 |
Terms of Use
10 |
11 | Welcome to Pastelog, an open-source Rich Notes application hosted on GitHub. By using our services, you agree to the following terms and conditions:
12 |
13 |
14 | You are responsible for your content and actions.
15 | You must not misuse our services for any illegal or unauthorized purposes.
16 | We reserve the right to modify or terminate our services at any time.
17 |
18 |
19 | For the complete Terms of Use, please visit our{' '}
20 |
21 | Terms of Use
22 | {' '}
23 | page.
24 |
25 |
26 |
27 |
Privacy Policy
28 |
29 | At Pastelog, we value your privacy and strive to protect your personal information. Our Privacy Policy outlines how we collect, use, and safeguard your data.
30 |
31 |
32 | We collect only the necessary information to provide our services.
33 | We do not share your personal data with third parties without your consent.
34 | We implement industry-standard security measures to protect your data.
35 |
36 |
37 | For more details, please visit our{' '}
38 |
39 | Privacy Policy
40 | {' '}
41 | page.
42 |
43 |
44 |
45 |
46 | Pastelog is an open-source project hosted on GitHub. You can contribute to the project or report issues by visiting our{' '}
47 |
48 | GitHub repository
49 |
50 | .
51 |
52 |
53 | If you have any questions or concerns, please feel free to contact us at{' '}
54 |
55 | {process.env.NEXT_PUBLIC_CONTACT_EMAIL}
56 |
57 | .
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default TermsAndPrivacy;
--------------------------------------------------------------------------------
/src/app/(main)/_components/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 | import { type ThemeProviderProps } from "next-themes/dist/types"
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children}
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/ThemeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | // app/components/ThemeSwitcher.tsx
2 | "use client";
3 |
4 | import { MoonIcon, SunIcon } from '@heroicons/react/24/solid';
5 | import { useTheme } from "next-themes";
6 | import { useEffect, useState } from "react";
7 | import Analytics from '../_services/Analytics';
8 | import IconButton from './IconButton';
9 |
10 | export enum Theme {
11 | LIGHT = "light",
12 | DARK = "dark",
13 | }
14 |
15 | export function ThemeSwitcher() {
16 | const [mounted, setMounted] = useState(false)
17 | const { theme, setTheme } = useTheme()
18 |
19 | useEffect(() => {
20 | setMounted(true)
21 | }, [])
22 |
23 | if (!mounted) return null
24 |
25 | return (
26 |
27 | {
33 | setTheme(theme == Theme.DARK ? 'light' : 'dark');
34 | Analytics.logThemeChange(theme === 'dark' ? 'light' : 'dark');
35 | }}>
36 | {theme === 'dark' ? : }
37 |
38 |
39 | )
40 | };
41 |
42 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/ToastProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ToastContainer } from "react-toastify";
4 | import "react-toastify/dist/ReactToastify.css";
5 |
6 | interface ToastProviderProps {
7 | children: React.ReactNode;
8 | }
9 |
10 | export default function ToastProvider({ children }: ToastProviderProps) {
11 | const contextClass = {
12 | success: "bg-blue-600",
13 | error: "bg-red-600",
14 | info: "bg-gray-600",
15 | warning: "bg-orange-400",
16 | default: "bg-indigo-600",
17 | dark: "bg-white-600 font-gray-300",
18 | };
19 |
20 | return (
21 | <>
22 | {children}
23 |
25 | contextClass[context?.type || "default"] +
26 | " relative flex p-1 min-h-10 rounded-md justify-between overflow-hidden cursor-pointer"
27 | }
28 | bodyClassName={() => "text-sm font-white font-med block p-3"}
29 | position="bottom-left"
30 | autoClose={2000}
31 | />
32 | >
33 | );
34 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/Welcome.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Constants } from "@/app/constants";
3 | import { useRouter } from 'next/navigation';
4 | import { useState } from 'react';
5 | import useSettings from "../_hooks/useSettings";
6 | import Analytics from "../_services/Analytics";
7 | import LogService from '../_services/logService';
8 | import { BeautifulMarkdown } from './features/BeautifulMarkdown';
9 | import { CreateAndShare } from './features/CreateAndShare';
10 | import { DarkMode } from './features/DarkMode';
11 | import { GeminiPowered } from './features/GeminiPowered';
12 | import { Introduction } from './features/Introduction';
13 | import { KeyboardShortcuts } from './features/KeyboardShortcuts';
14 | import { SaveLocally } from './features/SaveLocally';
15 | import { Navigation } from './Navigation';
16 |
17 | export default function Welcome() {
18 | const [loading, setLoading] = useState(false);
19 | const [index, setIndex] = useState(null);
20 | const [darkTheme, setDarkTheme] = useState(false);
21 | const router = useRouter();
22 | const { setNewUser } = useSettings();
23 |
24 | const scrollByScreenHeight = () => {
25 | const currentScrollY = window.scrollY;
26 | const nextScrollY = currentScrollY + window.innerHeight;
27 | window.scrollTo({
28 | top: nextScrollY,
29 | behavior: 'smooth'
30 | });
31 | };
32 |
33 | const saveLocally = async (documentIds: string[]) => {
34 | const logService = new LogService();
35 | for (const id of documentIds) {
36 | try {
37 | const log = await logService.fetchLogById(id);
38 | if (log) {
39 | await logService.saveLogToLocal(log);
40 | } else {
41 | console.warn(`Document with ID: ${id} not found`);
42 | }
43 | } catch (error) {
44 | }
45 | }
46 | }
47 |
48 | const handleGetStarted = async () => {
49 | setLoading(true); // Set loading state to true immediately
50 | saveLocally(Constants.publicLogIds);
51 | setNewUser(false);
52 | try {
53 | await new Promise(resolve => {
54 | setTimeout(() => {
55 | setLoading(false);
56 | router.push('/logs');
57 | resolve();
58 | }, 1500);
59 | });
60 | Analytics.logEvent('get_started', { action: 'click' });
61 | } catch (error) {
62 | setLoading(false);
63 | }
64 | };
65 |
66 | const toggleTheme = () => {
67 | setDarkTheme(prev => !prev); // Toggle between dark and light themes
68 | };
69 |
70 | const scrollToSection = (sectionIndex: number) => {
71 | setIndex(sectionIndex - 1);
72 | const sections = document.querySelectorAll('section');
73 | if (sections[sectionIndex!]) {
74 | sections[sectionIndex!].scrollIntoView({ behavior: 'smooth' });
75 | }
76 | };
77 |
78 | return (
79 |
80 |
85 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { cn } from "@/lib/utils"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDownIcon } from "@radix-ui/react-icons"
6 | import * as React from "react"
7 |
8 | const Accordion = AccordionPrimitive.Root
9 |
10 | const AccordionItem = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
19 | ))
20 | AccordionItem.displayName = "AccordionItem"
21 |
22 | const AccordionTrigger = React.forwardRef<
23 | React.ElementRef,
24 | React.ComponentPropsWithoutRef
25 | >(({ className, children, ...props }, ref) => (
26 |
27 | svg]:rotate-180",
31 | className
32 | )}
33 | {...props}
34 | >
35 | {children}
36 |
37 |
38 |
39 | ))
40 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
41 |
42 | const AccordionContent = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef
45 | >(({ className, children, ...props }, ref) => (
46 |
51 | {children}
52 |
53 | ))
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
55 |
56 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
57 |
58 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap 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 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | disabled: "bg-gray-200 text-gray-400 shadow-none cursor-not-allowed",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2",
26 | sm: "h-8 rounded-md px-3 text-xs",
27 | lg: "h-10 rounded-md px-8",
28 | icon: "h-9 w-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean
42 | state?: 'active' | 'disabled' | 'loading'
43 | }
44 |
45 | const Button = React.forwardRef(
46 | ({ className, variant, size, asChild = false, ...props }, ref) => {
47 | const Comp = asChild ? Slot : "button"
48 | return (
49 |
54 | )
55 | }
56 | )
57 | Button.displayName = "Button"
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "./button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
43 | : "[&:has([aria-selected])]:rounded-md"
44 | ),
45 | day: cn(
46 | buttonVariants({ variant: "ghost" }),
47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
48 | ),
49 | day_range_start: "day-range-start",
50 | day_range_end: "day-range-end",
51 | day_selected:
52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
53 | day_today: "bg-accent text-accent-foreground",
54 | day_outside:
55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
56 | day_disabled: "text-muted-foreground opacity-50",
57 | day_range_middle:
58 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
59 | day_hidden: "invisible",
60 | ...classNames,
61 | }}
62 | components={{
63 | IconLeft: ({ ...props }) => ,
64 | IconRight: ({ ...props }) => ,
65 | }}
66 | {...props}
67 | />
68 | )
69 | }
70 | Calendar.displayName = "Calendar"
71 |
72 | export { Calendar }
73 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { cn } from "@/lib/utils"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import * as React from "react"
7 |
8 | const Dialog = DialogPrimitive.Root
9 |
10 | const DialogTrigger = DialogPrimitive.Trigger
11 |
12 | const DialogPortal = DialogPrimitive.Portal
13 |
14 | const DialogClose = DialogPrimitive.Close
15 |
16 | const DialogOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ))
29 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
30 |
31 | const DialogContent = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef
34 | >(({ className, children, ...props }, ref) => (
35 |
36 |
37 |
45 | {children}
46 |
47 |
48 | Close
49 |
50 |
51 |
52 | ))
53 | DialogContent.displayName = DialogPrimitive.Content.displayName
54 |
55 | const DialogHeader = ({
56 | className,
57 | ...props
58 | }: React.HTMLAttributes) => (
59 |
66 | )
67 | DialogHeader.displayName = "DialogHeader"
68 |
69 | const DialogFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
80 | )
81 | DialogFooter.displayName = "DialogFooter"
82 |
83 | const DialogTitle = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 | DialogTitle.displayName = DialogPrimitive.Title.displayName
97 |
98 | const DialogDescription = React.forwardRef<
99 | React.ElementRef,
100 | React.ComponentPropsWithoutRef
101 | >(({ className, ...props }, ref) => (
102 |
107 | ))
108 | DialogDescription.displayName = DialogPrimitive.Description.displayName
109 |
110 | export {
111 | Dialog, DialogClose,
112 | DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/features/BeautifulMarkdown.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export function BeautifulMarkdown() {
4 | return (
5 |
6 |
7 | Beautiful Markdown with Syntax Highlighting
8 |
9 |
10 |
18 |
19 |
20 | );
21 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/features/CreateAndShare.tsx:
--------------------------------------------------------------------------------
1 | export function CreateAndShare() {
2 | return (
3 |
4 |
5 | Create and Share Logs
6 |
7 | Create stunning logs in minutes with markdown support and share with anyone using a unique URL
8 | Supports exporting logs in Image or Text format
9 |
10 |
17 |
18 |
19 | );
20 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/features/DarkMode.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { Button } from '../button';
3 |
4 | interface DarkModeProps {
5 | darkTheme: boolean;
6 | toggleTheme: () => void;
7 | }
8 |
9 | export function DarkMode({ darkTheme, toggleTheme }: DarkModeProps) {
10 | return (
11 |
12 |
13 |
14 | Dark Mode
15 |
16 |
Toggle between dark and light themes
17 |
21 | {darkTheme ? 'Light Theme' : 'Dark Theme'}
22 |
23 |
24 |
25 |
33 |
34 |
35 | );
36 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/features/GeminiPowered.tsx:
--------------------------------------------------------------------------------
1 | export function GeminiPowered() {
2 | return (
3 |
4 |
5 | Powered By Gemini
6 |
7 | Pastelog comes with Gemini Integrated to help you along the way.
8 |
9 |
16 |
17 |
18 | );
19 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/features/Introduction.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronDownIcon } from "@heroicons/react/24/solid";
2 | import Image from 'next/image';
3 | import { useEffect, useState } from 'react';
4 | import GradientText from '../GradientText';
5 | import { Button } from '../button';
6 |
7 | interface IntroductionProps {
8 | loading: boolean;
9 | handleGetStarted: () => void;
10 | scrollByScreenHeight: () => void;
11 | }
12 |
13 | export function Introduction({
14 | loading,
15 | handleGetStarted,
16 | scrollByScreenHeight
17 | }: IntroductionProps) {
18 | const tagLineWords = ['Easy', 'Fast', 'Powerful'];
19 | const [currentTagLine, setCurrentTagLine] = useState(0);
20 | const tagline = 'Publish Rich Text Notes, and access them with a unique link.';
21 |
22 | useEffect(() => {
23 | const interval = setInterval(() => {
24 | setCurrentTagLine((prev) => (prev + 1) % tagLineWords.length);
25 | }, 3000);
26 | return () => clearInterval(interval);
27 | }, []);
28 |
29 | return (
30 |
31 |
32 | Welcome to Pastelog!
33 |
34 | {/* Fixed height container to prevent layout shift */}
35 |
41 |
42 | {tagline}
43 |
49 | {loading ? (
50 |
51 | ) : "Get Started"}
52 |
53 | Show Your support on ProductHunt
54 |
55 |
60 |
61 |
62 |
67 |
68 |
69 | );
70 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/features/KeyboardShortcuts.tsx:
--------------------------------------------------------------------------------
1 | export function KeyboardShortcuts() {
2 | return (
3 |
4 |
5 | Keyboard Shortcuts for Faster Editing
6 |
7 |
8 |
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/features/SaveLocally.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export function SaveLocally() {
4 | return (
5 |
6 |
7 | Save Logs Locally
8 |
9 | Your logs are saved locally on device, so that you can access them without signing in.
10 |
11 |
19 |
20 |
21 | );
22 | }
--------------------------------------------------------------------------------
/src/app/(main)/_components/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
4 | import * as React from "react";
5 |
6 | import { cn } from "../../../lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardContent, HoverCardTrigger };
30 |
31 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ))
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
34 |
--------------------------------------------------------------------------------
/src/app/(main)/_components/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/src/app/(main)/_hooks/outsideclick.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react';
2 |
3 | const useClickOutside = (ref: RefObject, fn: () => void) => {
4 | useEffect(() => {
5 | const element = ref?.current;
6 | function handleClickOutside(event: Event) {
7 | const dialog = document.querySelector('.gemini-dialog-class');
8 | if (element && !element.contains(event.target as Node | null) && (!dialog || !dialog.contains(event.target as Node | null))) {
9 | fn();
10 | }
11 | }
12 | document.addEventListener('mousedown', handleClickOutside);
13 | return () => {
14 | document.removeEventListener('mousedown', handleClickOutside);
15 | };
16 | }, [ref, fn]);
17 | };
18 |
19 | export default useClickOutside;
--------------------------------------------------------------------------------
/src/app/(main)/_hooks/useSettings.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 |
3 | interface ISettings {
4 | newUser: boolean;
5 | }
6 |
7 | function initState(): ISettings {
8 | if (typeof window !== 'undefined') {
9 | const f = localStorage.getItem(`${process.env.NEXT_PUBLIC_NEW_USER_VISITED}`) ?? 'true';
10 | const firstVisit = f === 'true';
11 | return {
12 | newUser: firstVisit
13 | }
14 | }
15 | return {
16 | newUser: true,
17 | }
18 | }
19 |
20 | const useSettings = () => {
21 | const [settings, setSettings] = useState(initState);
22 |
23 | const toggleNewUser = useCallback(() => {
24 | const newUser = !settings.newUser;
25 | localStorage.setItem(`${process.env.NEXT_PUBLIC_NEW_USER_VISITED}`, newUser ? 'true' : 'false');
26 | setSettings({ newUser });
27 | }, []);
28 |
29 | const setNewUser = useCallback((newUser: boolean) => {
30 | localStorage.setItem(`${process.env.NEXT_PUBLIC_NEW_USER_VISITED}`, newUser ? 'true' : 'false');
31 | setSettings({ newUser });
32 | }, []);
33 |
34 | return {
35 | settings,
36 | toggleNewUser,
37 | setNewUser
38 | }
39 | }
40 |
41 |
42 | export default useSettings;
43 |
--------------------------------------------------------------------------------
/src/app/(main)/_hooks/useSidebar.ts:
--------------------------------------------------------------------------------
1 | // Context.tsx
2 |
3 | import { createContext, useContext } from 'react';
4 |
5 | interface SidebarContextProps {
6 | apiKey: string | null;
7 | setApiKey: (apiKey: string | null) => void;
8 | }
9 |
10 | export const SidebarContext = createContext({
11 | apiKey: null,
12 | setApiKey: () => { },
13 | });
14 |
15 | export function useSidebar() {
16 | const context = useContext(SidebarContext);
17 | if (!context) {
18 | throw new Error('useSidebar must be used within a SidebarProvider');
19 | }
20 | return context;
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/(main)/_hooks/useSmallScreen.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | function useSmallScreen(breakpoint: number = 640) {
4 | const [isSmallScreen, setIsSmallScreen] = useState(null);
5 |
6 | useEffect(() => {
7 | // Check if window is available (browser environment)
8 | if (typeof window === 'undefined') {
9 | return;
10 | }
11 |
12 | const mediaQuery = window.matchMedia(`(max-width: ${breakpoint}px)`);
13 |
14 | // Set initial state
15 | setIsSmallScreen(mediaQuery.matches);
16 |
17 | // Define the event handler
18 | const handleResize = (e: MediaQueryListEvent) => {
19 | setIsSmallScreen(e.matches);
20 | };
21 |
22 | // Add the event listener
23 | mediaQuery.addEventListener('change', handleResize);
24 |
25 | // Cleanup the listener on unmount
26 | return () => {
27 | mediaQuery.removeEventListener('change', handleResize);
28 | };
29 | }, [breakpoint]);
30 |
31 | // Handle the case when `isSmallScreen` is null, which happens on the server
32 | return isSmallScreen;
33 | }
34 |
35 | export default useSmallScreen;
--------------------------------------------------------------------------------
/src/app/(main)/_services/Analytics.ts:
--------------------------------------------------------------------------------
1 | // utils/Analytics.ts
2 | import { logEvent, setUserProperties } from 'firebase/analytics';
3 | import { analytics } from '../../../utils/firebase';
4 |
5 | class Analytics {
6 | private static instance: Analytics;
7 |
8 | private constructor() { }
9 |
10 | public static getInstance(): Analytics {
11 | if (!Analytics.instance) {
12 | Analytics.instance = new Analytics();
13 | }
14 | return Analytics.instance;
15 | }
16 |
17 | public logEvent(eventName: string, eventParams?: { [key: string]: any }): void {
18 | if (analytics) {
19 | logEvent(analytics, eventName, eventParams);
20 | }
21 | }
22 |
23 | public setUserProperty(name: string, value: string): void {
24 | if (analytics) {
25 | setUserProperties(analytics, { [name]: value });
26 | }
27 | }
28 |
29 | public logPageView(page_path: string, page_title: string): void {
30 | this.logEvent('page_view', { page_path, page_title });
31 | }
32 |
33 | // Add more custom methods as needed
34 | public logButtonClick(buttonName: string): void {
35 | this.logEvent('button_click', { button_name: buttonName });
36 | }
37 |
38 | public logSearch(searchTerm: string): void {
39 | this.logEvent('search', { search_term: searchTerm });
40 | }
41 |
42 | public logThemeChange(theme: string): void {
43 | this.logEvent('theme_change', { theme });
44 | }
45 | }
46 |
47 | export default Analytics.getInstance();
--------------------------------------------------------------------------------
/src/app/(main)/_services/AuthService.ts:
--------------------------------------------------------------------------------
1 | import { User as FirebaseUser, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
2 | import { auth } from '../../../utils/firebase';
3 | import { UserService } from './UserService';
4 | import LogService from './logService';
5 |
6 | export class AuthService {
7 | private userService: UserService;
8 |
9 | constructor() {
10 | this.userService = new UserService();
11 | }
12 | async signInWithGoogle(): Promise {
13 | const provider = new GoogleAuthProvider();
14 | const result = await signInWithPopup(auth, provider);
15 | await this.createOrUpdateUserInFirestore(result.user);
16 | const isFirstLogin = await this.isFirstTimeLogin(result.user.uid);
17 | if (isFirstLogin) {
18 | await this.handleFirstTimeLogin(result.user);
19 | }
20 | return result.user;
21 | }
22 |
23 | private async createOrUpdateUserInFirestore(user: FirebaseUser): Promise {
24 | const userModel = {
25 | email: user.email!,
26 | displayName: user.displayName,
27 | photoURL: user.photoURL,
28 | createdAt: new Date(user.metadata.creationTime!),
29 | lastLoginAt: new Date(),
30 | id: user.uid
31 | };
32 | await this.userService.createOrUpdateUser(userModel);
33 | }
34 | async signOut(): Promise {
35 | try {
36 | return auth.signOut();
37 | } catch (error) {
38 | console.error("Error signing out with Google", error);
39 | }
40 | }
41 |
42 | async isFirstTimeLogin(userId: string): Promise {
43 | const hasLoggedInBefore = localStorage.getItem(`user_${userId}_has_logged_in`) === 'true';
44 | return !hasLoggedInBefore;
45 | }
46 |
47 | private async handleFirstTimeLogin(user: FirebaseUser): Promise {
48 | const logService = new LogService();
49 | await logService.updateLogsForNewUser(user.uid);
50 | localStorage.setItem(`user_${user.uid}_has_logged_in`, 'true');
51 |
52 | }
53 |
54 | getCurrentUser(): FirebaseUser | null {
55 | return auth.currentUser;
56 | }
57 |
58 | onAuthStateChanged(callback: (user: FirebaseUser | null) => void): () => void {
59 | return auth.onAuthStateChanged(callback);
60 | }
61 | }
--------------------------------------------------------------------------------
/src/app/(main)/_services/BannerState.ts:
--------------------------------------------------------------------------------
1 | // useBannerState.ts
2 | import { doc, onSnapshot } from 'firebase/firestore';
3 | import { useEffect, useState } from 'react';
4 | import { db } from '../../../utils/firebase';
5 |
6 | interface BannerState {
7 | show: boolean;
8 | message: string;
9 | }
10 |
11 | export const useBannerState = () => {
12 | const [bannerState, setBannerState] = useState({ show: false, message: '' });
13 | const configCollection = `${process.env.NEXT_PUBLIC_FIREBASE_CONFIG_COLLECTION}`;
14 | const bannerDocument = `${process.env.NEXT_PUBLIC_FIREBASE_FEATURE_BANNER}`;
15 | useEffect(() => {
16 | const unsubscribe = onSnapshot(doc(db, configCollection ?? 'config', bannerDocument ?? 'banner'),
17 | (doc) => {
18 | if (doc.exists()) {
19 | const data = doc.data() as BannerState;
20 | setBannerState(data);
21 | } else {
22 | }
23 | },
24 | (error) => {
25 | }
26 | );
27 |
28 | return () => {
29 | unsubscribe();
30 | };
31 | }, []);
32 |
33 | return bannerState;
34 | };
35 |
36 | export default useBannerState;
--------------------------------------------------------------------------------
/src/app/(main)/_services/EditorState.ts:
--------------------------------------------------------------------------------
1 | // src/_services/EditorState.ts
2 | export class EditorHistoryState {
3 | private history: string[];
4 | private historyIndex: number;
5 |
6 | constructor(initialValue: string) {
7 | this.history = [initialValue];
8 | this.historyIndex = 0;
9 | }
10 |
11 | getCurrentValue(): string {
12 | return this.history[this.historyIndex];
13 | }
14 |
15 | updateValue(newValue: string) {
16 | if (newValue !== this.getCurrentValue()) {
17 | this.history = this.history.slice(0, this.historyIndex + 1);
18 | this.history.push(newValue);
19 | this.historyIndex = this.history.length - 1;
20 | }
21 | }
22 |
23 | undo(): string | null {
24 | if (this.historyIndex > 0) {
25 | this.historyIndex--;
26 | return this.getCurrentValue();
27 | }
28 | return null;
29 | }
30 |
31 | redo(): string | null {
32 | if (this.historyIndex < this.history.length - 1) {
33 | this.historyIndex++;
34 | return this.getCurrentValue();
35 | }
36 | return null;
37 | }
38 |
39 | // New method to update the current value without adding to history
40 | setCurrentValue(newValue: string) {
41 | if (this.getCurrentValue() !== newValue) {
42 | this.history[this.historyIndex] = newValue;
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/src/app/(main)/_services/MDFormatter.ts:
--------------------------------------------------------------------------------
1 | export class MarkdownFormatter {
2 | private value: string;
3 |
4 | constructor(initialValue: string = '') {
5 | this.value = initialValue;
6 | }
7 |
8 | public getValue(): string {
9 | return this.value;
10 | }
11 |
12 | public setValue(newValue: string): void {
13 | this.value = newValue;
14 | }
15 |
16 | public applyParanthesisFormatting(paranthesis: string, start: number, end: number,): { value: string, newCursorPos: number } {
17 | const selectedText = this.value.substring(start, end);
18 | const newValue =
19 | this.value.substring(0, start) +
20 | `${paranthesis}${selectedText}${paranthesis}` +
21 | this.value.substring(end);
22 |
23 | return { value: newValue, newCursorPos: end + 2 };
24 | }
25 |
26 | public applyFormatting(start: number, end: number, syntax: string): { value: string, newCursorPos: number } {
27 | let selectedText = this.value.substring(start, end);
28 |
29 | const trimmedText = selectedText.trim();
30 |
31 | if (trimmedText === '') {
32 | return { value: this.value, newCursorPos: end };
33 | }
34 |
35 | const trimStart = selectedText.indexOf(trimmedText);
36 | const trimEnd = trimStart + trimmedText.length;
37 |
38 | const formattedText =
39 | selectedText.substring(0, trimStart) +
40 | `${syntax}${trimmedText}${syntax}` +
41 | selectedText.substring(trimEnd);
42 |
43 | const newValue =
44 | this.value.substring(0, start) +
45 | formattedText +
46 | this.value.substring(end);
47 |
48 | const newCursorPos = start + formattedText.length;
49 |
50 | return { value: newValue, newCursorPos };
51 | }
52 |
53 | public applyLinkFormatting(start: number, end: number, url?: string): { value: string, newCursorPos: number } {
54 | const selectedText = this.value.substring(start, end);
55 | if (url) {
56 | const newValue =
57 | this.value.substring(0, start) +
58 | `[${selectedText}](${url})` +
59 | this.value.substring(end);
60 | return { value: newValue, newCursorPos: end + 3 };
61 | } else {
62 | const newValue =
63 | this.value.substring(0, start) +
64 | `[${selectedText}]()` +
65 | this.value.substring(end);
66 | return { value: newValue, newCursorPos: end + 3 };
67 | }
68 |
69 | }
70 |
71 | public applyListFormatting(start: number, listSyntax: string): { value: string, newCursorPos: number } {
72 | const lineStart = this.value.lastIndexOf('\n', start - 1) + 1;
73 | const newValue =
74 | this.value.substring(0, lineStart) +
75 | listSyntax +
76 | this.value.substring(lineStart);
77 |
78 | return { value: newValue, newCursorPos: start + listSyntax.length };
79 | }
80 |
81 | public applyCodeBlockFormatting(start: number, end: number): { value: string, newCursorPos: number } {
82 | const selectedText = this.value.substring(start, end);
83 | const newValue =
84 | this.value.substring(0, start) +
85 | `\n\`\`\`\n${selectedText}\n\`\`\`\n` +
86 | this.value.substring(end);
87 |
88 | return { value: newValue, newCursorPos: end + 8 };
89 | }
90 |
91 | public applyBlockquoteFormatting(start: number): { value: string, newCursorPos: number } {
92 | const lineStart = this.value.lastIndexOf('\n', start - 1) + 1;
93 | const newValue =
94 | this.value.substring(0, lineStart) +
95 | '> ' +
96 | this.value.substring(lineStart);
97 |
98 | return { value: newValue, newCursorPos: start + 2 };
99 | }
100 |
101 | public applyHeadingFormatting(start: number, level: number): { value: string, newCursorPos: number } {
102 | const lineStart = this.value.lastIndexOf('\n', start - 1) + 1;
103 | const headingSyntax = '#'.repeat(level) + ' ';
104 | const newValue =
105 | this.value.substring(0, lineStart) +
106 | headingSyntax +
107 | this.value.substring(lineStart);
108 |
109 | return { value: newValue, newCursorPos: start + headingSyntax.length };
110 | }
111 |
112 | public insertHorizontalRule(start: number): { value: string, newCursorPos: number } {
113 | const newValue =
114 | this.value.substring(0, start) +
115 | '\n---\n' +
116 | this.value.substring(start);
117 |
118 | return { value: newValue, newCursorPos: start + 5 };
119 | }
120 | }
--------------------------------------------------------------------------------
/src/app/(main)/_services/UserService.ts:
--------------------------------------------------------------------------------
1 | // src/services/firebaseService.ts
2 | import { Timestamp, collection, doc, getDoc, setDoc, updateDoc } from 'firebase/firestore';
3 | import { db } from '../../../utils/firebase';
4 |
5 | export class UserService {
6 | private usersCollection = collection(db, `${process.env.NEXT_PUBLIC_USERS_COLLECTION}`);
7 |
8 | // User methods
9 | async createOrUpdateUser(user: any): Promise {
10 | if (!user.id) throw new Error('User ID is required');
11 | const userRef = doc(this.usersCollection, user.id);
12 | await setDoc(userRef, user, { merge: true });
13 | }
14 |
15 | async getUserById(userId: string): Promise {
16 | const docRef = doc(this.usersCollection, userId);
17 | const docSnap = await getDoc(docRef);
18 | if (docSnap.exists()) {
19 | return docSnap.data();
20 | }
21 | }
22 |
23 | async updateUserLastLogin(userId: string): Promise {
24 | const userRef = doc(this.usersCollection, userId);
25 | await updateDoc(userRef, { lastLoginAt: Timestamp.now() });
26 | }
27 |
28 | async updateFirstTimeUserLogs(userId: string): Promise {
29 | const userRef = doc(this.usersCollection, userId);
30 | await updateDoc(userRef, { firstTimeUserLogUpdated: true });
31 | }
32 | }
--------------------------------------------------------------------------------
/src/app/(main)/_services/feature.ts:
--------------------------------------------------------------------------------
1 | import { collection, doc, getDocs, runTransaction, serverTimestamp, updateDoc } from 'firebase/firestore';
2 | import { db } from '../../../utils/firebase';
3 |
4 | class FeatureService {
5 | private configCollection = collection(db, `${process.env.NEXT_PUBLIC_FIREBASE_CONFIG_COLLECTION}`);
6 | private bannerDocument = `${process.env.NEXT_PUBLIC_FIREBASE_FEATURE_BANNER}`;
7 |
8 | updateBannerState = async (show: boolean, message: string) => {
9 | try {
10 | await updateDoc(doc(this.configCollection, this.bannerDocument), {
11 | show,
12 | message,
13 | });
14 | } catch (error) {
15 | }
16 | };
17 |
18 | async menuDoc(): Promise {
19 | const querySnapshot = await getDocs(this.configCollection);
20 | const menuFeature = querySnapshot.docs.find(doc => doc.id === 'menu');
21 | return menuFeature?.data() ?? {
22 | 'github': true,
23 | 'report': true,
24 | 'releaseNotes': false,
25 | 'tour': true,
26 | };
27 | }
28 |
29 | async shouldDeleteExpired(): Promise {
30 | const querySnapshot = await getDocs(this.configCollection);
31 | const notesFeature = querySnapshot.docs.find(doc => doc.id === 'notes');
32 | if (notesFeature) {
33 | const data = notesFeature.data();
34 | const lastExpiryCheck = data.lastExpiryCheck?.toDate();
35 | if (!lastExpiryCheck || await this.isMoreThan24HoursAgo(lastExpiryCheck)) {
36 | // Use a transaction to ensure atomic read-write operation
37 | await runTransaction(db, async (transaction) => {
38 | const docRef = notesFeature.ref;
39 | const doc = await transaction.get(docRef);
40 |
41 | if (doc.exists()) {
42 | const currentData = doc.data();
43 | const currentLastCheck = currentData.lastExpiryCheck?.toDate();
44 | if (!currentLastCheck || await this.isMoreThan24HoursAgo(currentLastCheck)) {
45 | transaction.update(docRef, {
46 | lastExpiryCheck: serverTimestamp()
47 | });
48 | return true;
49 | }
50 | }
51 | return false;
52 | });
53 | }
54 | }
55 | return false;
56 | }
57 |
58 | async isMoreThan24HoursAgo(date: Date): Promise {
59 | const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
60 | return date < twentyFourHoursAgo;
61 | }
62 | }
63 |
64 | export default FeatureService;
--------------------------------------------------------------------------------
/src/app/(main)/logs/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import PreviewPage from "@/app/(main)/_components/PreviewPage";
2 | import { Constants } from "@/app/constants";
3 | import { Metadata, ResolvingMetadata } from "next";
4 | import LogService from "../../_services/logService";
5 |
6 | type Props = {
7 | params: {
8 | id: string;
9 | };
10 | searchParams: URLSearchParams;
11 | };
12 |
13 | export async function generateMetadata(
14 | { params, searchParams }: Props,
15 | parent: ResolvingMetadata
16 | ): Promise {
17 | const id = params.id;
18 | const log = await new LogService().importLog(id);
19 | const meta = {
20 | title: log?.title || "Pastelog",
21 | description:
22 | log?.data || Constants.description,
23 | url: `https://pastelog.vercel.app/logs/${id}`,
24 | openGraph: {
25 | type: "website",
26 | siteName: "Pastelog",
27 | title: log?.title || "Pastelog",
28 | description:
29 | log?.data || Constants.description,
30 | url: `https://pastelog.vercel.app/logs/${id}`,
31 | images: [
32 | {
33 | url: "/images/frame.png",
34 | width: 512,
35 | height: 512,
36 | alt: "Pastelog",
37 | },
38 | ],
39 | },
40 | };
41 | return meta;
42 | }
43 |
44 | export default function LogPage({ params }: { params: { id: string } }) {
45 | const { id } = params;
46 | return ;
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/(main)/logs/app_layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { fetchMenuItems } from '@/lib/features/menus/menuSlice';
4 | import { setShowSideBar, toggleSideBar } from '@/lib/features/menus/sidebarSlice';
5 | import { AppDispatch, RootState } from '@/lib/store';
6 | import ClearIcon from '@mui/icons-material/Clear';
7 | import { useDisclosure } from '@nextui-org/react';
8 | import { useTheme } from 'next-themes';
9 | import { useRouter } from 'next/navigation';
10 | import React, { Suspense, useEffect } from 'react';
11 | import { FiSidebar } from "react-icons/fi";
12 | import { useDispatch, useSelector } from 'react-redux';
13 | import IconButton from "../_components/IconButton";
14 | import PSBanner from '../_components/PSBanner';
15 | import PSNavbar from '../_components/PSNavbar';
16 | import RouteClient from '../_components/RouteClient';
17 | import ShortcutWrapper from '../_components/ShortCutWrapper';
18 | import Sidebar from '../_components/Sidebar';
19 | import { Theme } from '../_components/ThemeSwitcher';
20 | import useBannerState from '../_services/BannerState';
21 |
22 | export default function AppLayout({ children }: { children: React.ReactNode }) {
23 | const { theme, setTheme } = useTheme();
24 | const bannerState = useBannerState();
25 | const [show, setShow] = React.useState(true);
26 | const { isOpen: searchOpen, onOpen: onSearchOpen, onClose: onSearchClose } = useDisclosure();
27 | const router = useRouter();
28 | const dispatch = useDispatch();
29 | const showSideBar = useSelector((state: RootState) => state.sidebar.showSideBar);
30 | const checkWindowSize = async () => {
31 | if (typeof window !== 'undefined') {
32 | if (showSideBar && window.innerWidth <= 768) {
33 | dispatch(setShowSideBar(false));
34 | }
35 | }
36 | };
37 | const toggleTheme = () => {
38 | setTheme(theme === 'dark' ? 'light' : 'dark');
39 | };
40 |
41 | const handleMainContentClick = () => {
42 | if (typeof window !== 'undefined') {
43 | if (window.innerWidth <= 768 && showSideBar) {
44 | dispatch(setShowSideBar(false));
45 | }
46 | }
47 | };
48 |
49 | useEffect(() => {
50 | dispatch(fetchMenuItems());
51 | checkWindowSize();
52 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
53 | if (mediaQuery.matches) {
54 | setTheme(Theme.DARK);
55 | } else {
56 | setTheme(Theme.LIGHT);
57 | }
58 | window.addEventListener('resize', checkWindowSize);
59 | return () => window.removeEventListener('resize', checkWindowSize);
60 | }, [setTheme, dispatch]);
61 |
62 | const handleShortCut = (key: string) => {
63 | switch (key) {
64 | case 'n':
65 | router.push('/logs');
66 | break;
67 | case 'd':
68 | toggleTheme();
69 | break;
70 | case 's':
71 | dispatch(toggleSideBar());
72 | break;
73 | default:
74 | break;
75 | }
76 | };
77 |
78 | return (
79 |
80 |
81 |
82 |
83 |
84 |
85 | {showSideBar && (
86 |
dispatch(setShowSideBar(!showSideBar))}
89 | ariaLabel="Close Sidebar"
90 | tooltipPlacement="bottom-start"
91 | >
92 |
93 |
94 | )}
95 |
96 |
97 |
102 |
103 | setShow(false)}>
106 |
107 |
108 |
109 |
110 |
113 |
114 | {children}
115 |
116 |
117 |
118 |
119 |
Loading... }>
120 |
121 |
122 |
123 |
124 | );
125 | }
126 |
127 |
--------------------------------------------------------------------------------
/src/app/(main)/logs/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { PSNavbarProvider } from '@/lib/Context/PSNavbarProvider';
4 | import { store } from '@/lib/store';
5 | import { Provider } from 'react-redux';
6 | import { ThemeProvider } from '../_components/ThemeProvider';
7 | import AppLayout from './app_layout';
8 |
9 | export default function LogsLayout({ children }: { children: React.ReactNode }) {
10 | return (
11 |
12 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/(main)/logs/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSearchParams } from 'next/navigation';
4 | import { Suspense } from 'react';
5 | import Pastelog from "../_components/Pastelog";
6 |
7 | function LogsContent() {
8 | const searchParams = useSearchParams()
9 | const id = searchParams.get('id')
10 | return (
11 |
16 | );
17 | }
18 |
19 | export default function LogsPage() {
20 | return (
21 | Loading...}>
22 |
23 |
24 | );
25 | }
--------------------------------------------------------------------------------
/src/app/(policies)/policies/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { PSNavbarProvider } from "@/lib/Context/PSNavbarProvider";
4 | import { store } from "@/lib/store";
5 | import { Provider } from "react-redux";
6 | import PSNavbar from "../../(main)/_components/PSNavbar";
7 |
8 | export default function PolicyLayout({ children }: { children: React.ReactNode }) {
9 | return (
10 |
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 | );
19 | }
--------------------------------------------------------------------------------
/src/app/(policies)/policies/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | import TermsAndPrivacy from '@/app/(main)/_components/TermsAndPrivacy';
3 |
4 | export default function TermsPage() {
5 | return
6 | };
--------------------------------------------------------------------------------
/src/app/(publish)/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { PSNavbarProvider } from "@/lib/Context/PSNavbarProvider";
4 | import { store } from "@/lib/store";
5 | import { Provider } from "react-redux";
6 | import PSNavbar from "../(main)/_components/PSNavbar";
7 | import { ThemeProvider } from '../(main)/_components/ThemeProvider';
8 |
9 | export default function PublishLayout({ children }: { children: React.ReactNode }) {
10 | return (
11 |
12 |
13 |
19 |
20 |
21 | {children}
22 |
23 |
24 |
25 |
26 | );
27 | }
--------------------------------------------------------------------------------
/src/app/(publish)/logs/publish/[id]/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | import PreviewPage from '@/app/(main)/_components/PreviewPage';
3 | import LogService from '@/app/(main)/_services/logService';
4 | import { Constants } from '@/app/constants';
5 | import { Metadata, ResolvingMetadata } from 'next';
6 |
7 | // This is required for dynamic routing in runtime
8 | export const dynamicParams = true;
9 |
10 | type Props = {
11 | params: {
12 | id: string;
13 | };
14 | searchParams: URLSearchParams;
15 | }
16 |
17 | export async function generateMetadata(
18 | { params, searchParams }: Props,
19 | parent: ResolvingMetadata
20 | ): Promise {
21 | const id = params.id
22 |
23 | const log = await new LogService().importLog(id);
24 |
25 | return {
26 | title: log?.title || "Pastelog",
27 | description: log?.data || Constants.description
28 | }
29 | }
30 |
31 | export default function PublishPage({ params }: { params: { id: string } }) {
32 | const { id } = params;
33 | return
36 | };
--------------------------------------------------------------------------------
/src/app/config.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/src/app/config.js
--------------------------------------------------------------------------------
/src/app/constants.ts:
--------------------------------------------------------------------------------
1 | export class Constants {
2 | static readonly publicLogIds = ['getting-started', 'shortcuts'];
3 | static readonly styles = {
4 | iconTheme: 'size-6 text-black dark:text-white',
5 | smallIconTheme: 'size-4 text-black dark:text-white'
6 | };
7 |
8 | static readonly description = "PasteLog is a simple, fast, and powerful pastebin. It is powered by firebase in the backend. It allows you to publish your logs, and access them from anywhere and any device via a unique link."
9 | }
10 |
11 | export enum LogType {
12 | TEXT = 'text',
13 | CODE = 'code',
14 | }
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | /* import markdown.css */
5 | @import url('./markdown.css');
6 | @import url('./style.css');
7 |
8 | @layer base {
9 | :root {
10 | --color-primary: #34d399;
11 | --color-secondary: #475569;
12 | --color-tertiary: #94a3b8;
13 | --color-surface: #e2e8f0;
14 | --color-background: #f8fafc;
15 | --color-code-surface: #f0f2f3;
16 | --color-code-onSurface: #191b1c;
17 | --color-accent: #cbd5e1;
18 | --color-block-surface: #e2e7f3;
19 | --color-link: #2563eb;
20 | --color-input-background: #f1f5f9;
21 | --color-selection: #e0e3ea;
22 | }
23 |
24 | .dark {
25 | --color-primary: #065f46;
26 | --color-secondary: #374151;
27 | --color-tertiary: #4b5563;
28 | --color-surface: #6b7280;
29 | --color-background: #1f2937;
30 | --color-sidebar: #111827;
31 | --color-code-surface: #222324;
32 | --color-code-onSurface: #e2e8f0;
33 | --color-accent: #9ca3af;
34 | --color-block-surface: #46516a;
35 | --color-link: #7aafe1;
36 | --color-input-background: #5f6877;
37 | --color-selection: #AFAFAF;
38 | }
39 | }
40 |
41 | [data-theme="dark"] .rdp,
42 | .dark .rdp {
43 | --rdp-accent-color: var(--rdp-accent-color-dark);
44 | --rdp-background-color: var(--rdp-background-color-dark);
45 | }
46 |
47 | html,
48 | body {
49 | @apply bg-background text-foreground;
50 | }
51 |
52 | ::-moz-selection {
53 | background: var(--color-selection);
54 | }
55 |
56 | ::-webkit-selection {
57 | background: var(--color-selection);
58 | }
59 |
60 | ::selection {
61 | background: var(--color-selection);
62 | }
63 |
64 | .line-break {
65 | white-space: pre-wrap;
66 | /* line height */
67 | line-height: 1.5;
68 | }
69 |
70 | /* body {
71 | color: rgb(var(--foreground-rgb));
72 | height: 100vh;
73 | background: linear-gradient(to bottom,
74 | transparent,
75 | rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
76 | } */
77 |
78 | @layer utilities {
79 | .text-balance {
80 | text-wrap: balance;
81 | }
82 | }
--------------------------------------------------------------------------------
/src/app/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maheshj01/pastelog/15b2da882a0296d5f81ea34a2753b2c4bd8c49b5/src/app/icon.ico
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata, Viewport } from "next";
2 | import { Inter } from "next/font/google";
3 | import { ThemeProvider } from './(main)/_components/ThemeProvider';
4 | import ToastProvider from "./(main)/_components/ToastProvider";
5 | import "./globals.css";
6 | import { Providers, SidebarProvider } from "./providers";
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | export const metadata: Metadata = {
10 | title: "Pastelog",
11 | applicationName: "Pastelog",
12 | description: "Create Stunning Rich Text Logs/Notes with markdown Support and Code Highlighting and share it with the world.",
13 | manifest: "/manifest.json",
14 | appleWebApp: {
15 | capable: true,
16 | statusBarStyle: "default",
17 | title: "Pastelog",
18 | // startUpImage: [],
19 | },
20 | formatDetection: {
21 | telephone: false,
22 | },
23 | openGraph: {
24 | type: "website",
25 | siteName: "Pastelog",
26 | title: {
27 | default: "Pastelog",
28 | template: "%s | Pastelog",
29 | },
30 | images: [
31 | {
32 | url: "/images/frame.png",
33 | width: 512,
34 | height: 512,
35 | alt: "Pastelog",
36 | },
37 | ],
38 | url: "https://pastelog.vercel.app",
39 | description: "Create Stunning Rich Text Logs/Notes with markdown Support and Code Highlighting and share it with the world.",
40 | },
41 | };
42 |
43 | export const viewport: Viewport = {
44 | themeColor: "#FFFFFF",
45 | };
46 |
47 | export default function RootLayout({
48 | children,
49 | }: Readonly<{
50 | children: React.ReactNode;
51 | }>) {
52 | return (
53 |
54 |
55 |
56 |
57 |
59 |
60 |
66 |
67 |
68 | {children}
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/markdown.css:
--------------------------------------------------------------------------------
1 | .editor {
2 | line-height: 1.2;
3 | }
4 |
5 | .reactMarkDown {
6 | /* word-break: break-all;
7 | white-space: pre-wrap; */
8 | word-wrap: break-word;
9 | }
10 |
11 | .reactMarkDown h1 {
12 | font-size: 2rem;
13 | font-weight: 700;
14 | line-height: 1;
15 | color: var(--color-black);
16 | margin-top: 0.5em;
17 | margin-bottom: 0.5em;
18 | }
19 |
20 | /* add standard styles for headings */
21 | .reactMarkDown h2 {
22 | font-size: 1.75rem;
23 | font-weight: 700;
24 | line-height: 1;
25 | margin-top: 0.5em;
26 | color: var(--color-black);
27 | margin-bottom: 0.5em;
28 | }
29 |
30 | .reactMarkDown h3 {
31 | font-size: 1.25rem;
32 | font-weight: 700;
33 | line-height: 1;
34 | margin-top: 0.5em;
35 | color: var(--color-black);
36 | margin-bottom: 0.5em;
37 | }
38 |
39 | .reactMarkDown h4 {
40 | font-size: 1.2rem;
41 | font-weight: 700;
42 | line-height: 1;
43 | margin-top: 0.5em;
44 | color: var(--color-black);
45 | margin-bottom: 0.5em;
46 | }
47 |
48 | .reactMarkDown h5 {
49 | font-size: 1.1rem;
50 | font-weight: 700;
51 | line-height: 1;
52 | margin-top: 0.5em;
53 | color: var(--color-black);
54 | margin-bottom: 0.5em;
55 | }
56 |
57 | .reactMarkDown h6 {
58 | font-size: 1rem;
59 | font-weight: 700;
60 | line-height: 1;
61 | margin-top: 0.5em;
62 | color: var(--color-black);
63 | margin-bottom: 0.5em;
64 | }
65 |
66 | /* add standard styles for paragraphs */
67 |
68 | .reactMarkDown p {
69 | font-size: 1rem;
70 | font-weight: 400;
71 | line-height: 1.5;
72 | margin-top: 0.5em;
73 | color: var(--color-black);
74 | margin-bottom: 0.5em;
75 | }
76 |
77 | /* add standard styles for lists */
78 |
79 | .reactMarkDown ul {
80 | font-size: 1rem;
81 | font-weight: 400;
82 | line-height: 1.5;
83 | margin-top: 0.5em;
84 | margin-bottom: 0.5em;
85 | }
86 |
87 | .reactMarkDown ol {
88 | font-size: 1rem;
89 | font-weight: 400;
90 | line-height: 1.5;
91 | margin-top: 0.5em;
92 | margin-bottom: 0.5em;
93 | }
94 |
95 | /* add standard styles for links */
96 |
97 | .reactMarkDown a {
98 | font-size: 1rem;
99 | font-weight: 400;
100 | line-height: 1.5;
101 | margin-top: 0.5em;
102 | margin-bottom: 0.5em;
103 | color: var(--color-link);
104 | text-decoration: none;
105 | }
106 |
107 | /* add standard styles for images */
108 |
109 | .reactMarkDown img {
110 | font-size: 1rem;
111 | font-weight: 400;
112 | line-height: 1.5;
113 | margin-top: 0.5em;
114 | margin-bottom: 0.5em;
115 | max-width: 100%;
116 | height: auto;
117 | }
118 |
119 | /* add standard styles for code blocks */
120 | .reactMarkDown pre {
121 | font-size: 0.8rem !important;
122 | font-weight: 400;
123 | line-height: 1.2;
124 | margin-top: 0.5em;
125 | margin-bottom: 0.5em;
126 | color: black;
127 | background-color: var(--color-code-surface);
128 | border-radius: 0.5em;
129 | /* Padding around highlighted code block */
130 | padding: 15px;
131 | overflow-x: auto;
132 | }
133 |
134 | /* add standard styles for inline code */
135 |
136 | .reactMarkDown code {
137 | font-size: 1rem;
138 | font-weight: 400;
139 | line-height: 1.5;
140 | margin-top: 0.5em;
141 | margin-bottom: 0.5em;
142 | color: var(--color-code-onSurface);
143 | border-radius: 0.5em;
144 | /* padding: 0.5em 0.5em; */
145 | }
146 |
147 | /* Make code inside headings match the heading's font size and weight */
148 | .reactMarkDown h1 code:not(pre code),
149 | .reactMarkDown h2 code:not(pre code),
150 | .reactMarkDown h3 code:not(pre code),
151 | .reactMarkDown h4 code:not(pre code),
152 | .reactMarkDown h5 code:not(pre code),
153 | .reactMarkDown h6 code:not(pre code) {
154 | font-size: inherit;
155 | font-weight: inherit;
156 | line-height: inherit;
157 | }
158 |
159 | /* Styles for inline code blocks */
160 | .reactMarkDown code:not(pre code) {
161 | font-size: 1rem;
162 | font-weight: 400;
163 | line-height: 1.5;
164 | margin-top: 0.5em;
165 | margin-bottom: 0.5em;
166 | background-color: var(--color-code-surface);
167 | color: var(--color-code-onSurface);
168 | border-radius: 0.6em;
169 | padding: 0.2em 0.4em;
170 | /* Add padding for inline code blocks */
171 | }
172 |
173 | /* add backtick styles for inline code */
174 |
175 | /* .reactMarkDown code::before:not(pre code) {
176 | content: '"`"',
177 | }
178 |
179 | .reactMarkDown code::after:not(pre code) {
180 | content: '"`"',
181 | }
182 |
183 | */
184 | /* add standard styles for horizontal rules */
185 |
186 | .reactMarkDown a {
187 | line-height: 100%;
188 | }
189 |
190 | .reactMarkDown p {
191 | line-height: 120%;
192 | text-align: justify;
193 | }
194 |
195 | .reactMarkDown hr {
196 | font-size: 1rem;
197 | font-weight: 400;
198 | line-height: 1.5;
199 | margin-top: 0.5em;
200 | margin-bottom: 0.5em;
201 | color: var(--color-black);
202 | background-color: var(--color-black);
203 | height: 1px;
204 | }
205 |
206 | /* add standard styles for blockquotes */
207 |
208 | .reactMarkDown blockquote {
209 | font-size: 1rem;
210 | font-weight: 400;
211 | line-height: 1.5;
212 | margin-top: 0.5em;
213 | margin-bottom: 0.5em;
214 | color: var(--color-black);
215 | background-color: var(--color-block-surface);
216 | border-left: 0.25em solid var(--color-primary);
217 | padding: 0.5em;
218 | }
219 |
220 | /* add standard styles for tables */
221 |
222 | .reactMarkDown table {
223 | font-size: 1rem;
224 | font-weight: 400;
225 | line-height: 1.5;
226 | margin-top: 0.5em;
227 | margin-bottom: 0.5em;
228 | width: 100%;
229 | border-collapse: collapse;
230 | }
231 |
232 | .reactMarkDown th {
233 | font-size: 1rem;
234 | font-weight: 700;
235 | line-height: 1.5;
236 | margin-top: 0.5em;
237 | margin-bottom: 0.5em;
238 | color: var(--color-black);
239 | background-color: var(--color-background);
240 | border: 1px solid var(--color-background);
241 | padding: 0.5em;
242 | }
243 |
244 | .reactMarkDown td {
245 | font-size: 1rem;
246 | font-weight: 400;
247 | line-height: 1.5;
248 | margin-top: 0.5em;
249 | margin-bottom: 0.5em;
250 | color: var(--color-black);
251 | background-color: var(--color-background);
252 | border: 1px solid var(--color-background);
253 | padding: 0.5em;
254 | }
255 |
256 | /* standard css for bold */
257 |
258 | .reactMarkDown strong {
259 | font-weight: 700;
260 | color: var(--color-black);
261 | }
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { store } from '@/lib/store';
3 | import { redirect } from 'next/navigation';
4 | import { Suspense, useEffect, useState } from 'react';
5 | import { Provider } from 'react-redux';
6 | import RouteClient from './(main)/_components/RouteClient';
7 | import Welcome from './(main)/_components/Welcome';
8 | import useSettings from './(main)/_hooks/useSettings';
9 |
10 | export default function Page() {
11 | const [loading, setLoading] = useState(true);
12 | const { settings } = useSettings();
13 | useEffect(() => {
14 | if (!settings.newUser) {
15 | redirect('/logs');
16 | }
17 | setLoading(false);
18 | }, [])
19 |
20 | if (loading) {
21 | return
24 | }
25 | return (
26 | }>
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/providers.tsx:
--------------------------------------------------------------------------------
1 | // app/providers.tsx
2 | 'use client'
3 |
4 | import { NextUIProvider } from '@nextui-org/react';
5 | import { ReactNode, useState } from 'react';
6 | import { SidebarContext } from './(main)/_hooks/useSidebar';
7 | export function Providers({ children }: { children: React.ReactNode }) {
8 | return (
9 |
10 | {children}
11 |
12 | )
13 | }
14 |
15 |
16 | export function SidebarProvider({ children }: { children: ReactNode }) {
17 | const [apiKey, setApiKey] = useState(null); // Add apiKey to the state
18 |
19 | return (
20 |
24 | {children}
25 |
26 | );
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/src/app/style.css:
--------------------------------------------------------------------------------
1 | /* This only contains CSS relates to custom components */
2 |
3 | .loader {
4 | border: 4px solid rgba(0, 0, 0, 0.1);
5 | width: 36px;
6 | height: 36px;
7 | border-radius: 50%;
8 | border-left-color: var(--color-surface);
9 | animation: spin 1s ease infinite;
10 | }
11 |
12 | /* mediaquery */
13 | @media (max-width: 768px) {
14 | .slide-main {
15 | /* translate x 1/3 */
16 | transform: translateX(33.3333%);
17 | transition: transform 0.5s;
18 | }
19 | }
20 |
21 | @keyframes spin {
22 | 0% {
23 | transform: rotate(0deg);
24 | }
25 |
26 | 100% {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
31 | @keyframes fadeOut {
32 | 0% {
33 | opacity: 1;
34 | }
35 |
36 | 100% {
37 | opacity: 0;
38 | }
39 | }
40 |
41 | @keyframes fadeIn {
42 | 0% {
43 | opacity: 0;
44 | }
45 |
46 | 100% {
47 | opacity: 1;
48 | }
49 | }
50 |
51 | @keyframes revealFromBottom {
52 | 0% {
53 | clip-path: inset(100% 0 0 0);
54 | }
55 |
56 | 100% {
57 | clip-path: inset(0 0 0 0);
58 | }
59 | }
60 |
61 | @keyframes revealFromTop {
62 | 0% {
63 | clip-path: inset(0 0 100% 0);
64 | }
65 |
66 | 100% {
67 | clip-path: inset(0 0 0 0);
68 | }
69 | }
70 |
71 | @keyframes revealFromLeft {
72 | 0% {
73 | clip-path: inset(0 100% 0 0);
74 | }
75 |
76 | 100% {
77 | clip-path: inset(0 0 0 0);
78 | }
79 | }
80 |
81 | .fade-out-animation {
82 | animation: fadeOut 0.5s ease-out forwards;
83 | }
84 |
85 | .fade-in-animation {
86 | animation: fadeIn 0.5s ease-out forwards;
87 | }
88 |
89 | .reveal-in-animation {
90 | animation: revealFromLeft 1s ease-out;
91 | overflow: hidden
92 | }
93 |
94 | .reveal-top-animation {
95 | animation: revealFromTop 1s ease-out;
96 | overflow: hidden
97 | }
98 |
99 | .tagline {
100 | display: inline-block;
101 | animation: fadeUpSlide 3.0s ease-in-out infinite;
102 | transform-origin: center;
103 | }
104 |
105 | @keyframes fadeUpSlide {
106 | 0% {
107 | opacity: 0;
108 | transform: translateY(20px);
109 | }
110 |
111 | 15% {
112 | opacity: 1;
113 | transform: translateY(0);
114 | }
115 |
116 | 80% {
117 | opacity: 1;
118 | transform: translateY(0);
119 | }
120 |
121 | 95% {
122 | opacity: 0;
123 | transform: translateY(-20px);
124 | }
125 |
126 | 100% {
127 | opacity: 0;
128 | transform: translateY(-20px);
129 | }
130 | }
--------------------------------------------------------------------------------
/src/lib/Context/PSNavbarProvider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, ReactNode, useContext, useState } from "react";
2 |
3 | interface NavbarContextProps {
4 | navbarTitle: string | null;
5 | setNavbarTitle: (title: string | null) => void;
6 | }
7 |
8 | const NavbarContext = createContext(undefined);
9 |
10 | export const PSNavbarProvider = ({ children }: { children: ReactNode }) => {
11 | const [navbarTitle, setNavbarTitle] = useState(null);
12 |
13 | return (
14 |
15 | {children}
16 |
17 | );
18 | };
19 |
20 | export const useNavbar = () => {
21 | const context = useContext(NavbarContext);
22 | if (!context) {
23 | throw new Error("useNavbar must be used within a NavbarProvider");
24 | }
25 | return context;
26 | };
27 |
--------------------------------------------------------------------------------
/src/lib/features/menus/authSlice.ts:
--------------------------------------------------------------------------------
1 | import { AuthService } from "@/app/(main)/_services/AuthService";
2 | import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
3 | import { User as FirebaseUser } from 'firebase/auth';
4 |
5 |
6 | interface AuthState {
7 | user: any | null,
8 | loading: boolean;
9 | isFirstLogin: boolean;
10 | error: string | null;
11 | }
12 |
13 | const initialState: AuthState = {
14 | user: null,
15 | loading: false,
16 | isFirstLogin: true,
17 | error: null,
18 | };
19 | export function mapFirebaseUserToUser(user: FirebaseUser): any {
20 | return {
21 | email: user.email!,
22 | displayName: user.displayName,
23 | photoURL: user.photoURL,
24 | createdAt: user.metadata.creationTime!,
25 | lastLoginAt: new Date().toISOString(),
26 | id: user.uid
27 | };
28 | }
29 |
30 | const authService = new AuthService();
31 |
32 | // Async thunk for signing in with Google
33 | export const signInWithGoogle = createAsyncThunk(
34 | 'auth/signInWithGoogle',
35 | async (_, { rejectWithValue }) => {
36 | try {
37 | const user = await authService.signInWithGoogle();
38 | return user;
39 | } catch (error) {
40 | return rejectWithValue(error instanceof Error ? error.message : "An unknown error occurred");
41 | }
42 | }
43 | );
44 |
45 | // Async thunk for signing out
46 | export const signOut = createAsyncThunk(
47 | 'auth/signOut',
48 | async (_, { rejectWithValue }) => {
49 | try {
50 | await authService.signOut();
51 | } catch (error) {
52 | return rejectWithValue(error instanceof Error ? error.message : "An unknown error occurred");
53 | }
54 | }
55 | );
56 |
57 | // Async thunk for checking if it's the first time login
58 | export const isFirstTimeLogin = createAsyncThunk(
59 | 'auth/isFirstTimeLogin',
60 | async (userId: string, { rejectWithValue }) => {
61 | try {
62 | const firstLogin = await authService.isFirstTimeLogin(userId);
63 | return firstLogin;
64 | } catch (error) {
65 | return rejectWithValue(error instanceof Error ? error.message : "An unknown error occurred");
66 | }
67 | }
68 | );
69 |
70 | const authSlice = createSlice({
71 | name: 'auth',
72 | initialState,
73 | reducers: {
74 | setUser: (state, action: PayloadAction) => {
75 | state.user = action.payload!;
76 | },
77 | setLoading: (state, action: PayloadAction) => {
78 | state.loading = action.payload;
79 | },
80 | setError: (state, action: PayloadAction) => {
81 | state.error = action.payload;
82 | },
83 | setIsFirstLogin: (state, action: PayloadAction) => {
84 | state.isFirstLogin = action.payload;
85 | }
86 | },
87 | extraReducers: (builder) => {
88 | builder
89 | .addCase(signInWithGoogle.pending, (state) => {
90 | state.loading = true;
91 | state.error = null;
92 | })
93 | .addCase(signInWithGoogle.fulfilled, (state: AuthState, action) => {
94 | state.user = action.payload;
95 | state.loading = false;
96 | })
97 | .addCase(signInWithGoogle.rejected, (state, action) => {
98 | state.loading = false;
99 | state.error = action.payload as string;
100 | })
101 | .addCase(signOut.pending, (state) => {
102 | state.loading = true;
103 | state.error = null;
104 | })
105 | .addCase(signOut.fulfilled, (state) => {
106 | state.user = null;
107 | state.loading = false;
108 | })
109 | .addCase(signOut.rejected, (state, action) => {
110 | state.loading = false;
111 | state.error = action.payload as string;
112 | })
113 | .addCase(isFirstTimeLogin.pending, (state) => {
114 | state.loading = true;
115 | state.error = null;
116 | })
117 | .addCase(isFirstTimeLogin.fulfilled, (state, action) => {
118 | state.isFirstLogin = action.payload;
119 | state.loading = false;
120 | })
121 | .addCase(isFirstTimeLogin.rejected, (state, action) => {
122 | state.loading = false;
123 | state.error = action.payload as string;
124 | });
125 | },
126 | });
127 |
128 | export const { setUser, setLoading, setError, setIsFirstLogin } = authSlice.actions;
129 | export default authSlice.reducer;
--------------------------------------------------------------------------------
/src/lib/features/menus/editorSlice.ts:
--------------------------------------------------------------------------------
1 | import DateUtils from "@/utils/DateUtils";
2 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
3 |
4 | interface EditorState {
5 | content: string;
6 | title: string;
7 | preview: boolean;
8 | publishing: boolean;
9 | expiryDate: string | null;
10 | importLoading: boolean;
11 | }
12 |
13 | const initialState: EditorState = {
14 | title: "",
15 | content: "",
16 | preview: false,
17 | publishing: false,
18 | expiryDate: DateUtils.getDateOffsetBy(30),
19 | importLoading: false,
20 | };
21 |
22 | const editorSlice = createSlice({
23 | name: "editor",
24 | initialState,
25 | reducers: {
26 | setTitle: (state, action: PayloadAction) => {
27 | state.title = action.payload;
28 | },
29 | setContent: (state, action: PayloadAction) => {
30 | state.content = action.payload;
31 | },
32 | setPreview: (state, action: PayloadAction) => {
33 | state.preview = action.payload;
34 | },
35 | togglePreview: (state) => {
36 | state.preview = !state.preview;
37 | },
38 | setPublishing: (state, action: PayloadAction) => {
39 | state.publishing = action.payload;
40 | },
41 | setExpiryDate: (state, action: PayloadAction) => {
42 | state.expiryDate = action.payload;
43 | },
44 | setImportLoading: (state, action: PayloadAction) => {
45 | state.importLoading = action.payload;
46 | },
47 | resetState: () => initialState,
48 | },
49 | });
50 |
51 | export const {
52 | resetState,
53 | setTitle,
54 | setContent,
55 | setPreview,
56 | togglePreview,
57 | setPublishing,
58 | setExpiryDate,
59 | setImportLoading,
60 | } = editorSlice.actions;
61 |
62 | export default editorSlice.reducer;
63 |
--------------------------------------------------------------------------------
/src/lib/features/menus/menuSlice.ts:
--------------------------------------------------------------------------------
1 | import FeatureService from '@/app/(main)/_services/feature';
2 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
3 |
4 | interface MenuState {
5 | menuItems: {
6 | releaseNotes: boolean;
7 | github: boolean;
8 | report: boolean;
9 | tour: boolean;
10 | terms: boolean;
11 | };
12 | loading: boolean;
13 | error: string | null;
14 | }
15 |
16 | const initialState: MenuState = {
17 | menuItems: {
18 | releaseNotes: true,
19 | github: true,
20 | report: true,
21 | tour: true,
22 | terms: true,
23 | },
24 | loading: false,
25 | error: null,
26 | };
27 |
28 | // Async thunk to fetch menu items
29 | export const fetchMenuItems = createAsyncThunk('menu/fetchMenuItems', async () => {
30 | const featureService = new FeatureService();
31 | const data = await featureService.menuDoc();
32 | return data;
33 | });
34 |
35 | const menuSlice = createSlice({
36 | name: 'menu',
37 | initialState,
38 | reducers: {},
39 | extraReducers: (builder) => {
40 | builder
41 | .addCase(fetchMenuItems.pending, (state) => {
42 | state.loading = true;
43 | state.error = null;
44 | })
45 | .addCase(fetchMenuItems.fulfilled, (state, action) => {
46 | state.loading = false;
47 | state.error = null;
48 | state.menuItems = action.payload;
49 | })
50 | .addCase(fetchMenuItems.rejected, (state, action) => {
51 | state.loading = false;
52 | state.error = action.error.message || 'Failed to fetch menu items';
53 | });
54 | },
55 | });
56 |
57 | export default menuSlice.reducer;
58 |
--------------------------------------------------------------------------------
/src/lib/features/menus/sidebarSlice.ts:
--------------------------------------------------------------------------------
1 | import { AuthService } from "@/app/(main)/_services/AuthService";
2 | import LogService from "@/app/(main)/_services/logService";
3 | import { LogType } from "@/app/constants";
4 | import DateUtils from "@/utils/DateUtils";
5 | import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
6 | import { Timestamp } from "firebase/firestore";
7 |
8 | interface SidebarState {
9 | id: string | null;
10 | selected: any;
11 | showSideBar: boolean;
12 | logs: any[];
13 | loading: boolean;
14 | navbarTitle: string;
15 | }
16 |
17 | export interface Note {
18 | id: string;
19 | title: string;
20 | data: string;
21 | createdAt: string;
22 | updatedAt: string;
23 | expiryDate: string;
24 | type: LogType;
25 | createdDate: Timestamp;
26 | lastUpdatedAt: Timestamp;
27 | isExpired: boolean,
28 | summary: string
29 | isPublic: false,
30 | userId: string,
31 | isMarkDown: boolean,
32 | }
33 |
34 | const initialState: SidebarState = {
35 | id: null,
36 | selected: null,
37 | showSideBar: true,
38 | logs: [],
39 | loading: false,
40 | navbarTitle: ''
41 | };
42 |
43 | const logService = new LogService();
44 | const authService = new AuthService();
45 |
46 | export function parseLog(log: any) {
47 | return {
48 | ...log,
49 | createdDate: DateUtils.timestampToISOString(log.createdDate),
50 | lastUpdatedAt: DateUtils.timestampToISOString(log.lastUpdatedAt),
51 | expiryDate: DateUtils.timestampToISOString(log.expiryDate),
52 | };
53 | }
54 |
55 | export const fetchLogs = createAsyncThunk(
56 | 'sidebar/fetchLogs',
57 | async (userId: string, { dispatch }) => {
58 | if (userId) {
59 | const isFirstLogin = await authService.isFirstTimeLogin(userId);
60 | if (isFirstLogin) {
61 | const logs = await logService.fetchLogsFromLocal();
62 | dispatch(setLogs(logs.map(parseLog)));
63 | }
64 | const remoteLogs = await logService.getLogsByUserId(userId);
65 | return remoteLogs.map(parseLog);
66 | }
67 | return await logService.fetchLogsFromLocal();
68 | }
69 | );
70 |
71 | export const fetchLogsFromLocal = createAsyncThunk(
72 | 'sidebar/fetchLogsFromLocal',
73 | async () => {
74 | return await logService.fetchLogsFromLocal();
75 | }
76 | );
77 |
78 | const sidebarSlice = createSlice({
79 | name: 'sidebarSlice',
80 | initialState,
81 | reducers: {
82 | setLoading: (state, action: PayloadAction) => {
83 | state.loading = action.payload;
84 | },
85 | setSelected: (state, action: PayloadAction) => {
86 | state.selected = action.payload;
87 | },
88 | setId: (state, action: PayloadAction) => {
89 | state.id = action.payload;
90 | },
91 | setShowSideBar: (state, action: PayloadAction) => {
92 | state.showSideBar = action.payload;
93 | },
94 | toggleSideBar: (state) => {
95 | state.showSideBar = !state.showSideBar;
96 | },
97 | setNavbarTitle: (state, action: PayloadAction) => {
98 | state.navbarTitle = action.payload;
99 | },
100 | refreshLogs: (state) => {
101 | // This will trigger a re-fetch through the component
102 | },
103 | setLogs(state, action) {
104 | state.logs = action.payload;
105 | },
106 | addLog(state, action) {
107 | state.logs = [action.payload, ...state.logs];
108 | },
109 | },
110 | extraReducers: (builder) => {
111 | builder
112 | .addCase(fetchLogs.pending, (state) => {
113 | state.loading = true;
114 | })
115 | .addCase(fetchLogs.fulfilled, (state, action) => {
116 | state.loading = false;
117 | state.logs = action.payload;
118 | })
119 | .addCase(fetchLogs.rejected, (state) => {
120 | state.loading = false;
121 | })
122 | }
123 | });
124 |
125 | export const { setSelected, setId, setShowSideBar, toggleSideBar, setLoading, setNavbarTitle, refreshLogs, setLogs, addLog } = sidebarSlice.actions;
126 |
127 | export default sidebarSlice.reducer;
--------------------------------------------------------------------------------
/src/lib/store.ts:
--------------------------------------------------------------------------------
1 | "use clent ";
2 | import { configureStore } from '@reduxjs/toolkit';
3 | import authSlice from './features/menus/authSlice';
4 | import editorSlice from './features/menus/editorSlice';
5 | import menuReducer from './features/menus/menuSlice';
6 | import sidebarSlice from './features/menus/sidebarSlice';
7 |
8 | export const store = configureStore({
9 | reducer: {
10 | menu: menuReducer,
11 | sidebar: sidebarSlice,
12 | auth: authSlice,
13 | editor: editorSlice,
14 | },
15 | });
16 |
17 | export type RootState = ReturnType;
18 | export type AppDispatch = typeof store.dispatch;
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/DateUtils.ts:
--------------------------------------------------------------------------------
1 | import { CalendarDate } from "@nextui-org/react";
2 | import { parseDate } from "@internationalized/date";
3 | import { Timestamp } from "firebase/firestore";
4 |
5 | class DateUtils {
6 | static toUTC(date: Date): string {
7 | return date.toISOString();
8 | }
9 |
10 | static toLocal(dateString: string): Date {
11 | return new Date(dateString);
12 | }
13 |
14 | static formatForDisplay(date: Date): string {
15 | return date.toLocaleString(); // Customize options as needed
16 | }
17 |
18 | // date in dd/mm/yyyy format
19 | static formatDateDDMMYYYY(date: Date): string {
20 | const day = String(date.getDate()).padStart(2, '0');
21 | const month = String(date.getMonth() + 1).padStart(2, '0');
22 | const year = date.getFullYear();
23 | return `${day}/${month}/${year}`;
24 | }
25 |
26 | // date in April 24, 2023 format
27 | static formatDateMMMMDDYYYY(date: Date): string {
28 | const options: Intl.DateTimeFormatOptions = {
29 | year: 'numeric',
30 | month: 'long',
31 | day: 'numeric',
32 | };
33 | return date.toLocaleDateString('en-US', options);
34 | }
35 |
36 | static getDateOffsetBy = (days: number): string => {
37 | const date = new Date();
38 | date.setDate(date.getDate() + days);
39 | return DateUtils.formatDateISO(date);
40 | };
41 |
42 | // returns date in ISO format
43 | static formatDateISO = (date: Date): string => {
44 | const month = String(date.getMonth() + 1).padStart(2, '0');
45 | const day = String(date.getDate()).padStart(2, '0');
46 | const year = date.getFullYear();
47 | return `${year}-${month}-${day}`;
48 | };
49 |
50 | static parsedDate = (date: Date): CalendarDate => {
51 | const isoDate = DateUtils.formatDateISO(date);
52 | const parsedDate = parseDate(isoDate);
53 | return parsedDate;
54 | }
55 |
56 | static timestampToISOString(input: Timestamp | string): string {
57 | if (typeof input === "string") return input;
58 | if (input instanceof Timestamp) return input.toDate().toISOString();
59 | return "";
60 | }
61 |
62 | static formatReadableDate = (dateInput: string | Timestamp | null | undefined): string => {
63 | if (!dateInput) return '';
64 |
65 | let date: Date;
66 |
67 | if (typeof dateInput === "string") {
68 | date = new Date(dateInput); // ISO string
69 | } else if (dateInput instanceof Timestamp) {
70 | date = dateInput.toDate(); // Firestore Timestamp
71 | } else {
72 | return '';
73 | }
74 |
75 | const options: Intl.DateTimeFormatOptions = {
76 | year: 'numeric',
77 | month: 'long',
78 | day: 'numeric',
79 | hour: 'numeric',
80 | minute: 'numeric',
81 | hour12: true, // Optional: 12-hour format
82 | };
83 |
84 | return date.toLocaleString('en-local', options);
85 | };
86 | }
87 |
88 | export default DateUtils;
89 |
--------------------------------------------------------------------------------
/src/utils/firebase.ts:
--------------------------------------------------------------------------------
1 | // src/utils/firebase.ts
2 | import { getAnalytics } from "firebase/analytics";
3 | import { initializeApp } from "firebase/app";
4 | import { getAuth } from "firebase/auth";
5 | import { getFirestore } from "firebase/firestore";
6 | const firebaseConfig = {
7 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
8 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
9 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
10 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
11 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
12 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
13 | };
14 |
15 | const app = initializeApp(firebaseConfig);
16 | const analytics = typeof window !== 'undefined' ? getAnalytics(app) : null;
17 | const db = getFirestore(app);
18 | const auth = getAuth(app);
19 | export { analytics, auth, db };
20 |
21 |
--------------------------------------------------------------------------------
/src/utils/toast_utils.tsx:
--------------------------------------------------------------------------------
1 | import { Id, Slide, ToastContent, ToastOptions, toast } from "react-toastify";
2 |
3 |
4 | export const defaultToastOptions: ToastOptions = {
5 | position: "top-center",
6 | autoClose: 4000,
7 | hideProgressBar: true,
8 | closeOnClick: true,
9 | pauseOnHover: true,
10 | draggable: true,
11 | progress: undefined,
12 | theme: "colored",
13 | transition: Slide,
14 | };
15 |
16 | type ToastType = "success" | "error" | "info" | "warning" | "default";
17 |
18 | /**
19 | * Display toast
20 | *
21 | * @param {ToastType} type
22 | * @param {ToastContent} content
23 | * @param {ToastOptions} [options=defaultToastOption]
24 | * @return {Id}
25 | */
26 | export const showToast = (
27 | type: ToastType,
28 | content: ToastContent,
29 | options: Partial = {},
30 | ): Id => {
31 | const optionsToApply = { ...defaultToastOptions, ...options };
32 |
33 | switch (type) {
34 | case "success":
35 | return toast.success(content, optionsToApply);
36 | case "error":
37 | return toast.error(content, optionsToApply);
38 | case "info":
39 | return toast.info(content, optionsToApply);
40 | case "warning":
41 | return toast.warn(content, optionsToApply);
42 | case "default":
43 | return toast(content, optionsToApply);
44 | default:
45 | return toast(content, optionsToApply);
46 | }
47 | };
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import { CalendarDate } from "@nextui-org/react";
2 | import { Timestamp } from "firebase/firestore";
3 | import html2canvas from "html2canvas";
4 |
5 | export const downloadImage = async () => {
6 | const preview = document.getElementById('preview');
7 | if (!preview) return;
8 |
9 | const codeBlocks = preview.querySelectorAll('code:not(pre code)');
10 |
11 | // Add custom class to fix rendering issues
12 | codeBlocks.forEach(block => {
13 | const codeBlock = block as HTMLElement; // Cast to HTMLElement
14 | codeBlock.style.paddingBottom = '1.0em';
15 | // codeBlock.style.backgroundColor = 'red';
16 | // codeBlock.style.display = 'inline-block';
17 | // codeBlock.style.verticalAlign = 'middle';
18 | });
19 |
20 | const previewElement = preview.querySelector('.reactMarkDown');
21 | const originalClasses = previewElement!.className;
22 |
23 | previewElement!.className = originalClasses.replace(/fade-in-animation|fade-out-animation/g, '').trim();
24 |
25 | // Ensure all images within the preview element are fully loaded
26 | const images = Array.from(preview.getElementsByTagName('img'));
27 | await Promise.all(images.map(img => new Promise((resolve, reject) => {
28 | if (img.complete) {
29 | resolve();
30 | } else {
31 | img.onload = () => resolve();
32 | img.onerror = () => reject();
33 | }
34 | if (!img.crossOrigin) {
35 | img.crossOrigin = 'anonymous';
36 | }
37 | })));
38 |
39 | // Capture the canvas and download the image
40 | html2canvas(preview, { useCORS: true }).then((canvas) => {
41 | const link = document.createElement('a');
42 | link.download = 'pastelog.png';
43 | link.href = canvas.toDataURL('image/png');
44 | link.click();
45 | // Restore the original styles after capturing the canvas
46 | codeBlocks.forEach(block => {
47 | const codeBlock = block as HTMLElement; // Cast to HTMLElement
48 | codeBlock.style.paddingBottom = '';
49 | });
50 | previewElement!.className = originalClasses;
51 | }).catch(error => {
52 | previewElement!.className = originalClasses;
53 | console.error(error);
54 | });
55 | };
56 |
57 | export const isExpired = (expiryDate: Timestamp | string | null | undefined): boolean => {
58 | if (!expiryDate) return false;
59 |
60 | let expiry: Date;
61 | if (expiryDate instanceof Timestamp) {
62 | expiry = expiryDate.toDate();
63 | } else if (typeof expiryDate === "string") {
64 | expiry = new Date(expiryDate);
65 | } else {
66 | return false;
67 | }
68 |
69 | const now = new Date();
70 | return expiry.getTime() <= now.getTime();
71 | };
72 |
73 | export const downloadText = (previewLog: any) => {
74 | if (!previewLog?.data) return;
75 | const element = document.createElement("a");
76 | const file = new Blob([previewLog.data], { type: 'text/plain' });
77 | element.href = URL.createObjectURL(file);
78 | element.download = "pastelog.txt";
79 | document.body.appendChild(element); // Required for this to work in FireFox
80 | element.click();
81 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | const { nextui } = require("@nextui-org/react");
3 |
4 | const config: Config = {
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
10 | ],
11 | theme: {
12 | screens: {
13 | xsm: '320px',
14 | sm: '480px',
15 | md: '768px',
16 | lg: '976px',
17 | xl: '1440px',
18 | },
19 | extend: {
20 | typography: {
21 | DEFAULT: {
22 | css: {
23 | 'code::before': {
24 | content: '""',
25 | },
26 | 'code::after': {
27 | content: '""',
28 | },
29 | },
30 | },
31 | },
32 | backgroundImage: {
33 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
34 | "gradient-conic":
35 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
36 | },
37 | colors: {
38 | primary: 'var(--color-primary)',
39 | secondary: 'var(--color-secondary)',
40 | accent: 'var(--color-accent)',
41 | background: 'var(--color-background)',
42 | surface: 'var(--color-surface)',
43 | },
44 | fontFamily: {
45 | sans: ['Inter', 'sans-serif'], // Custom font
46 | serif: ['Merriweather', 'serif'],
47 | },
48 | spacing: {
49 | '128': '32rem', // Custom spacing value
50 | },
51 | borderRadius: {
52 | 'none': '0',
53 | 'sm': '0.125rem',
54 | 'md': '0.375rem',
55 | 'lg': '0.5rem',
56 | 'xl': '0.75rem',
57 | '2xl': '1rem',
58 | '3xl': '1.5rem',
59 | '4xl': '2rem', // Custom border radius
60 | },
61 | },
62 | },
63 | darkMode: "class",
64 | plugins: [nextui(),
65 | require('@tailwindcss/typography')
66 | ]
67 | };
68 | export default config;
69 |
--------------------------------------------------------------------------------
/tree.md:
--------------------------------------------------------------------------------
1 | #### Component Tree
2 |
3 | |----------- app --------------|
4 | | | |
5 | | | |
6 | | | |
7 | (main) (policies) (publish)
8 | | | |
9 | | | |
10 | | | |
11 | /logs /policies /logs
12 | | |
13 | | |
14 | |
15 | ----- LogsLayout
16 | |
17 | Provider
18 | |
19 | PSNavbarProvider
20 | |
21 | Layout.tsx
22 | |
23 | AppLayout
24 | |
25 | / \
26 | Sidebar Main
27 | | |
28 | PSBanner | PSNavbar
29 |
30 |
31 | PreviewPage (/id) /id
32 | | |
33 |
34 | | |
35 | | |
36 |
37 | ### routes
38 | |----------- app --------------|
39 | | | |
40 | | | |
41 | | | |
42 | (main) (policies) (publish)
43 | | | |
44 | | | |
45 | | | |
46 | /logs /policies /logs
47 | | |
48 | | |
49 | | |
50 | /id /id
51 | | |
52 | | |
53 | | |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | }
29 | },
30 | "include": [
31 | "next-env.d.ts",
32 | "**/*.ts",
33 | "**/*.tsx",
34 | ".next/types/**/*.ts"
35 | ],
36 | "exclude": [
37 | "node_modules",
38 | "functions",
39 | ]
40 | }
--------------------------------------------------------------------------------