iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0
Mobile Development

從零開始學React Native系列 第 16

-【從零開始學React Native】15. 創建Todo Tracker——調整並新增路由

  • 分享至 

  • xImage
  •  

我們今天來使用react native的路由套件,我們使用React Navigation。

設置路由環境

安裝@react-navigation/native react-native-screens react-native-safe-area-context @react-navigation/bottom-tabs @react-navigation/native-stack。請參考安裝步驟

react-native-screens套件需要一個額外的設定步驟才能在 Android 裝置上正常運作,我們添加引入和方法。添加android.os.Bundle和onCreate,然後再重新執行build

// android\app\src\main\java\com\mytest\MainActivity.kt
package com.mytest

import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import android.os.Bundle

class MainActivity : ReactActivity() {

  /**
   * Returns the name of the main component registered from JavaScript. This is used to schedule
   * rendering of the component.
   */
  override fun getMainComponentName(): String = "MyTest"

  /**
   * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
   * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
   */
  override fun createReactActivityDelegate(): ReactActivityDelegate =
      DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(null)
  }
}

在App添加container

// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useColorScheme } from 'react-native';

import HomePage from './src/pages/home.page';
import AddTaskPage from './src/pages/add-task.page';
import StatisticsPage from './src/pages/statistics-page';
import UserPage from './src/pages/user.page';
import LoginPage from './src/pages/login.page';
import SettingsPage from './src/pages/settings.page';
import TaskDetailsPage from './src/pages/task-detail.page';

const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();

function HomeStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="HomeList" component={HomePage} options={{ title: '任務列表' }} />
      <Stack.Screen name="TaskDetails" component={TaskDetailsPage} options={{ title: '任務詳情' }} />
    </Stack.Navigator>
  );
}

function UserStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="UserProfile" component={UserPage} options={{ title: '用戶資料' }} />
      <Stack.Screen name="Settings" component={SettingsPage} options={{ title: '設置' }} />
      <Stack.Screen name="Login" component={LoginPage} options={{ title: '登入' }} />
    </Stack.Navigator>
  );
}

function MainTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeStack} options={{ title: '主頁' }} />
      <Tab.Screen name="AddTask" component={AddTaskPage} options={{ title: '新增' }} />
      <Tab.Screen name="Statistics" component={StatisticsPage} options={{ title: '統計' }} />
      <Tab.Screen name="User" component={UserStack} options={{ title: '用戶' }} />
    </Tab.Navigator>
  );
}

function App(): React.JSX.Element {
  const isDarkMode = useColorScheme() === 'dark';

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaProvider>
        <NavigationContainer>
          <MainTabs />
        </NavigationContainer>
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}

export default App;

調整頁面

接著調整其他的頁面

HomePage

  • Remove the isDarkMode prop.
  • Update the component to accept navigation prop
type HomePageProps = {
  navigation: any;
};
  • Replace onTaskSelect with navigation:
type HomePageProps = {
  navigation: any;
};

如下:

// src\pages\home.page.tsx
  navigation: any;
};

const HomePage: React.FC<HomePageProps> = ({navigation}) => {
  const [tasks, setTasks] = useAtom(tasksAtom);

  useEffect(() => {
    const loadedTasks = TaskService.getTasks();
    setTasks(loadedTasks);
  }, [setTasks]);

  const toggleTaskStatus = useCallback((id: string) => {
    const updatedTasks = tasks.map(task =>
      task.id === id ? { ...task, isDone: !task.isDone } : task
    );
    setTasks(updatedTasks);
    TaskService.saveTasks(updatedTasks);
  }, [tasks, setTasks]);

  const deleteTask = useCallback((id: string) => {
    const updatedTasks = tasks.filter(task => task.id !== id);
    setTasks(updatedTasks);
    TaskService.deleteTask(id);
  }, [tasks, setTasks]);

  const renderRightActions = useCallback((id: string) => {
    return (
      <TouchableOpacity
        style={styles.deleteButton}
        onPress={() => deleteTask(id)}
      >
        <Text style={styles.deleteButtonText}>Delete</Text>
      </TouchableOpacity>
    );
  }, [deleteTask]);

  const renderTask = useCallback(({ item }: { item: Task }) => (
    <Swipeable renderRightActions={() => renderRightActions(item.id)}>
      <TouchableOpacity onPress={() => navigation.navigate('TaskDetails', { taskId: item.id })}>
        <View
          style={[
            styles.taskItem,
            { backgroundColor: item.isDone ? '#E8F5E9' : 'transparent' },
          ]}
        >
          <TouchableOpacity onPress={() => toggleTaskStatus(item.id)}>
            <View style={styles.checkbox}>
              {item.isDone && <View style={styles.checkboxInner} />}
            </View>
          </TouchableOpacity>
          <View style={styles.taskContent}>
            <Text style={styles.taskTitle}>{item.title}</Text>
            <Text style={styles.taskDates}>
              {item.startDate} - {item.endDate}
            </Text>
            <Text style={styles.taskDescription}>{item.description}</Text>
            <View style={styles.taskTags}>
              {item.tags.map((tag, index) => (
                <View key={index} style={styles.tag}>
                  <Text style={styles.tagText}>{tag.title}</Text>
                </View>
              ))}
            </View>
          </View>
        </View>
      </TouchableOpacity>
    </Swipeable>
  ), [renderRightActions, navigation, toggleTaskStatus]);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>任務列表</Text>
      <TextInput
        style={styles.searchBar}
        placeholder="搜尋欄"
        placeholderTextColor={Colors.dark}
      />
      <FlatList
        style={styles.taskList}
        data={tasks}
        renderItem={renderTask}
        keyExtractor={item => item.id}
      />
    </View>
  );
};

