Repository: get-convex/turbo-expo-nextjs-clerk-convex-monorepo Branch: main Commit: da934aeb9612 Files: 77 Total size: 126.5 KB Directory structure: gitextract_vxwibcq7/ ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── apps/ │ ├── native/ │ │ ├── .example.env │ │ ├── .gitignore │ │ ├── ConvexClientProvider.tsx │ │ ├── app.json │ │ ├── babel.config.js │ │ ├── index.tsx │ │ ├── metro.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── (app)/ │ │ │ │ │ ├── _layout.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── notes/ │ │ │ │ │ ├── [noteId].tsx │ │ │ │ │ └── new.tsx │ │ │ │ ├── (auth)/ │ │ │ │ │ ├── _layout.tsx │ │ │ │ │ └── sign-in.tsx │ │ │ │ └── _layout.tsx │ │ │ └── screens/ │ │ │ ├── CreateNoteScreen.tsx │ │ │ ├── InsideNoteScreen.tsx │ │ │ ├── LoginScreen.tsx │ │ │ └── NotesDashboardScreen.tsx │ │ └── tsconfig.json │ └── web/ │ ├── .example.env │ ├── .gitignore │ ├── eslint.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── src/ │ │ ├── app/ │ │ │ ├── ConvexClientProvider.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── notes/ │ │ │ │ ├── [slug]/ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── Header.tsx │ │ │ ├── common/ │ │ │ │ ├── Logo.tsx │ │ │ │ ├── Menu.tsx │ │ │ │ ├── TestTimonialCard.tsx │ │ │ │ ├── UserNav.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ └── dropdown-menu.tsx │ │ │ ├── home/ │ │ │ │ ├── Benefits.tsx │ │ │ │ ├── ComplexToggle.tsx │ │ │ │ ├── Footer.tsx │ │ │ │ ├── FooterHero.tsx │ │ │ │ ├── Hero.tsx │ │ │ │ └── Testimonials.tsx │ │ │ └── notes/ │ │ │ ├── Checkbox.tsx │ │ │ ├── CreateNote.tsx │ │ │ ├── DeleteNote.tsx │ │ │ ├── NoteDetails.tsx │ │ │ ├── NoteItem.tsx │ │ │ └── Notes.tsx │ │ ├── lib/ │ │ │ └── utils.ts │ │ └── proxy.ts │ ├── tsconfig.json │ └── vercel.json ├── package.json ├── packages/ │ └── backend/ │ ├── .gitignore │ ├── convex/ │ │ ├── README.md │ │ ├── _generated/ │ │ │ ├── api.d.ts │ │ │ ├── api.js │ │ │ ├── dataModel.d.ts │ │ │ ├── server.d.ts │ │ │ └── server.js │ │ ├── auth.config.ts │ │ ├── notes.ts │ │ ├── openai.ts │ │ ├── schema.ts │ │ ├── tsconfig.json │ │ └── utils.ts │ └── package.json ├── pnpm-workspace.yaml ├── renovate.json └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules .pnp .pnp.js # testing coverage # next.js .next/ .swc/ out/ build # expo .expo # misc .DS_Store *.pem dist # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env.local .env.development.local .env.test.local .env.production.local .env # turbo .turbo ================================================ FILE: .prettierrc ================================================ { "arrowParens": "always" } ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2024 Convex, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Fullstack Monorepo Template This repo is a modern TypeScript monorepo for a shared notes app across web and native. It currently uses: - [Turborepo](https://turbo.build/repo) - [pnpm](https://pnpm.io/) - [Next.js 16](https://nextjs.org/) with the App Router - [Expo SDK 55](https://docs.expo.dev/) with Expo Router - [React 19](https://react.dev/) - [Convex](https://convex.dev/) - [Clerk](https://clerk.com/) - [OpenAI](https://platform.openai.com/docs/quickstart?api-mode=responses) for optional note summaries ## What’s inside - `apps/web` - the Next.js web app - `apps/native` - the Expo native app - `packages/backend` - the Convex backend and generated API types ## Quick start ### 1. Install dependencies ```sh pnpm install ``` ### 2. Configure Convex Run the backend setup command: ```sh pnpm --filter @packages/backend setup ``` This will log you into Convex, create or connect a project, and generate `packages/backend/.env.local`. ### 3. Configure Clerk for Convex Follow the official [Convex + Clerk guide](https://docs.convex.dev/auth/clerk). In the Clerk dashboard, enable the Convex integration and copy the Clerk JWT issuer domain. Add it to your Convex environment variables as: ```sh CLERK_JWT_ISSUER_DOMAIN=https://your-frontend-api.clerk.accounts.dev ``` If you want native social login, enable Google and Apple in Clerk as well. If you want AI summaries, also add: ```sh OPENAI_API_KEY=... ``` to your Convex environment variables. ### 4. Configure the app env files Create `.env.local` files in `apps/web` and `apps/native` from the provided `.example.env` files. - Use `CONVEX_URL` from `packages/backend/.env.local` for both `NEXT_PUBLIC_CONVEX_URL` and `EXPO_PUBLIC_CONVEX_URL` - Use your Clerk publishable key for both app env files - Use your Clerk secret key in `apps/web/.env.local` ### 5. Run the apps ```sh pnpm dev ``` This runs the backend, web app, and native app through Turbo. ## Deploying From `apps/web`, use Convex to deploy the backend and build the web app: ```sh cd ../../packages/backend && pnpm exec convex deploy --cmd 'cd ../../apps/web && pnpm build' --cmd-url-env-var-name NEXT_PUBLIC_CONVEX_URL ``` `apps/web/vercel.json` is already set up for this flow on Vercel. ## Adding dependencies Install dependencies in the package that actually uses them. Examples: ```sh pnpm --filter web-app add mypackage@latest pnpm --filter native-app add mypackage@latest pnpm --filter @packages/backend add mypackage@latest ``` ## Notes - The native app uses Expo Router route groups under `apps/native/src/app` - The web app protects note routes with `apps/web/src/proxy.ts` - Convex enforces note ownership server-side in `packages/backend/convex/notes.ts` ================================================ FILE: apps/native/.example.env ================================================ EXPO_PUBLIC_CONVEX_URL= EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY= ================================================ FILE: apps/native/.gitignore ================================================ node_modules/ .expo/ dist/ npm-debug.* *.jks *.p8 *.p12 *.key *.mobileprovision *.orig.* web-build/ # macOS .DS_Store # Temporary files created by Metro to check the health of the file watcher .metro-health-check* # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb # The following patterns were generated by expo-cli expo-env.d.ts # @end expo-cli ================================================ FILE: apps/native/ConvexClientProvider.tsx ================================================ import { type ReactNode } from "react"; import { ClerkProvider, useAuth } from "@clerk/clerk-expo"; import { tokenCache } from "@clerk/clerk-expo/token-cache"; import { ConvexReactClient } from "convex/react"; import { ConvexProviderWithClerk } from "convex/react-clerk"; const convexUrl = process.env.EXPO_PUBLIC_CONVEX_URL; const clerkPublishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY; if (!convexUrl) throw new Error( "Missing EXPO_PUBLIC_CONVEX_URL for the native Convex client", ); if (!clerkPublishableKey) throw new Error( "Missing EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY for the native Clerk client", ); const convex = new ConvexReactClient(convexUrl); export default function ConvexClientProvider({ children, }: { children: ReactNode; }) { return ( {children} ); } ================================================ FILE: apps/native/app.json ================================================ { "expo": { "newArchEnabled": true, "name": "NotesContract", "slug": "NotesContract", "scheme": "notescontract", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#0D87E1" }, "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" } }, "web": { "bundler": "metro", "favicon": "./assets/favicon.png" }, "plugins": [ [ "expo-router", { "root": "./src/app" } ], "expo-font", "expo-secure-store" ], "experiments": { "typedRoutes": true } } } ================================================ FILE: apps/native/babel.config.js ================================================ module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"], }; }; ================================================ FILE: apps/native/index.tsx ================================================ import * as WebBrowser from "expo-web-browser"; WebBrowser.maybeCompleteAuthSession(); import "expo-router/entry"; ================================================ FILE: apps/native/metro.config.js ================================================ const { getDefaultConfig } = require("expo/metro-config"); module.exports = getDefaultConfig(__dirname); ================================================ FILE: apps/native/package.json ================================================ { "name": "native-app", "version": "1.0.0", "main": "index.tsx", "scripts": { "dev": "expo start", "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", "typecheck": "tsc --noEmit" }, "dependencies": { "@clerk/clerk-expo": "^2.19.31", "@expo/vector-icons": "^15.1.1", "@packages/backend": "workspace:*", "expo": "~55.0.16", "expo-asset": "~55.0.16", "expo-auth-session": "~55.0.15", "expo-constants": "~55.0.15", "expo-font": "~55.0.6", "expo-linking": "~55.0.14", "expo-router": "~55.0.13", "expo-secure-store": "~55.0.13", "expo-system-ui": "~55.0.16", "expo-web-browser": "~55.0.14", "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.6", "react-native-gesture-handler": "~2.30.1", "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-reanimated": "~4.2.1", "react-native-responsive-fontsize": "^0.5.1", "react-native-safe-area-context": "5.6.2", "react-native-screens": "~4.23.0", "react-native-web": "~0.21.2", "react-native-worklets": "0.7.4" }, "devDependencies": { "@babel/core": "7.29.0", "@types/jest": "29.5.14", "@types/react": "19.2.14", "@types/react-test-renderer": "19.1.0", "jest": "29.7.0", "jest-expo": "55.0.16", "react-test-renderer": "19.2.0", "typescript": "5.9.3" }, "private": true } ================================================ FILE: apps/native/src/app/(app)/_layout.tsx ================================================ import { useAuth } from "@clerk/clerk-expo"; import { Redirect, Stack } from "expo-router"; export default function AppLayout() { const { isLoaded, isSignedIn } = useAuth(); if (!isLoaded) return null; if (!isSignedIn) return ; return ; } ================================================ FILE: apps/native/src/app/(app)/index.tsx ================================================ export { default } from "../../screens/NotesDashboardScreen"; ================================================ FILE: apps/native/src/app/(app)/notes/[noteId].tsx ================================================ export { default } from "../../../screens/InsideNoteScreen"; ================================================ FILE: apps/native/src/app/(app)/notes/new.tsx ================================================ export { default } from "../../../screens/CreateNoteScreen"; ================================================ FILE: apps/native/src/app/(auth)/_layout.tsx ================================================ import { useAuth } from "@clerk/clerk-expo"; import { Redirect, Stack } from "expo-router"; export default function AuthLayout() { const { isLoaded, isSignedIn } = useAuth(); if (!isLoaded) return null; if (isSignedIn) return ; return ; } ================================================ FILE: apps/native/src/app/(auth)/sign-in.tsx ================================================ export { default } from "../../screens/LoginScreen"; ================================================ FILE: apps/native/src/app/_layout.tsx ================================================ import { useFonts } from "expo-font"; import { Stack } from "expo-router"; import { Platform, StatusBar, View } from "react-native"; import ConvexClientProvider from "../../ConvexClientProvider"; const statusBarHeight = Platform.OS === "ios" ? 50 : (StatusBar.currentHeight ?? 0); export default function RootLayout() { const [loaded] = useFonts({ Bold: require("../assets/fonts/Inter-Bold.ttf"), SemiBold: require("../assets/fonts/Inter-SemiBold.ttf"), Medium: require("../assets/fonts/Inter-Medium.ttf"), Regular: require("../assets/fonts/Inter-Regular.ttf"), MBold: require("../assets/fonts/Montserrat-Bold.ttf"), MSemiBold: require("../assets/fonts/Montserrat-SemiBold.ttf"), MMedium: require("../assets/fonts/Montserrat-Medium.ttf"), MRegular: require("../assets/fonts/Montserrat-Regular.ttf"), MLight: require("../assets/fonts/Montserrat-Light.ttf"), }); if (!loaded) return null; return ( ); } ================================================ FILE: apps/native/src/screens/CreateNoteScreen.tsx ================================================ import React, { useEffect, useRef, useState } from "react"; import { StyleSheet, Text, View, Image, Dimensions, TouchableOpacity, TextInput, Keyboard, Animated, Alert, } from "react-native"; import { RFValue } from "react-native-responsive-fontsize"; import { AntDesign } from "@expo/vector-icons"; import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; import { api } from "@packages/backend/convex/_generated/api"; import { useConvexAuth, useMutation, useQuery } from "convex/react"; import { useRouter } from "expo-router"; const { width } = Dimensions.get("window"); export default function CreateNoteScreen() { const router = useRouter(); const { isLoading, isAuthenticated } = useConvexAuth(); const createNote = useMutation(api.notes.createNote); const openaiKeySet = useQuery(api.openai.openaiKeySet) ?? false; const [isAdvancedSummarizationEnabled, setIsAdvancedSummarizationEnabled] = useState(false); const [noteContent, setNoteContent] = useState(""); const [noteTitle, setNoteTitle] = useState(""); const footerY = useRef(new Animated.Value(0)).current; const toggleAdvancedSummarization = () => { setIsAdvancedSummarizationEnabled(!isAdvancedSummarizationEnabled); }; useEffect(() => { const keyboardDidShowListener = Keyboard.addListener( "keyboardDidShow", () => { // Slide down the footer Animated.timing(footerY, { toValue: 1, duration: 300, useNativeDriver: true, }).start(); }, ); const keyboardDidHideListener = Keyboard.addListener( "keyboardDidHide", () => { // Slide up the footer Animated.timing(footerY, { toValue: 0, duration: 300, useNativeDriver: true, }).start(); }, ); // Clean up function return () => { keyboardDidShowListener.remove(); keyboardDidHideListener.remove(); }; }, [footerY]); // Calculate the position of the footer based on the Animated.Value const footerTranslateY = footerY.interpolate({ inputRange: [0, 1], outputRange: [0, 100], // Adjust this range according to the height of your footer }); const createUserNote = async () => { if (!noteTitle.trim() || !noteContent.trim()) { Alert.alert("Missing fields", "Please add both a title and content."); return; } if (isLoading) { Alert.alert( "Please wait", "Authenticating with backend, try again in a moment.", ); return; } if (!isAuthenticated) { Alert.alert( "Not authenticated with backend", "Please wait a moment or re-open the app and try again.", ); return; } try { await createNote({ title: noteTitle, content: noteContent, isSummary: isAdvancedSummarizationEnabled, }); router.replace("/"); } catch (error) { Alert.alert("Failed to create note", String(error)); } }; return ( { router.back(); }} > Create a New Note Title setNoteTitle(val)} style={styles.inputField} placeholder="Note Title" placeholderTextColor="#A9A9A9" /> Content setNoteContent(val)} style={[styles.inputField, styles.inputFieldMulti]} multiline placeholder="Note Comments" placeholderTextColor="#A9A9A9" /> AI Features {/* Advanced Summarization Section */} {isAdvancedSummarizationEnabled && ( )} Advanced Summarization {openaiKeySet ? "" : " (Disabled)"} {openaiKeySet ? "Check this box if you want to generate summaries using AI." : "Please set OPENAI_API_KEY in your environment variables."} Create a New Note ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#fff", }, header: { backgroundColor: "#0D87E1", height: 67, justifyContent: "center", alignItems: "center", }, logo: { width: 46, height: 46, borderRadius: 20, resizeMode: "contain", }, underHeaderContainer: { width: width, height: 62, backgroundColor: "#fff", borderBottomWidth: 2, borderBottomColor: "#D9D9D9", flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, }, arrowBack: { width: 20, height: 20, resizeMode: "contain", }, title: { fontSize: RFValue(17.5), fontFamily: "MMedium", color: "#2D2D2D", }, inputContainer: { paddingHorizontal: 27, marginTop: 43, }, inputLabel: { fontSize: RFValue(15), fontFamily: "MMedium", color: "#000", marginBottom: 6, }, inputField: { backgroundColor: "#FFF", marginBottom: 30, fontSize: RFValue(15), fontFamily: "MLight", color: "#000", borderRadius: 8, paddingHorizontal: 14, paddingVertical: 12.5, borderWidth: 1, borderColor: "#D9D9D9", }, inputFieldMulti: { minHeight: 228, textAlignVertical: "top", paddingTop: 10, }, advancedSummarizationContainer: { paddingHorizontal: 27, marginTop: 10, }, advancedSummarizationCheckboxContainer: { flexDirection: "row", alignItems: "center", marginBottom: 8, }, checkbox: { width: RFValue(17.5), height: RFValue(17.5), borderRadius: RFValue(5), borderWidth: 1, borderColor: "#0D87E1", justifyContent: "center", alignItems: "center", marginRight: RFValue(10), backgroundColor: "#F9F5FF", }, checkboxDisabled: { width: RFValue(17.5), height: RFValue(17.5), borderRadius: RFValue(5), borderWidth: 1, borderColor: "#D9D9D9", justifyContent: "center", alignItems: "center", marginRight: RFValue(10), backgroundColor: "#F9F5FF", }, advancedSummarizationText: { fontSize: RFValue(15), fontFamily: "MLight", color: "#000", }, advancedSummarizationSubtext: { fontSize: RFValue(12.5), fontFamily: "MRegular", color: "#A9A9A9", paddingHorizontal: 30, }, newNoteButton: { flexDirection: "row", backgroundColor: "#0D87E1", borderRadius: 7, width: width / 1.6, alignSelf: "center", alignItems: "center", justifyContent: "center", minHeight: 44, position: "absolute", bottom: 35, shadowColor: "#000", shadowOffset: { width: 0, height: 3, }, shadowOpacity: 0.27, shadowRadius: 4.65, elevation: 6, }, newNoteButtonText: { color: "white", fontSize: RFValue(15), fontFamily: "MMedium", marginLeft: 10, }, newNoteButtonContainer: { position: "absolute", bottom: 0, alignSelf: "center", // ... other styles you need }, }); ================================================ FILE: apps/native/src/screens/InsideNoteScreen.tsx ================================================ import React, { useState } from "react"; import { api } from "@packages/backend/convex/_generated/api"; import { useQuery } from "convex/react"; import { useLocalSearchParams, useRouter } from "expo-router"; import { StyleSheet, Text, View, Image, Dimensions, TouchableOpacity, ScrollView, } from "react-native"; import { RFValue } from "react-native-responsive-fontsize"; const { width } = Dimensions.get("window"); export default function InsideNoteScreen() { const router = useRouter(); const { noteId } = useLocalSearchParams<{ noteId: string }>(); const note = useQuery(api.notes.getNote, { id: noteId ?? undefined, }); const [activeTab, setActiveTab] = useState("original"); const noteContentText = !note ? "Note not found" : activeTab === "original" ? note.content : note.summary ? note.summary : "No summary available"; return ( { router.back(); }} > {note?.title ?? "Note"} {noteContentText} {/* Sticky footer */} setActiveTab("original")} > Original setActiveTab("summary")} > Summary ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#F5F7FE", }, header: { backgroundColor: "#0D87E1", height: 67, justifyContent: "center", alignItems: "center", }, logo: { width: 46, height: 46, borderRadius: 20, resizeMode: "contain", }, underHeaderContainer: { width: width, height: 62, backgroundColor: "#fff", borderBottomWidth: 2, borderBottomColor: "#D9D9D9", flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, }, arrowBack: { width: 20, height: 20, resizeMode: "contain", }, title: { fontSize: RFValue(17.5), fontFamily: "MMedium", color: "#2D2D2D", }, contentContainer: { // Add styles for contentContainer if needed }, contentTitle: { fontSize: RFValue(17.5), fontFamily: "MMedium", color: "#000", textAlign: "center", marginTop: 28, }, contentDescription: { fontSize: RFValue(17.5), fontFamily: "MRegular", alignSelf: "center", textAlign: "justify", paddingLeft: 29, paddingRight: 21, marginTop: 30, }, footer: { flexDirection: "row", position: "absolute", bottom: 0, left: 0, right: 0, backgroundColor: "#fff", borderTopWidth: 1, borderTopColor: "#D9D9D9", }, footerTab: { flex: 1, padding: 12, justifyContent: "center", alignItems: "center", }, footerIcon: { width: 25, height: 25, resizeMode: "contain", }, activeTab: { backgroundColor: "#0D87E1", }, activeIcon: { tintColor: "#fff", }, inactiveIcon: { tintColor: "#000", }, footerText: { fontSize: RFValue(12.5), fontFamily: "MRegular", }, activeTabText: { color: "#fff", }, inactiveTabText: { color: "#000", }, }); ================================================ FILE: apps/native/src/screens/LoginScreen.tsx ================================================ import React from "react"; import { StyleSheet, View, Text, TouchableOpacity, Image } from "react-native"; import { useSSO } from "@clerk/clerk-expo"; import { RFValue } from "react-native-responsive-fontsize"; import { AntDesign } from "@expo/vector-icons"; import { useRouter } from "expo-router"; type OAuthStrategy = "oauth_google" | "oauth_apple"; const LoginScreen = () => { const router = useRouter(); const { startSSOFlow } = useSSO(); const onPress = async ({ strategy }: { strategy: OAuthStrategy }) => { try { const { createdSessionId, setActive } = await startSSOFlow({ strategy }); if (!createdSessionId || !setActive) return; await setActive({ session: createdSessionId }); router.replace("/"); } catch (err) { const message = String(err); if (message.includes("already signed in")) { return; } console.error("OAuth error", err); } }; return ( Log in to your account Welcome! Please login below. { onPress({ strategy: "oauth_google" }); }} > Continue with Google { onPress({ strategy: "oauth_apple" }); }} > Continue with Apple Don’t have an account? Sign up above. ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#fff", }, card: { backgroundColor: "#fff", padding: 10, alignItems: "center", width: "98%", }, logo: { width: 74, height: 74, marginTop: 20, }, title: { marginTop: 49, fontSize: RFValue(21), fontFamily: "SemiBold", }, subtitle: { marginTop: 8, fontSize: RFValue(14), color: "#000", fontFamily: "Regular", marginBottom: 32, textAlign: "center", }, input: { width: "100%", borderWidth: 1, borderColor: "#D0D5DD", borderRadius: 10, padding: 14, marginBottom: 16, fontFamily: "Regular", fontSize: RFValue(14), }, buttonEmail: { backgroundColor: "#0D87E1", padding: 15, borderRadius: 10, width: "100%", marginBottom: 24, minHeight: 44, }, buttonText: { textAlign: "center", color: "#FFF", fontFamily: "SemiBold", fontSize: RFValue(14), }, buttonTextWithIcon: { marginLeft: 10, }, dividerContainer: { flexDirection: "row", alignItems: "center", width: "100%", marginBottom: 24, }, divider: { flex: 1, height: 1, backgroundColor: "#000", }, dividerText: { marginHorizontal: 10, color: "#000", fontFamily: "Medium", }, buttonGoogle: { flexDirection: "row", alignItems: "center", justifyContent: "center", backgroundColor: "#FFF", borderRadius: 10, borderWidth: 1, borderColor: "#D0D5DD", width: "100%", marginBottom: 12, height: 44, }, buttonApple: { flexDirection: "row", justifyContent: "center", alignItems: "center", backgroundColor: "#FFF", padding: 15, borderRadius: 10, borderWidth: 1, borderColor: "#D0D5DD", width: "100%", marginBottom: 32, }, signupContainer: { flexDirection: "row", }, signupText: { color: "#4D9DE0", fontFamily: "SemiBold", }, googleIcon: { width: 24, height: 24, marginRight: 12, }, errorText: { fontSize: RFValue(14), color: "tomato", fontFamily: "Medium", alignSelf: "flex-start", marginBottom: 8, marginLeft: 4, }, }); export default LoginScreen; ================================================ FILE: apps/native/src/screens/NotesDashboardScreen.tsx ================================================ import React, { useState } from "react"; import { StyleSheet, View, Text, TextInput, TouchableOpacity, Image, FlatList, Dimensions, } from "react-native"; import { Feather, AntDesign } from "@expo/vector-icons"; import { RFValue } from "react-native-responsive-fontsize"; import { useUser } from "@clerk/clerk-expo"; import { api } from "@packages/backend/convex/_generated/api"; import { type Doc } from "@packages/backend/convex/_generated/dataModel"; import { useQuery } from "convex/react"; import { type Href, useRouter } from "expo-router"; const NotesDashboardScreen = () => { const router = useRouter(); const user = useUser(); const imageUrl = user?.user?.imageUrl; const firstName = user?.user?.firstName; const allNotes = useQuery(api.notes.getNotes); const [search, setSearch] = useState(""); const finalNotes = search && allNotes ? allNotes.filter( (note) => note.title.toLowerCase().includes(search.toLowerCase()) || note.content.toLowerCase().includes(search.toLowerCase()), ) : (allNotes ?? []); const renderItem = ({ item }: { item: Doc<"notes"> }) => ( { router.push(`/notes/${item._id}` as Href); }} activeOpacity={0.5} style={styles.noteItem} > {item.title} ); return ( {/* @ts-ignore, for css purposes */} Your Notes {imageUrl ? ( ) : ( {firstName ? firstName : ""} )} {!finalNotes || finalNotes.length === 0 ? ( Create your first note to{"\n"}get started ) : ( item._id} style={styles.notesList} contentContainerStyle={{ marginTop: 19, borderTopWidth: 0.5, borderTopColor: "rgba(0, 0, 0, 0.59)", }} /> )} { router.push("/notes/new"); }} style={styles.newNoteButton} > Create a New Note ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "white", }, header: { backgroundColor: "#0D87E1", height: 67, justifyContent: "center", alignItems: "center", }, logo: { width: 46, height: 46, borderRadius: 20, resizeMode: "contain", }, title: { fontSize: RFValue(17.5), fontFamily: "MMedium", alignSelf: "center", }, yourNotesContainer: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 13, marginTop: 19, }, avatarSmall: { width: 28, height: 28, borderRadius: 10, }, searchContainer: { flexDirection: "row", alignItems: "center", borderWidth: 1, borderColor: "grey", borderRadius: 10, padding: 10, marginHorizontal: 15, marginTop: 30, }, searchIcon: { marginRight: 10, }, searchInput: { flex: 1, fontSize: RFValue(15), fontFamily: "MRegular", color: "#2D2D2D", }, notesList: { flex: 1, }, noteItem: { padding: 20, borderBottomWidth: 0.5, borderBottomColor: "rgba(0, 0, 0, 0.59)", backgroundColor: "#F9FAFB", }, noteText: { fontSize: 16, fontFamily: "MLight", color: "#2D2D2D", }, newNoteButton: { flexDirection: "row", backgroundColor: "#0D87E1", borderRadius: 7, width: Dimensions.get("window").width / 1.6, alignSelf: "center", alignItems: "center", justifyContent: "center", minHeight: 44, position: "absolute", bottom: 35, shadowColor: "#000", shadowOffset: { width: 0, height: 3, }, shadowOpacity: 0.27, shadowRadius: 4.65, elevation: 6, }, newNoteButtonText: { color: "white", fontSize: RFValue(15), fontFamily: "MMedium", marginLeft: 10, }, switchContainer: { position: "absolute", top: 20, right: 20, }, emptyStateText: { textAlign: "center", alignSelf: "center", fontSize: RFValue(15), color: "grey", fontFamily: "MLight", }, emptyState: { width: "100%", height: "35%", marginTop: 19, backgroundColor: "#F9FAFB", justifyContent: "center", alignItems: "center", borderWidth: 0.5, borderColor: "rgba(0, 0, 0, 0.59)", }, }); export default NotesDashboardScreen; ================================================ FILE: apps/native/tsconfig.json ================================================ { "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "paths": { "@/*": ["./src/*"] } }, "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] } ================================================ FILE: apps/web/.example.env ================================================ NEXT_PUBLIC_CONVEX_URL= NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= CLERK_SECRET_KEY= ================================================ FILE: apps/web/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: apps/web/eslint.config.mjs ================================================ import { defineConfig, globalIgnores } from "eslint/config"; import nextVitals from "eslint-config-next/core-web-vitals"; export default defineConfig([ ...nextVitals, globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]), ]); ================================================ FILE: apps/web/package.json ================================================ { "name": "web-app", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint .", "typecheck": "tsc --noEmit" }, "dependencies": { "@clerk/nextjs": "^7.2.3", "@headlessui/react": "^2.2.10", "@heroicons/react": "^2.2.0", "@packages/backend": "workspace:*", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", "convex": "^1.35.1", "lucide-react": "^1.8.0", "next": "^16.2.4", "react": "^19.2.5", "react-dom": "^19.2.5", "tailwind-merge": "^3.5.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@tailwindcss/forms": "0.5.11", "@tailwindcss/postcss": "4.2.4", "@types/node": "^25.6.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "eslint": "^9.39.4", "eslint-config-next": "16.2.4", "postcss": "8.5.10", "tailwindcss": "4.2.4", "typescript": "6.0.3" } } ================================================ FILE: apps/web/postcss.config.js ================================================ module.exports = { plugins: { "@tailwindcss/postcss": {}, }, }; ================================================ FILE: apps/web/src/app/ConvexClientProvider.tsx ================================================ "use client"; import { type ReactNode } from "react"; import { useAuth } from "@clerk/nextjs"; import { ConvexReactClient } from "convex/react"; import { ConvexProviderWithClerk } from "convex/react-clerk"; const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; if (!convexUrl) throw new Error("Missing NEXT_PUBLIC_CONVEX_URL for the web Convex client"); const convex = new ConvexReactClient(convexUrl); export default function ConvexClientProvider({ children, }: { children: ReactNode; }) { return ( {children} ); } ================================================ FILE: apps/web/src/app/globals.css ================================================ @import 'tailwindcss'; @theme { --breakpoint-*: initial; --breakpoint-sm: 640px; --breakpoint-md: 768px; --breakpoint-lg: 1024px; --breakpoint-xl: 1440px; --color-primary: #0d87e1; --font-inter: Inter, sans-serif; --font-lato: Lato, sans-serif; --font-montserrat: Montserrat, sans-serif; --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops)); --background-image-gradient-conic: conic-gradient( from 180deg at 50% 50%, var(--tw-gradient-stops) ); } @utility container { margin-inline: auto; } /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentcolor); } } .linear_gradient { background: linear-gradient( 267deg, #fff -9.43%, #040218 -9.42%, #fff 4.63%, #d2e4f2 127.55% ); } .bg_image { background: linear-gradient( 181deg, rgba(255, 255, 255, 0) 57.92%, #fff 97.09%, rgba(255, 255, 255, 0) 127.09% ), url("/images/background.png"), lightgray 0% 0% / 261.2499952316284px 261.2499952316284px repeat; } .button { border-radius: 8px; background: linear-gradient( 267deg, #0983df -9.43%, #040218 -9.42%, #0d87e1 4.63%, #0983df 127.55% ); box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); } ================================================ FILE: apps/web/src/app/layout.tsx ================================================ import type { Metadata } from "next"; import { ClerkProvider } from "@clerk/nextjs"; import { Inter, Montserrat, Lato } from "next/font/google"; import { cn } from "@/lib/utils"; import "./globals.css"; import ConvexClientProvider from "./ConvexClientProvider"; const inter = Inter({ subsets: ["latin"] }); const montserrat = Montserrat({ subsets: ["latin"] }); const lato = Lato({ weight: "400", subsets: ["latin"] }); export const metadata: Metadata = { title: "Notes App", description: "This is an app to take notes.", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ================================================ FILE: apps/web/src/app/notes/[slug]/page.tsx ================================================ import Header from "@/components/Header"; import NoteDetails from "@/components/notes/NoteDetails"; import { Id } from "@packages/backend/convex/_generated/dataModel"; export default async function Page({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; return (
} />
); } ================================================ FILE: apps/web/src/app/notes/page.tsx ================================================ import Header from "@/components/Header"; import Notes from "@/components/notes/Notes"; export default function Home() { return (
); } ================================================ FILE: apps/web/src/app/page.tsx ================================================ "use client"; import Header from "@/components/Header"; import Benefits from "@/components/home/Benefits"; import Footer from "@/components/home/Footer"; import FooterHero from "@/components/home/FooterHero"; import Hero from "@/components/home/Hero"; import Testimonials from "@/components/home/Testimonials"; export default function Home() { return (
); } ================================================ FILE: apps/web/src/components/Header.tsx ================================================ "use client"; import { Disclosure, DisclosureButton, DisclosurePanel, } from "@headlessui/react"; import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; import { useUser } from "@clerk/nextjs"; import Link from "next/link"; import Logo from "./common/Logo"; import { UserNav } from "./common/UserNav"; import { usePathname } from "next/navigation"; type NavigationItem = { name: string; href: string; current: boolean; }; const navigation: NavigationItem[] = [ { name: "Benefits", href: "#Benefits", current: true }, { name: "Reviews", href: "#reviews", current: false }, ]; export default function Header() { const { user } = useUser(); const pathname = usePathname(); return ( {({ open }) => ( <>
{pathname === "/" && (
    {navigation.map((item) => (
  • {item.name}
  • ))}
)} {user ? (
) : (
Sign in Get Started
)}
{/* Mobile menu button*/} Open main menu {open ? (
{navigation.map((item) => ( {item.name} ))} {user ? (
See your Notes
) : (
Sign in Get Started
)}
)}
); } ================================================ FILE: apps/web/src/components/common/Logo.tsx ================================================ import Image from "next/image"; import Link from "next/link"; import React from "react"; interface Props { isMobile?: boolean; } const Logo = ({ isMobile }: Props) => { return (
logo {!isMobile ? (

UseNotes

) : null}
); }; export default Logo; ================================================ FILE: apps/web/src/components/common/Menu.tsx ================================================ import Link from "next/link"; interface Props { menuItems: { title: string; url: string; }[]; } const Menu = ({ menuItems }: Props) => { return (
    {menuItems.map((item, index) => (
  • {item.title}
  • ))}
); }; export default Menu; ================================================ FILE: apps/web/src/components/common/TestTimonialCard.tsx ================================================ import Image from "next/image"; interface Props { data: { feature?: boolean; review: string; profile: string; name: string; designation: string; }; } const TestTimonialCard = ({ data }: Props) => { return (
{Array(5) .fill(0) .map((data, index) => ( star ))}
{data.review}
ryan

{data.name}

{data.designation}

); }; export default TestTimonialCard; ================================================ FILE: apps/web/src/components/common/UserNav.tsx ================================================ import { useClerk } from "@clerk/nextjs"; import { LogOut, Paintbrush2 } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; import { Avatar, AvatarFallback, AvatarImage } from "./avatar"; import { Button } from "./button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu"; export function UserNav({ image, name, email, }: { image: string; name: string; email: string; }) { const { signOut } = useClerk(); return (

{name}

{email}

Dashboard { signOut({ redirectUrl: "/" }); }} className="hover:cursor-pointer hover:bg-gray-200" > Log out
); } ================================================ FILE: apps/web/src/components/common/avatar.tsx ================================================ import * as React from "react"; import * as AvatarPrimitive from "@radix-ui/react-avatar"; import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; export { Avatar, AvatarImage, AvatarFallback }; ================================================ FILE: apps/web/src/components/common/button.tsx ================================================ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", outline: "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-8", icon: "h-9 w-9", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( ); }, ); Button.displayName = "Button"; export { Button, buttonVariants }; ================================================ FILE: apps/web/src/components/common/dropdown-menu.tsx ================================================ import * as React from "react"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import { Check, ChevronRight, Circle } from "lucide-react"; import { cn } from "@/lib/utils"; const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; const DropdownMenuGroup = DropdownMenuPrimitive.Group; const DropdownMenuPortal = DropdownMenuPrimitive.Portal; const DropdownMenuSub = DropdownMenuPrimitive.Sub; const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef & { inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( {children} )); DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, sideOffset = 4, ...props }, ref) => ( )); DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef & { inset?: boolean; } >(({ className, inset, ...props }, ref) => ( )); DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, children, checked, ...props }, ref) => ( {children} )); DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} )); DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef & { inset?: boolean; } >(({ className, inset, ...props }, ref) => ( )); DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { return ( ); }; DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, }; ================================================ FILE: apps/web/src/components/home/Benefits.tsx ================================================ import Image from "next/image"; const benefits = [ { title: "Effortless Note-Taking", description: "Capture thoughts effortlessly with our intuitive interface", image: "/images/goodNews.png", }, { title: "Seamless Sync", description: "Access your notes anytime, anywhere, with seamless cloud synchronization.", image: "/images/cloudSync.png", }, { title: "Enhanced Productivity", description: "Let AI handle organization, so you can focus on what matters most.", image: "/images/googleCalander.png", }, { title: "AI-Powered Insights", description: "Gain valuable insights with smart analytics based on your note patterns.", image: "/images/bot.png", }, ]; const Benefits = () => { return (

Benefits

Why Choose UseNotes

{Array(3) .fill(0) .map((_, index) => ( line ))}
{benefits.map((benefit, index) => (
benefit

{benefit.title}

{benefit.description}

))}
); }; export default Benefits; ================================================ FILE: apps/web/src/components/home/ComplexToggle.tsx ================================================ import { useState } from "react"; import { Switch } from "@headlessui/react"; function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); } export default function ComplexToggle({ setIsSummary, isSummary, }: { setIsSummary: (value: boolean) => void; isSummary: boolean; }) { return ( Original Note {" "} AI Summary {" "} ); } ================================================ FILE: apps/web/src/components/home/Footer.tsx ================================================ import React from "react"; import Logo from "../common/Logo"; import Menu from "../common/Menu"; const menuItems = [ { title: "Home", url: "/", }, { title: "Benefits", url: "#Benefits", }, { title: "Get Started", url: "/notes", }, { title: "Reviews", url: "#reviews", }, ]; const Footer = () => { return ( <>

Take more efficient notes with UseNotes

Save countless hours of note-taking and organize your notes easier.

© 2023 UseNotes. All rights reserved.

Take more efficient notes with UseNotes

Save countless hours of note-taking and organize your notes easier.

© 2023 UseNotes. All rights reserved.
Icons by Icons8

); }; export default Footer; ================================================ FILE: apps/web/src/components/home/FooterHero.tsx ================================================ import Image from "next/image"; import Link from "next/link"; import React from "react"; const FooterHero = () => { return (

Start Your Intelligent Note-Taking Journey

Sign up now and experience the power of AI-enhanced note-taking with UseNotes

hero
); }; export default FooterHero; ================================================ FILE: apps/web/src/components/home/Hero.tsx ================================================ import Image from "next/image"; import Link from "next/link"; const Hero = () => { return (

The Ultimate
Note-Taking Experience

UseNotes harnesses the power of artificial intelligence to revolutionize the way you capture, organize, and recall your thoughts

hero
hero
); }; export default Hero; ================================================ FILE: apps/web/src/components/home/Testimonials.tsx ================================================ import Image from "next/image"; import TestTimonialCard from "../common/TestTimonialCard"; const TestimonialsData = [ { rating: 5, review: "Great note-taking application! The AI features make note-taking a breeze", name: "Ryan Lowry", designation: "Engineer & Author", profile: "/images/profile.png", feature: false, }, { rating: 5, review: "Really like the clean design of UseNotes. The AI-driven search is impressively accurate, adding a personal dimension to my notes. Fast and very easy to use.", name: "John Collins", designation: "Engineer & Author", profile: "/images/profile.png", feature: true, }, { rating: 5, review: "Simply brilliant! UseNotes has elevated my productivity.", name: "Moe Partuj", designation: "Student", profile: "/images/Moe-Partuj.jpeg", feature: false, }, ]; const Testimonials = () => { return (

Reviews

User Testimonials

{TestimonialsData.map((item, index) => ( ))}
); }; export default Testimonials; ================================================ FILE: apps/web/src/components/notes/Checkbox.tsx ================================================ const Checkbox = ({ isChecked, checkHandler, openaiKeySet, }: { isChecked: boolean; checkHandler: () => void; openaiKeySet: boolean; }) => { return (
{openaiKeySet ? (

Check this box if you want to generate summaries using AI

) : (

Set an OPENAI_API_KEY in your environment variables here.

)}
); }; export default Checkbox; ================================================ FILE: apps/web/src/components/notes/CreateNote.tsx ================================================ "use client"; import { Fragment, useRef, useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; import Image from "next/image"; import Checkbox from "./Checkbox"; import { api } from "@packages/backend/convex/_generated/api"; import { useMutation, useQuery } from "convex/react"; export default function CreateNote() { const [open, setOpen] = useState(false); const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [isChecked, setIsChecked] = useState(false); const cancelButtonRef = useRef(null); const createNote = useMutation(api.notes.createNote); const openaiKeySet = useQuery(api.openai.openaiKeySet) ?? true; const createUserNote = async () => { await createNote({ title, content, isSummary: isChecked, }); setOpen(false); }; return ( <>
<>
Create New Note
setTitle(e.target.value)} className="border shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] rounded-lg border-solid border-[#D0D5DD] bg-white w-full py-2.5 px-[14px] text-black text-[17px] not-italic font-light leading-[90.3%] tracking-[-0.425px] sm:text-2xl" />