├── .gitignore
├── .npmrc
├── README.md
├── app.db
├── app.db-journal
├── app.json
├── app
    ├── (tabs)
    │   ├── _layout.tsx
    │   ├── explore.tsx
    │   ├── index.tsx
    │   └── new-notion.tsx
    ├── +html.tsx
    ├── +not-found.tsx
    ├── _layout.tsx
    ├── doc-actions-sheet.tsx
    └── new-notion.tsx
├── assets
    ├── fonts
    │   └── SpaceMono-Regular.ttf
    └── images
    │   ├── adaptive-icon.png
    │   ├── favicon.png
    │   ├── icon.png
    │   ├── partial-react-logo.png
    │   ├── react-logo.png
    │   ├── react-logo@2x.png
    │   ├── react-logo@3x.png
    │   └── splash.png
├── babel.config.js
├── components
    ├── Collapsible.tsx
    ├── DraggableNotionList.tsx
    ├── DraggableNotionListItem.tsx
    ├── ExternalLink.tsx
    ├── HelloWave.tsx
    ├── NotionButton.tsx
    ├── ParallaxScrollView.tsx
    ├── ResentFileCard.tsx
    ├── ResentFiles.tsx
    ├── ThemedText.tsx
    ├── ThemedView.tsx
    ├── __tests__
    │   ├── ThemedText-test.tsx
    │   └── __snapshots__
    │   │   └── ThemedText-test.tsx.snap
    └── navigation
    │   └── TabBarIcon.tsx
├── constants
    ├── Colors.ts
    ├── Gradients.ts
    └── MarkdownStyle.ts
├── eas.json
├── hooks
    ├── useColorScheme.ts
    ├── useColorScheme.web.ts
    └── useThemeColor.ts
├── migrations
    ├── 20240703032200_one
    │   └── migration.sql
    ├── 20240703215900_add_order_to_notionfile
    │   └── migration.sql
    ├── 20240704014730_add_on_delete_cascade_for_notion_files
    │   └── migration.sql
    └── migration_lock.toml
├── myDbModule.ts
├── package.json
├── pnpm-lock.yaml
├── queries.sql
├── schema.prisma
├── scripts
    └── reset-project.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
 1 | node_modules/
 2 | .expo/
 3 | dist/
 4 | npm-debug.*
 5 | *.jks
 6 | *.p8
 7 | *.p12
 8 | *.key
 9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 | ios/
