Sekarang Kita akan coba tambahkan fitur deadline dan notifikasi. Namun, sebelumnya saya akan migrasi dengan menggunakan aplikasi expo-go karena pada expo via web fitur push notification tidak didukung. Jadi kalian bisa download aplikasi expo-go pada app store atau playstore.
Jika sudah di download , maka masih dengan project to-do-list sebelumya akan muncul halaman awal seperti berikut :
selanjutnya kita bisa ubah bagian index.tsx pada folder app/(tabs) dengan kodingan sebagai berikut :
import { Redirect } from "expo-router";
export default function Index() {
return <Redirect href="../../Login" />;
}
kode diatas akan mengarahkan aplikasi untuk membuka halaman Login.js (mohon disesuaikan dengan lokasi direktori kalian untuk penggunaan "/") .
Sekarang coba login, biasanya akan muncul error login gagal padahal data sudah tersimpan pada firestore. Untuk mengatasinya silahkan jalankan perintah berikut :
npx expo install @react-native-async-storage/async-storage
jika masih tidak bisa coba lakukan registrasi/signup dulu kemudian lakukan login kembali.
Selanjutnya kita edit tampilan pada Home.jsx dan TodoItem.jsx dengan menambahkan fitur deadline yaitu dengan menggunakan datetimepicker
MENAMBAHKAN DATETIMEPICKER UNTUK DEADLINE
A. Install DateTimePicker untuk React Native (via Expo)
npx expo install react-native-paper
npm install react-native-paper-dates date-fns
B. Edit Kode Home.jsx
import React, { useEffect, useState } from "react";
import { View, Text, FlatList, Modal, Alert, Pressable } from "react-native";
import { useRouter } from "expo-router";
import { signOut } from "firebase/auth";
import { auth, db } from "../../config/firebase";
import TodoItem from "./components/Todoitem";
import {
collection,
addDoc,
query,
where,
onSnapshot,
deleteDoc,
updateDoc,
doc,
orderBy,
} from "firebase/firestore";
import { TextInput, Provider as PaperProvider } from "react-native-paper";
import {
DatePickerModal,
TimePickerModal,
registerTranslation,
} from "react-native-paper-dates";
import { en } from "react-native-paper-dates";
import { SafeAreaView } from "react-native-safe-area-context";
registerTranslation("en", en);
export default function Home() {
const [todo, setTodo] = useState("");
const [todos, setTodos] = useState([]);
const [searchText, setSearchText] = useState("");
const [filter, setFilter] = useState("all");
const [deadlineDate, setDeadlineDate] = useState(undefined);
const [deadlineTime, setDeadlineTime] = useState({ hours: 12, minutes: 0 });
const [datePickerVisible, setDatePickerVisible] = useState(false);
const [timePickerVisible, setTimePickerVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [editTodo, setEditTodo] = useState(null);
const [editText, setEditText] = useState("");
const router = useRouter();
const user = auth.currentUser;
useEffect(() => {
if (!user) return;
const q = query(
collection(db, "todos"),
where("userId", "==", user.uid),
orderBy("createdAt", "desc")
);
const unsubscribe = onSnapshot(q, (snapshot) => {
const list = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setTodos(list);
});
return () => unsubscribe();
}, [user]);
const filteredTodos = todos
.filter((todo) =>
todo.title.toLowerCase().includes(searchText.toLowerCase())
)
.filter((todo) => {
if (filter === "completed") return todo.completed;
if (filter === "active") return !todo.completed;
return true;
});
const addTodo = async () => {
if (todo.trim() === "" || !deadlineDate) return;
// Gabungkan tanggal dan waktu
const fullDeadline = new Date(deadlineDate);
fullDeadline.setHours(deadlineTime.hours);
fullDeadline.setMinutes(deadlineTime.minutes);
await addDoc(collection(db, "todos"), {
title: todo,
completed: false,
createdAt: new Date(),
deadline: fullDeadline,
userId: user.uid,
});
setTodo("");
setDeadlineDate(undefined);
};
const toggleComplete = async (id, status) => {
await updateDoc(doc(db, "todos", id), { completed: !status });
};
const deleteTodo = async (id) => {
await deleteDoc(doc(db, "todos", id));
};
const openEditModal = (item) => {
setEditTodo(item);
setEditText(item.title);
setEditModalVisible(true);
};
const handleEditSave = async () => {
if (!editText.trim()) {
Alert.alert("Isi tidak boleh kosong");
return;
}
await updateDoc(doc(db, "todos", editTodo.id), {
title: editText,
});
setEditModalVisible(false);
setEditTodo(null);
setEditText("");
};
return (
<PaperProvider>
<SafeAreaView style={{ flex: 1, padding: 20 }}>
<Text style={{ fontSize: 24, fontWeight: "bold", marginBottom: 10 }}>
To-Do List
</Text>
<TextInput
placeholder="Cari to-do..."
value={searchText}
onChangeText={setSearchText}
mode="outlined"
style={{ marginBottom: 10 }}
/>
{/* Filter */}
<View
style={{
flexDirection: "row",
justifyContent: "center",
marginBottom: 10,
}}
>
{["all", "active", "completed"].map((f) => (
<Pressable
key={f}
onPress={() => setFilter(f)}
style={{
paddingVertical: 8,
paddingHorizontal: 16,
backgroundColor: filter === f ? "#007bff" : "#e0e0e0",
borderRadius: 6,
marginHorizontal: 4,
}}
>
<Text
style={{
color: filter === f ? "white" : "#333",
fontWeight: "bold",
}}
>
{f === "all" ? "Semua" : f === "active" ? "Aktif" : "Selesai"}
</Text>
</Pressable>
))}
</View>
{/* Input todo */}
<TextInput
placeholder="Tambah to-do..."
value={todo}
onChangeText={setTodo}
mode="outlined"
style={{ marginBottom: 10 }}
/>
{/* Date & Time picker */}
<Pressable onPress={() => setDatePickerVisible(true)}>
<TextInput
label="Pilih tanggal"
value={deadlineDate ? deadlineDate.toDateString() : ""}
editable={false}
mode="outlined"
style={{ marginBottom: 10 }}
/>
</Pressable>
<Pressable onPress={() => setTimePickerVisible(true)}>
<TextInput
label=""
value={`${deadlineTime.hours.toString().padStart(2, "0")}:${deadlineTime.minutes
.toString()
.padStart(2, "0")}`}
editable={false}
mode="outlined"
style={{ marginBottom: 10 }}
/>
</Pressable>
<DatePickerModal
locale="en"
mode="single"
visible={datePickerVisible}
onDismiss={() => setDatePickerVisible(false)}
date={deadlineDate}
onConfirm={({ date }) => {
setDeadlineDate(date);
setDatePickerVisible(false);
}}
/>
<TimePickerModal
visible={timePickerVisible}
onDismiss={() => setTimePickerVisible(false)}
onConfirm={({ hours, minutes }) => {
setDeadlineTime({ hours, minutes });
setTimePickerVisible(false);
}}
hours={deadlineTime.hours}
minutes={deadlineTime.minutes}
/>
<Pressable
onPress={addTodo}
style={{
backgroundColor: "#007bff",
padding: 12,
borderRadius: 6,
alignItems: "center",
marginBottom: 10,
}}
>
<Text style={{ color: "white", fontWeight: "bold" }}>Tambah</Text>
</Pressable>
<FlatList
data={filteredTodos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TodoItem
item={item}
onToggle={toggleComplete}
onDelete={deleteTodo}
onEdit={openEditModal}
/>
)}
/>
{/* Logout */}
<Pressable
onPress={() => {
signOut(auth);
router.replace("/Login");
}}
style={{
backgroundColor: "red",
padding: 12,
borderRadius: 6,
alignItems: "center",
marginTop: 20,
}}
>
<Text style={{ color: "white", fontWeight: "bold" }}>Logout</Text>
</Pressable>
{/* Modal Edit */}
<Modal
visible={editModalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setEditModalVisible(false)}
>
<View
style={{
flex: 1,
backgroundColor: "#000000aa",
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
backgroundColor: "white",
padding: 20,
width: "80%",
borderRadius: 10,
}}
>
<Text style={{ fontSize: 18, marginBottom: 10 }}>Edit To-Do</Text>
<TextInput
value={editText}
onChangeText={setEditText}
mode="outlined"
style={{ marginBottom: 10 }}
/>
<Pressable
onPress={handleEditSave}
style={{
backgroundColor: "#007bff",
padding: 12,
borderRadius: 6,
alignItems: "center",
}}
>
<Text style={{ color: "white", fontWeight: "bold" }}>
Simpan
</Text>
</Pressable>
<Pressable
onPress={() => setEditModalVisible(false)}
style={{
backgroundColor: "#aaa",
padding: 12,
borderRadius: 6,
alignItems: "center",
marginTop: 10,
}}
>
<Text style={{ color: "white", fontWeight: "bold" }}>
Batal
</Text>
</Pressable>
</View>
</View>
</Modal>
</SafeAreaView>
</PaperProvider>
);
}
C. Edit File TodoItem.jsx
import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
export default function TodoItem({ item, onToggle, onDelete, onEdit }) {
const now = new Date();
const deadline = item.deadline?.seconds ? new Date(item.deadline.seconds * 1000) : null;
const isOverdue = deadline && now > deadline && !item.completed;
return (
<View style={styles.item}>
<View style={{ flex: 1 }}>
<TouchableOpacity onPress={() => onToggle(item.id, item.completed)}>
<Text style={[styles.text, item.completed && styles.completed]}>
{item.title}
</Text>
</TouchableOpacity>
{deadline && (
<Text style={styles.deadline}>
Deadline:{" "}
{deadline.toLocaleDateString()}{" "}
{deadline.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</Text>
)}
{isOverdue && (
<Text style={styles.overdue}>⚠️ Lewat deadline</Text>
)}
</View>
<View style={styles.actions}>
<TouchableOpacity onPress={() => onEdit(item)}>
<Text style={styles.actionText}>✏️</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => onDelete(item.id)}>
<Text style={styles.actionText}>🗑️</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
item: {
padding: 12,
backgroundColor: "#f1f1f1",
borderRadius: 8,
marginVertical: 5,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
},
text: {
fontSize: 16,
fontWeight: "bold",
},
completed: {
textDecorationLine: "line-through",
color: "gray",
},
deadline: {
fontSize: 12,
color: "#555",
marginTop: 4,
},
overdue: {
fontSize: 12,
color: "red",
fontWeight: "bold",
marginTop: 2,
},
actions: {
flexDirection: "row",
gap: 10,
marginLeft: 10,
},
actionText: {
fontSize: 18,
},
});
D. Edit Bagian Modal Edit pada halaman Home.jsx
Pastikan kamu sudah mengimpor komponen berikut di atas file Home.jsx:
import { DatePickerModal, TimePickerModal } from "react-native-paper-dates";
import { Provider as PaperProvider, TextInput } from "react-native-paper";
Ganti bagian modal edit dengan kodingan berikut :
{/* Modal Edit */}
<Modal
visible={editModalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => setEditModalVisible(false)}
>
<View
style={{
flex: 1,
backgroundColor: "#000000aa",
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
backgroundColor: "white",
padding: 20,
width: "85%",
borderRadius: 10,
}}
>
<Text style={{ fontSize: 18, marginBottom: 10 }}>Edit To-Do</Text>
<TextInput
label=""
value={editText}
onChangeText={setEditText}
mode="outlined"
style={{ marginBottom: 10 }}
/>
{/* Deadline Display & Pickers */}
<Text style={{ marginBottom: 6 }}>Deadline:</Text>
<Pressable
onPress={() => setShowEditDate(true)}
style={{
padding: 10,
backgroundColor: "#e0e0e0",
borderRadius: 6,
marginBottom: 10,
}}
>
<Text>{editDeadline?.toLocaleDateString() || "Pilih tanggal"}</Text>
</Pressable>
<Pressable
onPress={() => setShowEditTime(true)}
style={{
padding: 10,
backgroundColor: "#e0e0e0",
borderRadius: 6,
marginBottom: 20,
}}
>
<Text>{editDeadline?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) || "Pilih waktu"}</Text>
</Pressable>
<Pressable
onPress={handleEditSave}
style={{
backgroundColor: "#007bff",
padding: 12,
borderRadius: 6,
alignItems: "center",
}}
>
<Text style={{ color: "white", fontWeight: "bold" }}>Simpan</Text>
</Pressable>
<Pressable
onPress={() => setEditModalVisible(false)}
style={{
backgroundColor: "#aaa",
padding: 12,
borderRadius: 6,
alignItems: "center",
marginTop: 10,
}}
>
<Text style={{ color: "white", fontWeight: "bold" }}>Batal</Text>
</Pressable>
</View>
</View>
{/* Date Picker Modal */}
<DatePickerModal
locale="id"
mode="single"
visible={showEditDate}
onDismiss={() => setShowEditDate(false)}
date={editDeadline}
onConfirm={({ date }) => {
const updatedDate = new Date(editDeadline || new Date());
updatedDate.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
setEditDeadline(updatedDate);
setShowEditDate(false);
}}
/>
{/* Time Picker Modal */}
<TimePickerModal
visible={showEditTime}
onDismiss={() => setShowEditTime(false)}
onConfirm={({ hours, minutes }) => {
const updatedTime = new Date(editDeadline || new Date());
updatedTime.setHours(hours);
updatedTime.setMinutes(minutes);
setEditDeadline(updatedTime);
setShowEditTime(false);
}}
hours={editDeadline?.getHours() || 0}
minutes={editDeadline?.getMinutes() || 0}
/>
</Modal>
Tambahkan State Berikut di bagian atas pada file Home.jsx:
const [editDeadline, setEditDeadline] = useState(new Date());
const [showEditDate, setShowEditDate] = useState(false);
const [showEditTime, setShowEditTime] = useState(false);
Update Fungsi openEditModal:
const openEditModal = (item) => {
setEditTodo(item);
setEditText(item.title);
setEditDeadline(item.deadline?.seconds ? new Date(item.deadline.seconds * 1000) : new Date());
setEditModalVisible(true);
};
Update Fungsi handleEditSave:
const handleEditSave = async () => {
if (!editText.trim()) {
Alert.alert("Isi tidak boleh kosong");
return;
}
await updateDoc(doc(db, "todos", editTodo.id), {
title: editText,
deadline: editDeadline,
});
setEditModalVisible(false);
setEditTodo(null);
setEditText("");
};
MENAMBAHKAN NAVBAR UNTUK ICON NOTIFIKASI/LONCENG
A. Tambahkan import tambahan. Tambahkan di bagian atas pada Home.jsx:
import { Ionicons } from "@expo/vector-icons"; // Untuk ikon lonceng
B.Tambahkan state dan fungsi baru. Di dalam komponen Home, tambahkan state dan fungsi:
const [showNotifList, setShowNotifList] = useState(false);
const [dueTodos, setDueTodos] = useState([]);
// Update setiap kali `todos` berubah
useEffect(() => {
const now = new Date();
const upcomingTodos = todos.filter((item) => {
const deadline = item.deadline?.seconds ? new Date(item.deadline.seconds * 1000) : null;
if (!deadline || item.completed) return false;
const diff = deadline.getTime() - now.getTime();
return diff < 60 * 60 * 1000 && diff > -60 * 60 * 1000; // Dalam 1 jam sebelum/sesudah deadline
});
setDueTodos(upcomingTodos);
}, [todos]);
C. Tambahkan navbar dengan ikon lonceng.
Tambahkan di atas <Text style={{ fontSize: 24, fontWeight: "bold", marginBottom: 10 }}>To-Do List</Text>:
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
<Text style={{ fontSize: 24, fontWeight: "bold" }}>To-Do List</Text>
<Pressable onPress={() => setShowNotifList(!showNotifList)} style={{ padding: 8 }}>
<Ionicons name="notifications-outline" size={28} color="#007bff" />
{dueTodos.length > 0 && (
<View style={{
position: "absolute",
right: 0,
top: 0,
backgroundColor: "red",
borderRadius: 999,
width: 16,
height: 16,
justifyContent: "center",
alignItems: "center",
}}>
<Text style={{ color: "white", fontSize: 10 }}>{dueTodos.length}</Text>
</View>
)}
</Pressable>
</View>
D. Tampilkan daftar notifikasi (popup atau list). Tambahkan di bawah <FlatList ... />, sebelum tombol Logout:
<Modal
visible={showNotifList}
animationType="slide"
transparent={true}
onRequestClose={() => setShowNotifList(false)}
>
<View style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center'
}}>
<View style={{
backgroundColor: "#fff",
padding: 20,
borderRadius: 10,
width: "85%",
maxHeight: "70%"
}}>
<Text style={{ fontWeight: "bold", fontSize: 18, marginBottom: 12 }}>
Notifikasi To-Do
</Text>
{dueTodos.length === 0 ? (
<Text style={{ fontStyle: "italic", color: "#888" }}>Tidak ada notifikasi</Text>
) : (
dueTodos.map((todo) => (
<View key={todo.id} style={{ marginBottom: 12 }}>
<Text style={{ fontWeight: "600" }}>{todo.title}</Text>
<Text style={{ fontSize: 12, color: "#555" }}>
Deadline: {new Date(todo.deadline.seconds * 1000).toLocaleString()}
</Text>
</View>
))
)}
<Pressable
onPress={() => setShowNotifList(false)}
style={{
marginTop: 20,
backgroundColor: "#007bff",
paddingVertical: 10,
borderRadius: 8,
alignItems: "center"
}}
>
<Text style={{ color: "#fff", fontWeight: "bold" }}>Tutup</Text>
</Pressable>
</View>
</View>
</Modal>
MENAMBAHKAN NOTIFIKASI
Untuk menambahkan push notification ketika ada to-do yang melewati atau mendekati deadline, kamu dapat menggunakan Expo Notifications.
A. Jalankan Perintah berikut :
npx expo install expo-notifications
B. Tambahkan import dan konfigurasi notifikasi di bagian atas file Home.jsx:
import * as Notifications from "expo-notifications";
C. Tambahkan fungsi permission & jadwal notifikasi. Tambahkan di dalam komponen Home:
useEffect(() => {
requestPermissions();
}, []);
const requestPermissions = async () => {
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Izin ditolak untuk notifikasi');
}
};
const scheduleNotification = async (title, body, dateTime) => {
if (dateTime <= new Date()) return; // Tidak kirim jika sudah lewat
await Notifications.scheduleNotificationAsync({
content: {
title,
body,
sound: true,
},
trigger: dateTime,
});
};
D. Modifikasi fungsi addTodo untuk menjadwalkan notifikasi. Tambahkan pemanggilan scheduleNotification di akhir fungsi addTodo:
const addTodo = async () => {
if (todo.trim() === "" || !deadlineDate) return;
const fullDeadline = new Date(deadlineDate);
fullDeadline.setHours(deadlineTime.hours);
fullDeadline.setMinutes(deadlineTime.minutes);
await addDoc(collection(db, "todos"), {
title: todo,
completed: false,
createdAt: new Date(),
deadline: fullDeadline,
userId: user.uid,
});
// 1 jam sebelum deadline
const oneHourBefore = new Date(fullDeadline.getTime() - 60 * 60 * 1000);
await scheduleNotification("To-do Reminder", `To-do '${todo}' akan segera berakhir!`, oneHourBefore);
// Saat deadline tiba
await scheduleNotification("Deadline Lewat!", `To-do '${todo}' telah melewati batas waktu.`, fullDeadline);
setTodo("");
setDeadlineDate(undefined);
};
Top comments (0)