diff --git a/.prettierignore b/.prettierignore index db4c6d9..8aef758 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ dist -node_modules \ No newline at end of file +node_modules +Button.jsx \ No newline at end of file diff --git a/public/empty.svg b/public/empty.svg new file mode 100644 index 0000000..ce04aba --- /dev/null +++ b/public/empty.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.css b/src/App.css index 7f31b0a..b55fd80 100644 --- a/src/App.css +++ b/src/App.css @@ -1,13 +1,13 @@ -button:not(.toggle-btn):not(.delete-btn):focus-visible { +button:not(.toggle-btn):focus-visible { outline: solid 4px var(--color-persianblue); outline-offset: 8px; } -button:not(.toggle-btn):not(.delete-btn):hover { +button:not(.toggle-btn):hover { background-color: var(--color-zinc); color: white; cursor: pointer; } -button:not(.toggle-btn):not(.delete-btn):active { +button:not(.toggle-btn):active { background-color: var(--color-persianblue); color: white; } @@ -18,3 +18,8 @@ button.active { button.active:hover { background-color: var(--color-persianblue); } +/* style for focus for dashboard tile link */ +.dash-tile-link:focus-visible { + outline: solid 4px var(--color-persianblue); + outline-offset: 8px; +} \ No newline at end of file diff --git a/src/components/Button.jsx b/src/components/Button.jsx index c077c74..2c49d88 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -1,3 +1,4 @@ +// prettier-ignore-file import { useNavigate } from 'react-router-dom'; import { tv } from 'tailwind-variants/lite'; @@ -10,18 +11,19 @@ const buttonVariants = tv({ sm: 'h-12 w-[162px] text-base font-semibold rounded-lg lg:h-14 lg:w-[220px] lg:text-xl', md: 'h-11 w-[218px] text-base font-semibold rounded-lg lg:h-14 lg:w-[286px] lg:text-xl xl:h-16 xl:w-[327px] xl:text-2xl', lg: 'h-11 w-[345px] text-base font-medium rounded-md lg:w-[501px]', - circ: 'h-11 w-11 text-2xl rounded-full', + xl: 'h-fit w-full md:w-[450px] text-sm sm:text-base font-medium rounded-md lg:w-[501px] 2xl:w-full p-3 justify-start', + circ: 'h-11 w-11 text-2xl rounded-full' }, color: { primary: 'bg-eerie text-white border-1 border-eerie', secondary: 'bg-white text-eerie border-1 border-eerie', - gradient: 'bg-gradient-to-b from-electricgreen to-persianblue text-white', + gradient: 'bg-gradient-to-b from-electricgreen to-persianblue text-white' }, }, // default button styles if no specified props defaultVariants: { size: 'sm', - color: 'primary', + color: 'primary' }, // conditional style cases for specific prop combinations compoundVariants: [ @@ -29,20 +31,12 @@ const buttonVariants = tv({ { color: 'secondary', size: 'md', - className: 'drop-shadow-none !font-normal', + className: 'drop-shadow-none !font-normal' }, ], }); -export default function Button({ - size, - color, - label, - onClick, - isActive, - to, - ...props -}) { +export default function Button({ size, color, label, onClick, isActive, to, title, subtitle, ...props }) { let navigate = useNavigate(); function handleClick() { @@ -54,16 +48,18 @@ export default function Button({ } return ( - ); } diff --git a/src/components/Dashboard/DashboardTile.jsx b/src/components/Dashboard/DashboardTile.jsx index 8f1a0e3..f26d1d9 100644 --- a/src/components/Dashboard/DashboardTile.jsx +++ b/src/components/Dashboard/DashboardTile.jsx @@ -1,4 +1,5 @@ import { tv } from "tailwind-variants/lite"; +import ProgressDay from "./ProgressDay"; const tileVariants = tv({ base: 'font-poppins flex flex-col justify-start py-6 px-4 rounded-lg shadow-[0_4px_15px_rgba(0,0,0,0.25)]', @@ -15,6 +16,7 @@ const tileVariants = tv({ type: { regular: '', data: '', + progress: '' }, }, }); @@ -30,15 +32,43 @@ const imgVariants = tv({ }, }); -export default function DashboardTile({ type, size, span, title, subtitle, imgSource, altText, dataSentence }){ +export default function DashboardTile({ type, size, span, title, subtitle, imgSource, altText, dataSentence, progressDays, onClick, onKeyDown }){ return ( -
+ // if an onClick prop exists, set a cursor pointer, add a role of button, and make it open to tab nav +
{type == "data" ? (

{title}

{dataSentence}

{subtitle}

+ ) : type == "progress" ? ( +
+

{title}

+

{subtitle}

+ { progressDays.length > 0 ? ( + /* div for progress map */ +
+ { progressDays.map(day => ( + + ))} +
+ ) : ( + // image for empty state - no completed challenges yet + {altText} + )} +
) : (

{title}

diff --git a/src/components/Dashboard/ProgressDay.jsx b/src/components/Dashboard/ProgressDay.jsx new file mode 100644 index 0000000..d7a61c3 --- /dev/null +++ b/src/components/Dashboard/ProgressDay.jsx @@ -0,0 +1,14 @@ +import { FaCircle, FaCircleCheck } from "react-icons/fa6" + +export default function ProgressDay({ day }) { + return ( +
+
+

Journal

+ +

Day {day}

+
+ +
+ ) +} \ No newline at end of file diff --git a/src/components/Dashboard/TaskModal.jsx b/src/components/Dashboard/TaskModal.jsx index 1e96f06..58f8865 100644 --- a/src/components/Dashboard/TaskModal.jsx +++ b/src/components/Dashboard/TaskModal.jsx @@ -1,75 +1,107 @@ import { useEffect, useState } from 'react'; import Button from "../Button"; import { IoClose } from "react-icons/io5"; +import { useNavigate } from 'react-router-dom'; +import { db } from '../../firebase'; +import { doc, updateDoc } from 'firebase/firestore'; -export default function TaskModal({ isOpen, onClose}) { +export default function TaskModal({ isOpen, onClose, user}) { + console.log('task modal is open', isOpen); + const navigate = useNavigate(); const [selectedTask, setSelectedTask] = useState([]); - // log the selectedTask after the array changes to update automatically - useEffect(() => { + // log the selectedTask after the array changes to update automatically + useEffect(() => { console.log('Updated Tasks:', selectedTask); }, [selectedTask]); function toggleTask(challenge) { // if the clicked challenge is not selectedTask, setSelectedTask - if (!selectedTask.includes(challenge)) { - // log the selection - console.log(`${challenge} button selected!`); - setSelectedTask([...selectedTask, challenge]); - // if the clicked challenge is already in selectedTask, filter it out of selectedTask - } else { - // log the deselection - console.log(`${challenge} button deselected!`); - setSelectedTask(selectedTask.filter(item => item !== challenge)); + if (!selectedTask.includes(challenge)) { + // log the selection + console.log(`${challenge} button selected!`); + setSelectedTask([...selectedTask, challenge]); + // if the clicked challenge is already in selectedTask, filter it out of selectedTask + } else { + // log the deselection + console.log(`${challenge} button deselected!`); + setSelectedTask(selectedTask.filter(item => item !== challenge)); + } + } + + // function to save selected tasks to firestore and local storage + async function saveTasks(){ + if (!user){ + console.log('User not authenticated yet') + return; + } + const userDocRef = doc(db, "users", user.uid); + try { + await updateDoc(userDocRef, { + challengesSelected: selectedTask + }); + + console.log("User saved these tasks:", selectedTask); + + // then route to the challenges page + navigate("/challenges"); + } catch (error) { + console.error("Error while saving user tasks to firestore", error); + console.log("Error sending tasks to database"); + } } - } if (!isOpen) return null; return ( -
-
+
+
-

Detox Challenge Options

+ {/* buttons */} -
+
-
); diff --git a/src/firebase.js b/src/firebase.js index 088faf2..a33ba6f 100644 --- a/src/firebase.js +++ b/src/firebase.js @@ -1,7 +1,7 @@ // Import the functions you need from the SDKs you need import { initializeApp } from 'firebase/app'; import { getFirestore } from 'firebase/firestore'; -import { getAuth } from 'firebase/auth'; +import { browserLocalPersistence, getAuth, setPersistence } from 'firebase/auth'; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries @@ -19,6 +19,7 @@ const firebaseConfig = { // Initialize Firebase const app = initializeApp(firebaseConfig); const auth = getAuth(app); +setPersistence(auth, browserLocalPersistence); // Initialize Cloud Firestore and get a reference to the service const db = getFirestore(app); export { db, auth }; diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 4b10d49..9e5e6dd 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,76 +1,161 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import Button from "../components/Button"; import DashboardTile from "../components/Dashboard/DashboardTile"; import TaskModal from "../components/Dashboard/TaskModal"; +import { auth, db } from "../firebase"; +import { doc, onSnapshot } from "firebase/firestore"; +import { onAuthStateChanged } from "firebase/auth"; +import { useNavigate } from "react-router-dom"; -let daysCompleted = 0; -let daysLeft = 30 - daysCompleted; let loginStreak = 1; export default function Dashboard() { + const [user, setUser] = useState(null); const [taskModalOpen, setTaskModalOpen] = useState(false); + // state for whether tasks have been selected or not from dashboard modal + const [hasTasks, setHasTasks] = useState(false); + // state for challenge task completed days + const [completedDays, setCompletedDays] = useState(0); + let daysCompleted = completedDays; + let daysLeft = 30 - daysCompleted; + + // define variable for the authenticated user + // const user = auth.currentUser; + + // useEffect to ensure the authenticated user loads before dashboard data and state is determined + useEffect(() => { + console.log("User is loading...") + const unsub = onAuthStateChanged(auth, (currentUser) => { + setUser(currentUser); + console.log('The user has been found') + }); + + return () => { + unsub(); + } + }, []); + + useEffect(() => { + console.log("About to load the data from the user...") + // dont run anything if the user isn't loaded + if (!user) { + console.log('user wasnt loaded yet or not found'); + return; + } else { + console.log('user found'); + } + + // define variables + const userDocRef = doc(db, "users", user.uid); + + // listen in real-time for changes to the document + const unsub = onSnapshot(userDocRef, (userDocSnap) => { + console.log('User has already selected these tasks: ', userDocSnap.data().challengesSelected); + // check if the document's challengesSelected field is > 0 to display the full dashboard and update state + if (userDocSnap.exists() && userDocSnap.data().challengesSelected.length > 0){ + setHasTasks(true); + } else { + setHasTasks(false); + console.log('user hasnt selected any tasks or deselected') + } + + // check the document's completedDays for the challenges to update state - add optional chaining in case thee user hasn't started any challenges yet + if (userDocSnap.data().journalProgress?.completedDays > 0){ + setCompletedDays(userDocSnap.data().journalProgress.completedDays); + } else { + console.log('No days completed yet') + setCompletedDays(0); + }; + }); + + // call this function once u exit so it stops looking for the changes in the database + return () => { + unsub(); + } + }, [user]); + + // define variable for a progress day array showing 3 most recent completed tasks, filter numbers over 0 for empty state + let progressArray = [completedDays, completedDays - 1, completedDays - 2].filter(num => num > 0); + console.log("Progress array:", progressArray); + + // logic to click tile and navigate to community + let navigate = useNavigate(); return (
{/* title and button */} -
-

Welcome Back!

+
+

+ {/* conditional render for new user vs returning */} + {hasTasks ? 'Welcome Back!' : 'Welcome New User!'} +

- setTaskModalOpen(false)} /> - {/* card grid */} -
- {/* row 1 */} - setTaskModalOpen(false)}/> + {/* conditional render for empty state */} + {hasTasks ? ( + /* card grid */ +
+ {/* row 1 */} + - - - - {/* row 2 */} - - -
+ + {/* row 2 */} + + navigate("/community")} + onKeyDown={(event) => event.key === 'Enter' ? navigate("/community") : null} + /> +
+ ) : ( +
+ illustration of person holding balloons with question marks +

Nothing to Display Yet!

+
+ )}
); } \ No newline at end of file diff --git a/src/pages/journal.jsx b/src/pages/journal.jsx index 9579a00..92cb9b9 100644 --- a/src/pages/journal.jsx +++ b/src/pages/journal.jsx @@ -4,7 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; // firebase import { db, auth } from '../firebase'; -import { doc, setDoc, onSnapshot } from 'firebase/firestore'; +import { doc, setDoc, onSnapshot, updateDoc } from 'firebase/firestore'; import { onAuthStateChanged } from 'firebase/auth'; //components @@ -148,6 +148,14 @@ const Journal = () => { console.error('Error saving journal entry:', error) ); + // firestore save for dashboard under users collection + const userDocRef = doc(db, "users", userId); + updateDoc(userDocRef, { + journalProgress: { + completedDays: finalEffectiveDays + } + }); + return { effectiveDays: finalEffectiveDays, entries: newEntries,