cresvin.pl

Todo App z Vite, Firebase i Tailwind - Praktyczny przewodnik

Praktyczny przewodnik krok po kroku, jak stworzyć aplikację typu Todo z wykorzystaniem Vite, Firebase i TailwindCSS. Naucz się tworzyć funkcjonalne i estetyczne interfejsy webowe od podstaw.

Wprowadzenie

W tym poradniku krok po kroku zbudujemy prostą aplikację TODO z użyciem React, Firebase i Firestore. Nauczysz się tworzyć projekt w Firebase, konfigurować Firestore, integrować bazę danych z aplikacją front-endową i obsługiwać operacje CRUD (tworzenie, odczyt, aktualizacja i usuwanie zadań).

Tworzenie aplikacji Firebase i bazy danych Firestore

Przejdź na oficjalna strona Firebase: https://firebase.google.com/.

Wejdź do konsoli Firebase, klikając “Get started in console”.

Get started in console

Kliknij “Get started with a Firebase project”.

Get started with a firebase project

Wprowadź nazwę swojego projektu, np. react-firebase-todo-app

Nazwanie projektu

Wyłącz funkcję Gemini AI (opcjonalnie).

Wyłączanie AI dla projektu

Wyłącz Google Analytics (opcjonalnie) i kliknij “Create project”.

Wyłączanie Google Analytics

Po utworzeniu projektu kliknij “Continue”

Kliknij ikonę </> (Web), aby dodać aplikację webową.

Dodawanie aplikacji webowej

Nazwij aplikację, np. React Firebase Todo App i kliknij “Register app”.

Nazwanie aplikacji

Skopiuj kod inicjalizujący Firebase i kliknij “Continue to console”.

Kopiowanie kodu inicjalizującego

Tworzenie bazy danych Firestore

W lewym panelu kliknij “Firestore Database”

Wybieranie bazy danych Firestore w lewym panelu

Kliknij “Create database”.

Tworzenie bazy danych Firestore

Kliknij przycisk “Next”.

Wybierz “Start in test mode” i kliknij “Create”.

Uruchamianie w trybie testowym

Konfiguracja i przygotowanie projektu Vite.

Utwórz projekt z Vite z szablonem React:

bash
pnpm create vite firebase-react-todo-app --template react
# lub:
npm create vite firebase-react-todo-app --template react

Przejdź do folderu projektu:

bash
cd firebase-react-todo-app

Zainstaluj zależności:

bash
pnpm i
# lub:
npm i

Zainstaluj TailwindCSS:

bash
pnpm add tailwindcss @tailwindcss/vite
# lub:
npm i tailwindcss @tailwindcss/vite

Zainstaluj Firebase:

bash
pnpm add firebase
# lub:
npm i firebase

Otwórz projekt w VS Code:

bash
code .

Usuń zbędne pliki starterowe od Vite

Usuń zbędne pliki:

Skonfiguruj TailwindCSS w vite.config.js:

vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
 
// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
});

Zaimportuj Tailwind w globalnym pliku CSS:

src/index.css
@import "tailwindcss";

Stwórz prosty interfejs użytkownika w pliku App.jsx:

src/App.jsx
export default function App() {
  return (
    <div className="h-screen grid place-content-center">
      <div className="w-96">
        <h1 className="text-4xl font-bold text-center">Zadania do zrobienia</h1>
        <div className="flex gap-1.5 my-4">
          <input
            placeholder="np. Pójdź na siłownie..."
            className="outline-none border-2 px-2.5 py-1.5 rounded-md w-full"
          />
          <button className="bg-black text-white hover:bg-black/80 transition-colors px-2.5 font-medium cursor-pointer rounded-md">
            Dodaj
          </button>
        </div>
        <div className="font-medium">Tutaj będą zadania</div>
      </div>
    </div>
  );
}

Uruchom serwer deweloperski:

bash
pnpm dev
# lub:
npm dev
Wygląd UI

Konfiguracja Firebase w aplikacji

Stwórz folder lib w src, a w nim plik firebase.js. Wklej kod inicjalizujący Firebase:

src/lib/firebase.js
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
 
const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
};
 
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);

Wysłanie zadań do Firestore

Zaimportuj potrzebne funkcje i ustaw stany:

src/App.jsx
import { useState } from "react";
import { addDoc, collection } from "firebase/firestore";
import { db } from "./lib/firebase";
 
export default function App() {
  const [todo, setTodo] = useState("");
 
  const handleAddTodo = async () => {
    try {
      const newTodo = { todo, done: false };
      setTodo("");
      await addDoc(collection(db, "todos"), newTodo);
    } catch (err) {
      console.error(err);
    }
  };
 
  return (
    ...
  );
}
Kolekcja zadań

Podłącz onChange do inputa oraz onClick do przycisku:

src/App.jsx
<input
  value={todo}
  onChange={(e) => setTodo(e.target.value)}
  ...
/>
 