13 | android/
14 | 
15 | # macOS
16 | .DS_Store
17 | 
18 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
19 | # The following patterns were generated by expo-cli
20 | 
21 | expo-env.d.ts
22 | # @end expo-cli
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | node-linker=hoisted
2 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | 
 2 | # Welcome to Local-First Notion Clone
 3 | 
 4 | This project is built using Prisma & Expo.
 5 | 
 6 | **Live demo at:** [x.com/betomoedano/status/1812483695418261620](https://x.com/betomoedano/status/1812483695418261620)
 7 | 
 8 | **YouTube, Source Code, Dependencies and More at:** [codewithbeto.dev/projects/notion-clone](https://codewithbeto.dev/projects/notion-clone)
 9 | 
10 | ## Get started
11 | 
12 | 1. Install dependencies with pnpm
13 | 
14 |    ```bash
15 |    pnpm install
16 |    ```
17 | 
18 | 2. prebuild app
19 | 
20 |    Haven't tested on Android yet!
21 | 
22 |    ```bash
23 |     npx expo prebuild -p ios --clean && pnpm ios
24 |    ```
25 | 
26 | You can create the database file and initial migration using Prisma migrate:
27 | 
28 | ```bash
29 | npx prisma@latest migrate dev
30 | ```
31 | 
32 | you can now generate the Prisma Client like this:
33 | 
34 | ```bash
35 | npx prisma@latest generate
36 | ```
37 | 
38 | ## Support My Work
39 | 
40 | If you find this project helpful and want to support my work, the best way is by enrolling in one of my courses:
41 | 
42 | - **React Native Course**: [codewithbeto.dev/learn](https://codewithbeto.dev/learn)
43 | - **React with TypeScript Course**: [codewithbeto.dev/learnReact](https://codewithbeto.dev/learnReact)
44 | - **Git & GitHub Course**: [codewithbeto.dev/learnGit](https://codewithbeto.dev/learnGit)
45 | 
46 | For other ways to support my work, please consider:
47 | 
48 | - **Become a Code with Beto channel member**: [YouTube Membership](https://www.youtube.com/channel/UCh247h68vszOMA_OWpGEa5g/join)
49 | - **GitHub Sponsors**: [Sponsor Me](https://github.com/sponsors/betomoedano)
50 | 
51 | You can also support me by using my referral links:
52 | 
53 | - Get an exclusive 40% discount on CodeCrafters: [Referral Link](https://app.codecrafters.io/join?via=betomoedano)
54 | - Get a 10% discount on Vexo Analytics with code "BETO10": [Vexo](https://vexo.co)
55 | - Sign up for Robinhood and we'll both pick our own gift stock 🎁: [Robinhood](https://join.robinhood.com/albertm-8254f5)
56 | - Get 500 MB of Dropbox storage: [Dropbox](https://www.dropbox.com/referrals/AAC52bYrrPqp8FZ7K5gxa-I74wecLpiQuB4?src=global9)
57 | 
58 | Your support helps me keep creating amazing projects!
59 | 
60 | 
61 | ## Connect with Me
62 | 
63 | - **Website**: [Code With Beto](https://codewithbeto.dev)
64 | - **X (formerly Twitter)**: [@betomoedano](https://x.com/betomoedano)
65 | - **GitHub**: [betomoedano](https://github.com/betomoedano)
66 | - **LinkedIn**: [Beto Moedano](https://www.linkedin.com/in/betomoedano/)
67 | - **Discord**: [Join Our Community](https://discord.com/invite/G2RnuUD8)
68 | - **Medium**: [@betomoedano01](https://medium.com/@betomoedano01)
69 | - **Figma**: [betomoedano](https://www.figma.com/@betomoedano)
70 |   
71 | 
--------------------------------------------------------------------------------
/app.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/app.db
--------------------------------------------------------------------------------
/app.db-journal:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/app.db-journal
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "expo": {
 3 |     "name": "notion-clone",
 4 |     "slug": "notion-clone",
 5 |     "version": "1.0.0",
 6 |     "orientation": "portrait",
 7 |     "icon": "./assets/images/icon.png",
 8 |     "scheme": "myapp",
 9 |     "userInterfaceStyle": "automatic",
10 |     "splash": {
11 |       "image": "./assets/images/splash.png",
12 |       "resizeMode": "contain",
13 |       "backgroundColor": "#ffffff"
14 |     },
15 |     "ios": {
16 |       "supportsTablet": true,
17 |       "bundleIdentifier": "com.betomoedano.notion-clone"
18 |     },
19 |     "android": {
20 |       "adaptiveIcon": {
21 |         "foregroundImage": "./assets/images/adaptive-icon.png",
22 |         "backgroundColor": "#ffffff"
23 |       },
24 |       "package": "com.betomoedano.notionclone"
25 |     },
26 |     "web": {
27 |       "bundler": "metro",
28 |       "output": "static",
29 |       "favicon": "./assets/images/favicon.png"
30 |     },
31 |     "plugins": [
32 |       "@prisma/react-native",
33 |       "expo-router"
34 |     ],
35 |     "experiments": {
36 |       "typedRoutes": true
37 |     },
38 |     "extra": {
39 |       "router": {
40 |         "origin": false
41 |       },
42 |       "eas": {
43 |         "projectId": "f71d029f-67c9-4aa1-aae4-15473a79f760"
44 |       }
45 |     }
46 |   }
47 | }
48 | 
--------------------------------------------------------------------------------
/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
 1 | import { Tabs } from "expo-router";
 2 | import React from "react";
 3 | 
 4 | import { TabBarIcon } from "@/components/navigation/TabBarIcon";
 5 | import { Colors } from "@/constants/Colors";
 6 | import { useColorScheme } from "@/hooks/useColorScheme";
 7 | 
 8 | export default function TabLayout() {
 9 |   const colorScheme = useColorScheme();
10 | 
11 |   return (
12 |     
19 |        (
23 |             
27 |           ),
28 |         }}
29 |       />
30 |        (
34 |             
38 |           ),
39 |         }}
40 |       />
41 |        (
46 |             
50 |           ),
51 |           headerShown: true,
52 |           headerTitle: "",
53 |           headerStyle: { shadowColor: "transparent" },
54 |         }}
55 |       />
56 |     
57 |   );
58 | }
59 | 
--------------------------------------------------------------------------------
/app/(tabs)/explore.tsx:
--------------------------------------------------------------------------------
  1 | import {
  2 |   StyleSheet,
  3 |   SafeAreaView,
  4 |   ScrollView,
  5 |   TouchableOpacity,
  6 |   View,
  7 | } from "react-native";
  8 | 
  9 | import { ThemedView } from "@/components/ThemedView";
 10 | import { extendedClient } from "@/myDbModule";
 11 | import { ThemedText } from "@/components/ThemedText";
 12 | import { Link } from "expo-router";
 13 | 
 14 | export default function ExploreScreen() {
 15 |   const today = new Date();
 16 |   const startOfToday = new Date(today.setHours(0, 0, 0, 0));
 17 |   const endOfToday = new Date(today.setHours(23, 59, 59, 999));
 18 |   const startOfLastWeek = new Date(startOfToday);
 19 |   startOfLastWeek.setDate(startOfToday.getDate() - startOfToday.getDay() - 6); // Start of last week (7 days ago)
 20 |   const endOfLastWeek = new Date(startOfToday);
 21 |   endOfLastWeek.setDate(startOfToday.getDate() - startOfToday.getDay()); // End of last week (yesterday)
 22 | 
 23 |   const todayFiles = extendedClient.notionFile.useFindMany({
 24 |     where: {
 25 |       updatedAt: {
 26 |         gte: startOfToday,
 27 |         lte: endOfToday,
 28 |       },
 29 |     },
 30 |     orderBy: {
 31 |       updatedAt: "desc",
 32 |     },
 33 |   });
 34 | 
 35 |   const lastWeekFiles = extendedClient.notionFile.useFindMany({
 36 |     where: {
 37 |       updatedAt: {
 38 |         gte: startOfLastWeek,
 39 |         lte: endOfLastWeek,
 40 |       },
 41 |     },
 42 |     orderBy: {
 43 |       updatedAt: "desc",
 44 |     },
 45 |   });
 46 | 
 47 |   return (
 48 |     
 49 |       
 50 |         
 54 |           
 55 |             Today
 56 |             {todayFiles.map((file) => (
 57 |               
 65 |                 
 66 |                   
 67 |                     - {file.icon} {file.title}
 68 |                   
 69 |                 
 70 |               
 71 |             ))}
 72 |           
 73 |           
 74 |             Last week
 75 |             {lastWeekFiles.map((file) => (
 76 |               
 84 |                 
 85 |                   
 86 |                     - {file.icon} {file.title}
 87 |                   
 88 |                 
 89 |               
 90 |             ))}
 91 |           
 92 |         
 93 |       
 94 |     
 95 |   );
 96 | }
 97 | 
 98 | const styles = StyleSheet.create({
 99 |   container: {
100 |     flex: 1,
101 |   },
102 |   section: {
103 |     marginBottom: 15,
104 |   },
105 |   item: {
106 |     marginBottom: 8,
107 |   },
108 | });
109 | 
--------------------------------------------------------------------------------
/app/(tabs)/index.tsx:
--------------------------------------------------------------------------------
 1 | import { StyleSheet, SafeAreaView, Button } from "react-native";
 2 | 
 3 | import { ThemedView } from "@/components/ThemedView";
 4 | import { extendedClient } from "@/myDbModule";
 5 | import ResentFiles from "@/components/ResentFiles";
 6 | import DraggableNotionList from "@/components/DraggableNotionList";
 7 | 
 8 | export default function HomeScreen() {
 9 |   const user = extendedClient.user.useFindFirst({ where: { id: 1 } });
10 | 
11 |   const createUser = () => {
12 |     const newUser = { name: "Beto", email: "beto@expo.dev" };
13 |     extendedClient.user.create({ data: newUser });
14 |   };
15 |   return (
16 |     
17 |       
18 |         {/*  */}
19 |         
20 |         
21 |       
22 |     
23 |   );
24 | }
25 | 
26 | const styles = StyleSheet.create({
27 |   container: {
28 |     flex: 1,
29 |   },
30 | });
31 | 
--------------------------------------------------------------------------------
/app/(tabs)/new-notion.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | import {
  3 |   StyleSheet,
  4 |   ScrollView,
  5 |   useColorScheme,
  6 |   TextInput,
  7 |   Keyboard,
  8 |   View,
  9 |   InputAccessoryView,
 10 |   Alert,
 11 |   Pressable,
 12 |   Text,
 13 |   TouchableOpacity,
 14 | } from "react-native";
 15 | 
 16 | import { ThemedView } from "@/components/ThemedView";
 17 | import { MarkdownTextInput } from "@expensify/react-native-live-markdown";
 18 | import { markdownDarkStyle, markdownStyle } from "@/constants/MarkdownStyle";
 19 | import { Colors } from "@/constants/Colors";
 20 | import { Link, Stack, useLocalSearchParams, useRouter } from "expo-router";
 21 | import NotionButton from "@/components/NotionButton";
 22 | import { extendedClient } from "@/myDbModule";
 23 | import { ThemedText } from "@/components/ThemedText";
 24 | import { NotionFile } from "@prisma/client";
 25 | 
 26 | const EXAMPLE_CONTENT = [
 27 |   "# Insert subtitle here!",
 28 |   "Hello, *world*! I'm excited to share this with you.",
 29 |   "Visit my website: codewithbeto.dev",
 30 |   "> This is a blockquote, a great way to highlight quotes or important notes.",
 31 |   "`inline code` is useful for highlighting code within a sentence.",
 32 |   "Here's a code block example:",
 33 |   "```\n// Codeblock\nconsole.log('🚀 Ready to launch!');\n```",
 34 |   "Mentions:",
 35 |   "- @here (notify everyone)",
 36 |   "- @beto@expo.dev (mention a specific user)",
 37 |   "Use #hashtags to organize content, like this: #mention-report",
 38 | ].join("\n");
 39 | const inputAccessoryViewID = "newNotion";
 40 | const defaultIcons = [
 41 |   "🚀",
 42 |   "👻",
 43 |   "🎨",
 44 |   "🎤",
 45 |   "🥁",
 46 |   "🎲",
 47 |   "📱",
 48 |   "🌟",
 49 |   "🔥",
 50 |   "💡",
 51 |   "🚗",
 52 |   "🌈",
 53 |   "📚",
 54 |   "💻",
 55 |   "🎧",
 56 |   "🏆",
 57 |   "⚽",
 58 |   "🍔",
 59 |   "🎂",
 60 |   "🎵",
 61 |   "✈️",
 62 |   "🎮",
 63 |   "🌍",
 64 |   "🍕",
 65 |   "📷",
 66 |   "📅",
 67 |   "🔍",
 68 |   "🔧",
 69 |   "📝",
 70 |   "🛠️",
 71 |   "💼",
 72 |   "📞",
 73 |   "📈",
 74 |   "🏠",
 75 |   "🎉",
 76 | ];
 77 | 
 78 | const randomIcon = () =>
 79 |   defaultIcons[Math.floor(Math.random() * defaultIcons.length)];
 80 | 
 81 | export default function NewNotionScreen() {
 82 |   const theme = useColorScheme();
 83 |   const routeParams = useLocalSearchParams<{
 84 |     parentId?: string;
 85 |     viewingFile?: string;
 86 |   }>();
 87 |   const router = useRouter();
 88 |   const viewingFile: NotionFile = routeParams.viewingFile
 89 |     ? JSON.parse(routeParams.viewingFile)
 90 |     : null;
 91 |   const childFiles = extendedClient.notionFile.useFindMany({
 92 |     where: {
 93 |       parentFileId: viewingFile?.id ?? -1,
 94 |     },
 95 |   });
 96 |   const parentFile = extendedClient.notionFile.useFindUnique({
 97 |     where: {
 98 |       id: viewingFile?.parentFileId ?? -1,
 99 |     },
100 |   });
101 |   // console.log(childFiles);
102 |   const titleRef = React.useRef(null);
103 |   const [title, setTitle] = React.useState(
104 |     viewingFile ? viewingFile?.title : ""
105 |   );
106 |   const [text, setText] = React.useState(
107 |     viewingFile ? viewingFile?.content : ""
108 |   );
109 |   const [icon, setIcon] = React.useState(
110 |     viewingFile ? viewingFile?.icon : () => randomIcon()
111 |   );
112 | 
113 |   const backgroundColor = Colors[theme!].background as any;
114 |   const textColor = Colors[theme!].text as any;
115 | 
116 |   React.useEffect(() => {
117 |     if (titleRef.current) {
118 |       titleRef.current?.focus();
119 |     }
120 |   }, [theme]);
121 | 
122 |   function handleSaveNotionFile() {
123 |     if (!title) return;
124 |     const data = {
125 |       title: title,
126 |       description: "",
127 |       coverPhoto: "",
128 |       icon: icon ?? randomIcon(),
129 |       content: text,
130 |       authorId: 1,
131 |       type: "default",
132 |       createdAt: new Date().toISOString(),
133 |       parentFileId: routeParams.parentId
134 |         ? Number(routeParams.parentId)
135 |         : viewingFile
136 |         ? viewingFile.parentFileId
137 |         : null,
138 |     };
139 | 
140 |     try {
141 |       if (viewingFile) {
142 |         console.log("updating");
143 |         extendedClient.notionFile.update({
144 |           where: { id: viewingFile.id },
145 |           data: data,
146 |         });
147 |       } else {
148 |         console.log("creating");
149 |         extendedClient.notionFile.create({
150 |           data: data,
151 |         });
152 |       }
153 | 
154 |       setTitle("");
155 |       setText("");
156 |       setIcon(randomIcon());
157 |       router.setParams({ parentId: "", viewingFile: "" });
158 |       if (router.canDismiss()) {
159 |         router.dismissAll();
160 |       }
161 |       router.replace("/(tabs)/");
162 |     } catch (e) {
163 |       Alert.alert("Something went wrong :(");
164 |     }
165 |   }
166 |   return (
167 |     <>
168 |       
175 |       
176 |         
179 |               title ? (
180 |                 
185 |               ) : (
186 |                  {
189 |                     router.setParams({ parentId: "", viewingFile: "" });
190 |                     if (router.canDismiss()) {
191 |                       router.dismissAll();
192 |                     }
193 |                     router.replace("/(tabs)/");
194 |                   }}
195 |                   containerStyle={{ marginRight: 10 }}
196 |                 />
197 |               ),
198 |           }}
199 |         />
200 | 
201 |         
202 |           
203 |             {icon && (
204 |               {icon}
205 |             )}
206 |             
216 |             {childFiles.length > 0 ? (
217 |               
218 |                 Inner files: {childFiles.length}
219 |                 {childFiles.map((child) => (
220 |                   
229 |                     
230 |                       
231 |                         - {child.icon} {child.title}
232 |                       
233 |                     
234 |                   
235 |                 ))}
236 |               
237 |             ) : null}
238 |             
251 |           
252 |         
253 | 
254 |         
258 |           
261 |             
262 |                setText(EXAMPLE_CONTENT)}
266 |               />
267 |                {}} />
268 |             
269 |             {/*  */}
275 |             {defaultIcons.slice(0, 6).map((icon) => (
276 |                setIcon(icon)}>
277 |                 {icon}
278 |               
279 |             ))}
280 |             {/*  */}
281 |              Keyboard.dismiss()}
284 |             />
285 |           
286 |         
287 |       
288 |     >
289 |   );
290 | }
291 | 
292 | const styles = StyleSheet.create({
293 |   container: { padding: 10 },
294 |   accesoryView: {
295 |     flexDirection: "row",
296 |     alignItems: "center",
297 |     justifyContent: "space-between",
298 |     paddingHorizontal: 7,
299 |     height: 50,
300 |     borderTopWidth: 0.5,
301 |   },
302 | });
303 | 
--------------------------------------------------------------------------------
/app/+html.tsx:
--------------------------------------------------------------------------------
 1 | import { ScrollViewStyleReset } from 'expo-router/html';
 2 | import { type PropsWithChildren } from 'react';
 3 | 
 4 | /**
 5 |  * This file is web-only and used to configure the root HTML for every web page during static rendering.
 6 |  * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
 7 |  */
 8 | export default function Root({ children }: PropsWithChildren) {
 9 |   return (
10 |     
11 |       
12 |         
13 |         
14 |         
15 | 
16 |         {/*
17 |           Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
18 |           However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
19 |         */}
20 |         
21 | 
22 |         {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
23 |         
24 |         {/* Add any additional  elements that you want globally available on web... */}
25 |       
26 |       {children}
27 |     
28 |   );
29 | }
30 | 
31 | const responsiveBackground = `
32 | body {
33 |   background-color: #fff;
34 | }
35 | @media (prefers-color-scheme: dark) {
36 |   body {
37 |     background-color: #000;
38 |   }
39 | }`;
40 | 
--------------------------------------------------------------------------------
/app/+not-found.tsx:
--------------------------------------------------------------------------------
 1 | import { Link, Stack } from 'expo-router';
 2 | import { StyleSheet } from 'react-native';
 3 | 
 4 | import { ThemedText } from '@/components/ThemedText';
 5 | import { ThemedView } from '@/components/ThemedView';
 6 | 
 7 | export default function NotFoundScreen() {
 8 |   return (
 9 |     <>
10 |       
11 |       
12 |         This screen doesn't exist.
13 |         
14 |           Go to home screen!
15 |         
16 |       
17 |     >
18 |   );
19 | }
20 | 
21 | const styles = StyleSheet.create({
22 |   container: {
23 |     flex: 1,
24 |     alignItems: 'center',
25 |     justifyContent: 'center',
26 |     padding: 20,
27 |   },
28 |   link: {
29 |     marginTop: 15,
30 |     paddingVertical: 15,
31 |   },
32 | });
33 | 
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
 1 | import {
 2 |   DarkTheme,
 3 |   DefaultTheme,
 4 |   ThemeProvider,
 5 | } from "@react-navigation/native";
 6 | import { useFonts } from "expo-font";
 7 | import { Stack } from "expo-router";
 8 | import * as SplashScreen from "expo-splash-screen";
 9 | import { useEffect, useState } from "react";
10 | import "react-native-reanimated";
11 | 
12 | import { useColorScheme } from "@/hooks/useColorScheme";
13 | import { initializeDb } from "@/myDbModule";
14 | import { GestureHandlerRootView } from "react-native-gesture-handler";
15 | import { ActionSheetProvider } from "@expo/react-native-action-sheet";
16 | 
17 | // Prevent the splash screen from auto-hiding before asset loading is complete.
18 | SplashScreen.preventAutoHideAsync();
19 | 
20 | export default function RootLayout() {
21 |   const colorScheme = useColorScheme();
22 |   const [loaded] = useFonts({
23 |     SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
24 |   });
25 |   const [isLoading, setIsLoading] = useState(true);
26 | 
27 |   useEffect(() => {
28 |     const setup = async () => {
29 |       try {
30 |         await initializeDb();
31 |       } catch (e) {
32 |         console.log(e);
33 |       }
34 |       setIsLoading(false);
35 |     };
36 | 
37 |     setup();
38 |   }, []);
39 | 
40 |   useEffect(() => {
41 |     if (loaded) {
42 |       SplashScreen.hideAsync();
43 |     }
44 |   }, [loaded]);
45 | 
46 |   if (!loaded && !isLoading) {
47 |     return null;
48 |   }
49 | 
50 |   return (
51 |     
52 |       
53 |         
56 |           
57 |             
58 |             
62 |             
63 |           
64 |         
65 |       
66 |     
67 |   );
68 | }
69 | 
--------------------------------------------------------------------------------
/app/doc-actions-sheet.tsx:
--------------------------------------------------------------------------------
 1 | import { Link, Stack } from "expo-router";
 2 | import { StyleSheet } from "react-native";
 3 | 
 4 | import { ThemedText } from "@/components/ThemedText";
 5 | import { ThemedView } from "@/components/ThemedView";
 6 | 
 7 | export default function DocActionsSheetScreen() {
 8 |   return (
 9 |     <>
10 |       
11 |       
12 |         This screen doesn't exist.
13 |         
14 |           Go to home screen!
15 |         
16 |       
17 |     >
18 |   );
19 | }
20 | 
21 | const styles = StyleSheet.create({
22 |   container: {
23 |     flex: 1,
24 |     alignItems: "center",
25 |     justifyContent: "center",
26 |     padding: 20,
27 |   },
28 |   link: {
29 |     marginTop: 15,
30 |     paddingVertical: 15,
31 |   },
32 | });
33 | 
--------------------------------------------------------------------------------
/app/new-notion.tsx:
--------------------------------------------------------------------------------
1 | import NewNotionScreen from "./(tabs)/new-notion";
2 | 
3 | export default NewNotionScreen;
4 | 
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/betomoedano/React-Native-Notion-Clone/24d83b54cf73e27b63a0d37ec5e432e3fe49cbc8/assets/images/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 |   api.cache(true);
3 |   return {
4 |     presets: ['babel-preset-expo'],
5 |   };
6 | };
7 | 
--------------------------------------------------------------------------------
/components/Collapsible.tsx:
--------------------------------------------------------------------------------
 1 | import Ionicons from "@expo/vector-icons/Ionicons";
 2 | import { PropsWithChildren, useState } from "react";
 3 | import { StyleSheet, TouchableOpacity, useColorScheme } from "react-native";
 4 | 
 5 | import { ThemedText } from "@/components/ThemedText";
 6 | import { ThemedView } from "@/components/ThemedView";
 7 | import { Colors } from "@/constants/Colors";
 8 | 
 9 | export function Collapsible({
10 |   children,
11 |   title,
12 | }: PropsWithChildren & { title: string }) {
13 |   const [isOpen, setIsOpen] = useState(false);
14 |   const theme = useColorScheme() ?? "light";
15 | 
16 |   return (
17 |     
18 |        setIsOpen((value) => !value)}
21 |         activeOpacity={0.8}
22 |       >
23 |         
28 |         {title}
29 |       
30 |       {isOpen && {children}}
31 |     
32 |   );
33 | }
34 | 
35 | const styles = StyleSheet.create({
36 |   heading: {
37 |     flexDirection: "row",
38 |     alignItems: "center",
39 |     gap: 6,
40 |   },
41 |   content: {
42 |     marginTop: 6,
43 |     marginLeft: 24,
44 |   },
45 | });
46 | 
--------------------------------------------------------------------------------
/components/DraggableNotionList.tsx:
--------------------------------------------------------------------------------
  1 | import { baseClient, extendedClient } from "@/myDbModule";
  2 | import { NotionFile } from "@prisma/client/react-native";
  3 | import React, { useEffect, useState } from "react";
  4 | import { TouchableOpacity, View } from "react-native";
  5 | import DraggableFlatList from "react-native-draggable-flatlist";
  6 | import { DraggableNotionListItem } from "./DraggableNotionListItem";
  7 | import { ThemedText } from "./ThemedText";
  8 | import { Ionicons } from "@expo/vector-icons";
  9 | import { useActionSheet } from "@expo/react-native-action-sheet";
 10 | import { Prisma } from "@prisma/client";
 11 | 
 12 | export default function DraggableNotionList() {
 13 |   const { showActionSheetWithOptions } = useActionSheet();
 14 |   const [sortedFiles, setSortedFiles] = useState([]);
 15 |   const [orderBy, setOrderBy] =
 16 |     useState({ order: "asc" });
 17 | 
 18 |   const files = extendedClient.notionFile.useFindMany({
 19 |     where: { parentFile: { is: null } }, // prevent fetching files that live inside files
 20 |     orderBy: orderBy,
 21 |   });
 22 | 
 23 |   useEffect(() => {
 24 |     setSortedFiles(files);
 25 |   }, [files, orderBy]);
 26 | 
 27 |   const handleActionSheet = () => {
 28 |     const options = ["Manual", "Creation Date", "Cancel"];
 29 |     const cancelButtonIndex = 2;
 30 | 
 31 |     showActionSheetWithOptions(
 32 |       {
 33 |         options,
 34 |         cancelButtonIndex,
 35 |       },
 36 |       (selectedIndex: number | undefined) => {
 37 |         switch (selectedIndex) {
 38 |           case 0: {
 39 |             // Manual
 40 |             setOrderBy({ order: "asc" });
 41 |             break;
 42 |           }
 43 |           case 1: {
 44 |             // Creation Date
 45 |             setOrderBy({ createdAt: "desc" });
 46 |           }
 47 |         }
 48 |       }
 49 |     );
 50 |   };
 51 | 
 52 |   const handleDragEnd = async (data: NotionFile[]) => {
 53 |     setSortedFiles(data);
 54 |     const updates = data.map((file, index) => {
 55 |       return baseClient.notionFile.update({
 56 |         where: { id: file.id },
 57 |         data: { order: index },
 58 |       });
 59 |     });
 60 |     await baseClient.$transaction(updates);
 61 |     await extendedClient.$refreshSubscriptions();
 62 | 
 63 |     // data.forEach((file, index) => {
 64 |     //   extendedClient.notionFile.update({
 65 |     //     where: { id: file.id },
 66 |     //     data: { order: index },
 67 |     //   });
 68 |     // });
 69 |   };
 70 | 
 71 |   return (
 72 |      handleDragEnd(data)}
 76 |       keyExtractor={(item) => item.id.toString()}
 77 |       renderItem={DraggableNotionListItem}
 78 |       ListHeaderComponent={() => (
 79 |         <>
 80 |           {/*  */}
 81 |           
 89 |             
 90 |               Private Files
 91 |             
 92 |             
 96 |               
 97 |               
 98 |             
 99 |           
100 |           {!sortedFiles.length && (
101 |             
105 |               Nothing to show!
106 |             
107 |           )}
108 |         >
109 |       )}
110 |     />
111 |   );
112 | }
113 | 
--------------------------------------------------------------------------------
/components/DraggableNotionListItem.tsx:
--------------------------------------------------------------------------------
  1 | import Ionicons from "@expo/vector-icons/Ionicons";
  2 | import { useState } from "react";
  3 | import {
  4 |   Pressable,
  5 |   StyleSheet,
  6 |   TouchableOpacity,
  7 |   View,
  8 |   useColorScheme,
  9 | } from "react-native";
 10 | 
 11 | import { ThemedText } from "@/components/ThemedText";
 12 | import { Colors } from "@/constants/Colors";
 13 | import { RenderItemParams } from "react-native-draggable-flatlist";
 14 | import { NotionFile } from "@prisma/client/react-native";
 15 | import { useActionSheet } from "@expo/react-native-action-sheet";
 16 | import { extendedClient } from "@/myDbModule";
 17 | import { Link } from "expo-router";
 18 | 
 19 | export function DraggableNotionListItem({
 20 |   drag,
 21 |   isActive,
 22 |   item,
 23 | }: RenderItemParams) {
 24 |   const theme = useColorScheme() ?? "light";
 25 |   const iconColor = theme === "light" ? Colors.light.icon : Colors.dark.icon;
 26 | 
 27 |   return (
 28 |     
 34 |   );
 35 | }
 36 | 
 37 | interface InnerNotionListItemProps {
 38 |   parentId: number | undefined;
 39 | }
 40 | function InnerNotionListItem({ parentId }: InnerNotionListItemProps) {
 41 |   const theme = useColorScheme() ?? "light";
 42 |   const iconColor = theme === "light" ? Colors.light.icon : Colors.dark.icon;
 43 | 
 44 |   const childs = extendedClient.notionFile.useFindMany({
 45 |     where: { parentFileId: parentId },
 46 |   });
 47 | 
 48 |   if (childs.length === 0)
 49 |     return No pages inside;
 50 |   return (
 51 |     
 52 |       {childs.map((notionFile: NotionFile) => (
 53 |         
 58 |       ))}
 59 |     
 60 |   );
 61 | }
 62 | 
 63 | interface NotionFileItemProps {
 64 |   notionFile: NotionFile;
 65 |   iconColor: string;
 66 |   drag?: () => void;
 67 |   isActive?: boolean;
 68 | }
 69 | 
 70 | function NotionFileItem({
 71 |   notionFile,
 72 |   iconColor,
 73 |   drag,
 74 |   isActive,
 75 | }: NotionFileItemProps) {
 76 |   const { showActionSheetWithOptions } = useActionSheet();
 77 |   const [isOpen, setIsOpen] = useState(false);
 78 | 
 79 |   const onPress = (id: number) => {
 80 |     const options = ["Delete", "Cancel"];
 81 |     const destructiveButtonIndex = 0;
 82 |     const cancelButtonIndex = 1;
 83 | 
 84 |     showActionSheetWithOptions(
 85 |       {
 86 |         options,
 87 |         cancelButtonIndex,
 88 |         destructiveButtonIndex,
 89 |       },
 90 |       (selectedIndex: number | undefined) => {
 91 |         switch (selectedIndex) {
 92 |           case destructiveButtonIndex: {
 93 |             extendedClient.notionFile.delete({
 94 |               where: {
 95 |                 id: id,
 96 |               },
 97 |             });
 98 |             break;
 99 |           }
100 |           case cancelButtonIndex: {
101 |             // Canceled
102 |           }
103 |         }
104 |       }
105 |     );
106 |   };
107 | 
108 |   return (
109 |     
110 |       
118 |         
124 |           
125 |              setIsOpen((value) => !value)}>
126 |               
132 |             
133 |             
134 |               {notionFile.icon} {notionFile.title}
135 |             
136 |           
137 |           
138 |              onPress(notionFile.id)}>
139 |               
144 |             
145 |             
151 |               
152 |             
153 |           
154 |         
155 |       
156 |       {isOpen ? (
157 |         
158 |           
159 |         
160 |       ) : null}
161 |     
162 |   );
163 | }
164 | 
165 | const styles = StyleSheet.create({
166 |   heading: {
167 |     height: 40,
168 |     flexDirection: "row",
169 |     alignItems: "center",
170 |     justifyContent: "space-between",
171 |     paddingHorizontal: 12,
172 |   },
173 |   content: {
174 |     marginLeft: 24,
175 |   },
176 | });
177 | 
--------------------------------------------------------------------------------
/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
 1 | import { Link } from 'expo-router';
 2 | import { openBrowserAsync } from 'expo-web-browser';
 3 | import { type ComponentProps } from 'react';
 4 | import { Platform } from 'react-native';
 5 | 
 6 | type Props = Omit, 'href'> & { href: string };
 7 | 
 8 | export function ExternalLink({ href, ...rest }: Props) {
 9 |   return (
10 |      {
15 |         if (Platform.OS !== 'web') {
16 |           // Prevent the default behavior of linking to the default browser on native.
17 |           event.preventDefault();
18 |           // Open the link in an in-app browser.
19 |           await openBrowserAsync(href);
20 |         }
21 |       }}
22 |     />
23 |   );
24 | }
25 | 
--------------------------------------------------------------------------------
/components/HelloWave.tsx:
--------------------------------------------------------------------------------
 1 | import { StyleSheet } from 'react-native';
 2 | import Animated, {
 3 |   useSharedValue,
 4 |   useAnimatedStyle,
 5 |   withTiming,
 6 |   withRepeat,
 7 |   withSequence,
 8 | } from 'react-native-reanimated';
 9 | 
