├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── README.md ├── app ├── (auth) │ ├── layout.tsx │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx ├── (dashboard) │ ├── action-items │ │ ├── action-items-wrapper.tsx │ │ └── page.tsx │ ├── record │ │ └── page.tsx │ └── recordings │ │ ├── [recordingId] │ │ ├── _components │ │ │ ├── action-form.tsx │ │ │ └── note-card.tsx │ │ └── page.tsx │ │ ├── _components │ │ ├── columns.tsx │ │ ├── data-table-column-header.tsx │ │ ├── data-table-pagination.tsx │ │ ├── data-table-row-actions.tsx │ │ ├── data-table-toolbar.tsx │ │ ├── data-table-view-options.tsx │ │ ├── data-table.tsx │ │ └── notes-wrapper.tsx │ │ └── page.tsx ├── (marketing) │ ├── _component │ │ ├── bento-grid.tsx │ │ ├── bentogrid.tsx │ │ ├── hero-highlight-section.tsx │ │ ├── hero-highlight.tsx │ │ └── marketing-bentogrid.tsx │ └── page.tsx ├── (profile) │ ├── layout.tsx │ └── user-profile │ │ └── [[...user-profile]] │ │ └── page.tsx ├── (public) │ └── share │ │ └── [noteId] │ │ └── page.tsx ├── error.tsx ├── globals.css ├── layout.tsx ├── loading.tsx └── not-found.tsx ├── bun.lockb ├── components.json ├── components ├── delete-model.tsx ├── footer.tsx ├── navbar.tsx ├── providers │ └── convex-provider.tsx ├── sharechat-model.tsx ├── skeltons.tsx ├── theme-provider.tsx ├── theme-toggle.tsx ├── ui │ ├── alert-dialog.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── skeleton.tsx │ ├── table.tsx │ └── tabs.tsx └── user-nav.tsx ├── constants └── index.ts ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── assembly.ts ├── auth.config.ts ├── gemini.ts ├── notes.ts ├── schema.ts └── tsconfig.json ├── hooks └── use-origin.tsx ├── lib └── utils.ts ├── license.md ├── middleware.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public ├── desktop.png ├── favicon.ico ├── mobile.png ├── next.svg ├── nonrecording_mic.svg ├── recording_mic.svg ├── thumbnail.png └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # make account here for keys: https://www.convex.dev 2 | CONVEX_DEPLOYMENT= 3 | NEXT_PUBLIC_CONVEX_URL= 4 | 5 | # make account here for keys: https://clerk.com 6 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 7 | CLERK_SECRET_KEY= 8 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 9 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 10 | NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL=/record 11 | NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/record 12 | 13 | # make account here for keys: https://www.assemblyai.com 14 | ASSEMBLY_API_KEY= 15 | 16 | # make account here for keys: https://ai.google.dev/gemini-api/docs/api-key 17 | GOOGLE_API_KEY= 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | dist 5 | .next 6 | out 7 | node_modules 8 | public 9 | 10 | # Ignore specific config files 11 | .env 12 | .env.local 13 | 14 | # Ignore package manager lock files 15 | bun.lockb 16 | 17 | # Ignore Next.js generated files 18 | .next/ 19 | 20 | # Ignore the build output directory 21 | out/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": [ 8 | "next/core-web-vitals", 9 | "plugin:@typescript-eslint/recommended-type-checked", 10 | "plugin:@typescript-eslint/stylistic-type-checked" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/array-type": "off", 14 | "@typescript-eslint/no-unsafe-assignment": "off", 15 | "@typescript-eslint/no-unsafe-member-access": "off", 16 | "@typescript-eslint/consistent-type-definitions": "off", 17 | "@typescript-eslint/consistent-type-imports": [ 18 | "warn", 19 | { 20 | "prefer": "type-imports", 21 | "fixStyle": "inline-type-imports" 22 | } 23 | ], 24 | "@typescript-eslint/no-unused-vars": [ 25 | "warn", 26 | { 27 | "argsIgnorePattern": "^_" 28 | } 29 | ], 30 | "@typescript-eslint/require-await": "off", 31 | "@typescript-eslint/no-misused-promises": [ 32 | "error", 33 | { 34 | "checksVoidReturn": { 35 | "attributes": false 36 | } 37 | } 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | dist 5 | .next 6 | out 7 | node_modules 8 | public 9 | 10 | # Ignore specific config files 11 | .env 12 | .env.local 13 | 14 | # Ignore package manager lock files 15 | bun.lockb 16 | 17 | # Ignore Next.js generated files 18 | .next/ 19 | 20 | # Ignore the build output directory 21 | out/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "plugins": ["prettier-plugin-tailwindcss"], 4 | "singleQuote": true, 5 | "semi": false, 6 | "trailingComma": "es5", 7 | "printWidth": 80, 8 | "tabWidth": 2, 9 | "useTabs": false, 10 | "bracketSpacing": true, 11 | "arrowParens": "always", 12 | "endOfLine": "lf" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Formatting using Prettier by default for all languages 3 | "editor.formatOnPaste": true, 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "explicit" 8 | }, 9 | // Formatting using Prettier for JavaScript, overrides VSCode default. 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | // Linting using ESLint. 14 | "eslint.validate": [ 15 | "javascript", 16 | "javascriptreact", 17 | "typescript", 18 | "typescriptreact" 19 | ], 20 | // Disable TypeScript surveys. 21 | "typescript.surveys.enabled": false, 22 | "typescript.tsdk": "node_modules/typescript/lib" 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

