DEV Community

sandatya widhi
sandatya widhi

Posted on

13. Menambahkan Fitur Deadline dan Push Notifikasi

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" />;
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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("");
};
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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";

Enter fullscreen mode Exit fullscreen mode

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,
  });
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)