10 | import { ThemedText } from '@/components/ThemedText';
11 | 
12 | export function HelloWave() {
13 |   const rotationAnimation = useSharedValue(0);
14 | 
15 |   rotationAnimation.value = withRepeat(
16 |     withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
17 |     4 // Run the animation 4 times
18 |   );
19 | 
20 |   const animatedStyle = useAnimatedStyle(() => ({
21 |     transform: [{ rotate: `${rotationAnimation.value}deg` }],
22 |   }));
23 | 
24 |   return (
25 |     
26 |       👋
27 |     
28 |   );
29 | }
30 | 
31 | const styles = StyleSheet.create({
32 |   text: {
33 |     fontSize: 28,
34 |     lineHeight: 32,
35 |     marginTop: -6,
36 |   },
37 | });
38 | 
--------------------------------------------------------------------------------
/components/NotionButton.tsx:
--------------------------------------------------------------------------------
 1 | import { ComponentProps } from "react";
 2 | import { Ionicons } from "@expo/vector-icons";
 3 | import {
 4 |   StyleProp,
 5 |   StyleSheet,
 6 |   TouchableOpacity,
 7 |   View,
 8 |   ViewStyle,
 9 |   useColorScheme,
10 | } from "react-native";
11 | import { ThemedText } from "./ThemedText";
12 | import { Colors } from "@/constants/Colors";
13 | 
14 | interface NotionButtonProps {
15 |   onPress: () => void;
16 |   title?: string;
17 |   iconName?: ComponentProps["name"];
18 |   containerStyle?: StyleProp;
19 | }
20 | export default function NotionButton({
21 |   onPress,
22 |   iconName,
23 |   title,
24 |   containerStyle,
25 | }: NotionButtonProps) {
26 |   const theme = useColorScheme();
27 |   return (
28 |     
39 |       {iconName && (
40 |         
41 |       )}
42 |       {title && (
43 |         
48 |           {title}
49 |         
50 |       )}
51 |     
52 |   );
53 | }
54 | 
55 | const styles = StyleSheet.create({
56 |   container: {
57 |     padding: 7,
58 |     borderRadius: 40,
59 |     flexDirection: "row",
60 |     alignItems: "center",
61 |     gap: 7,
62 |   },
63 | });
64 | 
--------------------------------------------------------------------------------
/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
 1 | import type { PropsWithChildren, ReactElement } from 'react';
 2 | import { StyleSheet, useColorScheme } from 'react-native';
 3 | import Animated, {
 4 |   interpolate,
 5 |   useAnimatedRef,
 6 |   useAnimatedStyle,
 7 |   useScrollViewOffset,
 8 | } from 'react-native-reanimated';
 9 | 