NotesGPT

3 |
NotesGPT seamlessly converts your voice notes into organized summaries and clear action items using AI.
4 |
5 | 6 |
7 | notessgpt.vercel.app 8 |
9 |
10 | 11 | ![Thumbnail](/public/thumbnail.png) 12 | 13 | Key Features: 14 | 15 | - Landing page 🛬 16 | - Light and Dark mode 🌓 17 | - Authentication 🔐 18 | - Real-time voice-to-text conversion 🗣️➡️📝 19 | - Organized summaries generation 📑 20 | - Generate transcript using AI 📑 21 | - Use google gemini ai for title, summary generation 🚀 22 | - Use assembly ai for generating transcript of voice 🔊 23 | - Ability to add action items ✅ 24 | - Ability to search through action items 🔎 25 | - Share note functionality 🌍 26 | 27 | ### Prerequisites 28 | 29 | **You should have Nodejs and Bun installed on your system** 30 | 31 | ### Cloning the repository 32 | 33 | ```shell 34 | git clone https://github.com/abdtriedcoding/notesGPT.git 35 | ``` 36 | 37 | ### Install packages 38 | 39 | ```shell 40 | bun i 41 | ``` 42 | 43 | ### Setup .env file taking refrence from .env.example file 44 | 45 | ### Setup Convex 46 | 47 | ```shell 48 | bunx convex dev 49 | 50 | ``` 51 | 52 | ### Start the app 53 | 54 | ```shell 55 | bun run dev 56 | ``` 57 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs' 2 | 3 | export default function Signin() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs' 2 | 3 | export default function Signup() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /app/(dashboard)/action-items/action-items-wrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { Input } from '@/components/ui/input' 5 | import { type api } from '@/convex/_generated/api' 6 | import { EmptyState } from '@/components/skeltons' 7 | import { type Preloaded, usePreloadedQuery } from 'convex/react' 8 | import NoteCard from '../recordings/[recordingId]/_components/note-card' 9 | 10 | export default function ActionItemsWrapper(props: { 11 | preloadedActionItems: Preloaded 12 | }) { 13 | const [search, setSearch] = useState('') 14 | const userActionItems = usePreloadedQuery(props.preloadedActionItems) 15 | 16 | if (userActionItems.length === 0) return 17 | 18 | const actionItems = userActionItems.filter( 19 | (e) => 20 | e.action.toLocaleLowerCase().includes(search.toLocaleLowerCase()) || 21 | e.title?.toLocaleLowerCase().includes(search.toLocaleLowerCase()) 22 | ) 23 | 24 | return ( 25 |
26 |

Action Items

27 |

28 | {userActionItems?.length ?? 0} tasks 29 |