<button onClick={handleAddTodo}>Dodaj</button>

Odczytywanie danych z Firestore

Zaimportuj useEffect i getDocs:

src/App.jsx
import { useEffect } from "react";
import { getDocs } from "firebase/firestore";

Stwórz stan todos i funkcję fetchTodos:

src/App.jsx
const [todos, setTodos] = useState([]);
 
const fetchTodos = async () => {
  try {
    const snapshot = await getDocs(collection(db, "todos"));
    const data = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
    setTodos(data);
  } catch (err) {
    console.error(err);
  }
};
 
useEffect(() => {
  fetchTodos();
}, []);

Wyświetl zadanie

src/App.jsx
<div className="font-medium flex flex-col gap-1.5">
  {todos.map((todo) => (
    <p key={todo.id}>{todo.todo}</p>
  ))}
</div>
Dodawanie nowego zadania

Aktualizacja zadań

Zaimportuj updateDoc i doc:

src/App.jsx
import { updateDoc, doc } from "firebase/firestore";

Stwórz funkcję aktualizującą:

src/App.jsx
const handleUpdateTodo = async (todo) => {
  try {
    await updateDoc(doc(db, "todos", todo.id), { done: !todo.done });
    await fetchTodos();
  } catch (err) {
    console.error(err);
  }
};

Podłącz do onClick i dodaj stylowanie:

src/App.jsx
<p
  onClick={() => handleUpdateTodo(todo)}
  className={`select-none ${todo.done ? "line-through text-zinc-500" : ""}`}
>
  {todo.todo}
</p>
Aktualizowanie zadań

Usuwanie zadań

Zaimportuj deleteDoc:

src/App.jsx
import { deleteDoc } from "firebase/firestore";

Stwórz funkcję handleDeleteTodo:

src/App.jsx
const handleDeleteTodo = async (id) => {
  try {
    await deleteDoc(doc(db, "todos", id));
    await fetchTodos();
  } catch (err) {
    console.error(err);
  }
};

Podłącz do onDoubleClick:

src/App.jsx
<p
  onClick={() => handleUpdateTodo(todo)}
  onDoubleClick={() => handleDeleteTodo(todo.id)}
  className={`select-none ${todo.done ? "line-through text-zinc-500" : ""}`}
>
  {todo.todo}
</p>
Usuwanie zadanie poprzez podwójne kliknięcie

Gratulacje!

Zbudowałeś prostą aplikację TODO z React i Firebase obsługującą dodawanie, odczytywanie, aktualizowanie i usuwanie danych.

Cały kod

src/App.jsx
import {
  addDoc,
  collection,
  deleteDoc,
  doc,
  getDocs,
  updateDoc,
} from "firebase/firestore";
import { useEffect, useState } from "react";
import { db } from "./lib/firebase";
 
export default function App() {
  const [todo, setTodo] = useState("");
  const [todos, setTodos] = useState([]);
 
  const fetchTodos = async () => {
    try {
      const snapshot = await getDocs(collection(db, "todos"));
      const data = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
      setTodos(data);
    } catch (err) {
      console.error(err);
    }
  };
 
  const handleUpdateTodo = async (todo) => {
    try {
      await updateDoc(doc(db, "todos", todo.id), { done: !todo.done });
      await fetchTodos();
    } catch (err) {
      console.error(err);
    }
  };
 
  useEffect(() => {
    fetchTodos();
  }, []);
 
  const handleAddTodo = async () => {
    try {
      const newTodo = { todo, done: false };
      setTodo("");
      await addDoc(collection(db, "todos"), newTodo);
    } catch (err) {
      console.error(err);
    }
  };
 
  const handleDeleteTodo = async (id) => {
    try {
      await deleteDoc(doc(db, "todos", id));
      await fetchTodos();
    } catch (err) {
      console.error(err);
    }
  };
 
  return (
    <div className="h-screen grid place-content-center">
      <div className="w-96">
        <h1 className="text-4xl font-bold text-center">Zadania do zrobienia</h1>
        <div className="flex gap-1.5 my-4">
          <input
            placeholder="np. Pójdź na siłownie..."
            className="outline-none border-2 px-2.5 py-1.5 rounded-md w-full"
            value={todo}
            onChange={(e) => setTodo(e.target.value)}
          />
          <button
            onClick={handleAddTodo}
            className="bg-black text-white hover:bg-black/80 transition-colors px-2.5 font-medium cursor-pointer rounded-md"
          >
            Dodaj
          </button>
        </div>
        <div className="font-medium flex flex-col gap-1.5">
          {todos.map((todo) => (
            <p
              key={todo.id}
              onClick={() => handleUpdateTodo(todo)}
              onDoubleClick={() => handleDeleteTodo(todo.id)}
              className={`select-none ${
                todo.done ? "line-through text-zinc-500" : ""
              }`}
            >
              {todo.todo}
            </p>
          ))}
        </div>
      </div>
    </div>
  );
}