10 | import { ThemedView } from '@/components/ThemedView';
11 | 
12 | const HEADER_HEIGHT = 250;
13 | 
14 | type Props = PropsWithChildren<{
15 |   headerImage: ReactElement;
16 |   headerBackgroundColor: { dark: string; light: string };
17 | }>;
18 | 
19 | export default function ParallaxScrollView({
20 |   children,
21 |   headerImage,
22 |   headerBackgroundColor,
23 | }: Props) {
24 |   const colorScheme = useColorScheme() ?? 'light';
25 |   const scrollRef = useAnimatedRef();
26 |   const scrollOffset = useScrollViewOffset(scrollRef);
27 | 
28 |   const headerAnimatedStyle = useAnimatedStyle(() => {
29 |     return {
30 |       transform: [
31 |         {
32 |           translateY: interpolate(
33 |             scrollOffset.value,
34 |             [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
35 |             [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
36 |           ),
37 |         },
38 |         {
39 |           scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
40 |         },
41 |       ],
42 |     };
43 |   });
44 | 
45 |   return (
46 |     
47 |       
48 |         
54 |           {headerImage}
55 |         
56 |         {children}
57 |       
58 |     
59 |   );
60 | }
61 | 
62 | const styles = StyleSheet.create({
63 |   container: {
64 |     flex: 1,
65 |   },
66 |   header: {
67 |     height: 250,
68 |     overflow: 'hidden',
69 |   },
70 |   content: {
71 |     flex: 1,
72 |     padding: 32,
73 |     gap: 16,
74 |     overflow: 'hidden',
75 |   },
76 | });
77 | 
--------------------------------------------------------------------------------
/components/ResentFileCard.tsx:
--------------------------------------------------------------------------------
 1 | import { StyleSheet, TouchableOpacity, useColorScheme } from "react-native";
 2 | 
 3 | import { ThemedText } from "@/components/ThemedText";
 4 | import { extendedClient } from "@/myDbModule";
 5 | import { Colors } from "@/constants/Colors";
 6 | import { LinearGradient } from "expo-linear-gradient";
 7 | import { getRandomGradient } from "@/constants/Gradients";
 8 | import { useMemo } from "react";
 9 | import Animated, {
10 |   LinearTransition,
11 |   SlideInLeft,
12 |   SlideOutLeft,
13 | } from "react-native-reanimated";
14 | import { NotionFile } from "@prisma/client/react-native";
15 | import { useNavigation } from "expo-router";
16 | 
17 | export default function ResentFileCard(file: NotionFile) {
18 |   const navigation = useNavigation();
19 |   const theme = useColorScheme();
20 |   const color = useMemo(() => getRandomGradient(), []);
21 |   const { id, title, coverPhoto, icon } = file;
22 |   return (
23 |     
36 |       
38 |           // @ts-ignore
39 |           navigation.navigate("new-notion", {
40 |             viewingFile: JSON.stringify(file),
41 |           })
42 |         }
43 |         onLongPress={() => extendedClient.notionFile.delete({ where: { id } })}
44 |         style={[
45 |           styles.container,
46 |           {
47 |             backgroundColor: Colors[theme!].backgroundSecondary,
48 |             borderColor: Colors[theme!].text + "20",
49 |           },
50 |         ]}
51 |       >
52 |         {coverPhoto ? (
53 |           <>>
54 |         ) : (
55 |           
63 |         )}
64 |         {icon}
65 |         
74 |           {title}
75 |         
76 |       
77 |     
78 |   );
79 | }
80 | 
81 | const styles = StyleSheet.create({
82 |   container: {
83 |     width: 120,
84 |     height: 120,
85 |     borderRadius: 16,
86 |     borderWidth: 0.5,
87 |   },
88 |   icon: {
89 |     fontSize: 32,
90 |     lineHeight: 36,
91 |     position: "absolute",
92 |     top: 40,
93 |     left: 6,
94 |   },
95 | });
96 | 
--------------------------------------------------------------------------------
/components/ResentFiles.tsx:
--------------------------------------------------------------------------------
 1 | import { StyleSheet, ScrollView, useColorScheme, View } from "react-native";
 2 | 
 3 | import { ThemedText } from "@/components/ThemedText";
 4 | import { extendedClient } from "@/myDbModule";
 5 | import ResentFileCard from "./ResentFileCard";
 6 | import { Colors } from "@/constants/Colors";
 7 | 
 8 | export default function ResentFiles() {
 9 |   const theme = useColorScheme();
10 |   const files = extendedClient.notionFile.useFindMany({
11 |     orderBy: { createdAt: "desc" },
12 |     take: 6,
13 |     where: { parentFileId: { equals: null } },
14 |   });
15 | 
16 |   return (
17 |     
18 |       
25 |         Jump back in
26 |       
27 |       {!files.length && (
28 |         
32 |           Nothing to show!
33 |         
34 |       )}
35 |       
43 |         {files.map((file) => (
44 |           
45 |         ))}
46 |       
47 |     
48 |   );
49 | }
50 | 
51 | const styles = StyleSheet.create({
52 |   container: {
53 |     marginBottom: 10,
54 |   },
55 | });
56 | 
--------------------------------------------------------------------------------
/components/ThemedText.tsx:
--------------------------------------------------------------------------------
 1 | import { Text, type TextProps, StyleSheet } from 'react-native';
 2 | 
 3 | import { useThemeColor } from '@/hooks/useThemeColor';
 4 | 
 5 | export type ThemedTextProps = TextProps & {
 6 |   lightColor?: string;
 7 |   darkColor?: string;
 8 |   type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
 9 | };
10 | 
11 | export function ThemedText({
12 |   style,
13 |   lightColor,
14 |   darkColor,
15 |   type = 'default',
16 |   ...rest
17 | }: ThemedTextProps) {
18 |   const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19 | 
20 |   return (
21 |     
33 |   );
34 | }
35 | 
36 | const styles = StyleSheet.create({
37 |   default: {
38 |     fontSize: 16,
39 |     lineHeight: 24,
40 |   },
41 |   defaultSemiBold: {
42 |     fontSize: 16,
43 |     lineHeight: 24,
44 |     fontWeight: '600',
45 |   },
46 |   title: {
47 |     fontSize: 32,
48 |     fontWeight: 'bold',
49 |     lineHeight: 32,
50 |   },
51 |   subtitle: {
52 |     fontSize: 20,
53 |     fontWeight: 'bold',
54 |   },
55 |   link: {
56 |     lineHeight: 30,
57 |     fontSize: 16,
58 |     color: '#0a7ea4',
59 |   },
60 | });
61 | 
--------------------------------------------------------------------------------
/components/ThemedView.tsx:
--------------------------------------------------------------------------------
 1 | import { View, type ViewProps } from 'react-native';
 2 | 
 3 | import { useThemeColor } from '@/hooks/useThemeColor';
 4 | 
 5 | export type ThemedViewProps = ViewProps & {
 6 |   lightColor?: string;
 7 |   darkColor?: string;
 8 | };
 9 | 
10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11 |   const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12 | 
13 |   return ;
14 | }
15 | 
--------------------------------------------------------------------------------
/components/__tests__/ThemedText-test.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react';
 2 | import renderer from 'react-test-renderer';
 3 | 
 4 | import { ThemedText } from '../ThemedText';
 5 | 
 6 | it(`renders correctly`, () => {
 7 |   const tree = renderer.create(Snapshot test!).toJSON();
 8 | 
 9 |   expect(tree).toMatchSnapshot();
10 | });
11 | 
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/ThemedText-test.tsx.snap:
--------------------------------------------------------------------------------
 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
 2 | 
 3 | exports[`renders correctly 1`] = `
 4 | 
22 |   Snapshot test!
23 | 
24 | `;
25 | 
--------------------------------------------------------------------------------
/components/navigation/TabBarIcon.tsx:
--------------------------------------------------------------------------------
 1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
 2 | 
 3 | import Ionicons from '@expo/vector-icons/Ionicons';
 4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet';
 5 | import { type ComponentProps } from 'react';
 6 | 
 7 | export function TabBarIcon({ style, ...rest }: IconProps['name']>) {
 8 |   return ;
 9 | }
