iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0
Mobile Development

從零開始學React Native系列 第 10

【從零開始學React Native】9. 創建Todo Tracker——添加狀態管理和調整新增頁面

  • 分享至 

  • xImage
  •  

今天我們來添加一些套件來管理狀態,並且調整我們的頁面。

安裝套件

現在我們安裝一些套件,jotai zod react-native-gesture-handler react-native-date-picker

!!!!這裡記得安裝完要rebuild,也就是跑yarn android或ios來重新構建(由於react-native-date-picker)。

並且套件如下:

  • jotai: 輕量級的狀態管理庫,用於管理應用程序的狀態。
  • zod: TypeScript優先的模式聲明和驗證庫,用於數據驗證。
  • react-native-gesture-handler: 提供原生觸摸和手勢系統的替代品,用於實現滑動刪除等功能。

創建狀態管理

接下來創建我們的狀態管理

創建atom

// src\stores\atoms.ts
import { atom } from 'jotai';
import { Task } from '../types';

export const tasksAtom = atom<Task[]>([]);

設置App結構

在App添加View。

import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, StatusBar, useColorScheme } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { Colors } from 'react-native/Libraries/NewAppScreen';
import HomePage from './src/pages/home.page';
import AddTaskPage from './src/pages/add-task.page';
import StatisticsPage from './src/pages/statistics-page';
import SettingsPage from './src/pages/settings.page';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

// ...

function App(): React.JSX.Element {
  return (
    <SafeAreaProvider>
      <MainContent />
    </SafeAreaProvider>
  );
}

// ...

export default App;

調整首頁(HomePage)

接下來調整一下我們的首頁,滑動刪除功能。並使用 Jotai 來管理任務狀態

// src\pages\home.page.tsx
import React, { useCallback } from 'react';
import { View, Text, TextInput, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
import { Colors } from 'react-native/Libraries/NewAppScreen';
import { Task, Tags } from '../types';
import { Swipeable } from 'react-native-gesture-handler';
import { useAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 10,
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  // ...
  tag: {
    backgroundColor: '#E0E0E0',
    borderRadius: 10,
    padding: 5,
    marginRight: 5,
    marginBottom: 5,
  },
  tagText: {
    fontSize: 12,
  },
  deleteButton: {
    backgroundColor: 'red',
    justifyContent: 'center',
    alignItems: 'center',
    width: 80,
    height: '100%',
  },
  deleteButtonText: {
    color: 'white',
    fontWeight: 'bold',
  },
});

type HomePageProps = {
  isDarkMode?: boolean;
};

const generateRandomTasks = (count: number): Task[] => {
  const generateRandomDate = (start: Date, end: Date) => {
    return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString().split('T')[0];
  };

  const possibleTags: Tags[] = [
    { title: '工作' },
    { title: '個人' },
    { title: '學習' },
    { title: '娛樂' },
  ];

  const tasks: Task[] = [];
  for (let i = 0; i < count; i++) {
    const startDate = generateRandomDate(new Date(), new Date(Date.now() + 30 * 24 * 60 * 60 * 1000));
    const endDate = generateRandomDate(new Date(startDate), new Date(Date.now() + 60 * 24 * 60 * 60 * 1000));
    tasks.push({
      id: `task-${i + 1}`,
      title: `任務 ${i + 1}`,
      startDate,
      endDate,
      description: `隨機生成的第 ${i + 1} 筆內容。`,
      isDone: Math.random() < 0.3,
      tags: possibleTags.filter(() => Math.random() < 0.3),
      subTasks: [],
    });
  }
  return tasks;
};

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

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

  const deleteTask = useCallback((id: string) => {
    setTasks(prevTasks => prevTasks.filter(task => task.id !== id));
  }, [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)}>
      <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>
    </Swipeable>
  ), [toggleTaskStatus, renderRightActions]);

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

export default HomePage;

挑整添加任務頁面(AddTaskPage)

現在調整一下add task頁面