AddTaskPage

  • Remove the isDarkMode prop.
  • Add navigation prop
type AddTaskPageProps = {
  navigation: any;
};
  • After successfully adding a task, navigate back
navigation.goBack();

如下:

// src\pages\add-task.page.tsx
type AddTaskPageProps = {
  navigation: any;
};

const AddTaskPage: React.FC<AddTaskPageProps> = ({ navigation }) => {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [startDate, setStartDate] = useState<Date | null>(null);
  const [endDate, setEndDate] = useState<Date | null>(null);
  const [openStartDate, setOpenStartDate] = useState(false);
  const [openEndDate, setOpenEndDate] = useState(false);
  const [tags, setTags] = useState<Tags[]>([]);
  const [currentTag, setCurrentTag] = useState('');

  const setTasks = useSetAtom(tasksAtom);

  const validateTask = (task: Task): string[] => {
    const errors: string[] = [];
    if (!task.title.trim()) {
      errors.push('Title is required');
    }
    if (task.startDate && task.endDate && new Date(task.startDate) > new Date(task.endDate)) {
      errors.push('End date should be after start date');
    }
    return errors;
  };

  const handleSubmit = () => {
    const newTask: Task = {
      id: Date.now().toString(),
      title,
      startDate: startDate ? startDate.toISOString().split('T')[0] : null,
      endDate: endDate ? endDate.toISOString().split('T')[0] : null,
      description,
      isDone: false,
      tags,
      subTasks: [],
    };

    const validationErrors = validateTask(newTask);

    if (validationErrors.length > 0) {
      Alert.alert('Validation Error', validationErrors.join('\n'));
      return;
    }

    TaskService.addTask(newTask);
    setTasks(prevTasks => [...prevTasks, newTask]);
    // Reset form fields
    setTitle('');
    setDescription('');
    setStartDate(null);
    setEndDate(null);
    setTags([]);
    Alert.alert('Success', 'Task added successfully');
    navigation.goBack();
  };

  const handleAddTag = () => {
    if (currentTag.trim()) {
      setTags([...tags, { title: currentTag.trim() }]);
      setCurrentTag('');
    }
  };

  const handleRemoveTag = (index: number) => {
    setTags(tags.filter((_, i) => i !== index));
  };

UserPage

修改一下路由名稱,如下

// src\pages\user.page.tsx
type UserPageProps = {
  navigation: any;
};

const UserPage: React.FC<UserPageProps> = ({ navigation }) => {
  const [user, setUser] = useAtom(userAtom);
  const [isLoggedIn, setIsLoggedIn] = useAtom(isLoggedInAtom);

  const handleLogout = () => {
    UserService.logout();
    setUser(null);
    setIsLoggedIn(false);
  };

  if (!isLoggedIn) {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>請先登入</Text>
        <TouchableOpacity
          style={styles.loginButton}
          onPress={() => navigation.navigate('Login')}
        >
          <Text style={styles.loginButtonText}>登入</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <ScrollView style={styles.container}>
      <View style={styles.profileHeader}>
        <Image
          source={{ uri: user?.avatar || 'https://via.placeholder.com/150' }}
          style={styles.avatar}
        />
        <Text style={styles.username}>{user?.username}</Text>
        <Text style={styles.email}>{user?.email}</Text>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>用戶信息</Text>
        <View style={styles.infoItem}>
          <Text style={styles.infoLabel}>ID:</Text>
          <Text style={styles.infoValue}>{user?.id}</Text>
        </View>
        <View style={styles.infoItem}>
          <Text style={styles.infoLabel}>創建日期:</Text>
          <Text style={styles.infoValue}>{user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}</Text>
        </View>
      </View>

      <TouchableOpacity
        style={styles.settingsButton}
        onPress={() => navigation.navigate('Settings')}
      >
        <Text style={styles.settingsButtonText}>設置</Text>
      </TouchableOpacity>

      <TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
        <Text style={styles.logoutButtonText}>登出</Text>
      </TouchableOpacity>
    </ScrollView>
  );
};

LoginPage

修改路由名稱

// src\pages\login.page.tsx
type LoginPageProps = {
  navigation: any;
};

const LoginPage: React.FC<LoginPageProps> = ({ navigation }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const setUser = useSetAtom(userAtom);
  const setIsLoggedIn = useSetAtom(isLoggedInAtom);

  const handleLogin = async () => {
    try {
      const user = await UserService.login(email, password);
      setUser(user);
      setIsLoggedIn(true);
      navigation.navigate('UserProfile');
    } catch (error) {
      Alert.alert('登錄失敗', '請檢查您的郵箱和密碼');
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>登錄</Text>
      <TextInput
        style={styles.input}
        placeholder="郵箱"
        value={email}
        onChangeText={setEmail}
        keyboardType="email-address"
        autoCapitalize="none"
      />
      <TextInput
        style={styles.input}
        placeholder="密碼"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <TouchableOpacity style={styles.loginButton} onPress={handleLogin}>
        <Text style={styles.loginButtonText}>登錄</Text>
      </TouchableOpacity>
    </View>
  );
};

TaskDetailsPage

添加新service

//  src\services\task.service.ts
export const TaskService = {
  saveTasks: (tasks: Task[]) => {
    storage.set(TASKS_KEY, JSON.stringify(tasks));
  },

  getTasks: (): Task[] => {
    const tasksJson = storage.getString(TASKS_KEY);
    return tasksJson ? JSON.parse(tasksJson) : [];
  },

  getTaskById: (taskId: string): Task | undefined => {
    const tasks = TaskService.getTasks();
    return tasks.find(task => task.id === taskId);
  },

如下:

// src\pages\task-detail.page.tsx
import React, { useCallback, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Switch, TextInput, ScrollView, useColorScheme } from 'react-native';
import { useAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';
import { Task } from '../types';
import { TaskService } from '../services/task.service';
import { RouteProp } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';

type RootStackParamList = {
  HomeList: undefined;
  TaskDetails: { taskId: string };
};

type TaskDetailsPageProps = {
  navigation: NativeStackScreenProps<RootStackParamList, 'TaskDetails'>;
  route: RouteProp<RootStackParamList, 'TaskDetails'>;
};

const TaskDetailsPage: React.FC<TaskDetailsPageProps> = ({
  navigation,
  route,
}) => {
  const isDarkMode = useColorScheme() === 'dark';
  const { taskId } = route.params;
  const [isEditing, setIsEditing] = useState(false);
  const [editedTask, setEditedTask] = useState<Task | null>(null);
  const [, setTasks] = useAtom(tasksAtom);

  useEffect(() => {
    const loadTask = async () => {
      const task = await TaskService.getTaskById(taskId);
      if (task) {
        setEditedTask(task);
      } else {
        // Handle case when task is not found
        navigation.goBack();
      }
    };
    loadTask();
  }, [taskId, navigation]);

  const handleEdit = useCallback(() => {
    setIsEditing(true);
  }, []);

  const handleSave = useCallback(async () => {
    if (editedTask) {
      await TaskService.updateTask(editedTask);
      setTasks(await TaskService.getTasks());
      setIsEditing(false);
    }
  }, [editedTask, setTasks]);

  const handleDelete = useCallback(async () => {
    if (editedTask) {
      await TaskService.deleteTask(editedTask.id);
      setTasks(await TaskService.getTasks());
      navigation.goBack();
    }
  }, [editedTask, setTasks, navigation]);

  const toggleCompletion = useCallback(async () => {
    if (editedTask) {
      const updatedTask = { ...editedTask, isDone: !editedTask.isDone };
      setEditedTask(updatedTask);
      await TaskService.updateTask(updatedTask);
      setTasks(await TaskService.getTasks());
    }
  }, [editedTask, setTasks]);

  const handleInputChange = useCallback((field: keyof Task, value: string) => {
    setEditedTask(prev => prev ? { ...prev, [field]: value } : null);
  }, []);

  const addTag = useCallback(() => {
    setEditedTask(prev => prev ? {
      ...prev,
      tags: [...prev.tags, { title: '新標籤' }],
    } : null);
  }, []);

  const removeTag = useCallback((index: number) => {
    setEditedTask(prev => prev ? {
      ...prev,
      tags: prev.tags.filter((_, i) => i !== index),
    } : null);
  }, []);

  const updateTag = useCallback((index: number, newTitle: string) => {
    setEditedTask(prev => prev ? {
      ...prev,
      tags: prev.tags.map((tag, i) => i === index ? { ...tag, title: newTitle } : tag),
    } : null);
  }, []);

  if (!editedTask) {
    return null; // Or a loading indicator
  }

  return (
    <ScrollView style={[styles.container, { backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff' }]}>
      <TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
        <Text style={[styles.backButtonText, { color: isDarkMode ? '#ffffff' : '#007AFF' }]}>返回</Text>
      </TouchableOpacity>

      {isEditing ? (
        <>
          <TextInput
            style={[styles.input, { color: isDarkMode ? '#ffffff' : '#000000' }]}
            value={editedTask.title}
            onChangeText={(value) => handleInputChange('title', value)}
            placeholder="任務標題"
            placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
          />
          <TextInput
            style={[styles.input, styles.descriptionInput, { color: isDarkMode ? '#ffffff' : '#000000' }]}
            value={editedTask.description}
            onChangeText={(value) => handleInputChange('description', value)}
            placeholder="任務描述"
            placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
            multiline
          />
          <TextInput
            style={[styles.input, { color: isDarkMode ? '#ffffff' : '#000000' }]}
            value={editedTask?.startDate ?? ''}
            onChangeText={(value) => handleInputChange('startDate', value)}
            placeholder="開始日期 (YYYY-MM-DD)"
            placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
          />
          <TextInput
            style={[styles.input, { color: isDarkMode ? '#ffffff' : '#000000' }]}
            value={editedTask?.endDate ?? ''}
            onChangeText={(value) => handleInputChange('endDate', value)}
            placeholder="結束日期 (YYYY-MM-DD)"
            placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
          />
        </>
      ) : (
        <>
          <Text style={[styles.title, { color: isDarkMode ? '#ffffff' : '#000000' }]}>{editedTask.title}</Text>
          <Text style={[styles.description, { color: isDarkMode ? '#dddddd' : '#333333' }]}>{editedTask.description}</Text>
          <Text style={[styles.info, { color: isDarkMode ? '#bbbbbb' : '#666666' }]}>開始日期: {editedTask.startDate}</Text>
          <Text style={[styles.info, { color: isDarkMode ? '#bbbbbb' : '#666666' }]}>結束日期: {editedTask.endDate}</Text>
        </>
      )}

      <View style={styles.tagsContainer}>
        {editedTask.tags.map((tag, index) => (
          <View key={index} style={[styles.tag, { backgroundColor: isDarkMode ? '#333333' : '#E0E0E0' }]}>
            {isEditing ? (
              <>
                <TextInput
                  style={[styles.tagInput, { color: isDarkMode ? '#ffffff' : '#000000' }]}
                  value={tag.title}
                  onChangeText={(value) => updateTag(index, value)}
                />
                <TouchableOpacity onPress={() => removeTag(index)}>
                  <Text style={[styles.removeTagText, { color: isDarkMode ? '#ff6b6b' : '#FF3B30' }]}>X</Text>
                </TouchableOpacity>
              </>
            ) : (
              <Text style={[styles.tagText, { color: isDarkMode ? '#ffffff' : '#000000' }]}>{tag.title}</Text>
            )}
          </View>
        ))}
        {isEditing && (
          <TouchableOpacity style={[styles.addTagButton, { backgroundColor: isDarkMode ? '#4a4a4a' : '#E0E0E0' }]} onPress={addTag}>
            <Text style={[styles.addTagButtonText, { color: isDarkMode ? '#ffffff' : '#007AFF' }]}>+ 添加標籤</Text>
          </TouchableOpacity>
        )}
      </View>

      <View style={styles.actionContainer}>
        {isEditing ? (
          <TouchableOpacity style={[styles.actionButton, { backgroundColor: isDarkMode ? '#4a4a4a' : '#007AFF' }]} onPress={handleSave}>
            <Text style={styles.actionButtonText}>保存</Text>
          </TouchableOpacity>
        ) : (
          <TouchableOpacity style={[styles.actionButton, { backgroundColor: isDarkMode ? '#4a4a4a' : '#007AFF' }]} onPress={handleEdit}>
            <Text style={styles.actionButtonText}>編輯</Text>
          </TouchableOpacity>
        )}
        <TouchableOpacity style={[styles.actionButton, styles.deleteButton, { backgroundColor: isDarkMode ? '#8b0000' : '#FF3B30' }]} onPress={handleDelete}>
          <Text style={styles.actionButtonText}>刪除</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.completionContainer}>
        <Text style={[styles.completionText, { color: isDarkMode ? '#ffffff' : '#000000' }]}>任務完成</Text>
        <Switch
          value={editedTask.isDone}
          onValueChange={toggleCompletion}
          trackColor={{ false: isDarkMode ? '#767577' : '#E0E0E0', true: isDarkMode ? '#81b0ff' : '#007AFF' }}
          thumbColor={editedTask.isDone ? (isDarkMode ? '#f5dd4b' : '#f4f3f4') : '#f4f3f4'}
        />
      </View>
    </ScrollView>
  );
};

SettingsPage

如下

// src\pages\settings.page.tsx
import React from 'react';
import { View, Text, StyleSheet, Switch, TouchableOpacity, ScrollView } from 'react-native';
import { useAtom } from 'jotai';
import { userAtom } from '../stores/atoms';

type SettingsPageProps = {
  navigation: any;
};

const SettingsPage: React.FC<SettingsPageProps> = ({ navigation }) => {
  const [user] = useAtom(userAtom);
  const [notifications, setNotifications] = React.useState(true);
  const [autoSync, setAutoSync] = React.useState(true);

  const handleLogout = () => {
    console.log('User logged out');
    // navigation.navigate('Login');
  };

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>設置</Text>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>應用設置</Text>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>深色模式</Text>
          {/* <Switch value={isDarkMode} onValueChange={toggleDarkMode} /> */}
        </View>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>通知</Text>
          <Switch value={notifications} onValueChange={setNotifications} />
        </View>
        <View style={styles.settingItem}>
          <Text style={styles.settingLabel}>自動同步</Text>
          <Switch value={autoSync} onValueChange={setAutoSync} />
        </View>
      </View>

      <TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
        <Text style={styles.logoutButtonText}>登出</Text>
      </TouchableOpacity>

      <View style={styles.aboutSection}>
        <Text style={styles.sectionTitle}>關於應用</Text>
        <Text style={styles.aboutText}>版本: 1.0.0</Text>
        <Text style={styles.aboutText}>開發者: Your Company Name</Text>
      </View>
    </ScrollView>
  );
};

StatisticsPage

修改如下

// src\pages\statistics-page.tsx
import React, { useRef, useEffect, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, useColorScheme } from 'react-native';
import * as echarts from 'echarts/core';
import { SkiaChart, SVGRenderer } from '@wuba/react-native-echarts';
import { GaugeChart, PieChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
import { useAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';

echarts.use([SVGRenderer, GaugeChart, PieChart, BarChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent]);

type StatisticsPageProps = {};

const { width } = Dimensions.get('window');
const chartWidth = width - 40; // Assuming 20px padding on each side

const StatisticsPage: React.FC<StatisticsPageProps> = () => {
  const completionRateChartRef = useRef<any>(null);
  const taskDistributionChartRef = useRef<any>(null);
  const taskTrendChartRef = useRef<any>(null);
  const isDarkMode = useColorScheme() === 'dark';
  const [tasks] = useAtom(tasksAtom);

  const taskStats = useMemo(() => {
    const totalTasks = tasks.length;
    const completedTasks = tasks.filter(task => task.isDone).length;
    const completionRate = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;

    const tagCounts = tasks.reduce((acc, task) => {
      task.tags.forEach(tag => {
        acc[tag.title] = (acc[tag.title] || 0) + 1;
      });
      return acc;
    }, {} as Record<string, number>);

    const tasksByMonth = tasks.reduce((acc, task) => {
      const month = task?.startDate ? new Date(task.startDate).getMonth() : new Date().getMonth();
      acc[month] = (acc[month] || 0) + 1;
      return acc;
    }, {} as Record<number, number>);

    return { totalTasks, completedTasks, completionRate, tagCounts, tasksByMonth };
  }, [tasks]);
// ...

心得

今天改成使用react navigator來進行路由導航。明天開始創接簡易的API。


上一篇
【從零開始學React Native】14. 創建Todo Tracker——存儲資料在本地(二)
下一篇
【從零開始學React Native】16. 什麼是BAAS
系列文
從零開始學React Native20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言