10 | 
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
 3 |  * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
 4 |  */
 5 | 
 6 | const tintColorLight = "#000";
 7 | const tintColorDark = "#fff";
 8 | 
 9 | export const Colors = {
10 |   light: {
11 |     text: "#11181C",
12 |     background: "#fff",
13 |     backgroundSecondary: "#00000005",
14 |     tint: tintColorLight,
15 |     icon: "#687076",
16 |     tabIconDefault: "#687076",
17 |     tabIconSelected: tintColorLight,
18 |   },
19 |   dark: {
20 |     text: "#ECEDEE",
21 |     background: "#151718",
22 |     backgroundSecondary: "#ffffff05",
23 |     tint: tintColorDark,
24 |     icon: "#9BA1A6",
25 |     tabIconDefault: "#9BA1A6",
26 |     tabIconSelected: tintColorDark,
27 |   },
28 | };
29 | 
--------------------------------------------------------------------------------
/constants/Gradients.ts:
--------------------------------------------------------------------------------
 1 | import { LinearGradientProps } from "expo-linear-gradient";
 2 | 
 3 | const gradients = [
 4 |   { name: "Oceanic", colors: ["#0093E9", "#80D0C7"] },
 5 |   { name: "Cotton Candy", colors: ["#D9AFD9", "#97D9E1"] },
 6 |   { name: "Sunset", colors: ["#FAD961", "#F76B1C"] },
 7 |   { name: "Beachside", colors: ["#00CDAC", "#02AAB0"] },
 8 |   { name: "Peachy", colors: ["#FFA751", "#FFE259"] },
 9 |   { name: "Pumpkin", colors: ["#D4145A", "#FBB03B"] },
10 |   { name: "Arendelle", colors: ["#9796f0", "#fbc7d4"] },
11 |   { name: "High Tide", colors: ["#0082C8", "#667DB6"] },
12 | ];
13 | 
14 | export function getRandomGradient(): LinearGradientProps["colors"] {
15 |   const randomIndex = Math.floor(Math.random() * gradients.length);
16 |   return gradients[randomIndex].colors;
17 | }
18 | 
--------------------------------------------------------------------------------
/constants/MarkdownStyle.ts:
--------------------------------------------------------------------------------
 1 | import { Platform } from "react-native";
 2 | import type { MarkdownStyle } from "@expensify/react-native-live-markdown";
 3 | import { Colors } from "./Colors";
 4 | 
 5 | const FONT_FAMILY_MONOSPACE = Platform.select({
 6 |   ios: "Courier",
 7 |   default: "monospace",
 8 | });
 9 | 