// src\pages\add-task.page.tsx
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Alert } from 'react-native';
import { useSetAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';
import { Task, Tags } from '../types/index';
import DatePicker from 'react-native-date-picker';

type AddTaskPageProps = {
  isDarkMode: boolean;
};

const AddTaskPage: React.FC<AddTaskPageProps> = ({ isDarkMode }) => {
  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;
    }

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

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

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

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>新增任務畫面</Text>

      <TextInput
        style={styles.input}
        placeholder="任務標題"
        value={title}
        onChangeText={setTitle}
      />

      <TextInput
        style={[styles.input, styles.multilineInput]}
        placeholder="任務描述"
        multiline
        numberOfLines={3}
        value={description}
        onChangeText={setDescription}
      />

      <TouchableOpacity style={styles.input} onPress={() => setOpenStartDate(true)}>
        <Text>{startDate ? startDate.toLocaleDateString() : '選擇開始日期'}</Text>
      </TouchableOpacity>

      <DatePicker
        modal
        open={openStartDate}
        date={startDate || new Date()}
        onConfirm={(date) => {
          setOpenStartDate(false);
          setStartDate(date);
        }}
        onCancel={() => {
          setOpenStartDate(false);
        }}
        mode="date"
      />

      <TouchableOpacity style={styles.input} onPress={() => setOpenEndDate(true)}>
        <Text>{endDate ? endDate.toLocaleDateString() : '選擇結束日期'}</Text>
      </TouchableOpacity>

      <DatePicker
        modal
        open={openEndDate}
        date={endDate || new Date()}
        onConfirm={(date) => {
          setOpenEndDate(false);
          setEndDate(date);
        }}
        onCancel={() => {
          setOpenEndDate(false);
        }}
        mode="date"
      />

      <View style={styles.tagInputContainer}>
        <TextInput
          style={styles.tagInput}
          placeholder="輸入標籤"
          value={currentTag}
          onChangeText={setCurrentTag}
          onSubmitEditing={handleAddTag}
        />
        <TouchableOpacity style={styles.addTagButton} onPress={handleAddTag}>
          <Text style={styles.addTagButtonText}>添加</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.tagsContainer}>
        {tags.map((tag, index) => (
          <TouchableOpacity
            key={index}
            style={styles.tag}
            onPress={() => handleRemoveTag(index)}
          >
            <Text style={styles.tagText}>{tag.title}</Text>
          </TouchableOpacity>
        ))}
      </View>

      <TouchableOpacity style={styles.submitButton} onPress={handleSubmit}>
        <Text style={styles.submitButtonText}>新增任務</Text>
      </TouchableOpacity>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 10,
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  input: {
    height: 40,
    borderColor: 'gray',
    borderWidth: 1,
    borderRadius: 5,
    paddingHorizontal: 10,
    marginBottom: 10,
    justifyContent: 'center',
  },
  multilineInput: {
    height: 80,
  },
  tagInputContainer: {
    flexDirection: 'row',
    marginBottom: 10,
  },
  tagInput: {
    flex: 1,
    height: 40,
    borderColor: 'gray',
    borderWidth: 1,
    borderRadius: 5,
    paddingHorizontal: 10,
    marginRight: 10,
  },
  addTagButton: {
    backgroundColor: '#007AFF',
    padding: 10,
    borderRadius: 5,
    justifyContent: 'center',
  },
  addTagButtonText: {
    color: 'white',
    fontWeight: 'bold',
  },
  tagsContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginBottom: 10,
  },
  tag: {
    backgroundColor: '#E0E0E0',
    borderRadius: 15,
    padding: 5,
    paddingHorizontal: 10,
    margin: 2,
  },
  tagText: {
    fontSize: 12,
  },
  submitButton: {
    backgroundColor: '#007AFF',
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
    marginTop: 10,
  },
  submitButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default AddTaskPage;

今天調整了首頁和新增頁面,並使用了jotai作為我們的狀態管理。明天會接續將其他部分完善。


上一篇
【從零開始學React Native】8. 創建Todo Tracker——改進首頁頁面
下一篇
【從零開始學React Native】10. 創建Todo Tracker——調整任務細節和統計頁面
系列文
從零開始學React Native20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言