diff --git a/next.config.mjs b/next.config.mjs index 4678774..425666a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + domains: ['img.clerk.com'], + }, +}; export default nextConfig; diff --git a/src/app/components/AddNewTradeBtn.js b/src/app/components/AddNewTradeBtn.js index 6501fe6..7cab9ad 100644 --- a/src/app/components/AddNewTradeBtn.js +++ b/src/app/components/AddNewTradeBtn.js @@ -1,4 +1,4 @@ -import { FaPlus } from 'react-icons/fa'; // Importing a plus icon from react-icons +import { FaPlus } from 'react-icons/fa'; import PropTypes from 'prop-types'; const AddNewTradeBtn = ({ onAddTrade }) => { diff --git a/src/app/components/AddNewTradeFormModal.js b/src/app/components/AddNewTradeFormSidebar.js similarity index 97% rename from src/app/components/AddNewTradeFormModal.js rename to src/app/components/AddNewTradeFormSidebar.js index 1c46cbe..6d73664 100644 --- a/src/app/components/AddNewTradeFormModal.js +++ b/src/app/components/AddNewTradeFormSidebar.js @@ -112,17 +112,17 @@ const Content = ({ tickers = MOCK_TICKERS, onSubmit, onClose }) => {
{!isSubmitted && ( <> -

+

Add New Trade

-

+

Fill out the form below to record your trade details.

@@ -543,19 +543,19 @@ Content.propTypes = { onClose: PropTypes.func.isRequired, }; -const AddNewTradeFormModal = ({ tickers = MOCK_TICKERS, onSubmit, onClose }) => { +const AddNewTradeFormSidebar = ({ tickers = MOCK_TICKERS, onSubmit, onClose }) => { return createPortal( - + , document.body, ); }; -AddNewTradeFormModal.propTypes = { +AddNewTradeFormSidebar.propTypes = { tickers: PropTypes.arrayOf(PropTypes.string), onSubmit: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, }; -export default AddNewTradeFormModal; +export default AddNewTradeFormSidebar; diff --git a/src/app/components/AllTradesBtn.js b/src/app/components/AllTradesBtn.js index 54ab0b4..070af9b 100644 --- a/src/app/components/AllTradesBtn.js +++ b/src/app/components/AllTradesBtn.js @@ -1,5 +1,5 @@ -import { FaPlus } from 'react-icons/fa'; // Importing a plus icon from react-icons import PropTypes from 'prop-types'; +import { FaPlus } from 'react-icons/fa'; const AllTradesBtn = ({ onSeeAllTrades }) => { return ( diff --git a/src/app/components/CustomCalendar.js b/src/app/components/CustomCalendar.js index ec5c4a3..f6514a2 100644 --- a/src/app/components/CustomCalendar.js +++ b/src/app/components/CustomCalendar.js @@ -23,7 +23,7 @@ const TradingCalendarView = ({ trades, onUserSelectedDate }) => { const end = endOfMonth(currentMonth); const days = eachDayOfInterval({ start, end }); - // Aggregate trades by date + // aggregate trades by date const totalsByDate = trades.reduce((acc, trade) => { const tradeDate = format(new Date(trade.tradeDate), 'yyyy-MM-dd'); const outcome = parseFloat(trade.priceClosed) - parseFloat(trade.priceOpened); @@ -33,7 +33,7 @@ const TradingCalendarView = ({ trades, onUserSelectedDate }) => { return acc; }, {}); - // Monthly stats calculations + // monthly stats calculations const monthlyStats = trades.reduce( (acc, trade) => { const tradeDate = new Date(trade.tradeDate); @@ -56,12 +56,9 @@ const TradingCalendarView = ({ trades, onUserSelectedDate }) => { const handleToday = () => setCurrentMonth(today); const handleDayClick = (day) => { - console.log(day); const dateKey = format(day, 'yyyy-MM-dd'); - console.log(dateKey); - if (totalsByDate[dateKey]) { - onUserSelectedDate(dateKey); // Call the prop with the clicked date if it has trades data + onUserSelectedDate(dateKey); } }; @@ -186,23 +183,23 @@ const TradingCalendarView = ({ trades, onUserSelectedDate }) => { {days.map((day) => { const dateKey = format(day, 'yyyy-MM-dd'); const total = totalsByDate[dateKey] || 0; - const isWeekend = [0, 6].includes(getDay(day)); // Check if it's Saturday or Sunday + const isWeekend = [0, 6].includes(getDay(day)); const highlightToday = isToday(day) ? 'ring-2 ring-indigo-600' : ''; - const isClickable = totalsByDate[dateKey] !== undefined; // Check if the day has trade data + const isClickable = totalsByDate[dateKey] !== undefined; return (
isClickable && handleDayClick(day)} // Only call handleDayClick if clickable + onClick={() => isClickable && handleDayClick(day)} className={`p-4 rounded-lg shadow-sm ${highlightToday} ${isClickable ? 'cursor-pointer' : 'cursor-default'}`} style={{ background: isWeekend - ? 'rgba(169, 169, 169, 0.3)' // Light gray for weekends in light mode + ? 'rgba(169, 169, 169, 0.3)' : total > 0 - ? 'rgba(0, 128, 0, 0.1)' // Green for positive trades + ? 'rgba(0, 128, 0, 0.1)' : total < 0 - ? 'rgba(255, 0, 0, 0.1)' // Red for negative trades - : 'var(--background)', // Default background for days with no trades + ? 'rgba(255, 0, 0, 0.1)' + : 'var(--background)', color: 'var(--foreground)', }} > diff --git a/src/app/components/CustomCalendarMobile.js b/src/app/components/CustomCalendarMobile.js index 7063421..e329ad4 100644 --- a/src/app/components/CustomCalendarMobile.js +++ b/src/app/components/CustomCalendarMobile.js @@ -13,7 +13,7 @@ const TradingCalendarViewMobile = ({ trades, onUserSelectedDate }) => { const start = startOfMonth(currentMonth); const end = endOfMonth(currentMonth); - // Aggregate trades by date + // aggregate trades by date const totalsByDate = trades.reduce((acc, trade) => { const tradeDate = format(new Date(trade.tradeDate), 'yyyy-MM-dd'); const outcome = parseFloat(trade.priceClosed) - parseFloat(trade.priceOpened); @@ -23,7 +23,7 @@ const TradingCalendarViewMobile = ({ trades, onUserSelectedDate }) => { return acc; }, {}); - // Monthly stats calculations + // monthly stats calculations const monthlyStats = trades.reduce( (acc, trade) => { const tradeDate = new Date(trade.tradeDate); @@ -51,12 +51,9 @@ const TradingCalendarViewMobile = ({ trades, onUserSelectedDate }) => { }); const handleDayClick = (day) => { - console.log(day); const dateKey = format(day, 'yyyy-MM-dd'); - console.log(dateKey); - if (totalsByDate[dateKey]) { - onUserSelectedDate(dateKey); // Call the prop with the clicked date if it has trades data + onUserSelectedDate(dateKey); } }; @@ -161,13 +158,13 @@ const TradingCalendarViewMobile = ({ trades, onUserSelectedDate }) => { {/* Days List */}
{days.map((day) => { - const isWeekend = [0, 6].includes(day.date.getDay()); // Check if it's a weekend + const isWeekend = [0, 6].includes(day.date.getDay()); const textColor = !isWeekend && day.total > 0 ? 'text-green-600' : !isWeekend && day.total < 0 ? 'text-red-600' - : 'text-gray-400'; // Grayed-out for weekends and no trades + : 'text-gray-400'; const highlightToday = isToday(day.date) ? 'ring-2 ring-indigo-600' : ''; return ( @@ -176,12 +173,12 @@ const TradingCalendarViewMobile = ({ trades, onUserSelectedDate }) => { className={`flex items-center justify-between p-3 rounded-lg shadow-sm ${highlightToday}`} style={{ background: isWeekend - ? 'rgba(169, 169, 169, 0.3)' // Light gray for weekends + ? 'rgba(169, 169, 169, 0.3)' : day.total > 0 - ? 'rgba(0, 128, 0, 0.1)' // Green for positive trades + ? 'rgba(0, 128, 0, 0.1)' : day.total < 0 - ? 'rgba(255, 0, 0, 0.1)' // Red for negative trades - : 'var(--background)', // Default background for no trades + ? 'rgba(255, 0, 0, 0.1)' + : 'var(--background)', color: 'var(--foreground)', }} onClick={() => !isWeekend && handleDayClick(day.date)} @@ -196,7 +193,7 @@ const TradingCalendarViewMobile = ({ trades, onUserSelectedDate }) => {
{isWeekend - ? 'No Trades' // Always "No Trades" for weekends + ? 'No Trades' // always "No Trades" for weekends : day.total > 0 ? `+${day.total}` : day.total < 0 diff --git a/src/app/components/DeleteAccountSidebar.js b/src/app/components/DeleteAccountSidebar.js new file mode 100644 index 0000000..78a20c0 --- /dev/null +++ b/src/app/components/DeleteAccountSidebar.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; +import TransitionOverlay from './TransitionOverlay'; + +function Content({ handleDeleteAccount, onClose, isDeletingUserDetails }) { + return ( + <> +

+ Are you sure you want to delete your account? +

+

+ This action is irreversible and will permanently delete all your data. +

+
+ + +
+ + ); +} + +Content.propTypes = { + handleDeleteAccount: PropTypes.func.isRequired, + isDeletingUserDetails: PropTypes.bool, + deletingUserDetailsError: PropTypes.string, + onClose: PropTypes.func.isRequired, +}; + +const DeleteAccountSidebar = ({ isDeletingUserDetails, handleDeleteAccount, onClose }) => { + return createPortal( + + + , + document.body, + ); +}; + +export default DeleteAccountSidebar; diff --git a/src/app/components/EditTradeBtn.js b/src/app/components/EditTradeBtn.js index 72df42f..1f26b52 100644 --- a/src/app/components/EditTradeBtn.js +++ b/src/app/components/EditTradeBtn.js @@ -4,9 +4,7 @@ import PropTypes from 'prop-types'; const EditTradeBtn = ({ onEditTrade }) => { return (
- {/* Container for button and label */}
- {/* The button itself */} {!isSubmitted && ( <> -

+

Edit Trade Details

-

+

Update the form below to edit your trade details.

@@ -499,7 +502,7 @@ Content.propTypes = { onClose: PropTypes.func.isRequired, }; -const EditTradeFormModal = ({ +const EditTradeFormSidebar = ({ tradeToEdit = MOCK_TRADE_SUBMISSION, tickers = MOCK_TICKERS, onSubmit, @@ -512,11 +515,11 @@ const EditTradeFormModal = ({ ); }; -EditTradeFormModal.propTypes = { +EditTradeFormSidebar.propTypes = { tradeToEdit: PropTypes.object, tickers: PropTypes.array, onSubmit: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, }; -export default EditTradeFormModal; +export default EditTradeFormSidebar; diff --git a/src/app/components/FeatureChecklist.js b/src/app/components/FeatureChecklist.js index a3fdc8c..c0b3c74 100644 --- a/src/app/components/FeatureChecklist.js +++ b/src/app/components/FeatureChecklist.js @@ -8,7 +8,7 @@ const FeatureChecklist = () => {

What We Offer @@ -19,15 +19,15 @@ const FeatureChecklist = () => {
@@ -35,7 +35,7 @@ const FeatureChecklist = () => {

Personalized Account @@ -43,7 +43,7 @@ const FeatureChecklist = () => {

@@ -62,7 +62,7 @@ const FeatureChecklist = () => {

@@ -98,7 +98,7 @@ const FeatureChecklist = () => {
diff --git a/src/app/components/PostAccountUpdateSidebar.js b/src/app/components/PostAccountUpdateSidebar.js new file mode 100644 index 0000000..6410dd5 --- /dev/null +++ b/src/app/components/PostAccountUpdateSidebar.js @@ -0,0 +1,62 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; +import TransitionOverlay from './TransitionOverlay'; + +function AccountUpdateSuccessContent({ onClose }) { + return ( + <> +

Settings Updated!

+

Please refresh the page to see latest changes.

+
+ +
+ + ); +} + +AccountUpdateSuccessContent.propTypes = { + onClose: PropTypes.func.isRequired, +}; + +function AccountUpdateErrorContent({ onClose, error }) { + return ( + <> +

Settings Update Error

+

{error}

+
+ +
+ + ); +} + +AccountUpdateErrorContent.propTypes = { + onClose: PropTypes.func.isRequired, + error: PropTypes.string.isRequired, +}; + +const PostAccountUpdateSidebar = ({ onClose, accountUpdateError }) => { + return createPortal( + + {accountUpdateError ? ( + + ) : ( + + )} + , + document.body, + ); +}; + +export default PostAccountUpdateSidebar; diff --git a/src/app/components/TradesSidebar.js b/src/app/components/TradesByDateSidebar.js similarity index 97% rename from src/app/components/TradesSidebar.js rename to src/app/components/TradesByDateSidebar.js index 570d302..9226a1e 100644 --- a/src/app/components/TradesSidebar.js +++ b/src/app/components/TradesByDateSidebar.js @@ -133,13 +133,13 @@ Content.propTypes = { onClose: PropTypes.func.isRequired, }; -const TradesSidebar = ({ date = '2024-12-21', trades = [], onClose }) => { +const TradesByDateSidebar = ({ date = '2024-12-21', trades = [], onClose }) => { return createPortal( , - document.body, // Render modal into the body + document.body, ); }; -export default TradesSidebar; +export default TradesByDateSidebar; diff --git a/src/app/components/TransitionOverlay.js b/src/app/components/TransitionOverlay.js index 40a8379..de62321 100644 --- a/src/app/components/TransitionOverlay.js +++ b/src/app/components/TransitionOverlay.js @@ -6,7 +6,6 @@ const TransitionOverlay = ({ onClose, children }) => { const entryTimerRef = useRef(null); const exitTimerRef = useRef(null); - // Trigger visibility state change after the component mounts useEffect(() => { entryTimerRef.current = setTimeout(() => { setIsVisible(true); @@ -17,9 +16,8 @@ const TransitionOverlay = ({ onClose, children }) => { clearTimeout(entryTimerRef.current); } }; - }, []); // This runs when the modal is first opened + }, []); - // Handling Scroll Locking (On Mount and Cleanup) useEffect(() => { document.body.classList.add('overflow-hidden'); return () => { @@ -27,18 +25,13 @@ const TransitionOverlay = ({ onClose, children }) => { }; }, []); - // Handle the visibility transition (and call onClose after exit animation) const handleClose = () => { - // Trigger exit animation by setting visibility to false setIsVisible(false); - - // After the exit animation ends (500ms), call onClose to remove the modal exitTimerRef.current = setTimeout(() => { onClose(); - }, 150); // Duration should match the transition duration + }, 150); }; - // Cleanup timeout when the modal is unmounted or handleClose is triggered again useEffect(() => { return () => { if (exitTimerRef.current) { diff --git a/src/app/dashboard/page.js b/src/app/dashboard/page.js index 3bce665..c54698a 100644 --- a/src/app/dashboard/page.js +++ b/src/app/dashboard/page.js @@ -1,138 +1,99 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import TradingCalendarView from '../components/CustomCalendar'; import TradingCalendarViewMobile from '../components/CustomCalendarMobile'; import AddNewTradeBtn from '../components/AddNewTradeBtn'; -import AddNewTradeFormModal from '../components/AddNewTradeFormModal'; +import AddNewTradeFormSidebar from '../components/AddNewTradeFormSidebar'; import { MOCK_TRADE_SUBMISSION, MOCK_TRADES_SIMPLE } from '../data/mock-trades'; import EditTradeBtn from '../components/EditTradeBtn'; -import EditTradeFormModal from '../components/EditTradeFormModal'; -import TradesSidebar from '../components/TradesSidebar'; -import { useUser } from '@clerk/nextjs'; -// import { doc, getDoc, setDoc } from 'firebase/firestore'; -// import { FIREBASE_DB } from '../../services/firebase/config'; -// import { getData } from '@/services/firebase/getData'; -import { fetchUserById, saveNewUser } from '@/utils/users'; -// import { useAuth, useUser } from '@clerk/nextjs'; +import EditTradeFormSidebar from '../components/EditTradeFormSidebar'; +import TradesByDateSidebar from '../components/TradesByDateSidebar'; +import { useUserDetails } from '../hooks/useUserDetails'; +import { useIsMobile } from '../hooks/useIsMobile'; const DashboardPage = () => { - const [isMobile, setIsMobile] = useState(false); - const [isAddNewTradeFormModalOpen, setIsAddNewTradeFormModalOpen] = useState(false); - const [isEditTradeFormModalOpen, setIsEditTradeFormModalOpen] = useState(false); + const isMobile = useIsMobile(768); + const { + userDetails, + isLoading: isLoadingUserDetails, + error: userDetailsError, + } = useUserDetails(); + + const [isAddNewTradeFormSidebarOpen, setIsAddNewTradeFormSidebarOpen] = useState(false); + const [isEditTradeFormSidebarOpen, setIsEditTradeFormSidebarOpen] = useState(false); const [isTradesSidebarOpen, setIsTradesSidebarOpen] = useState(false); - const [selectedDate, setSelectedDate] = useState(null); // Track the selected date - const [userProfile, setUserProfile] = useState(null); - - const { user } = useUser() || {}; - - console.log({ user }); - - useEffect(() => { - async function setupUserProfile() { - // check if user exists and logged in - if (!user) return; - - // get user id of logged in user from clerk auth hook - const userId = user?.id; - - if (!userId || typeof userId !== 'string' || !userId?.length) return; - - // fetch user by user id from firebase DB - const [fetchUserError, fetchedUser] = await fetchUserById(userId); - - if (fetchUserError) return; - - // if user exists, save the data to local userProfile state - if (fetchedUser !== null) { - console.log('saved user profile to local state'); - setUserProfile(fetchedUser); - } else { - console.log('ready to create new user to DB'); - - // if user does not exist, create a new user document - const newUser = { - userId: user?.id, - fullName: user?.fullName || '', - email: user?.emailAddresses[0]?.emailAddress || '', - profilePicture: user?.imageUrl || '', - joinedAt: new Date().toISOString(), - }; - const [saveNewUserError, newUserId] = await saveNewUser(userId, newUser); - - if (saveNewUserError) return; - - // refetch user id from firebase DB and save the data to local userProfile state - const [fetchNewUserError, fetchedNewUser] = await fetchUserById(newUserId); - - if (fetchNewUserError) return; - - // if user exists, save the data to local userProfile state - if (fetchedNewUser !== null) { - console.log('saved user profile to local state'); - setUserProfile(fetchedNewUser); - } - } - } - setupUserProfile(); - }, [user]); - - useEffect(() => { - const checkScreenSize = () => { - if (window.innerWidth <= 768) { - setIsMobile(true); - } else { - setIsMobile(false); - } - }; - - window.addEventListener('resize', checkScreenSize); - - checkScreenSize(); - - return () => window.removeEventListener('resize', checkScreenSize); - }, []); + const [userSelectedDate, setUserSelectedDate] = useState(null); const handleAddNewTrade = () => { - setIsAddNewTradeFormModalOpen(true); + setIsAddNewTradeFormSidebarOpen(true); }; - const handleCloseAddNewTradeFormModal = () => { - setIsAddNewTradeFormModalOpen(false); + const handleCloseAddNewTradeFormSidebar = () => { + setIsAddNewTradeFormSidebarOpen(false); }; const handleUpdateTrade = () => { - setIsEditTradeFormModalOpen(true); + setIsEditTradeFormSidebarOpen(true); }; - const handleCloseEditTradeFormModal = () => { - setIsEditTradeFormModalOpen(false); + const handleCloseEditTradeFormSidebar = () => { + setIsEditTradeFormSidebarOpen(false); }; const handleCloseTradesSidebar = () => { setIsTradesSidebarOpen(false); }; - // Update selected date when user clicks on a date const handleUserSelectedDate = (date) => { - setSelectedDate(date); - setIsTradesSidebarOpen(true); // Open sidebar when a date is selected + setUserSelectedDate(date); + setIsTradesSidebarOpen(true); }; - console.log({ userProfile }); + const handleNewTradeSubmission = (formData) => { + const newTrade = { + ticker: formData?.ticker || '', + tradeType: formData?.tradeType || 'long', + tradeDate: (formData?.tradeDate && new Date(formData?.tradeDate).toISOString()) || '', + shares: formData?.shares || '', + priceOpened: formData?.priceOpened || '', + priceClosed: formData?.priceClosed || '', + stopLoss: formData?.stopLoss || '', + takeProfit: formData?.takeProfit || '', + notes: formData?.notes || '', + }; + console.log({ newTrade }); + }; + + if (isLoadingUserDetails) { + return ( +
+

Loading your account details...

+
+ ); + } + + if (userDetailsError) { + return ( +
+

{userDetailsError}

+
+ ); + } return (
+ {userDetails &&
{JSON.stringify({ userDetails }, null, 2)}
} {isMobile ? ( ) : ( )}
@@ -140,30 +101,28 @@ const DashboardPage = () => { - {isAddNewTradeFormModalOpen && ( - console.log({ formData })} + {isAddNewTradeFormSidebarOpen && ( + handleNewTradeSubmission(formData)} /> )} - {isEditTradeFormModalOpen && ( - console.log({ formData })} /> )} - {/* Sidebar Modal */} - {isTradesSidebarOpen && selectedDate && ( - // - - // )}
); diff --git a/src/app/hooks/useIsMobile.js b/src/app/hooks/useIsMobile.js new file mode 100644 index 0000000..b384b69 --- /dev/null +++ b/src/app/hooks/useIsMobile.js @@ -0,0 +1,19 @@ +import { useState, useEffect } from 'react'; + +export const useIsMobile = (breakpoint = 768) => { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkScreenSize = () => { + setIsMobile(window.innerWidth <= breakpoint); + }; + + window.addEventListener('resize', checkScreenSize); + checkScreenSize(); + + return () => { + window.removeEventListener('resize', checkScreenSize); + }; + }, [breakpoint]); + return isMobile; +}; diff --git a/src/app/hooks/useUserDetails.js b/src/app/hooks/useUserDetails.js new file mode 100644 index 0000000..f7b22af --- /dev/null +++ b/src/app/hooks/useUserDetails.js @@ -0,0 +1,72 @@ +import { useState, useEffect } from 'react'; +import { useUser } from '@clerk/nextjs'; +import { fetchUserById, saveNewUser } from '@/utils/users'; + +export const useUserDetails = () => { + const [userDetails, setUserDetails] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { user } = useUser() || {}; + + useEffect(() => { + const setupUserDetails = async () => { + if (!user?.id) { + setIsLoading(false); + return; + } + + try { + const userId = user.id; + const [fetchUserByIdError, fetchedUser] = await fetchUserById(userId); + + if (fetchUserByIdError) { + console.error(fetchUserByIdError); + throw new Error( + 'Oops! Unable to get user details at the moment. Try refreshing the page OR try again later', + ); + } + if (fetchedUser) { + setUserDetails(fetchedUser); + } else { + const newUser = { + userId, + fullName: user.fullName || '', + email: user.emailAddresses?.[0]?.emailAddress || '', + profilePicture: user.imageUrl || '', + joinedAt: new Date().toISOString(), + }; + + const [saveNewUserError, newUserId] = await saveNewUser(newUser); + + if (saveNewUserError || !newUserId) { + console.error(saveNewUserError); + throw new Error( + 'Oops! Unable to save new user details at the moment. Try refreshing the page OR try again later', + ); + } + const [fetchNewUserError, fetchedNewUser] = await fetchUserById(userId); + + if (fetchNewUserError) { + console.error(fetchNewUserError); + throw new Error( + 'Oops! Unable to get user details at the moment. Try refreshing the page OR try again later', + ); + } + setUserDetails(fetchedNewUser); + } + } catch (error) { + setError(error.message); + } finally { + setIsLoading(false); + } + }; + + setupUserDetails(); + }, [user]); + + return { + userDetails, + isLoading, + error, + }; +}; diff --git a/src/app/settings/page.js b/src/app/settings/page.js index 95b1801..275557c 100644 --- a/src/app/settings/page.js +++ b/src/app/settings/page.js @@ -1,13 +1,24 @@ 'use client'; +import { useState, useEffect } from 'react'; +import { useUserDetails } from '../hooks/useUserDetails'; +import Image from 'next/image'; +import DeleteAccountSidebar from '../components/DeleteAccountSidebar'; +import { deleteUserById, updateUserById } from '@/utils/users'; +import PostAccountUpdateSidebar from '../components/PostAccountUpdateSidebar'; import { useUser } from '@clerk/nextjs'; -import { useState } from 'react'; const AccountPage = () => { - const { user, isLoaded, isSignedIn } = useUser(); + const { user } = useUser(); - const [profileData, setProfileData] = useState({ - fullName: user?.fullName || '', + const { + userDetails, + isLoading: isLoadingUserDetails, + error: userDetailsError, + } = useUserDetails(); + + const [settingsData, setSettingsData] = useState({ + fullName: '', theme: 'light', dashboardViews: { monthly: true, @@ -16,13 +27,46 @@ const AccountPage = () => { }, }); + const [isDeleteAccountSidebarOpen, setDeleteAccountSidebarOpen] = useState(false); + const [isPostAccountUpdateSidebarOpen, setIsPostAccountUpdateSidebarOpen] = useState(false); + + const [isUpdatingUserDetails, setIsUpdatingUserDetails] = useState(false); + const [updatingUserDetailsError, setUpdatingUserDetailsError] = useState(null); + + const [isDeletingUserDetails, setIsDeletingUserDetails] = useState(false); + + useEffect(() => { + if (userDetails) { + setSettingsData((prev) => ({ + ...prev, + fullName: userDetails?.fullName || '', + theme: typeof userDetails?.theme === 'string' ? userDetails?.theme : 'light', + dashboardViews: { + ...prev.dashboardViews, + monthly: + typeof userDetails?.dashboardViews?.monthly === 'boolean' + ? userDetails?.dashboardViews?.monthly + : true, + weekly: + typeof userDetails?.dashboardViews?.weekly === 'boolean' + ? userDetails?.dashboardViews?.weekly + : true, + daily: + typeof userDetails?.dashboardViews?.daily === 'boolean' + ? userDetails?.dashboardViews?.daily + : true, + }, + })); + } + }, [userDetails]); + const handleProfileChange = (e) => { const { name, value } = e.target; - setProfileData((prev) => ({ ...prev, [name]: value })); + setSettingsData((prev) => ({ ...prev, [name]: value })); }; const handleViewToggle = (view) => { - setProfileData((prev) => ({ + setSettingsData((prev) => ({ ...prev, dashboardViews: { ...prev.dashboardViews, @@ -31,17 +75,92 @@ const AccountPage = () => { })); }; - if (!isLoaded || !isSignedIn) { + const handleDeleteAccount = () => { + setIsDeletingUserDetails(true); + setTimeout(async () => { + await deleteUserById(userDetails?.userId); + await user.delete(); + window.location.href = '/'; + }, 1500); + }; + + const openDeleteAccountSidebar = () => { + setDeleteAccountSidebarOpen(true); + }; + + const closeDeleteAccountSidebar = () => { + setDeleteAccountSidebarOpen(false); + }; + + const closeAccountUpdatedSidebar = () => { + window.location.href = '/settings'; + }; + + const hasSettingsChanged = () => { + if (settingsData.fullName !== userDetails?.fullName) return true; + if (settingsData.theme !== userDetails?.theme) return true; + if (settingsData.dashboardViews.daily !== userDetails?.dashboardViews?.daily) return true; + if (settingsData.dashboardViews.monthly !== userDetails?.dashboardViews?.monthly) return true; + if (settingsData.dashboardViews.weekly !== userDetails?.dashboardViews?.weekly) return true; + return false; + }; + + const onSaveChanges = async () => { + if (!hasSettingsChanged()) return; + setIsUpdatingUserDetails(true); + + try { + const updatedUser = { + ...userDetails, + theme: settingsData.theme, + fullName: settingsData.fullName, + dashboardViews: { + ...(userDetails?.dashboardViews || {}), + monthly: settingsData.dashboardViews.monthly, + weekly: settingsData.dashboardViews.weekly, + daily: settingsData.dashboardViews.daily, + }, + }; + const [updateUserError, fetchedUpdatedUser] = await updateUserById( + userDetails?.userId, + updatedUser, + ); + + if (updateUserError || !fetchedUpdatedUser) { + console.error(updateUserError); + throw new Error( + 'Oops! Unable to save new changes to your account at the moment. Try again later.', + ); + } + } catch (error) { + setUpdatingUserDetailsError(error.message); + } finally { + setIsPostAccountUpdateSidebarOpen(true); + } + }; + + const disableSaveChanges = !hasSettingsChanged() || isUpdatingUserDetails; + + if (isLoadingUserDetails) { return ( -
+

Loading your account details...

-
+ + ); + } + + if (userDetailsError) { + return ( +
+

{userDetailsError}

; +
); } return (
+ {userDetails &&
{JSON.stringify({ userDetails }, null, 2)}
} {/* Page Header */}

Account Settings

@@ -53,38 +172,70 @@ const AccountPage = () => { {/* Profile Section */}

Profile Details

-
- {/* Full Name Input */} -
- - +
+ {/* Profile Picture */} +
+ {userDetails?.profilePicture ? ( + Profile Picture + ) : null}
- {/* Theme Preference Dropdown */} -
- - + {/* Input Fields */} +
+ {/* Full Name Input */} +
+ + +
+ + {/* Email Field (Disabled) */} +
+ + +
+ + {/* Theme Preference Dropdown */} +
+ + +
@@ -93,7 +244,7 @@ const AccountPage = () => {

Dashboard Views

- {Object.entries(profileData.dashboardViews).map(([view, isVisible]) => ( + {Object.entries(settingsData.dashboardViews).map(([view, isVisible]) => (
{ {/* Save Changes */}
+ + {/* Account Deletion Section */} +
+

Delete Account

+

+ Once you delete your account, all your data will be permanently removed and cannot be + recovered. +

+ +
+ + {/* Delete Confirmation Sidebar */} + {isDeleteAccountSidebarOpen && ( + + )} + + {/* Account Updated Sidebar */} + {isPostAccountUpdateSidebarOpen && ( + + )}
); }; diff --git a/src/services/firebase/addData.js b/src/services/firebase/addData.js deleted file mode 100644 index 06ac755..0000000 --- a/src/services/firebase/addData.js +++ /dev/null @@ -1,26 +0,0 @@ -import { doc, setDoc } from 'firebase/firestore'; -import { FIREBASE_DB } from './config'; // Adjust the path as needed - -/** - * Save a piece of data to a Firestore collection. - * @param {string} collectionName - The name of the collection. - * @param {string} documentId - The ID of the document to save or update. - * @param {object} data - The data to save. - * @returns {Promise<{ success: boolean, message?: string, error?: string }>} - * Returns an object with `success: true` if successful, or `success: false` and `error` if not. - */ -export async function addData(collectionName, documentId, data) { - try { - const docRef = doc(FIREBASE_DB, collectionName, documentId); - await setDoc(docRef, data); - - return { - data: documentId, - }; - } catch (error) { - console.error(`Error saving document to "${collectionName}/${documentId}":`, error); - return { - error: `Error saving document to "${collectionName}/${documentId}"`, - }; - } -} diff --git a/src/services/firebase/getData.js b/src/services/firebase/getData.js deleted file mode 100644 index 2f2e54c..0000000 --- a/src/services/firebase/getData.js +++ /dev/null @@ -1,32 +0,0 @@ -import { doc, getDoc } from 'firebase/firestore'; -import { FIREBASE_DB } from './config'; // Adjust the path as needed - -/** - * Fetch a document from Firestore. - * @param {string} collectionName - The name of the collection. - * @param {string} documentId - The ID of the document to fetch. - * @returns {Promise<{ success: boolean, data?: object, error?: string }>} - * Returns an object with `success: true` and `data` if successful, or `success: false` and `error` if not. - */ -export async function getData(collectionName, documentId) { - try { - const docRef = doc(FIREBASE_DB, collectionName, documentId); - const docSnap = await getDoc(docRef); - - if (docSnap.exists()) { - return { - data: docSnap.data(), - }; - } else { - console.warn(`No document found in collection "${collectionName}" with ID "${documentId}".`); - return { - data: null, - }; - } - } catch (error) { - console.error(`Error fetching document from "${collectionName}/${documentId}":`, error); - return { - error: `Error fetching document from "${collectionName}/${documentId}`, - }; - } -} diff --git a/src/services/firebase/index.js b/src/services/firebase/index.js new file mode 100644 index 0000000..cf1cf05 --- /dev/null +++ b/src/services/firebase/index.js @@ -0,0 +1,65 @@ +import { deleteDoc, doc, getDoc, setDoc } from 'firebase/firestore'; +import { FIREBASE_DB } from './config'; + +export async function addData(collectionName, documentId, data) { + try { + const docRef = doc(FIREBASE_DB, collectionName, documentId); + await setDoc(docRef, data); + return { data: documentId }; + } catch (error) { + console.error(`Error saving document to "${collectionName}/${documentId}":`, error); + return { + error: `Error saving document to "${collectionName}/${documentId}"`, + }; + } +} + +export async function getData(collectionName, documentId) { + try { + const docRef = doc(FIREBASE_DB, collectionName, documentId); + const docSnap = await getDoc(docRef); + if (docSnap.exists()) { + return { data: docSnap.data() }; + } else { + console.warn(`No document found in collection "${collectionName}" with ID "${documentId}".`); + return { data: null }; + } + } catch (error) { + console.error(`Error fetching document from "${collectionName}/${documentId}":`, error); + return { + error: `Error fetching document from "${collectionName}/${documentId}`, + }; + } +} + +export async function updateData(collectionName, documentId, updatedData) { + try { + const docRef = doc(FIREBASE_DB, collectionName, documentId); + const docSnap = await getDoc(docRef); + const existingData = (docSnap.exists() && docSnap.data()) || {}; + const mergedData = { + ...existingData, + ...updatedData, + }; + await setDoc(docRef, mergedData); + return { data: mergedData }; + } catch (error) { + console.error(`Error updating document in "${collectionName}/${documentId}":`, error); + return { + error: `Error updating document in "${collectionName}/${documentId}"`, + }; + } +} + +export async function deleteData(collectionName, documentId) { + try { + const docRef = doc(FIREBASE_DB, collectionName, documentId); + await deleteDoc(docRef); + return { data: documentId }; + } catch (error) { + console.error(`Error deleting document from "${collectionName}/${documentId}":`, error); + return { + error: `Error deleting document from "${collectionName}/${documentId}"`, + }; + } +} diff --git a/src/utils/users.js b/src/utils/users.js index c6c3648..58ab417 100644 --- a/src/utils/users.js +++ b/src/utils/users.js @@ -1,12 +1,80 @@ -import { addData } from '@/services/firebase/addData'; -import { getData } from '@/services/firebase/getData'; +import { addData, deleteData, getData, updateData } from '@/services/firebase'; export async function fetchUserById(userId) { + if (!userId || typeof userId !== 'string' || !userId.length) { + return ['Please provide a valid userId to fetch user', null]; + } const { error, data } = await getData('users', userId); return [error, data]; } -export async function saveNewUser(userId, newUser) { +export async function saveNewUser(newUser) { + const { userId } = newUser || {}; + if (!userId || typeof userId !== 'string' || !userId.length) { + return ['Please provide a valid `userId` to save new user', null]; + } + if (!isNewUserValid(newUser)) { + return ['Please provide a valid `newUser` object', null]; + } const { error, data } = await addData('users', userId, newUser); return [error, data]; } + +export async function updateUserById(userId, updatedUser) { + if (!userId || typeof userId !== 'string' || !userId.length) { + return ['Please provide a valid userId to update user', null]; + } + const { error, data } = await updateData('users', userId, updatedUser); + return [error, data]; +} + +export async function deleteUserById(userId) { + if (!userId || typeof userId !== 'string' || !userId.length) { + return ['Please provide a valid userId to delete user', null]; + } + const { error, data } = await deleteData('users', userId); + return [error, data]; +} + +const isNewUserValid = (newUser) => { + if (!newUser) { + console.error('Invalid user: `newUser` is null or undefined.'); + return false; + } + + const { userId, email, profilePicture, joinedAt } = newUser; + + const isNonEmptyString = (str) => typeof str === 'string' && str.trim().length > 0; + const isValidEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + const isValidDate = (date) => !isNaN(new Date(date).getTime()); + const isValidUrl = (url) => { + try { + new URL(url); + return true; + } catch { + return false; + } + }; + + if (!isNonEmptyString(userId)) { + console.error('Invalid `newUser.userId`:', userId); + return false; + } + + if (!isValidEmail(email)) { + console.error('Invalid `newUser.email`:', email); + return false; + } + + if (profilePicture && !isValidUrl(profilePicture)) { + console.error('Invalid `newUser.profilePicture`:', profilePicture); + return false; + } + + if (!isValidDate(joinedAt)) { + console.error('Invalid `newUser.joinedAt`:', joinedAt); + return false; + } + + return true; +};