10 | export const markdownStyle: MarkdownStyle = {
11 |   syntax: {
12 |     color: Colors.light.text,
13 |   },
14 |   link: {
15 |     color: Colors.light.tint,
16 |   },
17 |   h1: {
18 |     fontSize: 22,
19 |   },
20 |   emoji: {
21 |     fontSize: 32,
22 |   },
23 |   blockquote: {
24 |     borderColor: Colors.light.tint,
25 |     borderWidth: 2,
26 |     marginLeft: 6,
27 |     paddingLeft: 6,
28 |   },
29 |   code: {
30 |     fontFamily: FONT_FAMILY_MONOSPACE,
31 |     fontSize: 16,
32 |     color: Colors.light.text,
33 |     backgroundColor: "#00000010",
34 |   },
35 |   pre: {
36 |     fontFamily: FONT_FAMILY_MONOSPACE,
37 |     fontSize: 16,
38 |     color: Colors.light.text,
39 |     backgroundColor: "#00000010",
40 |   },
41 |   mentionHere: {
42 |     color: Colors.light.text,
43 |     backgroundColor: "yellow",
44 |   },
45 |   mentionUser: {
46 |     color: Colors.light.text,
47 |     backgroundColor: "cyan",
48 |   },
49 | };
50 | 
51 | export const markdownDarkStyle: MarkdownStyle = {
52 |   syntax: {
53 |     color: Colors.dark.text,
54 |   },
55 |   link: {
56 |     color: Colors.dark.tint,
57 |   },
58 |   h1: {
59 |     fontSize: 22,
60 |   },
61 |   emoji: {
62 |     fontSize: 32,
63 |   },
64 |   blockquote: {
65 |     borderColor: Colors.dark.tint,
66 |     borderWidth: 2,
67 |     marginLeft: 6,
68 |     paddingLeft: 6,
69 |   },
70 |   code: {
71 |     fontFamily: FONT_FAMILY_MONOSPACE,
72 |     fontSize: 16,
73 |     color: Colors.dark.tint,
74 |     backgroundColor: "#ffffff10",
75 |   },
76 |   pre: {
77 |     fontFamily: FONT_FAMILY_MONOSPACE,
78 |     fontSize: 16,
79 |     color: Colors.dark.text,
80 |     backgroundColor: "#00000010",
81 |   },
82 |   mentionHere: {
83 |     color: Colors.light.text,
84 |     backgroundColor: "yellow",
85 |   },
86 |   mentionUser: {
87 |     color: "white",
88 |     backgroundColor: "#0891b2",
89 |   },
90 | };
91 | 
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "cli": {
 3 |     "version": ">= 10.2.4"
 4 |   },
 5 |   "build": {
 6 |     "development": {
 7 |       "developmentClient": true,
 8 |       "distribution": "internal"
 9 |     },
10 |     "preview": {
11 |       "distribution": "internal"
12 |     },
13 |     "production": {}
14 |   },
15 |   "submit": {
16 |     "production": {}
17 |   }
18 | }
19 | 
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 | 
--------------------------------------------------------------------------------
/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // NOTE: The default React Native styling doesn't support server rendering.
2 | // Server rendered styles should not change between the first render of the HTML
3 | // and the first render on the client. Typically, web developers will use CSS media queries
4 | // to render different styles on the client and server, these aren't directly supported in React Native
5 | // but can be achieved using a styling library like Nativewind.
6 | export function useColorScheme() {
7 |   return 'light';
8 | }
9 | 
--------------------------------------------------------------------------------
/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Learn more about light and dark modes:
 3 |  * https://docs.expo.dev/guides/color-schemes/
 4 |  */
 5 | 
 6 | import { useColorScheme } from 'react-native';
 7 | 
 8 | import { Colors } from '@/constants/Colors';
 9 | 
