();
36 |
37 | useEffect(() => {
38 | appStateListener.current = AppState.addEventListener(
39 | "change",
40 | (appState) => {
41 | if (appState === "active") {
42 | console.log("AppState?");
43 | // getCurrentLocation();
44 | }
45 | }
46 | );
47 |
48 | return () => {
49 | appStateListener.current!.remove();
50 | };
51 | }, []);
52 | }
53 |
--------------------------------------------------------------------------------
/constants/css-animations.ts:
--------------------------------------------------------------------------------
1 | import { css } from "react-native-reanimated";
2 |
3 | const angle = 4;
4 | export const cssAnimations = css.create({
5 | shakeX: {
6 | animationName: {
7 | "10%, 30%, 50%, 70%, 90%": {
8 | transform: [
9 | {
10 | translateX: -10,
11 | },
12 | {
13 | translateY: 0,
14 | },
15 | ],
16 | },
17 | "20%, 40%, 60%, 80%": {
18 | transform: [
19 | {
20 | translateX: 10,
21 | },
22 | {
23 | translateY: 0,
24 | },
25 | ],
26 | },
27 | },
28 | },
29 | tada: {
30 | animationName: {
31 | from: {
32 | transform: [
33 | {
34 | scale: 1,
35 | },
36 | {
37 | rotateZ: `${angle}deg`,
38 | },
39 | ],
40 | },
41 | "10%, 20%": {
42 | transform: [
43 | {
44 | scale: 1,
45 | },
46 | {
47 | rotateZ: `-${angle}deg`,
48 | },
49 | ],
50 | },
51 | "30%, 50%, 70%, 90%": {
52 | transform: [
53 | {
54 | scale: 1,
55 | },
56 |
57 | {
58 | rotateZ: `${angle}deg`,
59 | },
60 | ],
61 | },
62 | "40%, 60%, 80%": {
63 | transform: [
64 | {
65 | scale: 1,
66 | },
67 | {
68 | rotateZ: `-${angle}deg`,
69 | },
70 | ],
71 | },
72 | to: {
73 | transform: [
74 | {
75 | scale: 1,
76 | },
77 | {
78 | rotateZ: `-${angle}deg`,
79 | },
80 | ],
81 | },
82 | },
83 | },
84 | });
85 |
--------------------------------------------------------------------------------
/drizzle/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "6",
3 | "dialect": "sqlite",
4 | "id": "083328c7-55e7-499d-8633-6e451176af9f",
5 | "prevId": "00000000-0000-0000-0000-000000000000",
6 | "tables": {
7 | "todos": {
8 | "name": "todos",
9 | "columns": {
10 | "id": {
11 | "name": "id",
12 | "type": "integer",
13 | "primaryKey": true,
14 | "notNull": true,
15 | "autoincrement": true
16 | },
17 | "content": {
18 | "name": "content",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "autoincrement": false
23 | },
24 | "date": {
25 | "name": "date",
26 | "type": "text",
27 | "primaryKey": false,
28 | "notNull": true,
29 | "autoincrement": false
30 | },
31 | "description": {
32 | "name": "description",
33 | "type": "text",
34 | "primaryKey": false,
35 | "notNull": false,
36 | "autoincrement": false,
37 | "default": "''"
38 | },
39 | "created_at": {
40 | "name": "created_at",
41 | "type": "text",
42 | "primaryKey": false,
43 | "notNull": false,
44 | "autoincrement": false,
45 | "default": "(CURRENT_TIMESTAMP)"
46 | },
47 | "done": {
48 | "name": "done",
49 | "type": "integer",
50 | "primaryKey": false,
51 | "notNull": false,
52 | "autoincrement": false,
53 | "default": 0
54 | }
55 | },
56 | "indexes": {},
57 | "foreignKeys": {},
58 | "compositePrimaryKeys": {},
59 | "uniqueConstraints": {},
60 | "checkConstraints": {}
61 | }
62 | },
63 | "views": {},
64 | "enums": {},
65 | "_meta": {
66 | "schemas": {},
67 | "tables": {},
68 | "columns": {}
69 | },
70 | "internal": {
71 | "indexes": {}
72 | }
73 | }
--------------------------------------------------------------------------------
/drizzle/meta/0001_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "6",
3 | "dialect": "sqlite",
4 | "id": "df2ad12f-3caf-49a6-be42-bf04059e2e9b",
5 | "prevId": "083328c7-55e7-499d-8633-6e451176af9f",
6 | "tables": {
7 | "todos": {
8 | "name": "todos",
9 | "columns": {
10 | "id": {
11 | "name": "id",
12 | "type": "integer",
13 | "primaryKey": true,
14 | "notNull": true,
15 | "autoincrement": true
16 | },
17 | "content": {
18 | "name": "content",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "autoincrement": false
23 | },
24 | "date": {
25 | "name": "date",
26 | "type": "integer",
27 | "primaryKey": false,
28 | "notNull": true,
29 | "autoincrement": false
30 | },
31 | "description": {
32 | "name": "description",
33 | "type": "text",
34 | "primaryKey": false,
35 | "notNull": false,
36 | "autoincrement": false,
37 | "default": "''"
38 | },
39 | "created_at": {
40 | "name": "created_at",
41 | "type": "text",
42 | "primaryKey": false,
43 | "notNull": false,
44 | "autoincrement": false,
45 | "default": "(CURRENT_TIMESTAMP)"
46 | },
47 | "done": {
48 | "name": "done",
49 | "type": "integer",
50 | "primaryKey": false,
51 | "notNull": false,
52 | "autoincrement": false,
53 | "default": 0
54 | }
55 | },
56 | "indexes": {},
57 | "foreignKeys": {},
58 | "compositePrimaryKeys": {},
59 | "uniqueConstraints": {},
60 | "checkConstraints": {}
61 | }
62 | },
63 | "views": {},
64 | "enums": {},
65 | "_meta": {
66 | "schemas": {},
67 | "tables": {},
68 | "columns": {}
69 | },
70 | "internal": {
71 | "indexes": {}
72 | }
73 | }
--------------------------------------------------------------------------------
/constants/openweather.ts:
--------------------------------------------------------------------------------
1 | import { LucideIconName } from "./types";
2 |
3 | export const API_URL = `https://api.openweathermap.org/data/2.5/forecast/daily`;
4 |
5 | // ?lat=44.34&lon=10.99&cnt=7&appid={API key}
6 |
7 | export interface ForecastPayload {
8 | city: City;
9 | cod: string;
10 | message: number;
11 | cnt: number;
12 | list: DayWeather[];
13 | }
14 |
15 | export interface City {
16 | id: number;
17 | name: string;
18 | coord: Coord;
19 | country: string;
20 | population: number;
21 | timezone: number;
22 | }
23 |
24 | export interface Coord {
25 | lon: number;
26 | lat: number;
27 | }
28 |
29 | export interface DayWeather {
30 | dt: number;
31 | sunrise: number;
32 | sunset: number;
33 | temp: Temp;
34 | feels_like: FeelsLike;
35 | pressure: number;
36 | humidity: number;
37 | weather: Weather[];
38 | speed: number;
39 | deg: number;
40 | gust: number;
41 | clouds: number;
42 | pop: number;
43 | rain?: number;
44 | }
45 |
46 | export interface Temp {
47 | day: number;
48 | min: number;
49 | max: number;
50 | night: number;
51 | eve: number;
52 | morn: number;
53 | }
54 |
55 | export interface FeelsLike {
56 | day: number;
57 | night: number;
58 | eve: number;
59 | morn: number;
60 | }
61 |
62 | export interface Weather {
63 | id: number;
64 | main: string;
65 | description: string;
66 | icon: string;
67 | }
68 |
69 | export function getIcon(id: number): LucideIconName {
70 | if (id >= 200 && id < 300) {
71 | return "CloudRain";
72 | } else if (id >= 300 && id < 500) {
73 | return "CloudHail";
74 | } else if (id >= 500 && id < 600) {
75 | return "CloudRain";
76 | } else if (id >= 600 && id < 700) {
77 | return "Snowflake";
78 | } else if (id >= 700 && id < 800) {
79 | return "CloudFog";
80 | } else if (id === 800) {
81 | return "SunMedium";
82 | } else if (id >= 801 && id < 803) {
83 | return "Cloudy";
84 | } else if (id >= 802 && id < 900) {
85 | return "Cloudy";
86 | } else if (id === 905 || (id >= 951 && id <= 956)) {
87 | return "Wind";
88 | } else if (id >= 900 && id < 1000) {
89 | return "CloudRain";
90 | }
91 |
92 | return "Sun";
93 | }
94 |
--------------------------------------------------------------------------------
/components/Todo.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@/db/init";
2 | import { todos } from "@/db/schema";
3 | import { hitSlop } from "@/utils/misc";
4 | import { eq } from "drizzle-orm";
5 | import { Check } from "lucide-react-native";
6 | import { Alert, Text, TouchableOpacity, View } from "react-native";
7 | import Animated, { ZoomIn, ZoomOut } from "react-native-reanimated";
8 |
9 | export function Todo({ todo }: { todo: typeof todos.$inferSelect }) {
10 | return (
11 | {
15 | Alert.alert(
16 | "Delete todo",
17 | `Are you sure you want to delete \n"${todo.content}"`,
18 | [
19 | {
20 | text: "Cancel",
21 | style: "cancel",
22 | },
23 | {
24 | text: "Delete",
25 | style: "destructive",
26 | onPress: async () => {
27 | await db.delete(todos).where(eq(todos.id, todo.id)).execute();
28 | },
29 | },
30 | ]
31 | );
32 | // db.delete(todos).where(eq(todos.id, todo.id)).execute();
33 | }}
34 | onPress={() => {
35 | db.update(todos)
36 | .set({
37 | done: Boolean(todo.done) ? 0 : 1,
38 | })
39 | .where(eq(todos.id, todo.id))
40 | .execute();
41 | }}>
42 |
43 |
49 | {Boolean(todo.done) && (
50 |
53 |
54 |
55 | )}
56 |
57 |
61 | {todo.content}
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/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, /components, /hooks, /scripts, and /constants directories 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 oldDirs = ["app", "components", "hooks", "constants", "scripts"];
14 | const newDir = "app-example";
15 | const newAppDir = "app";
16 | const newDirPath = path.join(root, newDir);
17 |
18 | const indexContent = `import { Text, View } from "react-native";
19 |
20 | export default function Index() {
21 | return (
22 |
29 | Edit app/index.tsx to edit this screen.
30 |
31 | );
32 | }
33 | `;
34 |
35 | const layoutContent = `import { Stack } from "expo-router";
36 |
37 | export default function RootLayout() {
38 | return ;
39 | }
40 | `;
41 |
42 | const moveDirectories = async () => {
43 | try {
44 | // Create the app-example directory
45 | await fs.promises.mkdir(newDirPath, { recursive: true });
46 | console.log(`📁 /${newDir} directory created.`);
47 |
48 | // Move old directories to new app-example directory
49 | for (const dir of oldDirs) {
50 | const oldDirPath = path.join(root, dir);
51 | const newDirPath = path.join(root, newDir, dir);
52 | if (fs.existsSync(oldDirPath)) {
53 | await fs.promises.rename(oldDirPath, newDirPath);
54 | console.log(`➡️ /${dir} moved to /${newDir}/${dir}.`);
55 | } else {
56 | console.log(`➡️ /${dir} does not exist, skipping.`);
57 | }
58 | }
59 |
60 | // Create new /app directory
61 | const newAppDirPath = path.join(root, newAppDir);
62 | await fs.promises.mkdir(newAppDirPath, { recursive: true });
63 | console.log("\n📁 New /app directory created.");
64 |
65 | // Create index.tsx
66 | const indexPath = path.join(newAppDirPath, "index.tsx");
67 | await fs.promises.writeFile(indexPath, indexContent);
68 | console.log("📄 app/index.tsx created.");
69 |
70 | // Create _layout.tsx
71 | const layoutPath = path.join(newAppDirPath, "_layout.tsx");
72 | await fs.promises.writeFile(layoutPath, layoutContent);
73 | console.log("📄 app/_layout.tsx created.");
74 |
75 | console.log("\n✅ Project reset complete. Next steps:");
76 | console.log(
77 | "1. Run `npx expo start` to start a development server.\n2. Edit app/index.tsx to edit the main screen.\n3. Delete the /app-example directory when you're done referencing it."
78 | );
79 | } catch (error) {
80 | console.error(`Error during script execution: ${error}`);
81 | }
82 | };
83 |
84 | moveDirectories();
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ---
11 |
12 | ---
13 |
14 |
15 |
16 | # MonoDo 👋
17 |
18 | TestFlight: https://testflight.apple.com/join/A5G6jwVv
19 |
20 | Android: https://play.google.com/store/apps/details?id=com.animatereactnative.monodo
21 |
22 |
23 |
24 | Switch the theme (Easter egg 🥚)
25 |
26 |
27 |
28 | | Closed |
29 | Opened |
30 |
31 |
32 |
33 |
34 |
35 | |
36 |
37 |
38 |
39 |
40 | |
41 |
42 |
43 |
44 |
45 | ## Get started
46 |
47 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
48 |
49 | 1. Install dependencies
50 |
51 | ```bash
52 | npm install
53 | ```
54 |
55 | 2. Start the app
56 |
57 | ```bash
58 | npx expo start
59 | ```
60 |
61 | In the output, you'll find options to open the app in a
62 |
63 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
64 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
65 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
66 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
67 |
68 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
69 |
70 | ## Get a fresh project
71 |
72 | When you're ready, run:
73 |
74 | ```bash
75 | npm run reset-project
76 | ```
77 |
78 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
79 |
80 | ## Learn more
81 |
82 | To learn more about developing your project with Expo, look at the following resources:
83 |
84 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
85 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
86 |
87 | ## Join the community
88 |
89 | Join our community of developers creating universal apps.
90 |
91 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
92 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
93 |
94 | ### Inspiration
95 |
96 | > [!TIP]
97 | > This project was 100% inspired by [Brett](https://x.com/thebtjackson/status/1881325871304421532). Thank you!
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monodo",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start --dev-client",
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 | "generate": "drizzle-kit generate",
14 | "studio": "drizzle-kit studio",
15 | "build:development": "eas build --profile development --platform ios",
16 | "build:preview": "eas build --profile preview --platform ios",
17 | "build:production": "eas build --profile production",
18 | "push:update": "eas update --channel production && npx sentry-expo-upload-sourcemaps dist",
19 | "push:update:clean": "eas update --channel production --clear-cache && npx sentry-expo-upload-sourcemaps dist"
20 | },
21 | "jest": {
22 | "preset": "jest-expo"
23 | },
24 | "dependencies": {
25 | "@animatereactnative/accordion": "^0.1.5",
26 | "@animatereactnative/stagger": "^0.3.0",
27 | "@expo-google-fonts/barlow": "^0.2.3",
28 | "@expo/vector-icons": "^15.0.2",
29 | "@legendapp/state": "^3.0.0-beta.26",
30 | "@react-native-masked-view/masked-view": "^0.3.2",
31 | "@react-navigation/bottom-tabs": "^7.4.0",
32 | "@react-navigation/elements": "^2.6.3",
33 | "@react-navigation/native": "^7.1.8",
34 | "@sentry/react-native": "~6.20.0",
35 | "@tanstack/react-query": "^5.65.1",
36 | "babel-plugin-inline-import": "^3.0.0",
37 | "dayjs": "^1.11.13",
38 | "drizzle-orm": "^0.38.4",
39 | "expo": "^54.0.0",
40 | "expo-blur": "~15.0.7",
41 | "expo-constants": "~18.0.8",
42 | "expo-dev-client": "~6.0.12",
43 | "expo-drizzle-studio-plugin": "^0.1.1",
44 | "expo-file-system": "~19.0.14",
45 | "expo-font": "~14.0.8",
46 | "expo-haptics": "~15.0.7",
47 | "expo-linear-gradient": "~15.0.7",
48 | "expo-linking": "~8.0.8",
49 | "expo-location": "~19.0.7",
50 | "expo-router": "~6.0.4",
51 | "expo-splash-screen": "~31.0.10",
52 | "expo-sqlite": "~16.0.8",
53 | "expo-status-bar": "~3.0.8",
54 | "expo-symbols": "~1.0.7",
55 | "expo-system-ui": "~6.0.7",
56 | "expo-updates": "~29.0.10",
57 | "expo-web-browser": "~15.0.7",
58 | "lucide-react-native": "^0.473.0",
59 | "moti": "^0.29.0",
60 | "nativewind": "^4.2.1",
61 | "react": "19.1.0",
62 | "react-dom": "19.1.0",
63 | "react-native": "0.81.4",
64 | "react-native-css-animations": "^0.1.1",
65 | "react-native-edge-to-edge": "^1.4.3",
66 | "react-native-gesture-handler": "~2.28.0",
67 | "react-native-keyboard-controller": "1.18.5",
68 | "react-native-reanimated": "^4.1.0",
69 | "react-native-safe-area-context": "~5.6.0",
70 | "react-native-screens": "~4.16.0",
71 | "react-native-svg": "15.12.1",
72 | "react-native-web": "^0.21.0",
73 | "react-native-webview": "13.15.0",
74 | "react-native-worklets": "^0.5.1",
75 | "tailwindcss": "^3.4.17",
76 | "vexo-analytics": "^1.5.2"
77 | },
78 | "devDependencies": {
79 | "@babel/core": "^7.25.2",
80 | "@types/jest": "^29.5.12",
81 | "@types/react": "~19.1.10",
82 | "@types/react-test-renderer": "^18.3.0",
83 | "drizzle-kit": "^0.30.2",
84 | "jest": "^29.2.1",
85 | "jest-expo": "~54.0.11",
86 | "react-test-renderer": "18.3.1",
87 | "typescript": "~5.9.2"
88 | },
89 | "private": true
90 | }
91 |
--------------------------------------------------------------------------------
/components/Week.tsx:
--------------------------------------------------------------------------------
1 | import { DayWeather } from "@/constants/openweather";
2 | import { useStoreLocation } from "@/hooks/useStoreLocation";
3 | import { location$ } from "@/state/location";
4 | import { weatherQuery$ } from "@/state/weather";
5 | import { getWeekDays } from "@/utils/constants";
6 | import { currentDay } from "@legendapp/state/helpers/time";
7 | import { observer } from "@legendapp/state/react";
8 | import MaskedView from "@react-native-masked-view/masked-view";
9 | import dayjs from "dayjs";
10 | import { BlurView } from "expo-blur";
11 | import { LinearGradient } from "expo-linear-gradient";
12 | import React, { useMemo } from "react";
13 | import { Platform, ScrollView, StyleSheet, View } from "react-native";
14 | import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
15 |
16 | import { useAnimatedRef } from "react-native-reanimated";
17 | import {
18 | SafeAreaView,
19 | useSafeAreaInsets,
20 | } from "react-native-safe-area-context";
21 | import { Day } from "./Day";
22 |
23 | export const Week = observer(() => {
24 | const list = weatherQuery$.list.get();
25 | const { latitude, longitude } = location$.get();
26 |
27 | const data = useMemo(() => {
28 | return list?.reduce(
29 | (acc, item) => ({
30 | ...acc,
31 | [dayjs(item.dt * 1000).format("YYYY-MM-DD")]: item,
32 | }),
33 | {} as Record
34 | );
35 | }, [list, latitude, longitude]);
36 |
37 | useStoreLocation();
38 |
39 | // To handle gestures, check https://github.com/kirillzyusko/react-native-keyboard-controller/blob/main/example/src/screens/Examples/InteractiveKeyboard/index.tsx#L90
40 | const { bottom, top } = useSafeAreaInsets();
41 | const ref = useAnimatedRef();
42 | const _currentDay = currentDay.get();
43 | const weekDays = useMemo(() => getWeekDays(), [_currentDay]);
44 |
45 | // Compensate for Android
46 | const topSpacing = 32 + top;
47 |
48 | return (
49 | <>
50 |
58 |
59 | {weekDays.map((day, index) => (
60 |
66 | ))}
67 |
68 |
69 |
75 |
84 | }
85 | style={[StyleSheet.absoluteFill]}>
86 |
92 |
93 |
94 | >
95 | );
96 | });
97 |
--------------------------------------------------------------------------------
/components/Todos.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@/db/init";
2 | import { todos } from "@/db/schema";
3 | import { hitSlop } from "@/utils/misc";
4 | import { Stagger } from "@animatereactnative/stagger";
5 | import dayjs from "dayjs";
6 | import { between } from "drizzle-orm";
7 | import { useLiveQuery } from "drizzle-orm/expo-sqlite";
8 | import { Plus } from "lucide-react-native";
9 | import React, { useRef, useState } from "react";
10 | import { Pressable, Text, TextInput, View } from "react-native";
11 | import Animated, {
12 | FadeInDown,
13 | FadeOutDown,
14 | FadeOutLeft,
15 | LinearTransition,
16 | } from "react-native-reanimated";
17 | import { Todo } from "./Todo";
18 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
19 |
20 | export function Todos({ day }: { day: string }) {
21 | const { data } = useLiveQuery(
22 | db
23 | .select()
24 | .from(todos)
25 | .where(
26 | between(
27 | todos.date,
28 | dayjs(day).startOf("day").toDate(),
29 | dayjs(day).endOf("day").toDate()
30 | )
31 | )
32 | .orderBy(todos.created_at),
33 | [day]
34 | );
35 |
36 | const [content, setContent] = useState("");
37 | const inputRef = useRef(null);
38 |
39 | const addTodo = () => {
40 | inputRef.current?.clear();
41 | inputRef.current?.blur();
42 | db.insert(todos)
43 | .values({
44 | date: dayjs(day).toDate(),
45 | content: content,
46 | })
47 | .run();
48 | setContent("");
49 | };
50 | const isDisabled = !content || content === "";
51 |
52 | return (
53 |
54 | FadeOutLeft}
59 | // enterDirection={-1}
60 | >
61 | {data?.map((todo, index) => (
62 |
63 | ))}
64 |
65 |
70 | {
76 | if (!isDisabled) {
77 | addTodo();
78 | }
79 | }}
80 | onChangeText={(text) => {
81 | setContent(text.trim());
82 | }}
83 | className='flex-1 border-b border-black/50 dark:border-white/50 rounded-md p-2 font-barlow-400 dark:text-white/60 placeholder:dark:text-white/30'
84 | placeholder='What needs to be done?'
85 | />
86 |
92 |
93 |
94 |
95 | Add
96 |
97 |
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "monodo",
4 | "slug": "monodo",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "monodo",
9 | "userInterfaceStyle": "automatic",
10 | "newArchEnabled": true,
11 | "ios": {
12 | "supportsTablet": true,
13 | "bundleIdentifier": "com.animatereactnative.monodo",
14 | "config": {
15 | "usesNonExemptEncryption": false
16 | },
17 | "entitlements": {
18 | "aps-environment": "development",
19 | "com.apple.security.application-groups": [
20 | "group.com.animatereactnative.monodo"
21 | ]
22 | }
23 | },
24 | "android": {
25 | "adaptiveIcon": {
26 | "foregroundImage": "./assets/images/adaptive-icon.png",
27 | "backgroundColor": "#ffffff"
28 | },
29 | "package": "com.animatereactnative.monodo",
30 | "permissions": [
31 | "android.permission.ACCESS_COARSE_LOCATION",
32 | "android.permission.ACCESS_FINE_LOCATION",
33 | "android.permission.FOREGROUND_SERVICE",
34 | "android.permission.FOREGROUND_SERVICE_LOCATION"
35 | ],
36 | "blockedPermissions": [
37 | "android.permission.WRITE_EXTERNAL_STORAGE",
38 | "android.permission.READ_EXTERNAL_STORAGE",
39 | "android.permission.READ_MEDIA_IMAGES",
40 | "android.permission.READ_MEDIA_VIDEO"
41 | ]
42 | },
43 | "web": {
44 | "bundler": "metro",
45 | "output": "static",
46 | "favicon": "./assets/images/favicon.png"
47 | },
48 | "plugins": [
49 | "expo-router",
50 | [
51 | "expo-splash-screen",
52 | {
53 | "image": "./assets/images/splash.png",
54 | "imageWidth": 1000,
55 | "resizeMode": "cover",
56 | "backgroundColor": "#ffffff",
57 | "enableFullScreenImage_legacy": true,
58 | "android": {
59 | "resizeMode": "contain",
60 | "backgroundColor": "#ffffff",
61 | "image": "./assets/images/icon.png",
62 | "imageWidth": 300
63 | }
64 | }
65 | ],
66 | "expo-sqlite",
67 | [
68 | "expo-location",
69 | {
70 | "locationWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location. Only when you are using the app. This is used to display the weather for each day based on your location.",
71 | "isAndroidForegroundServiceEnabled": true
72 | }
73 | ],
74 | [
75 | "react-native-edge-to-edge",
76 | {
77 | "android": {
78 | "parentTheme": "Light",
79 | "enforceNavigationBarContrast": false
80 | }
81 | }
82 | ],
83 | [
84 | "@sentry/react-native/expo",
85 | {
86 | "url": "https://sentry.io/",
87 | "project": "monodo",
88 | "organization": "animatereactnative"
89 | }
90 | ],
91 | "expo-font",
92 | "expo-web-browser"
93 | ],
94 | "experiments": {
95 | "typedRoutes": true
96 | },
97 | "extra": {
98 | "router": {
99 | "origin": false
100 | },
101 | "eas": {
102 | "projectId": "5b5466b2-9d6f-43dd-ac13-0ae1a08050cb"
103 | }
104 | },
105 | "owner": "catalinmiron",
106 | "runtimeVersion": {
107 | "policy": "appVersion"
108 | },
109 | "updates": {
110 | "url": "https://u.expo.dev/5b5466b2-9d6f-43dd-ac13-0ae1a08050cb"
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/utils/cssInterop";
2 | import {
3 | Barlow_300Light,
4 | Barlow_400Regular,
5 | Barlow_500Medium,
6 | Barlow_700Bold,
7 | Barlow_900Black,
8 | useFonts,
9 | } from "@expo-google-fonts/barlow";
10 | import {
11 | DarkTheme,
12 | DefaultTheme,
13 | ThemeProvider,
14 | } from "@react-navigation/native";
15 | import { Stack, useNavigationContainerRef } from "expo-router";
16 | import * as SplashScreen from "expo-splash-screen";
17 | import { StatusBar } from "expo-status-bar";
18 | import { useEffect } from "react";
19 | import "react-native-reanimated";
20 | import "../global.css";
21 |
22 | import { useColorScheme } from "@/hooks/useColorScheme";
23 | import { useLocalMigrations } from "@/hooks/useLocalMigrations";
24 | import { vexo } from "vexo-analytics";
25 |
26 | // Prevent the splash screen from auto-hiding before asset loading is complete.
27 | SplashScreen.preventAutoHideAsync();
28 | SplashScreen.setOptions({
29 | fade: true,
30 | duration: 500,
31 | });
32 |
33 | import { queryClient } from "@/constants/queryClient";
34 | import { useExpoUpdates } from "@/hooks/useExpoUpdates";
35 | import * as Sentry from "@sentry/react-native";
36 | import { QueryClientProvider } from "@tanstack/react-query";
37 | import Constants, { ExecutionEnvironment } from "expo-constants";
38 | import { GestureHandlerRootView } from "react-native-gesture-handler";
39 | import { KeyboardProvider } from "react-native-keyboard-controller";
40 |
41 | const navigationIntegration = Sentry.reactNavigationIntegration({
42 | enableTimeToInitialDisplay:
43 | Constants.executionEnvironment === ExecutionEnvironment.StoreClient, // Only in native builds, not in Expo Go.
44 | });
45 | Sentry.init({
46 | dsn: "https://73cab5913e1839cb5f83a625a4431681@o1316893.ingest.us.sentry.io/4508840580481024",
47 |
48 | // uncomment the line below to enable Spotlight (https://spotlightjs.com)
49 | // spotlight: __DEV__,
50 | tracesSampleRate: 1.0,
51 | integrations: [navigationIntegration],
52 | enableNativeFramesTracking:
53 | Constants.executionEnvironment === ExecutionEnvironment.StoreClient,
54 | });
55 | vexo(process.env.EXPO_PUBLIC_VEXO_API_KEY as string);
56 |
57 | function RootLayout() {
58 | const colorScheme = useColorScheme();
59 | const [loaded] = useFonts({
60 | Barlow_300Light: Barlow_300Light,
61 | Barlow_400Regular: Barlow_400Regular,
62 | Barlow_500Medium: Barlow_500Medium,
63 | Barlow_700Bold: Barlow_700Bold,
64 | Barlow_900Black: Barlow_900Black,
65 | });
66 |
67 | useLocalMigrations();
68 | if (!__DEV__) {
69 | useExpoUpdates();
70 | }
71 |
72 | useEffect(() => {
73 | if (loaded) {
74 | SplashScreen.hideAsync();
75 | }
76 | }, [loaded]);
77 |
78 | const ref = useNavigationContainerRef();
79 | useEffect(() => {
80 | if (ref) {
81 | navigationIntegration.registerNavigationContainer(ref);
82 | }
83 | }, [ref]);
84 |
85 | if (!loaded) {
86 | return null;
87 | }
88 |
89 | return (
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | }
105 |
106 | export default Sentry.wrap(RootLayout);
107 |
--------------------------------------------------------------------------------
/components/Day.tsx:
--------------------------------------------------------------------------------
1 | import { DayWeather, getIcon } from "@/constants/openweather";
2 | import { location$ } from "@/state/location";
3 | import {
4 | localFormatter,
5 | weekDayFormatter,
6 | WeekDayIndex,
7 | } from "@/utils/constants";
8 | import Accordion from "@animatereactnative/accordion";
9 | import { currentDay } from "@legendapp/state/helpers/time";
10 | import { observer } from "@legendapp/state/react";
11 | import dayjs from "dayjs";
12 | import { Text, useWindowDimensions } from "react-native";
13 | import Animated, {
14 | AnimatedRef,
15 | FadeInDown,
16 | FadeInRight,
17 | measure,
18 | runOnUI,
19 | scrollTo,
20 | useAnimatedRef,
21 | } from "react-native-reanimated";
22 | import { AnimatedScrollView } from "react-native-reanimated/lib/typescript/component/ScrollView";
23 | import { useSafeAreaInsets } from "react-native-safe-area-context";
24 | import { Icon } from "./Icon";
25 | import { Todos } from "./Todos";
26 |
27 | // day will be formatted as YYYY-MM-DD / check @/utils/constants
28 | export const Day = observer(
29 | ({
30 | day,
31 | weather,
32 | scrollRef,
33 | }: {
34 | day: string;
35 | weather?: DayWeather;
36 | scrollRef: AnimatedRef;
37 | }) => {
38 | const { height } = useWindowDimensions();
39 | const { top, bottom } = useSafeAreaInsets();
40 | const isCurrentDay = dayjs(day).isSame(dayjs(currentDay.get()), "day");
41 | const aRef = useAnimatedRef();
42 | const currentDayIndex = dayjs(day).isoWeekday() as WeekDayIndex;
43 |
44 | const dayBg = {
45 | 1: `bg-stone-900/5 dark:bg-black/5`,
46 | 2: `bg-stone-900/10 dark:bg-black/10`,
47 | 3: `bg-stone-900/15 dark:bg-black/15`,
48 | 4: `bg-stone-900/20 dark:bg-black/20`,
49 | 5: `bg-stone-900/25 dark:bg-black/25`,
50 | 6: `bg-stone-900/30 dark:bg-black/30`,
51 | 7: `bg-stone-900/35 dark:bg-black/45`, // Sunday
52 | } as const;
53 | return (
54 | {
61 | runOnUI(() => {
62 | const measurement = measure(aRef);
63 | if (isOn) {
64 | scrollTo(scrollRef, 0, measurement!.pageY - height / 2, true);
65 | } else {
66 | scrollTo(scrollRef, 0, -measurement!.pageY, true);
67 | }
68 | })();
69 | }}
70 | style={{
71 | minHeight: (height - top - bottom) / 7,
72 | // backgroundColor: weekDayColors[dayjs(day).weekday()],
73 | // experimental_backgroundImage: `linear-gradient(to bottom, ${
74 | // weekDayColors[dayjs(day).weekday()]
75 | // }, rgba(0,0,0,0.1))`,
76 | }}>
77 |
78 |
79 |
80 | {dayjs(day).format(weekDayFormatter)}
81 |
82 |
83 |
84 | {dayjs(day).format(localFormatter)}
85 |
86 | {weather && (
87 |
93 |
98 |
99 | {weather?.temp.day.toFixed(1)}°C
100 |
101 |
102 | )}
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | );
112 | }
113 | );
114 |
--------------------------------------------------------------------------------