30 | setSearch(e.target.value)} 33 | placeholder="Search through action items..." 34 | /> 35 | {actionItems.length === 0 ? ( 36 |

No Result Found

37 | ) : ( 38 | actionItems.map((item) => ) 39 | )} 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/(dashboard)/action-items/page.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthToken } from '@/lib/utils' 2 | import { preloadQuery } from 'convex/nextjs' 3 | import { api } from '@/convex/_generated/api' 4 | import ActionItemsWrapper from './action-items-wrapper' 5 | 6 | export default async function ActionItemsPage() { 7 | const token = await getAuthToken() 8 | const preloadedActionItems = await preloadQuery( 9 | api.notes.getActionItems, 10 | {}, 11 | { token } 12 | ) 13 | 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /app/(dashboard)/record/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Image from 'next/image' 4 | import { useEffect, useState } from 'react' 5 | import { useRouter } from 'next/navigation' 6 | import { formatTime, getCurrentFormattedDate } from '@/lib/utils' 7 | 8 | import { useMutation } from 'convex/react' 9 | import { api } from '@/convex/_generated/api' 10 | 11 | export default function RecordPage() { 12 | const router = useRouter() 13 | const [isRunning, setIsRunning] = useState(false) 14 | const [totalSeconds, setTotalSeconds] = useState(0) 15 | const [mediaRecorder, setMediaRecorder] = useState(null) 16 | const [title, setTitle] = useState('Record your voice note') 17 | 18 | const generateUploadUrl = useMutation(api.notes.generateUploadUrl) 19 | const createNote = useMutation(api.notes.createNote) 20 | 21 | useEffect(() => { 22 | let interval: NodeJS.Timeout 23 | 24 | if (isRunning) { 25 | interval = setInterval(() => { 26 | setTotalSeconds((prevTotalSeconds) => prevTotalSeconds + 1) 27 | }, 1000) 28 | } 29 | 30 | return () => clearInterval(interval) 31 | }, [isRunning]) 32 | 33 | async function startRecording() { 34 | setIsRunning(true) 35 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) 36 | const recorder = new MediaRecorder(stream) 37 | const audioChunks: Blob[] = [] 38 | 39 | recorder.ondataavailable = (e) => { 40 | audioChunks.push(e.data) 41 | } 42 | 43 | recorder.onstop = async () => { 44 | const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' }) 45 | const postUrl = await generateUploadUrl() 46 | const result = await fetch(postUrl, { 47 | method: 'POST', 48 | headers: { 'Content-Type': 'audio/mp3' }, 49 | body: audioBlob, 50 | }) 51 | const { storageId } = await result.json() 52 | const noteId = await createNote({ storageId }) 53 | router.push(`/recordings/${noteId}`) 54 | } 55 | setMediaRecorder(recorder) 56 | recorder.start() 57 | } 58 | 59 | function stopRecording() { 60 | mediaRecorder?.stop() 61 | setIsRunning(false) 62 | } 63 | 64 | const handleRecordClick = async () => { 65 | if (title === 'Record your voice note') { 66 | setTitle('Recording...') 67 | await startRecording() 68 | } else if (title === 'Recording...') { 69 | setTitle('Processing...') 70 | stopRecording() 71 | } 72 | } 73 | 74 | return ( 75 |
76 |

{title}

77 |

{getCurrentFormattedDate()}

78 |
79 |
80 |
85 |
89 |
90 |
91 |

92 | {formatTime(Math.floor(totalSeconds / 60))}: 93 | {formatTime(totalSeconds % 60)} 94 |

95 |
96 |
97 |
98 | 117 |
118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/[recordingId]/_components/action-form.tsx: -------------------------------------------------------------------------------- 1 | import { toast } from 'sonner' 2 | import { useState } from 'react' 3 | import { useMutation } from 'convex/react' 4 | import { api } from '@/convex/_generated/api' 5 | import { type Id } from '@/convex/_generated/dataModel' 6 | 7 | import { Input } from '@/components/ui/input' 8 | import { Button } from '@/components/ui/button' 9 | 10 | export default function ActionForm({ id }: { id: Id<'notes'> }) { 11 | const [input, setInput] = useState('') 12 | const createActionItem = useMutation(api.notes.createActionItem) 13 | 14 | const handleCreateAction = () => { 15 | if (!input.trim()) { 16 | return toast.error('Input is empty') 17 | } 18 | 19 | const promise = createActionItem({ 20 | noteId: id, 21 | action: input.trim(), 22 | }) 23 | toast.promise(promise, { 24 | loading: 'Creating action item...', 25 | success: 'Action item created', 26 | error: 'Failed to create action item', 27 | }) 28 | setInput('') 29 | } 30 | 31 | return ( 32 |
36 | setInput(e.target.value)} 39 | type="text" 40 | placeholder="Add action for this note..." 41 | /> 42 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/[recordingId]/_components/note-card.tsx: -------------------------------------------------------------------------------- 1 | import { toast } from 'sonner' 2 | import { formatDate } from '@/lib/utils' 3 | import { useMutation } from 'convex/react' 4 | import { api } from '@/convex/_generated/api' 5 | import { Checkbox } from '@/components/ui/checkbox' 6 | import { type Id } from '@/convex/_generated/dataModel' 7 | import { Card, CardContent } from '@/components/ui/card' 8 | 9 | interface ActionItemProps { 10 | _id: Id<'actionItems'> 11 | _creationTime: number 12 | userId: string 13 | noteId: Id<'notes'> 14 | action: string 15 | title?: string 16 | preview?: boolean 17 | } 18 | 19 | export default function NoteCard({ 20 | _creationTime, 21 | action, 22 | title, 23 | _id, 24 | preview, 25 | }: ActionItemProps) { 26 | const removeActionItem = useMutation(api.notes.removeActionItem) 27 | 28 | const handleremoveActionItem = () => { 29 | const promise = removeActionItem({ 30 | id: _id, 31 | }) 32 | toast.promise(promise, { 33 | loading: 'Deleting action item...', 34 | success: 'Action item completed', 35 | error: ' Failed to delete action item', 36 | }) 37 | } 38 | 39 | return ( 40 | 41 | 42 |
43 | {!preview && } 44 |

{action}

45 |
46 |

47 | From: {title ?? 'No Title'} 48 |

49 |

50 | {formatDate(_creationTime)} 51 |

52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/[recordingId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useQuery } from 'convex/react' 4 | import { useParams } from 'next/navigation' 5 | import { api } from '@/convex/_generated/api' 6 | import { type Doc, type Id } from '@/convex/_generated/dataModel' 7 | 8 | import NoteCard from './_components/note-card' 9 | import ActionForm from './_components/action-form' 10 | import { ActionItemSkelton } from '@/components/skeltons' 11 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' 12 | 13 | interface NoteWithActionItem { 14 | note: Doc<'notes'> 15 | actionItems: Doc<'actionItems'>[] 16 | } 17 | 18 | export default function RecordingIdPage() { 19 | const params = useParams() 20 | const { recordingId } = params 21 | 22 | const noteWithActionItems = useQuery(api.notes.getNoteById, { 23 | id: recordingId as Id<'notes'>, 24 | }) 25 | 26 | if (noteWithActionItems === undefined) { 27 | return 28 | } 29 | 30 | const { note, actionItems }: NoteWithActionItem = noteWithActionItems 31 | 32 | return ( 33 | <> 34 | 35 | 36 | Transcript 37 | Summary 38 | Action Items 39 | 40 | {note?.transcription} 41 | {note?.summary} 42 | 43 | 44 | {actionItems.map((item) => ( 45 | 46 | ))} 47 | 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/columns.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { formatDate } from '@/lib/utils' 4 | import { type ColumnDef } from '@tanstack/react-table' 5 | import { type Id } from '@/convex/_generated/dataModel' 6 | import { DataTableRowActions } from './data-table-row-actions' 7 | import { DataTableColumnHeader } from './data-table-column-header' 8 | 9 | interface Note { 10 | _creationTime: number 11 | _id: Id<'notes'> 12 | audioFileId: string 13 | audioFileUrl: string 14 | summary?: string 15 | title?: string 16 | transcription?: string 17 | userId: string 18 | } 19 | 20 | export const columns: ColumnDef[] = [ 21 | { 22 | accessorKey: 'title', 23 | header: ({ column }) => ( 24 | 25 | ), 26 | cell: ({ row }) => { 27 | return ( 28 | 29 | {row.getValue('title')} 30 | 31 | ) 32 | }, 33 | }, 34 | { 35 | accessorKey: '_creationTime', 36 | header: ({ column }) => ( 37 | 38 | ), 39 | cell: ({ row }) => { 40 | return ( 41 | 42 | {formatDate(row.getValue('_creationTime'))} 43 | 44 | ) 45 | }, 46 | }, 47 | { 48 | id: 'actions', 49 | cell: ({ row }) => { 50 | const { _id } = row.original 51 | return 52 | }, 53 | }, 54 | ] 55 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/data-table-column-header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import { type Column } from '@tanstack/react-table' 3 | import { Button } from '@/components/ui/button' 4 | import { 5 | ArrowDownIcon, 6 | ArrowUpIcon, 7 | ChevronsUpDown, 8 | EyeOff, 9 | } from 'lucide-react' 10 | import { 11 | DropdownMenu, 12 | DropdownMenuContent, 13 | DropdownMenuItem, 14 | DropdownMenuSeparator, 15 | DropdownMenuTrigger, 16 | } from '@/components/ui/dropdown-menu' 17 | 18 | interface DataTableColumnHeaderProps 19 | extends React.HTMLAttributes { 20 | column: Column 21 | title: string 22 | } 23 | 24 | export function DataTableColumnHeader({ 25 | column, 26 | title, 27 | className, 28 | }: DataTableColumnHeaderProps) { 29 | if (!column.getCanSort()) { 30 | return
{title}
31 | } 32 | 33 | return ( 34 |
35 | 36 | 37 | 51 | 52 | 53 | column.toggleSorting(false)}> 54 | 55 | Asc 56 | 57 | column.toggleSorting(true)}> 58 | 59 | Desc 60 | 61 | 62 | column.toggleVisibility(false)}> 63 | 64 | Hide 65 | 66 | 67 | 68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/data-table-pagination.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectItem, 5 | SelectTrigger, 6 | SelectValue, 7 | } from '@/components/ui/select' 8 | import { 9 | ArrowLeft, 10 | ArrowRightIcon, 11 | ChevronLeftIcon, 12 | ChevronRightIcon, 13 | } from 'lucide-react' 14 | import { Button } from '@/components/ui/button' 15 | import { type Table } from '@tanstack/react-table' 16 | 17 | interface DataTablePaginationProps { 18 | table: Table 19 | } 20 | 21 | export function DataTablePagination({ 22 | table, 23 | }: DataTablePaginationProps) { 24 | return ( 25 |
26 |
27 |

Rows per page

28 | 45 |
46 |
47 | Page {table.getState().pagination.pageIndex + 1} of{' '} 48 | {table.getPageCount()} 49 |
50 |
51 | 60 | 69 | 78 | 87 |
88 |
89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/data-table-row-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuTrigger, 7 | } from '@/components/ui/dropdown-menu' 8 | import { Ellipsis } from 'lucide-react' 9 | import { Button } from '@/components/ui/button' 10 | 11 | import { toast } from 'sonner' 12 | import { useMutation } from 'convex/react' 13 | import { useRouter } from 'next/navigation' 14 | import { api } from '@/convex/_generated/api' 15 | import { type Id } from '@/convex/_generated/dataModel' 16 | import { DeleteModel } from '@/components/delete-model' 17 | import { ShareChatModel } from '@/components/sharechat-model' 18 | 19 | export function DataTableRowActions({ id }: { id: Id<'notes'> }) { 20 | const router = useRouter() 21 | const removeNote = useMutation(api.notes.removeNote) 22 | 23 | const handelRemoveNote = () => { 24 | const promise = removeNote({ 25 | id, 26 | }) 27 | toast.promise(promise, { 28 | loading: 'Deleting Note...', 29 | success: 'Note Deleted', 30 | error: ' Failed to delete note.', 31 | }) 32 | } 33 | 34 | return ( 35 | 36 | 37 | 44 | 45 | 46 | 53 | 54 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/data-table-toolbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Input } from '@/components/ui/input' 4 | import { type Table } from '@tanstack/react-table' 5 | import { DataTableViewOptions } from './data-table-view-options' 6 | 7 | interface DataTableToolbarProps { 8 | table: Table 9 | } 10 | 11 | export function DataTableToolbar({ 12 | table, 13 | }: DataTableToolbarProps) { 14 | return ( 15 |
16 |
17 | 21 | table.getColumn('title')?.setFilterValue(event.target.value) 22 | } 23 | className="h-8 w-[250px] lg:w-[350px]" 24 | /> 25 |
26 | 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/data-table-view-options.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Table } from '@tanstack/react-table' 4 | import { 5 | DropdownMenu, 6 | DropdownMenuCheckboxItem, 7 | DropdownMenuContent, 8 | DropdownMenuLabel, 9 | DropdownMenuSeparator, 10 | DropdownMenuTrigger, 11 | } from '@/components/ui/dropdown-menu' 12 | import { Button } from '@/components/ui/button' 13 | import { MoreHorizontalIcon } from 'lucide-react' 14 | 15 | interface DataTableViewOptionsProps { 16 | table: Table 17 | } 18 | 19 | export function DataTableViewOptions({ 20 | table, 21 | }: DataTableViewOptionsProps) { 22 | return ( 23 | 24 | 25 | 33 | 34 | 35 | Toggle columns 36 | 37 | {table 38 | .getAllColumns() 39 | .filter( 40 | (column) => 41 | typeof column.accessorFn !== 'undefined' && column.getCanHide() 42 | ) 43 | .map((column) => { 44 | return ( 45 | column.toggleVisibility(!!value)} 50 | > 51 | {column.id} 52 | 53 | ) 54 | })} 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/data-table.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { 5 | type ColumnDef, 6 | type ColumnFiltersState, 7 | type SortingState, 8 | type VisibilityState, 9 | flexRender, 10 | getCoreRowModel, 11 | getFacetedRowModel, 12 | getFacetedUniqueValues, 13 | getFilteredRowModel, 14 | getPaginationRowModel, 15 | getSortedRowModel, 16 | useReactTable, 17 | } from '@tanstack/react-table' 18 | 19 | import { 20 | Table, 21 | TableBody, 22 | TableCell, 23 | TableHead, 24 | TableHeader, 25 | TableRow, 26 | } from '@/components/ui/table' 27 | 28 | import { DataTablePagination } from './data-table-pagination' 29 | import { DataTableToolbar } from './data-table-toolbar' 30 | 31 | interface DataTableProps { 32 | columns: ColumnDef[] 33 | data: TData[] 34 | } 35 | 36 | export function DataTable({ 37 | columns, 38 | data, 39 | }: DataTableProps) { 40 | const [rowSelection, setRowSelection] = React.useState({}) 41 | const [columnVisibility, setColumnVisibility] = 42 | React.useState({}) 43 | const [columnFilters, setColumnFilters] = React.useState( 44 | [] 45 | ) 46 | const [sorting, setSorting] = React.useState([]) 47 | 48 | const table = useReactTable({ 49 | data, 50 | columns, 51 | state: { 52 | sorting, 53 | columnVisibility, 54 | rowSelection, 55 | columnFilters, 56 | }, 57 | enableRowSelection: true, 58 | onRowSelectionChange: setRowSelection, 59 | onSortingChange: setSorting, 60 | onColumnFiltersChange: setColumnFilters, 61 | onColumnVisibilityChange: setColumnVisibility, 62 | getCoreRowModel: getCoreRowModel(), 63 | getFilteredRowModel: getFilteredRowModel(), 64 | getPaginationRowModel: getPaginationRowModel(), 65 | getSortedRowModel: getSortedRowModel(), 66 | getFacetedRowModel: getFacetedRowModel(), 67 | getFacetedUniqueValues: getFacetedUniqueValues(), 68 | }) 69 | 70 | return ( 71 |
72 | 73 |
74 | 75 | 76 | {table.getHeaderGroups().map((headerGroup) => ( 77 | 78 | {headerGroup.headers.map((header) => { 79 | return ( 80 | 81 | {header.isPlaceholder 82 | ? null 83 | : flexRender( 84 | header.column.columnDef.header, 85 | header.getContext() 86 | )} 87 | 88 | ) 89 | })} 90 | 91 | ))} 92 | 93 | 94 | {table.getRowModel().rows?.length ? ( 95 | table.getRowModel().rows.map((row) => ( 96 | 100 | {row.getVisibleCells().map((cell) => ( 101 | 102 | {flexRender( 103 | cell.column.columnDef.cell, 104 | cell.getContext() 105 | )} 106 | 107 | ))} 108 | 109 | )) 110 | ) : ( 111 | 112 | 116 | No results. 117 | 118 | 119 | )} 120 | 121 |
122 |
123 | 124 |
125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/_components/notes-wrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from 'next/link' 4 | import { columns } from './columns' 5 | import { DataTable } from './data-table' 6 | import { type api } from '@/convex/_generated/api' 7 | import { Mic, PencilLine } from 'lucide-react' 8 | import { buttonVariants } from '@/components/ui/button' 9 | import { type Preloaded, usePreloadedQuery } from 'convex/react' 10 | 11 | export function NotesWrapper(props: { 12 | preloadedNotes: Preloaded 13 | }) { 14 | const userNotes = usePreloadedQuery(props.preloadedNotes) 15 | 16 | return ( 17 | <> 18 |
19 |

Welcome back!

20 |

21 | Here's a list of all your notes! 22 |

23 |
24 | {userNotes.length === 0 ? ( 25 | 26 | ) : ( 27 | 28 | )} 29 | 30 | ) 31 | } 32 | 33 | function EmptyState() { 34 | return ( 35 |
36 |
37 | 41 | Record a New Voice Note 42 | 43 | 44 | 48 | View Action Items 49 | 50 | 51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /app/(dashboard)/recordings/page.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthToken } from '@/lib/utils' 2 | import { preloadQuery } from 'convex/nextjs' 3 | import { api } from '@/convex/_generated/api' 4 | import { NotesWrapper } from './_components/notes-wrapper' 5 | 6 | export default async function RecordingPage() { 7 | const token = await getAuthToken() 8 | const preloadedNotes = await preloadQuery( 9 | api.notes.getUserNotes, 10 | {}, 11 | { token } 12 | ) 13 | 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /app/(marketing)/_component/bento-grid.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | export const BentoGrid = ({ 4 | className, 5 | children, 6 | }: { 7 | className?: string 8 | children?: React.ReactNode 9 | }) => { 10 | return ( 11 |
17 | {children} 18 |
19 | ) 20 | } 21 | 22 | export const BentoGridItem = ({ 23 | className, 24 | title, 25 | description, 26 | header, 27 | icon, 28 | }: { 29 | className?: string 30 | title?: string | React.ReactNode 31 | description?: string | React.ReactNode 32 | header?: React.ReactNode 33 | icon?: React.ReactNode 34 | }) => { 35 | return ( 36 |
42 | {header} 43 |
44 | {icon} 45 |
46 | {title} 47 |
48 |
49 | {description} 50 |
51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /app/(marketing)/_component/bentogrid.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { cn } from '@/lib/utils' 5 | import { motion } from 'framer-motion' 6 | import { BentoGrid, BentoGridItem } from './bento-grid' 7 | import { 8 | Box, 9 | CheckCircle, 10 | File, 11 | Frown, 12 | Goal, 13 | Lightbulb, 14 | Mic, 15 | User, 16 | XCircle, 17 | } from 'lucide-react' 18 | 19 | export function BentoGridTemplate() { 20 | return ( 21 | 22 | {items.map((item, i) => ( 23 | p:text-lg]', item.className)} 29 | icon={item.icon} 30 | /> 31 | ))} 32 | 33 | ) 34 | } 35 | 36 | const SkeletonOne = () => { 37 | const variants = { 38 | initial: { 39 | x: 0, 40 | }, 41 | animate: { 42 | x: 10, 43 | rotate: 5, 44 | transition: { 45 | duration: 0.2, 46 | }, 47 | }, 48 | } 49 | const variantsSecond = { 50 | initial: { 51 | x: 0, 52 | }, 53 | animate: { 54 | x: -10, 55 | rotate: -5, 56 | transition: { 57 | duration: 0.2, 58 | }, 59 | }, 60 | } 61 | 62 | return ( 63 | 68 | 72 |
73 |
74 | 75 | 79 |
80 |
81 | 82 | 86 |
87 |
88 | 89 | 90 | ) 91 | } 92 | const SkeletonTwo = () => { 93 | const variants = { 94 | initial: { 95 | width: 0, 96 | }, 97 | animate: { 98 | width: '100%', 99 | transition: { 100 | duration: 0.2, 101 | }, 102 | }, 103 | hover: { 104 | width: ['0%', '100%'], 105 | transition: { 106 | duration: 2, 107 | }, 108 | }, 109 | } 110 | const arr = new Array(6).fill(0) 111 | return ( 112 | 118 | {arr.map((_, i) => ( 119 | 127 | ))} 128 | 129 | ) 130 | } 131 | const SkeletonThree = () => { 132 | const variants = { 133 | initial: { 134 | backgroundPosition: '0 50%', 135 | }, 136 | animate: { 137 | backgroundPosition: ['0, 50%', '100% 50%', '0 50%'], 138 | }, 139 | } 140 | return ( 141 | 157 | 158 | 159 | ) 160 | } 161 | const SkeletonFour = () => { 162 | const first = { 163 | initial: { 164 | x: 20, 165 | rotate: -5, 166 | }, 167 | hover: { 168 | x: 0, 169 | rotate: 0, 170 | }, 171 | } 172 | const second = { 173 | initial: { 174 | x: -20, 175 | rotate: 5, 176 | }, 177 | hover: { 178 | x: 0, 179 | rotate: 0, 180 | }, 181 | } 182 | return ( 183 | 189 | 193 | 194 |

195 | You are not going to able to organize notes easily ? 196 |

197 |

198 | Take Action 199 |

200 |
201 | 202 | 203 |

204 | On your way to meet your goal 205 |

206 |

207 | On Track 208 |

209 |
210 | 214 | 215 |

216 | Your going to miss your goal this month 217 |

218 |

219 | Helpless 220 |

221 |
222 |
223 | ) 224 | } 225 | const SkeletonFive = () => { 226 | const variants = { 227 | initial: { 228 | x: 0, 229 | }, 230 | animate: { 231 | x: 10, 232 | rotate: 5, 233 | transition: { 234 | duration: 0.2, 235 | }, 236 | }, 237 | } 238 | const variantsSecond = { 239 | initial: { 240 | x: 0, 241 | }, 242 | animate: { 243 | x: -10, 244 | rotate: -5, 245 | transition: { 246 | duration: 0.2, 247 | }, 248 | }, 249 | } 250 | 251 | return ( 252 | 257 | 261 | 262 |

263 | How can i manage my notes or daily notes efficiently ? 264 |

265 |
266 | 270 |

Use NotesGPT.

271 |
272 | 273 | 274 | ) 275 | } 276 | const items = [ 277 | { 278 | title: 'Voice-to-Notes Transformation', 279 | description: ( 280 | 281 | Convert your voice memos into actionable insights effortlessly. 282 | 283 | ), 284 | header: , 285 | className: 'md:col-span-1', 286 | icon: , 287 | }, 288 | { 289 | title: 'Streamlined Organization', 290 | description: ( 291 | 292 | Effortlessly summarize and organize your voice notes into clear action 293 | items. 294 | 295 | ), 296 | header: , 297 | className: 'md:col-span-1', 298 | icon: , 299 | }, 300 | { 301 | title: 'Instant Note Generation', 302 | description: ( 303 | 304 | Say it, summarize it, do it. Let NotesGPT handle the rest. 305 | 306 | ), 307 | header: , 308 | className: 'md:col-span-1', 309 | icon: , 310 | }, 311 | { 312 | title: 'Add task Setting and Tracking', 313 | description: ( 314 | 315 | Add task and track them achieve them faster. Let NotesGPT help you stay 316 | on track. 317 | 318 | ), 319 | header: , 320 | className: 'md:col-span-2', 321 | icon: , 322 | }, 323 | 324 | { 325 | title: 'Text Summarization', 326 | description: ( 327 | Get summarize notes with AI technology. 328 | ), 329 | header: , 330 | className: 'md:col-span-1', 331 | icon: , 332 | }, 333 | ] 334 | -------------------------------------------------------------------------------- /app/(marketing)/_component/hero-highlight-section.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { motion } from 'framer-motion' 4 | import { HeroHighlight, Highlight } from './hero-highlight' 5 | 6 | export function HeroHighlightSection() { 7 | return ( 8 | 9 | 24 | With NotesGPT, you can easliy organize your notes or task for every{' '} 25 | 26 | day, week, month 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /app/(marketing)/_component/hero-highlight.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { cn } from '@/lib/utils' 5 | import { useMotionValue, motion, useMotionTemplate } from 'framer-motion' 6 | 7 | export const HeroHighlight = ({ 8 | children, 9 | className, 10 | containerClassName, 11 | }: { 12 | children: React.ReactNode 13 | className?: string 14 | containerClassName?: string 15 | }) => { 16 | const mouseX = useMotionValue(0) 17 | const mouseY = useMotionValue(0) 18 | 19 | function handleMouseMove({ 20 | currentTarget, 21 | clientX, 22 | clientY, 23 | }: React.MouseEvent) { 24 | if (!currentTarget) return 25 | const { left, top } = currentTarget.getBoundingClientRect() 26 | 27 | mouseX.set(clientX - left) 28 | mouseY.set(clientY - top) 29 | } 30 | return ( 31 |
38 |
39 | 58 | 59 |
{children}
60 |
61 | ) 62 | } 63 | 64 | export const Highlight = ({ 65 | children, 66 | className, 67 | }: { 68 | children: React.ReactNode 69 | className?: string 70 | }) => { 71 | return ( 72 | 94 | {children} 95 | 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /app/(marketing)/_component/marketing-bentogrid.tsx: -------------------------------------------------------------------------------- 1 | import { Balancer } from 'react-wrap-balancer' 2 | import { BentoGridTemplate } from './bentogrid' 3 | 4 | export default function MarketingBentoGrid() { 5 | return ( 6 |
7 |
8 |
9 |

13 | The new 14 |

15 | 16 |

20 | Golden Standard 21 |

22 | 23 |

27 | 28 | NotesGPT seamlessly converts your voice notes into organized 29 | summaries and clear action items using AI. 30 | 31 |

32 |
33 |
34 |
35 | 36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Image from 'next/image' 3 | import { cn } from '@/lib/utils' 4 | import { Twitter } from 'lucide-react' 5 | import Balancer from 'react-wrap-balancer' 6 | import { Button, buttonVariants } from '@/components/ui/button' 7 | import MarketingBentoGrid from './_component/marketing-bentogrid' 8 | import { HeroHighlightSection } from './_component/hero-highlight-section' 9 | 10 | export default function MarketingPage() { 11 | return ( 12 |
13 | 22 | Introducing on 23 | 24 |

28 | 29 | AI-Powered{' '} 30 | 31 | Voice Notes{' '} 32 | 33 | Management 34 | 35 |

36 |

40 | 41 | NotesGPT seamlessly converts your voice notes into organized summaries 42 | and clear action items using AI. 43 | 44 |

45 | 53 |
57 |
58 | mobile 68 |
69 |
70 | Desktop 80 |
81 |
82 | 83 | 84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /app/(profile)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function UserProfileLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /app/(profile)/user-profile/[[...user-profile]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserProfile } from '@clerk/nextjs' 2 | 3 | const UserProfilePage = () => 4 | 5 | export default UserProfilePage 6 | -------------------------------------------------------------------------------- /app/(public)/share/[noteId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useQuery } from 'convex/react' 4 | import { useParams } from 'next/navigation' 5 | import { api } from '@/convex/_generated/api' 6 | import { type Doc, type Id } from '@/convex/_generated/dataModel' 7 | 8 | import { ActionItemSkelton, EmptyState } from '@/components/skeltons' 9 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' 10 | import NoteCard from '@/app/(dashboard)/recordings/[recordingId]/_components/note-card' 11 | 12 | interface NoteWithActionItem { 13 | note: Doc<'notes'> 14 | actionItems: Doc<'actionItems'>[] 15 | } 16 | 17 | export default function SharePage() { 18 | const params = useParams() 19 | const { noteId } = params 20 | 21 | const noteWithActionItems = useQuery(api.notes.getNoteById, { 22 | id: noteId as Id<'notes'>, 23 | }) 24 | 25 | if (noteWithActionItems === undefined) { 26 | return 27 | } 28 | 29 | const { note, actionItems }: NoteWithActionItem = noteWithActionItems 30 | return ( 31 | 32 | 33 | Transcript 34 | Summary 35 | Action Items 36 | 37 | {note?.transcription} 38 | {note?.summary} 39 | 40 | {actionItems.length > 0 ? ( 41 | actionItems.map((item) => ( 42 | 43 | )) 44 | ) : ( 45 | 46 | )} 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@/components/ui/button' 4 | 5 | export default function Error({ 6 | reset, 7 | }: { 8 | error: Error & { digest?: string } 9 | reset: () => void 10 | }) { 11 | return ( 12 |
13 |

Something went wrong!

14 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .recording-box { 6 | background: linear-gradient( 7 | 143deg, 8 | #e648f4 8.37%, 9 | rgba(73, 80, 240, 0.62) 71.62% 10 | ); 11 | filter: blur(13.100000381469727px); 12 | } 13 | 14 | .record-animation { 15 | animation: rotate360 infinite 5s linear; 16 | } 17 | 18 | @keyframes rotate360 { 19 | 0% { 20 | transform: rotate(0deg); 21 | } 22 | 23 | 100% { 24 | transform: rotate(360deg); 25 | } 26 | } 27 | 28 | @layer base { 29 | :root { 30 | --background: 0 0% 100%; 31 | --foreground: 240 10% 3.9%; 32 | 33 | --card: 0 0% 100%; 34 | --card-foreground: 240 10% 3.9%; 35 | 36 | --popover: 0 0% 100%; 37 | --popover-foreground: 240 10% 3.9%; 38 | 39 | --primary: 240 5.9% 10%; 40 | --primary-foreground: 0 0% 98%; 41 | 42 | --secondary: 240 4.8% 95.9%; 43 | --secondary-foreground: 240 5.9% 10%; 44 | 45 | --muted: 240 4.8% 95.9%; 46 | --muted-foreground: 240 3.8% 46.1%; 47 | 48 | --accent: 240 4.8% 95.9%; 49 | --accent-foreground: 240 5.9% 10%; 50 | 51 | --destructive: 0 84.2% 60.2%; 52 | --destructive-foreground: 0 0% 98%; 53 | 54 | --border: 240 5.9% 90%; 55 | --input: 240 5.9% 90%; 56 | --ring: 240 10% 3.9%; 57 | 58 | --radius: 0.5rem; 59 | } 60 | 61 | .dark { 62 | --background: 240 10% 3.9%; 63 | --foreground: 0 0% 98%; 64 | 65 | --card: 240 10% 3.9%; 66 | --card-foreground: 0 0% 98%; 67 | 68 | --popover: 240 10% 3.9%; 69 | --popover-foreground: 0 0% 98%; 70 | 71 | --primary: 0 0% 98%; 72 | --primary-foreground: 240 5.9% 10%; 73 | 74 | --secondary: 240 3.7% 15.9%; 75 | --secondary-foreground: 0 0% 98%; 76 | 77 | --muted: 240 3.7% 15.9%; 78 | --muted-foreground: 240 5% 64.9%; 79 | 80 | --accent: 240 3.7% 15.9%; 81 | --accent-foreground: 0 0% 98%; 82 | 83 | --destructive: 0 62.8% 30.6%; 84 | --destructive-foreground: 0 0% 98%; 85 | 86 | --border: 240 3.7% 15.9%; 87 | --input: 240 3.7% 15.9%; 88 | --ring: 240 4.9% 83.9%; 89 | } 90 | } 91 | 92 | @layer base { 93 | * { 94 | @apply border-border; 95 | } 96 | body { 97 | @apply bg-background text-foreground; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Toaster } from 'sonner' 3 | import { type Metadata } from 'next' 4 | import { Poppins } from 'next/font/google' 5 | import { Analytics } from '@vercel/analytics/react' 6 | import { ThemeProvider } from '@/components/theme-provider' 7 | import { ConvexProvider } from '@/components/providers/convex-provider' 8 | 9 | import Navbar from '@/components/navbar' 10 | import Footer from '@/components/footer' 11 | 12 | const font = Poppins({ subsets: ['latin'], weight: ['500'] }) 13 | 14 | export const metadata: Metadata = { 15 | title: { 16 | default: 'NotesGPT', 17 | template: `%s - "NotesGPT`, 18 | }, 19 | metadataBase: new URL('https://notessgpt.vercel.app'), 20 | description: 21 | 'NotesGPT seamlessly converts your voice notes into organized summaries and clear action items using AI.', 22 | keywords: [ 23 | 'Voice Note Conversion', 24 | 'AI Note Making', 25 | 'Automated Summaries', 26 | 'Task Management', 27 | 'Project Management', 28 | 'Voice to Text', 29 | ], 30 | authors: [ 31 | { 32 | name: 'abdtriedcoding', 33 | url: 'https://abdullahsidd.vercel.app', 34 | }, 35 | ], 36 | creator: 'abdtriedcoding', 37 | openGraph: { 38 | type: 'website', 39 | locale: 'en_US', 40 | url: 'https://notessgpt.vercel.app', 41 | title: 'NotesGPT', 42 | description: 43 | 'NotesGPT seamlessly converts your voice notes into organized summaries and clear action items using AI.', 44 | siteName: 'NotesGPT', 45 | images: [ 46 | { 47 | url: '/thumbnail.png', 48 | width: 1200, 49 | height: 630, 50 | alt: 'NotesGPT', 51 | }, 52 | ], 53 | }, 54 | twitter: { 55 | card: 'summary_large_image', 56 | title: 'NotesGPT', 57 | description: 58 | 'NotesGPT seamlessly converts your voice notes into organized summaries and clear action items using AI.', 59 | images: ['/thumbnail.png'], 60 | creator: '@abdtriedcoding', 61 | }, 62 | icons: { 63 | icon: '/favicon.ico', 64 | }, 65 | } 66 | 67 | export default function RootLayout({ 68 | children, 69 | }: Readonly<{ 70 | children: React.ReactNode 71 | }>) { 72 | return ( 73 | 74 | 75 | 76 | 82 | 83 |
84 | 85 | {children} 86 | 87 |
88 |