10 | export function useThemeColor(
11 |   props: { light?: string; dark?: string },
12 |   colorName: keyof typeof Colors.light & keyof typeof Colors.dark
13 | ) {
14 |   const theme = useColorScheme() ?? 'light';
15 |   const colorFromProps = props[theme];
16 | 
17 |   if (colorFromProps) {
18 |     return colorFromProps;
19 |   } else {
20 |     return Colors[theme][colorName];
21 |   }
22 | }
23 | 
--------------------------------------------------------------------------------
/migrations/20240703032200_one/migration.sql:
--------------------------------------------------------------------------------
 1 | -- CreateTable
 2 | CREATE TABLE "User" (
 3 |     "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 4 |     "name" TEXT NOT NULL,
 5 |     "email" TEXT NOT NULL DEFAULT '',
 6 |     "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 7 |     "updatedAt" DATETIME NOT NULL
 8 | );
 9 | 
10 | -- CreateTable
11 | CREATE TABLE "NotionFile" (
12 |     "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
13 |     "coverPhoto" TEXT NOT NULL,
14 |     "icon" TEXT NOT NULL,
15 |     "title" TEXT NOT NULL,
16 |     "description" TEXT NOT NULL,
17 |     "content" TEXT NOT NULL,
18 |     "type" TEXT NOT NULL,
19 |     "authorId" INTEGER NOT NULL,
20 |     "parentFileId" INTEGER,
21 |     "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
22 |     "updatedAt" DATETIME NOT NULL,
23 |     CONSTRAINT "NotionFile_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
24 |     CONSTRAINT "NotionFile_parentFileId_fkey" FOREIGN KEY ("parentFileId") REFERENCES "NotionFile" ("id") ON DELETE SET NULL ON UPDATE CASCADE
25 | );
26 | 
--------------------------------------------------------------------------------
/migrations/20240703215900_add_order_to_notionfile/migration.sql:
--------------------------------------------------------------------------------
 1 | -- RedefineTables
 2 | PRAGMA defer_foreign_keys=ON;
 3 | PRAGMA foreign_keys=OFF;
 4 | CREATE TABLE "new_NotionFile" (
 5 |     "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 6 |     "coverPhoto" TEXT NOT NULL,
 7 |     "icon" TEXT NOT NULL,
 8 |     "title" TEXT NOT NULL,
 9 |     "description" TEXT NOT NULL,
10 |     "content" TEXT NOT NULL,
11 |     "type" TEXT NOT NULL,
12 |     "authorId" INTEGER NOT NULL,
13 |     "parentFileId" INTEGER,
14 |     "order" INTEGER NOT NULL DEFAULT 0,
15 |     "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
16 |     "updatedAt" DATETIME NOT NULL,
17 |     CONSTRAINT "NotionFile_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
18 |     CONSTRAINT "NotionFile_parentFileId_fkey" FOREIGN KEY ("parentFileId") REFERENCES "NotionFile" ("id") ON DELETE SET NULL ON UPDATE CASCADE
19 | );
20 | INSERT INTO "new_NotionFile" ("authorId", "content", "coverPhoto", "createdAt", "description", "icon", "id", "parentFileId", "title", "type", "updatedAt") SELECT "authorId", "content", "coverPhoto", "createdAt", "description", "icon", "id", "parentFileId", "title", "type", "updatedAt" FROM "NotionFile";
21 | DROP TABLE "NotionFile";
22 | ALTER TABLE "new_NotionFile" RENAME TO "NotionFile";
23 | PRAGMA foreign_keys=ON;
24 | PRAGMA defer_foreign_keys=OFF;
25 | 
--------------------------------------------------------------------------------
/migrations/20240704014730_add_on_delete_cascade_for_notion_files/migration.sql:
--------------------------------------------------------------------------------
 1 | -- RedefineTables
 2 | PRAGMA defer_foreign_keys=ON;
 3 | PRAGMA foreign_keys=OFF;
 4 | CREATE TABLE "new_NotionFile" (
 5 |     "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 6 |     "coverPhoto" TEXT NOT NULL,
 7 |     "icon" TEXT NOT NULL,
 8 |     "title" TEXT NOT NULL,
 9 |     "description" TEXT NOT NULL,
10 |     "content" TEXT NOT NULL,
11 |     "type" TEXT NOT NULL,
12 |     "authorId" INTEGER NOT NULL,
13 |     "parentFileId" INTEGER,
14 |     "order" INTEGER NOT NULL DEFAULT 0,
15 |     "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
16 |     "updatedAt" DATETIME NOT NULL,
17 |     CONSTRAINT "NotionFile_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
18 |     CONSTRAINT "NotionFile_parentFileId_fkey" FOREIGN KEY ("parentFileId") REFERENCES "NotionFile" ("id") ON DELETE CASCADE ON UPDATE CASCADE
19 | );
20 | INSERT INTO "new_NotionFile" ("authorId", "content", "coverPhoto", "createdAt", "description", "icon", "id", "order", "parentFileId", "title", "type", "updatedAt") SELECT "authorId", "content", "coverPhoto", "createdAt", "description", "icon", "id", "order", "parentFileId", "title", "type", "updatedAt" FROM "NotionFile";
21 | DROP TABLE "NotionFile";
22 | ALTER TABLE "new_NotionFile" RENAME TO "NotionFile";
23 | PRAGMA foreign_keys=ON;
24 | PRAGMA defer_foreign_keys=OFF;
25 | 
--------------------------------------------------------------------------------
/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "sqlite"
--------------------------------------------------------------------------------
/myDbModule.ts:
--------------------------------------------------------------------------------
 1 | import { PrismaClient } from "@prisma/client/react-native";
 2 | import { reactiveHooksExtension } from "@prisma/react-native";
 3 | 
 4 | export const baseClient = new PrismaClient({
 5 |   log: [
 6 |     // { emit: "stdout", level: "query" },
 7 |     { emit: "stdout", level: "info" },
 8 |     { emit: "stdout", level: "warn" },
 9 |     { emit: "stdout", level: "error" },
10 |   ],
11 | });
12 | 
13 | export const extendedClient = baseClient.$extends(reactiveHooksExtension());
14 | 
15 | export async function initializeDb() {
16 |   try {
17 |     await baseClient.$applyPendingMigrations();
18 |     console.log("db initialized!");
19 |   } catch (e) {
20 |     console.error(`failed to apply migrations: ${e}`);
21 |     throw new Error(
22 |       "Applying migrations failed, your app is now in an inconsistent state. We cannot guarantee safety, it is now your responsibility to reset the database or tell the user to re-install the app"
23 |     );
24 |   }
25 | }
26 | 
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "notion-clone",
 3 |   "main": "expo-router/entry",
 4 |   "version": "1.0.0",
 5 |   "scripts": {
 6 |     "start": "expo start",
 7 |     "reset-project": "node ./scripts/reset-project.js",
 8 |     "android": "expo run:android",
 9 |     "ios": "expo run:ios",
10 |     "web": "expo start --web",
11 |     "test": "jest --watchAll",
12 |     "lint": "expo lint"
13 |   },
14 |   "jest": {
15 |     "preset": "jest-expo"
16 |   },
17 |   "dependencies": {
18 |     "@expensify/react-native-live-markdown": "^0.1.97",
19 |     "@expo/react-native-action-sheet": "^4.1.0",
20 |     "@expo/vector-icons": "^14.0.2",
21 |     "@prisma/client": "5.16.1",
22 |     "@prisma/react-native": "5.16.1",
23 |     "@react-navigation/native": "^6.0.2",
24 |     "expo": "~51.0.28",
25 |     "expo-constants": "~16.0.2",
26 |     "expo-dev-client": "~4.0.23",
27 |     "expo-font": "~12.0.9",
28 |     "expo-linear-gradient": "~13.0.2",
29 |     "expo-linking": "~6.3.1",
30 |     "expo-router": "~3.5.23",
31 |     "expo-splash-screen": "~0.27.5",
32 |     "expo-status-bar": "~1.12.1",
33 |     "expo-system-ui": "~3.0.7",
34 |     "expo-web-browser": "~13.0.3",
35 |     "react": "18.2.0",
36 |     "react-dom": "18.2.0",
37 |     "react-native": "0.74.5",
38 |     "react-native-draggable-flatlist": "^4.0.1",
39 |     "react-native-gesture-handler": "~2.16.1",
40 |     "react-native-quick-base64": "2.1.2",
41 |     "react-native-reanimated": "~3.10.1",
42 |     "react-native-safe-area-context": "4.10.5",
43 |     "react-native-screens": "3.31.1",
44 |     "react-native-web": "~0.19.10"
45 |   },
46 |   "devDependencies": {
47 |     "@babel/core": "^7.20.0",
48 |     "@types/jest": "^29.5.12",
49 |     "@types/react": "~18.2.45",
50 |     "@types/react-test-renderer": "^18.0.7",
51 |     "jest": "^29.2.1",
52 |     "jest-expo": "~51.0.3",
53 |     "react-test-renderer": "18.2.0",
54 |     "typescript": "~5.3.3"
55 |   },
56 |   "private": true
57 | }
58 | 
--------------------------------------------------------------------------------
/queries.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO User (name) VALUES ('John Doe');
2 | INSERT INTO NotionFile (coverPhoto, icon, title, description, content, type, authorId) 
3 | VALUES ('https://i.ytimg.com/vi/XoumiwHbKDg/maxresdefault.jpg', '🔥', 'Snapchat Clone', 'Sample Description', 'Sample Content', 'text', 1);
4 | 
--------------------------------------------------------------------------------
/schema.prisma:
--------------------------------------------------------------------------------
 1 | generator client {
 2 |   provider        = "prisma-client-js"
 3 |   previewFeatures = ["reactNative"]
 4 | }
 5 | 
 6 | datasource db {
 7 |   provider = "sqlite"
 8 |   url      = "file:./app.db"
 9 | }
