TGB
Trần Gia Bảo

Exercise Management - Copy this React, Tailwind Component to your project

Import React, { useState, useEffect } from "react"; import { FaEdit, FaTrash, FaCheck, FaTimes, FaSpinner } from "react icons/fa"; import { collection, getDocs, addDoc, deleteDoc, doc, updateDoc, query, where, } from "firebase/firestore"; import { db } from "@/config/firebase"; import ProtectedPage from "@/components/templates/protected route"; const ExerciseManagement = () => { const [exercises, setExercises] = useState([]); const [newExercise, setNewExercise] = useState({ title: "", topic: "", deadline: "", }); const [editingId, setEditingId] = useState(null); const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [deleteId, setDeleteId] = useState(null); const [activeTab, setActiveTab] = useState("all"); useEffect(() => { const fetchExercises = async () => { try { const exercisesCollection = collection(db, "exercises"); const exercisesSnapshot = await getDocs(exercisesCollection); const exercisesList = exercisesSnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data(), })); setExercises(exercisesList); } catch (error) { console.error("Error fetching exercises:", error); } }; fetchExercises(); }, []); const validateForm = (exercise) => { const errors = {}; if (!exercise.title.trim()) errors.title = "Title is required"; if (!exercise.topic.trim()) errors.topic = "Topic is required"; if (!exercise.deadline) errors.deadline = "Deadline is required"; return errors; }; const handleSubmit = async (e) => { e.preventDefault(); const validationErrors = validateForm(newExercise); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } setIsLoading(true); try { const exercisesRef = collection(db, "exercises"); const docRef = await addDoc(exercisesRef, { ...newExercise, status: "pending", createdAt: new Date(), }); setExercises([ ...exercises, { ...newExercise, id: docRef.id, status: "pending" }, ]); setNewExercise({ title: "", topic: "", deadline: "" }); setErrors({}); } catch (error) { console.error("Error adding exercise:", error); } finally { setIsLoading(false); } }; const handleEdit = (id) => { setEditingId(id); }; const handleUpdate = async (id) => { const exercise = exercises.find((ex) => ex.id === id); const validationErrors = validateForm(exercise); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } setIsLoading(true); try { const exerciseRef = doc(db, "exercises", id); await updateDoc(exerciseRef, { title: exercise.title, topic: exercise.topic, deadline: exercise.deadline, updatedAt: new Date(), }); setExercises(exercises.map((ex) => (ex.id === id ? { ...ex } : ex))); setEditingId(null); setErrors({}); } catch (error) { console.error("Error updating exercise:", error); } finally { setIsLoading(false); } }; const confirmDelete = (id) => { setDeleteId(id); setShowDeleteModal(true); }; const handleDelete = async () => { setIsLoading(true); try { const exerciseRef = doc(db, "exercises", deleteId); await deleteDoc(exerciseRef); setExercises(exercises.filter((ex) => ex.id !== deleteId)); setShowDeleteModal(false); setDeleteId(null); } catch (error) { console.error("Error deleting exercise:", error); } finally { setIsLoading(false); } }; const filteredExercises = exercises.filter((ex) => { if (activeTab === "all") return true; return ex.status === activeTab; }); return ( <ProtectedPage> <div className="container mx auto px 4 py 8"> <h1 className="text 3xl font bold mb 8">Exercises</h1> {/* <form onSubmit={handleSubmit} className="mb 8 bg white p 6 rounded lg shadow md" > <div className="grid md:grid cols 2 gap 4"> <div> <label htmlFor="title" className="block mb 2 font semibold"> Title </label> <input type="text" id="title" className={`w full p 2 border rounded ${ errors.title ? "border red 500" : "border gray 300" }`} value={newExercise.title} onChange={(e) => setNewExercise({ ...newExercise, title: e.target.value }) } aria label="Exercise title" /> {errors.title && ( <p className="text red 500 text sm mt 1">{errors.title}</p> )} </div> <div> <label htmlFor="deadline" className="block mb 2 font semibold"> Deadline </label> <input type="datetime local" id="deadline" className={`w full p 2 border rounded ${ errors.deadline ? "border red 500" : "border gray 300" }`} value={newExercise.deadline} onChange={(e) => setNewExercise({ ...newExercise, deadline: e.target.value }) } aria label="Exercise deadline" /> {errors.deadline && ( <p className="text red 500 text sm mt 1">{errors.deadline}</p> )} </div> </div> <div className="mt 4"> <label htmlFor="topic" className="block mb 2 font semibold"> Topic </label> <textarea id="topic" className={`w full p 2 border rounded min h [150px] ${ errors.topic ? "border red 500" : "border gray 300" }`} value={newExercise.topic} onChange={(e) => setNewExercise({ ...newExercise, topic: e.target.value }) } aria label="Exercise topic" placeholder="Enter detailed topic description..." /> {errors.topic && ( <p className="text red 500 text sm mt 1">{errors.topic}</p> )} </div> <button type="submit" className="mt 4 bg blue 500 text white px 4 py 2 rounded hover:bg blue 600 transition colors" disabled={isLoading} > {isLoading ? ( <FaSpinner className="animate spin inline" /> ) : ( "Add Exercise" )} </button> </form> */} <div className="mb 6"> <div className="flex space x 4"> <button className={`px 4 py 2 rounded ${ activeTab === "all" ? "bg blue 500 text white" : "bg gray 200" }`} onClick={() => setActiveTab("all")} > All </button> <button className={`px 4 py 2 rounded ${ activeTab === "pending" ? "bg blue 500 text white" : "bg gray 200" }`} onClick={() => setActiveTab("pending")} > Pending </button> <button className={`px 4 py 2 rounded ${ activeTab === "reviewed" ? "bg blue 500 text white" : "bg gray 200" }`} onClick={() => setActiveTab("reviewed")} > Reviewed </button> </div> </div> <div className="space y 4"> {filteredExercises.map((exercise) => ( <div key={exercise.id} className="bg white p 6 rounded lg shadow md hover:shadow lg transition shadow" > {editingId === exercise.id ? ( <div className="space y 4"> <input type="text" className="w full p 2 border rounded" value={exercise.title} onChange={(e) => setExercises( exercises.map((ex) => ex.id === exercise.id ? { ...ex, title: e.target.value } : ex ) ) } /> <textarea className="w full p 2 border rounded min h [150px]" value={exercise.topic} onChange={(e) => setExercises( exercises.map((ex) => ex.id === exercise.id ? { ...ex, topic: e.target.value } : ex ) ) } /> <input type="datetime local" className="w full p 2 border rounded" value={exercise.deadline} onChange={(e) => setExercises( exercises.map((ex) => ex.id === exercise.id ? { ...ex, deadline: e.target.value } : ex ) ) } /> </div> ) : ( <div className="flex justify between items start"> <div> <h3 className="text xl font semibold">{exercise.title}</h3> <p className="text gray 600 whitespace pre wrap mt 2"> {exercise.topic} </p> <p className="text sm text gray 500 mt 2"> Deadline: {new Date(exercise.deadline).toLocaleString()} </p> <span className={`inline block px 2 py 1 rounded text sm mt 2 ${ exercise.status === "pending" ? "bg yellow 100 text yellow 800" : "bg green 100 text green 800" }`} > {exercise.status} </span> </div> <div className="flex space x 2"> <a href={`/guide/c/exercise/${exercise.id}`} className="px 4 py 2 bg blue 500 text white rounded hover:bg blue 600 transition colors" > Submit </a> </div> </div> )} </div> ))} </div> </div> </ProtectedPage> ); }; export default ExerciseManagement; update theme to dark

Prompt
Component Preview

About

ExerciseManagement - Manage exercises seamlessly with a user-friendly interface, built with React and Tailwind. Add, edit, delete, and. Download instantly!

Share

Last updated 1 month ago