iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
Mobile Development

從零開始學React Native系列 第 11

【從零開始學React Native】10. 創建Todo Tracker——調整任務細節和統計頁面

  • 分享至 

  • xImage
  •  

調整任務細節頁面

首先我們先調整我們的任務細節頁面

// src\pages\task-detail.page.tsx
import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Switch, TextInput, ScrollView } from 'react-native';
import { Task } from '../types';

type TaskDetailsPageProps = {
  isDarkMode: boolean;
  task: Task;
  onBack: () => void;
  onTaskUpdate: (updatedTask: Task) => void;
  onTaskDelete: (taskId: string) => void;
};

const TaskDetailsPage: React.FC<TaskDetailsPageProps> = ({
  isDarkMode,
  task,
  onBack,
  onTaskUpdate,
  onTaskDelete,
}) => {
  const [isEditing, setIsEditing] = useState(false);
  const [editedTask, setEditedTask] = useState(task);

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

  const handleSave = useCallback(() => {
    onTaskUpdate(editedTask);
    setIsEditing(false);
  }, [editedTask, onTaskUpdate]);

  const handleDelete = useCallback(() => {
    onTaskDelete(task.id);
  }, [onTaskDelete, task.id]);

  const toggleCompletion = useCallback(() => {
    const updatedTask = { ...editedTask, isDone: !editedTask.isDone };
    setEditedTask(updatedTask);
    onTaskUpdate(updatedTask);
  }, [editedTask, onTaskUpdate]);

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

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

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

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

  return (
    <ScrollView style={[styles.container, { backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff' }]}>
      <TouchableOpacity style={styles.backButton} onPress={onBack}>
        <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' }]}>{task.title}</Text>
          <Text style={[styles.description, { color: isDarkMode ? '#dddddd' : '#333333' }]}>{task.description}</Text>
          <Text style={[styles.info, { color: isDarkMode ? '#bbbbbb' : '#666666' }]}>開始日期: {task.startDate}</Text>
          <Text style={[styles.info, { color: isDarkMode ? '#bbbbbb' : '#666666' }]}>結束日期: {task.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>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  backButton: {
    marginBottom: 20,
  },
  backButtonText: {
    fontSize: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  description: {
    fontSize: 16,
    marginBottom: 20,
  },
  info: {
    fontSize: 14,
    marginBottom: 5,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 5,
    padding: 10,
    marginBottom: 10,
    fontSize: 16,
  },
  descriptionInput: {
    height: 100,
    textAlignVertical: 'top',
  },
  tagsContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginTop: 10,
    marginBottom: 20,
  },
  tag: {
    borderRadius: 20,
    paddingVertical: 5,
    paddingHorizontal: 10,
    marginRight: 5,
    marginBottom: 5,
    flexDirection: 'row',
    alignItems: 'center',
  },
  tagText: {
    fontSize: 12,
  },
  tagInput: {
    fontSize: 12,
    padding: 0,
    height: 20,
    width: 80,
  },
  removeTagText: {
    fontSize: 12,
    marginLeft: 5,
  },
  addTagButton: {
    borderRadius: 20,
    paddingVertical: 5,
    paddingHorizontal: 10,
  },
  addTagButtonText: {
    fontSize: 12,
  },
  actionContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginTop: 20,
  },
  actionButton: {
    padding: 10,
    borderRadius: 5,
    flex: 1,
    marginRight: 10,
  },
  deleteButton: {
    marginRight: 0,
  },
  actionButtonText: {
    color: 'white',
    textAlign: 'center',
    fontWeight: 'bold',
  },
  completionContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginTop: 20,
  },
  completionText: {
    fontSize: 16,
  },
});

export default React.memo(TaskDetailsPage);

調整統計頁面

// src\pages\statistics-page.tsx
import React, { useRef, useEffect, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions } 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 { Task } from '../types';

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

type StatisticsPageProps = {
  isDarkMode: boolean;
  tasks: Task[];
};

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

const StatisticsPage: React.FC<StatisticsPageProps> = ({ isDarkMode, tasks }) => {
  const completionRateChartRef = useRef<any>(null);
  const taskDistributionChartRef = useRef<any>(null);
  const taskTrendChartRef = useRef<any>(null);

  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]);

  useEffect(() => {
    const theme = isDarkMode ? 'dark' : 'light';
    const textColor = isDarkMode ? '#ffffff' : '#000000';

    const completionRateOption = {
      series: [
        {
          type: 'gauge',
          startAngle: 90,
          endAngle: -270,
          pointer: { show: false },
          progress: {
            show: true,
            overlap: false,
            roundCap: true,
            clip: false,
            itemStyle: {
              borderWidth: 1,
              borderColor: isDarkMode ? '#ffffff' : '#464646',
            },
          },
          axisLine: {
            lineStyle: {
              width: 30,
            },
          },
          splitLine: { show: false },
          axisTick: { show: false },
          axisLabel: { show: false },
          data: [{
            value: taskStats.completionRate,
            name: '完成率',
            title: { offsetCenter: ['0%', '-20%'] },
            detail: { offsetCenter: ['0%', '0%'] },
          }],
          title: {
            fontSize: 14,
            color: textColor,
          },
          detail: {
            width: 50,
            height: 14,
            fontSize: 24,
            color: textColor,
            borderColor: textColor,
            borderRadius: 20,
            borderWidth: 1,
            formatter: '{value.toFixed(2)}%',
          },
        },
      ],
    };

    const taskDistributionOption = {
      tooltip: {
        trigger: 'item',
      },
      legend: {
        orient: 'vertical',
        left: 'left',
        textStyle: {
          color: textColor,
        },
      },
      series: [
        {
          name: '任務分佈',
          type: 'pie',
          radius: '50%',
          data: Object.entries(taskStats.tagCounts).map(([name, value]) => ({ name, value })),
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: 'rgba(0, 0, 0, 0.5)',
            },
          },
          label: {
            color: textColor,
          },
        },
      ],
    };

    const taskTrendOption = {
      title: {
        text: '月度任務趨勢',
        left: 'center',
        textStyle: {
          color: textColor,
        },
      },
      xAxis: {
        type: 'category',
        data: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
        axisLabel: {
          color: textColor,
        },
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          color: textColor,
        },
      },
      series: [{
        data: Array.from({ length: 12 }, (_, i) => taskStats.tasksByMonth[i] || 0),
        type: 'bar',
        itemStyle: {
          color: isDarkMode ? '#8e9aaf' : '#4a4e69',
        },
      }],
    };

    if (completionRateChartRef.current) {
      const chart = echarts.init(completionRateChartRef.current, theme, {
        renderer: 'svg',
        width: chartWidth,
        height: 250,
      });
      chart.setOption(completionRateOption);
    }

    if (taskDistributionChartRef.current) {
      const chart = echarts.init(taskDistributionChartRef.current, theme, {
        renderer: 'svg',
        width: chartWidth,
        height: 300,
      });
      chart.setOption(taskDistributionOption);
    }

    if (taskTrendChartRef.current) {
      const chart = echarts.init(taskTrendChartRef.current, theme, {
        renderer: 'svg',
        width: chartWidth,
        height: 250,
      });
      chart.setOption(taskTrendOption);
    }

    return () => {
      completionRateChartRef.current?.dispose();
      taskDistributionChartRef.current?.dispose();
      taskTrendChartRef.current?.dispose();
    };
  }, [isDarkMode, taskStats]);

  return (
    <ScrollView style={[styles.container, { backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff' }]}>
      <Text style={[styles.title, { color: isDarkMode ? '#ffffff' : '#000000' }]}>統計資訊</Text>

      <View style={styles.statsContainer}>
        <Text style={[styles.statsText, { color: isDarkMode ? '#dddddd' : '#333333' }]}>總任務數: {taskStats.totalTasks}</Text>
        <Text style={[styles.statsText, { color: isDarkMode ? '#dddddd' : '#333333' }]}>已完成任務: {taskStats.completedTasks}</Text>
        <Text style={[styles.statsText, { color: isDarkMode ? '#dddddd' : '#333333' }]}>完成率: {taskStats.completionRate.toFixed(2)}%</Text>
      </View>

      <View style={styles.chartContainer}>
        <Text style={[styles.chartTitle, { color: isDarkMode ? '#ffffff' : '#000000' }]}>任務完成率</Text>
        <SkiaChart ref={completionRateChartRef} />
      </View>

      <View style={styles.chartContainer}>
        <Text style={[styles.chartTitle, { color: isDarkMode ? '#ffffff' : '#000000' }]}>任務分佈</Text>
        <SkiaChart ref={taskDistributionChartRef} />
      </View>

      <View style={styles.chartContainer}>
        <Text style={[styles.chartTitle, { color: isDarkMode ? '#ffffff' : '#000000' }]}>任務趨勢</Text>
        <SkiaChart ref={taskTrendChartRef} />
      </View>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  statsContainer: {
    marginBottom: 20,
    backgroundColor: 'rgba(200, 200, 200, 0.1)',
    padding: 15,
    borderRadius: 10,
  },
  statsText: {
    fontSize: 16,
    marginBottom: 5,
  },
  chartContainer: {
    marginBottom: 30,
    alignItems: 'center',
    backgroundColor: 'rgba(200, 200, 200, 0.1)',
    padding: 15,
    borderRadius: 10,
  },
  chartTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
});

export default React.memo(StatisticsPage);

調整App和Home頁面

現在我們把生成資料的部分移到App上,並調整Home頁面

// App.tsx
import React, { useEffect, 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';
import TaskDetailsPage from './src/pages/task-detail.page';
import { Tags, Task } from './src/types';

function MainContent() {
  const [isDarkMode, setIsDarkMode] = useState(useColorScheme() === 'dark');
  const [currentPage, setCurrentPage] = useState('home');
  const [tasks, setTasks] = useState<Task[]>([]);
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);

  useEffect(() => {
    // Generate random tasks when the app starts
    setTasks(generateRandomTasks(10));
  }, []);

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
    flex: 1,
  };

  const toggleDarkMode = () => {
    setIsDarkMode(!isDarkMode);
  };

  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: '娛樂' },
    ];

    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 renderPage = () => {
    switch (currentPage) {
      case 'home':
        return (
          <HomePage
            isDarkMode={isDarkMode}
            tasks={tasks}
            setTasks={setTasks}
            onTaskSelect={(task) => {
              setSelectedTask(task);
              setCurrentPage('taskDetails');
            }}
          />
        );
      case 'add':
        return <AddTaskPage isDarkMode={isDarkMode} />;
      case 'stats':
        return <StatisticsPage isDarkMode={isDarkMode} tasks={tasks} />;
      case 'settings':
        return <SettingsPage isDarkMode={isDarkMode} toggleDarkMode={toggleDarkMode} />;
      case 'taskDetails':
        return selectedTask ? (
          <TaskDetailsPage
            isDarkMode={isDarkMode}
            task={selectedTask}
            onBack={() => setCurrentPage('home')}
            onTaskUpdate={(updatedTask) => {
              setTasks(prevTasks =>
                prevTasks.map(task =>
                  task.id === updatedTask.id ? updatedTask : task
                )
              );
              setCurrentPage('home');
            }}
            onTaskDelete={(taskId) => {
              setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId));
              setCurrentPage('home');
            }}
          />
        ) : null;
      default:
        return <HomePage isDarkMode={isDarkMode} tasks={tasks} setTasks={setTasks} />;
    }
  };

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaView style={backgroundStyle}>
        <StatusBar
          barStyle={isDarkMode ? 'light-content' : 'dark-content'}
          backgroundColor={backgroundStyle.backgroundColor}
        />
        <View style={styles.topArea}>
          <Text style={styles.topText}>我的應用</Text>
        </View>
        <View style={styles.container}>
          {renderPage()}
        </View>
        <View style={styles.bottomNav}>
          <TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('home')}>
            <Text style={styles.navText}>主頁</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('add')}>
            <Text style={styles.navText}>新增</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('stats')}>
            <Text style={styles.navText}>統計</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.navItem} onPress={() => setCurrentPage('settings')}>
            <Text style={styles.navText}>設置</Text>
          </TouchableOpacity>
        </View>
      </SafeAreaView>
    </GestureHandlerRootView>
  );
}

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

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  topArea: {
    height: 50,
    justifyContent: 'center',
    alignItems: 'center',
    borderBottomWidth: 1,
    borderBottomColor: '#ccc',
    backgroundColor: '#f8f8f8',
  },
  topText: {
    fontSize: 18,
    fontWeight: 'bold',
  },
  bottomNav: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center',
    height: 50,
    borderTopWidth: 1,
    borderTopColor: '#ccc',
    backgroundColor: '#f8f8f8',
  },
  navItem: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    height: '100%',
  },
  navText: {
    fontSize: 12,
  },
});

