iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
Mobile Development

從零開始學React Native系列 第 15

【從零開始學React Native】14. 創建Todo Tracker——存儲資料在本地(二)

  • 分享至 

  • xImage
  •  

我們今天來創建存儲使用者在本地。

創建狀態和service

創建user service,並且登入部份我們先使用mock方法來模擬

// src\services\user.service.ts

import { MMKV } from 'react-native-mmkv';
import { User } from '../types';

const secureStorage = new MMKV({
  id: 'secure-user-storage',
  encryptionKey: 'secret-key',
});

const USER_KEY = 'user';
const IS_LOGGED_IN_KEY = 'isLoggedIn';
const TOKEN_KEY = 'token';

const mockApiCall = <T>(data: T): Promise<T> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(data);
    }, 1000);
  });
};

export const UserService = {
  saveUser: (user: User) => {
    secureStorage.set(USER_KEY, JSON.stringify(user));
  },

  getUser: (): User | null => {
    const userJson = secureStorage.getString(USER_KEY);
    return userJson ? JSON.parse(userJson) : null;
  },

  setLoggedIn: (isLoggedIn: boolean) => {
    secureStorage.set(IS_LOGGED_IN_KEY, isLoggedIn);
  },

  isLoggedIn: (): boolean => {
    return secureStorage.getBoolean(IS_LOGGED_IN_KEY) || false;
  },

  saveToken: (token: string) => {
    secureStorage.set(TOKEN_KEY, token);
  },

  getToken: (): string | null => {
    return secureStorage.getString(TOKEN_KEY) || null;
  },

  clearUserData: () => {
    secureStorage.delete(USER_KEY);
    secureStorage.delete(IS_LOGGED_IN_KEY);
    secureStorage.delete(TOKEN_KEY);
  },

  login: async (email: string, password: string): Promise<User> => {
    const user: User = await mockApiCall({
      id: '1',
      username: email.split('@')[0],
      email: email,
      createdAt: new Date().toISOString(),
    });

    const token = 'mock-jwt-token';

    UserService.saveUser(user);
    UserService.setLoggedIn(true);
    UserService.saveToken(token);

    return user;
  },

  logout: () => {
    UserService.clearUserData();
  },
};

修改並添加user到我們的atom

// src\stores\atoms.ts
import { atom } from 'jotai';
import { Task, User } from '../types';
import { TaskService } from '../services/task.service';
import { UserService } from '../services/user.service';

export const userAtom = atom<User | null>(UserService.getUser());
export const isLoggedInAtom = atom<boolean>(UserService.isLoggedIn());
export const tokenAtom = atom<string | null>(UserService.getToken());
export const tasksAtom = atom<Task[]>(TaskService.getTasks());

創建登入頁面

接下來我們創建Login Page

// src\pages\login.page.tsx

import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { useSetAtom } from 'jotai';
import { userAtom, isLoggedInAtom } from '../stores/atoms';
import { UserService } from '../services/user.service';

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('user');
    } 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>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  input: {
    width: '100%',
    height: 40,
    borderColor: 'gray',
    borderWidth: 1,
    borderRadius: 5,
    marginBottom: 10,
    paddingHorizontal: 10,
  },
  loginButton: {
    backgroundColor: '#007AFF',
    padding: 10,
    borderRadius: 5,
    width: '100%',
    alignItems: 'center',
  },
  loginButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default LoginPage;

並且在App上添加該page到nav,並稍微修正一下render page的方法。

// App.tsx

// ...
import UserPage from './src/pages/user.page';
import LoginPage from './src/pages/login.page';

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);
  const [user, setUser] = useAtom(userAtom);

  useEffect(() => {
    // 生成隨機任務當應用啟動時
    setTasks(generateRandomTasks(10));
    // 模擬用戶登入
    setUser({
      id: '1',
      username: 'JohnDoe',
      email: 'johndoe@example.com',
      createdAt: new Date().toISOString(),
    });
  }, []);

  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 = () => {
    const navigation = {
      navigate: (pageName: string) => {
        setCurrentPage(pageName);
      }
    };
  
    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 'user':
        return <UserPage navigation={navigation} />;
      case 'login':
        return <LoginPage navigation={navigation} />;
      case 'settings':
        return <SettingsPage isDarkMode={isDarkMode} toggleDarkMode={toggleDarkMode} navigation={navigation} />;
      case 'taskDetails':
        return selectedTask ? (
          <TaskDetailsPage
            isDarkMode={isDarkMode}
            task={selectedTask}
            onBack={() => setCurrentPage('home')}
          />
        ) : null;
      default:
        return <HomePage isDarkMode={isDarkMode} tasks={tasks} setTasks={setTasks} />;
    }
  };

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

  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('user')}>
            <Text style={styles.navText}>用戶</Text>
          </TouchableOpacity>
        </View>
      </SafeAreaView>
    </GestureHandlerRootView>
  );
}

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

// ...

調整使用者頁面

接著調整User page,並且在未登入時可以點擊進行登入。

// src\pages\user.page.tsx
import React from 'react';
import { View, Text, StyleSheet, Image, TouchableOpacity, ScrollView } from 'react-native';
import { useAtom } from 'jotai';
import { userAtom, isLoggedInAtom } from '../stores/atoms';
import { UserService } from '../services/user.service';

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

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
  },
  profileHeader: {
    alignItems: 'center',
    marginBottom: 20,
  },
  avatar: {
    width: 100,
    height: 100,
    borderRadius: 50,
    marginBottom: 10,
  },
  username: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 5,
  },
  email: {
    fontSize: 16,
    color: '#666',
  },
  section: {
    marginBottom: 20,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  infoItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 5,
  },
  infoLabel: {
    fontWeight: 'bold',
  },
  infoValue: {
    color: '#666',
  },
  settingsButton: {
    backgroundColor: '#007AFF',
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
    marginBottom: 10,
  },
  settingsButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
  loginButton: {
    backgroundColor: '#007AFF',
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
  },
  loginButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
  logoutButton: {
    backgroundColor: '#FF3B30',
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
  },
  logoutButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default UserPage;

心得

今天我們實現了簡易的使用者頁面和登入頁面,明天會先對所有的部分進行調整,並添加路由系統。


上一篇
【從零開始學React Native】13. 創建Todo Tracker——存儲資料在本地
下一篇
-【從零開始學React Native】15. 創建Todo Tracker——調整並新增路由
系列文
從零開始學React Native20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言