10 | 
11 | model User {
12 |   id           Int           @id @default(autoincrement())
13 |   name         String
14 |   email        String        @default("")
15 |   notionFiles  NotionFile[]
16 |   createdAt    DateTime      @default(now())
17 |   updatedAt    DateTime      @updatedAt
18 | }
19 | 
20 | model NotionFile {
21 |   id           Int           @id @default(autoincrement())
22 |   coverPhoto   String
23 |   icon         String
24 |   title        String
25 |   description  String
26 |   content      String
27 |   type         String
28 |   authorId     Int
29 |   author       User          @relation(fields: [authorId], references: [id], onDelete: Cascade)
30 |   parentFileId Int?          // Nullable to allow top-level files
31 |   parentFile   NotionFile?   @relation("FileHierarchy", fields: [parentFileId], references: [id], onDelete: Cascade)
32 |   subFiles     NotionFile[]  @relation("FileHierarchy")
33 |   order        Int           @default(0)
34 |   createdAt    DateTime      @default(now())
35 |   updatedAt    DateTime      @updatedAt
36 | }
37 | 
--------------------------------------------------------------------------------
/scripts/reset-project.js:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env node
 2 | 
 3 | /**
 4 |  * This script is used to reset the project to a blank state.
 5 |  * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
 6 |  * You can remove the `reset-project` script from package.json and safely delete this file after running it.
 7 |  */
 8 | 
 9 | const fs = require('fs');
10 | const path = require('path');
11 | 
12 | const root = process.cwd();
13 | const oldDirPath = path.join(root, 'app');
14 | const newDirPath = path.join(root, 'app-example');
15 | const newAppDirPath = path.join(root, 'app');
16 | 
17 | const indexContent = `import { Text, View } from "react-native";
18 | 
19 | export default function Index() {
20 |   return (
21 |     
28 |       Edit app/index.tsx to edit this screen.
29 |     
30 |   );
31 | }
32 | `;
33 | 
34 | const layoutContent = `import { Stack } from "expo-router";
35 | 
36 | export default function RootLayout() {
37 |   return (
38 |     
39 |       
40 |     
41 |   );
42 | }
43 | `;
44 | 
45 | fs.rename(oldDirPath, newDirPath, (error) => {
46 |   if (error) {
47 |     return console.error(`Error renaming directory: ${error}`);
48 |   }
49 |   console.log('/app moved to /app-example.');
50 | 
51 |   fs.mkdir(newAppDirPath, { recursive: true }, (error) => {
52 |     if (error) {
53 |       return console.error(`Error creating new app directory: ${error}`);
54 |     }
55 |     console.log('New /app directory created.');
56 | 
57 |     const indexPath = path.join(newAppDirPath, 'index.tsx');
58 |     fs.writeFile(indexPath, indexContent, (error) => {
59 |       if (error) {
60 |         return console.error(`Error creating index.tsx: ${error}`);
61 |       }
62 |       console.log('app/index.tsx created.');
63 | 
64 |       const layoutPath = path.join(newAppDirPath, '_layout.tsx');
65 |       fs.writeFile(layoutPath, layoutContent, (error) => {
66 |         if (error) {
67 |           return console.error(`Error creating _layout.tsx: ${error}`);
68 |         }
69 |         console.log('app/_layout.tsx created.');
70 |       });
71 |     });
72 |   });
73 | });
74 | 
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "expo/tsconfig.base",
 3 |   "compilerOptions": {
 4 |     "strict": true,
 5 |     "paths": {
 6 |       "@/*": [
 7 |         "./*"
 8 |       ]
 9 |     }
10 |   },
11 |   "include": [
12 |     "**/*.ts",
13 |     "**/*.tsx",
14 |     ".expo/types/**/*.ts",
15 |     "expo-env.d.ts"
16 |   ]
17 | }
18 | 
--------------------------------------------------------------------------------