export default App;


在調整我們的HomePage。

// 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';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 10,
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  searchBar: {
    height: 40,
    borderColor: 'gray',
    borderWidth: 1,
    borderRadius: 5,
    paddingHorizontal: 10,
    marginBottom: 10,
  },
  taskList: {
    flex: 1,
  },
  taskItem: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#ccc',
  },
  checkbox: {
    width: 24,
    height: 24,
    borderWidth: 2,
    borderColor: '#007AFF',
    borderRadius: 12,
    marginRight: 10,
    justifyContent: 'center',
    alignItems: 'center',
  },
  checkboxInner: {
    width: 12,
    height: 12,
    borderRadius: 6,
    backgroundColor: '#007AFF',
  },
  taskContent: {
    flex: 1,
  },
  taskTitle: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  taskDates: {
    fontSize: 12,
    color: '#666',
  },
  taskDescription: {
    fontSize: 14,
    marginTop: 5,
  },
  taskTags: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginTop: 5,
  },
  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;
  tasks: Task[];
  setTasks: React.Dispatch<React.SetStateAction<Task[]>>;
  onTaskSelect?: (task: Task) => void;
};

const HomePage: React.FC<HomePageProps> = ({ isDarkMode, tasks, setTasks, onTaskSelect }) => {
  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)}>
      <TouchableOpacity onPress={() => onTaskSelect(item)}>
        <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>
  ), [toggleTaskStatus, renderRightActions, onTaskSelect]);

  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;

現在我們已經調整了目前的統計和任務詳細頁面。明天接著完成其他部分。


上一篇
【從零開始學React Native】9. 創建Todo Tracker——添加狀態管理和調整新增頁面
下一篇
【從零開始學React Native】11. 創建Todo Tracker——調整新建頁面
系列文
從零開始學React Native20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言