From 55ace024ec7a54996c991db0a6bdaa8b162ef6db Mon Sep 17 00:00:00 2001 From: rakib Date: Fri, 7 Feb 2025 18:55:36 -0500 Subject: [PATCH] User Boards --- components/auth-state.tsx | 6 +- components/boards/board.tsx | 15 +- components/boards/boards.tsx | 76 ++++---- components/boards/column.tsx | 35 ++-- components/boards/item.tsx | 16 +- components/boards/tasks.tsx | 28 ++- components/form.tsx | 323 ++++++++++++++++++--------------- components/profile/profile.tsx | 110 +++++++++++ firebase.ts | 27 ++- main.scss | 10 +- pages/_app.js | 28 ++- pages/profile/[id].mdx | 5 + pages/profile/_meta.json | 20 ++ pages/profile/index.mdx | 5 + shared/ID.ts | 3 +- shared/constants.ts | 50 +++++ shared/models/User.ts | 16 +- styles/_utility.scss | 16 ++ theme.config.tsx | 2 +- 19 files changed, 545 insertions(+), 246 deletions(-) create mode 100644 components/profile/profile.tsx create mode 100644 pages/profile/[id].mdx create mode 100644 pages/profile/_meta.json create mode 100644 pages/profile/index.mdx diff --git a/components/auth-state.tsx b/components/auth-state.tsx index 3b3c2b3..9fb56df 100644 --- a/components/auth-state.tsx +++ b/components/auth-state.tsx @@ -2,10 +2,12 @@ import { useContext } from 'react'; import { StateContext } from '../pages/_app'; export default function AuthState({ classes }: any) { - let { authState } = useContext(StateContext); + let { user, authState } = useContext(StateContext); return ( - {authState} + {user != null ? ( + `Welcome, ${user?.name}` + ) : authState} ) } \ No newline at end of file diff --git a/components/boards/board.tsx b/components/boards/board.tsx index dfa7b5d..9d51222 100644 --- a/components/boards/board.tsx +++ b/components/boards/board.tsx @@ -26,7 +26,7 @@ export default function Board(props) { let [board, setBoard] = useState(props.board); let [showSearch, setShowSearch] = useState(false); let [showConfirm, setShowConfirm] = useState(false); - let { boards, setBoards, setLoading, setSystemStatus, completeFiltered, setCompleteFiltered, setPage, IDs, setIDs } = useContext(StateContext); + let { user, boards, setBoards, setLoading, setSystemStatus, completeFiltered, setCompleteFiltered, setPage, IDs, setIDs } = useContext(StateContext); const filterSubtasks = (e?: any) => { if (board.hideAllTasks) { @@ -118,8 +118,19 @@ export default function Board(props) { const newColumn = { itemIds: [], id: columnID, + boardID: board?.id, itemType: ItemTypes.Item, title: formFields[0].value, + created: formatDate(new Date()), + updated: formatDate(new Date()), + ...(user != null && { + creator: { + id: user?.id, + uid: user?.uid, + name: user?.name, + email: user?.email, + } + }), }; setBoard({ @@ -132,7 +143,7 @@ export default function Board(props) { } }); - setIDs([...IDs, columnID]) + setIDs([...IDs, columnID]); e.target.reset(); setTimeout(() => { diff --git a/components/boards/boards.tsx b/components/boards/boards.tsx index ebd126e..f17224b 100644 --- a/components/boards/boards.tsx +++ b/components/boards/boards.tsx @@ -1,5 +1,7 @@ import Board from './board'; import { toast } from 'react-toastify'; +import { isValid } from '../../shared/constants'; +import { updateUserFields } from '../../firebase'; import { useState, useEffect, useContext } from 'react'; import { Droppable, Draggable, DragDropContext } from 'react-beautiful-dnd'; import { capWords, dev, formatDate, generateUniqueID, replaceAll, StateContext } from '../../pages/_app'; @@ -7,8 +9,6 @@ import { capWords, dev, formatDate, generateUniqueID, replaceAll, StateContext } export enum ItemTypes { Item = `Item`, Image = `Image`, - // Task = `Task`, - // Video = `Video`, } export enum BoardTypes { @@ -22,7 +22,7 @@ export enum BoardTypes { export default function Boards(props) { let [updates, setUpdates] = useState(0); - const { rte, boards, setBoards, router, setLoading, setSystemStatus, IDs, setIDs, setRte } = useContext(StateContext); + const { user, rte, boards, setBoards, router, setLoading, setSystemStatus, IDs, setIDs, setRte } = useContext(StateContext); const addNewBoard = (e) => { e.preventDefault(); @@ -39,44 +39,43 @@ export default function Boards(props) { let newColumn2ID = `column_2_${generateUniqueID(IDs)}`; let newBoard = { - // rows: [].concat(...getBoardColumnsFromType(boardType).map(col => col.rows)), - // type: (boardType == BoardTypes.SelectBoardType ? BoardTypes.KanbanBoard : boardType) ?? BoardTypes.KanbanBoard, - created: formatDate(new Date()), + items: [], + titleWidth, expanded: true, + id: newBoardID, name: boardName, + ...(user != null && { + creator: { + id: user?.id, + uid: user?.uid, + name: user?.name, + email: user?.email, + } + }), + created: formatDate(new Date()), + updated: formatDate(new Date()), columnOrder: [ newColumn1ID, newColumn2ID, ], columns: { [newColumn1ID]: { + itemIds: [], + title: `Active`, id: newColumn1ID, - title: `active`, - itemIds: [] }, [newColumn2ID]: { + itemIds: [], + title: `Complete`, id: newColumn2ID, - title: `complete`, - itemIds: [] } }, - id: newBoardID, - titleWidth, - items: [], } - // if (dev()) { - // console.log(`addNewBoard`, newBoard); - // console.log(`boards`, boards); - // } - setBoards([newBoard, ...boards]); setIDs([...IDs, newBoard.id, newColumn1ID, newColumn2ID]); e.target.reset(); - // window.requestAnimationFrame(() => { - // return boardsSection.scrollTop = boardsSection.scrollHeight; - // }); setTimeout(() => { setLoading(false); setSystemStatus(`Created Board ${newBoard.name}.`); @@ -87,11 +86,6 @@ export default function Boards(props) { dev() && console.log(`Boards Drag`, dragEndEvent); const { destination, source, draggableId, type } = dragEndEvent; - // if (dev()) { - // console.log(`dragEndEvent`, dragEndEvent); - // console.log(`dragEndEvent Inner`, {source, destination, draggableId, type}); - // } - if (!destination) { return; } @@ -110,14 +104,30 @@ export default function Boards(props) { setRte(replaceAll(router.route, `/`, `_`)); if (updates > 0) { - localStorage.setItem(`boards`, JSON.stringify(boards)); - dev() && console.log(`Updated Boards`, boards); + let updatedBoards = boards; + let boardsHaveCreator = boards.every(brd => isValid(brd?.creator)); + + if (user != null) { + if (!boardsHaveCreator) { + updatedBoards = boards.map(brd => ({ + ...brd, + creator: { + id: user?.id, + uid: user?.uid, + name: user?.name, + email: user?.email, + }, + })) + } + updateUserFields(user?.id, { boards: updatedBoards }); + localStorage.setItem(`user`, JSON.stringify({ ...user, boards: updatedBoards })); + } else { + localStorage.setItem(`local_boards`, JSON.stringify(updatedBoards)); + } + + localStorage.setItem(`boards`, JSON.stringify(updatedBoards)); + dev() && console.log(`Updated Boards`, updatedBoards); } - - // if (dev()) { - // console.log(`Updates`, updates); - // console.log(`Boards`, boards); - // } setUpdates(updates + 1); return () => { diff --git a/components/boards/column.tsx b/components/boards/column.tsx index a5bebd1..078b5ab 100644 --- a/components/boards/column.tsx +++ b/components/boards/column.tsx @@ -13,7 +13,7 @@ export default function Column(props) { let { board, column, hideAllTasks } = props; let [showConfirm, setShowConfirm] = useState(false); let [itemTypeMenuOpen, setItemTypeMenuOpen] = useState(false); - let { boards, setBoards, setLoading, setSystemStatus, completeFiltered, IDs, setIDs, selected, menuPosition } = useContext(StateContext); + let { user, boards, setBoards, setLoading, setSystemStatus, completeFiltered, IDs, setIDs, selected, menuPosition } = useContext(StateContext); const itemActiveFilters = (itm) => { if (completeFiltered) { @@ -76,27 +76,6 @@ export default function Column(props) { localStorage.setItem(`boards`, JSON.stringify(boards)); } - // const adjustColumnsLayout = (column, columnsNum: number) => { - // if (columnsNum >= 0 && columnsNum <= 3 && column.layoutCols != columnsNum) { - // column.layoutCols = columnsNum; - // } else { - // column.layoutCols = 1; - // } - - // props.setBoard(prevBoard => { - // return { - // ...prevBoard, - // updated: formatDate(new Date()), - // columns: { - // ...prevBoard?.columns, - // [column?.id]: column, - // }, - // } - // }); - - // localStorage.setItem(`boards`, JSON.stringify(boards)); - // } - const deleteColumn = (columnId, index, initialConfirm = true) => { if (showConfirm == true) { if (!initialConfirm) { @@ -170,9 +149,20 @@ export default function Column(props) { subtasks: [], complete: false, description: ``, + boardID: props?.board?.id, + listID: props?.column?.id, type: props?.column?.itemType, created: formatDate(new Date()), + updated: formatDate(new Date()), content: capitalizeAllWords(content), + ...(user != null && { + creator: { + id: user?.id, + uid: user?.uid, + name: user?.name, + email: user?.email, + } + }), } props.setBoard({ @@ -303,6 +293,7 @@ export default function Column(props) { {!hideAllTasks && item.subtasks && ( diff --git a/components/boards/item.tsx b/components/boards/item.tsx index 4d19c42..3d71ce8 100644 --- a/components/boards/item.tsx +++ b/components/boards/item.tsx @@ -26,24 +26,12 @@ export const getSubTaskPercentage = (subtasks: any[], item, isActive = null) => return subtasksProgress; } -export const getTypeIcon = (type, plain?) => { +export const getTypeIcon = (type) => { switch (type) { default: return `+`; - // case ItemTypes.Task: - // if (plain) { - // return `✔` - // } else { - // return ( - // - // ✔ - // - // ); - // } case ItemTypes.Image: return ; - // case ItemTypes.Video: - // return ; } } @@ -225,7 +213,7 @@ export default function Item({ item, count, column, itemIndex, board, setBoard } {(item?.type == ItemTypes.Item) && ( - {getTypeIcon(item?.type, true)} + {getTypeIcon(item?.type)} )} {itemIndex + 1} diff --git a/components/boards/tasks.tsx b/components/boards/tasks.tsx index fdd1940..2feeeed 100644 --- a/components/boards/tasks.tsx +++ b/components/boards/tasks.tsx @@ -96,8 +96,8 @@ const SortableSubtaskItem = ({ item, subtask, isLast, column, index, changeLabel } export default function Tasks(props) { - let { item, column, showForm = true } = props; - let { boards, setLoading, setSystemStatus } = useContext(StateContext); + let { item, column, board, showForm = true } = props; + let { user, boards, setLoading, setSystemStatus } = useContext(StateContext); let [deletedTaskIDs, setDeletedTaskIDs] = useState([]); let [subtasks, setSubtasks] = useState(item?.subtasks?.length ? item.subtasks : []); @@ -127,15 +127,15 @@ export default function Tasks(props) { // Toggle complete const completeSubtask = (e, subtask) => { setLoading(true); - setSystemStatus(`Marking Task as ${subtask?.complete ? 'Reopened' : 'Complete'}.`); + setSystemStatus(`Marking Task as ${subtask?.complete ? `Reopened` : `Complete`}.`); subtask.complete = !subtask?.complete; subtask.updated = formatDate(new Date()); item.updated = formatDate(new Date()); - localStorage.setItem('boards', JSON.stringify(boards)); + localStorage.setItem(`boards`, JSON.stringify(boards)); setTimeout(() => { - setSystemStatus(`Marked Task as ${subtask?.complete ? 'Complete' : 'Reopened'}.`); + setSystemStatus(`Marked Task as ${subtask?.complete ? `Complete` : `Reopened`}.`); setLoading(false); }, 1000); }; @@ -144,7 +144,7 @@ export default function Tasks(props) { const addSubtask = (e) => { e.preventDefault(); setLoading(true); - setSystemStatus('Creating Task.'); + setSystemStatus(`Creating Task.`); const formFields = e.target.children; const newTaskText = formFields[0].value.trim(); @@ -158,8 +158,20 @@ export default function Tasks(props) { const newSubtask = { id: subtaskID, complete: false, - task: capitalizeAllWords(newTaskText), + itemID: item?.id, + listID: column?.id, + boardID: board?.id, created: formatDate(new Date()), + updated: formatDate(new Date()), + task: capitalizeAllWords(newTaskText), + ...(user != null && { + creator: { + id: user?.id, + uid: user?.uid, + name: user?.name, + email: user?.email, + } + }), }; const updatedTasks = [ @@ -198,7 +210,7 @@ export default function Tasks(props) { // Delete subtask const deleteSubtask = (e, subtask) => { setLoading(true); - setSystemStatus('Deleting Task.'); + setSystemStatus(`Deleting Task.`); const subtaskIDToDelete = subtask.id; setDeletedTaskIDs((prev) => [...prev, subtaskIDToDelete]); diff --git a/components/form.tsx b/components/form.tsx index 4924d51..ca30dae 100644 --- a/components/form.tsx +++ b/components/form.tsx @@ -1,10 +1,12 @@ 'use client'; -import { db } from '../firebase'; import { toast } from 'react-toastify'; -import { collection, getDocs } from 'firebase/firestore'; +import { User } from '../shared/models/User'; +import { addUserToDatabase, auth, db } from '../firebase'; import { useContext, useEffect, useRef, useState } from 'react'; -import { defaultContent, formatDate, StateContext, showAlert, dev } from '../pages/_app'; +import { formatDate, StateContext, showAlert } from '../pages/_app'; +import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'; +import { findHighestNumberInArrayByKey, isValid, removeNullAndUndefinedProperties } from '../shared/constants'; export const convertHexToRGB = (HexString?:any, returnObject?: any) => { let r = parseInt(HexString.slice(1, 3), 16), @@ -25,63 +27,166 @@ export const isShadeOfBlack = (HexString?:any) => { return (rgb?.r < darkColorBias) && (rgb?.g < darkColorBias) && (rgb?.b < darkColorBias); } +export const renderErrorMessage = (erMsg: string) => { + let erMsgQuery = erMsg?.toLowerCase(); + if (erMsgQuery.includes(`invalid-email`)) { + return `Please use a valid email.`; + } else if (erMsgQuery?.includes(`email-already-in-use`)) { + return `Email is already in use.`; + } else if (erMsgQuery?.includes(`weak-password`)) { + return `Password should be at least 6 characters`; + } else if (erMsgQuery?.includes(`wrong-password`) || erMsgQuery?.includes(`invalid-login-credentials`)) { + return `Incorrect Password`; + } else if (erMsgQuery?.includes(`user-not-found`)) { + return `User Not Found`; + } else { + return erMsg; + } +} + export default function Form(props?: any) { - const { style } = props; + const { navForm, style } = props; const loadedRef = useRef(false); const [loaded, setLoaded] = useState(false); - const [alertOpen, setAlertOpen] = useState(false); - const { user, setUser, updates, setUpdates, setUsers, setContent, authState, setAuthState, emailField, setEmailField, users, setFocus, setHighScore, color, setColor, dark, setDark, setLists } = useContext(StateContext); - - const genUUID = (latestUsers?:any, potentialUser?:any) => { - return `${latestUsers.length + 1} ${potentialUser?.name} ${potentialUser?.registered.split(` `)[0] + ` ` + potentialUser?.registered.split(` `)[1] + ` ` + potentialUser?.registered.split(` `)[2]}`; - } - - // const setPageViews = (id?:any, user?: any) => { - // setDoc(doc(db, `pageViews`, id), { ...user, id }).then(newSub => { - // localStorage.setItem(`user`, JSON.stringify({ ...user, id })); - // setUser({ ...user, id }); - // setUpdates(updates + 1); - // return newSub; - // }).catch(error => console.log(error)); - // } - - const changeColor = (colorRangePickerEvent?: any) => { - // Set Current Color to Hex String. (Example: `#0b4366`); - let currentColor: any = colorRangePickerEvent.target.value; - localStorage.setItem(`color`, JSON.stringify(currentColor)); - setColor(currentColor); + const { user, setUser, setBoards, updates, setUpdates, setContent, authState, setAuthState, emailField, setEmailField, users, setColor, setDark, useDatabase } = useContext(StateContext); - // Conver Hex to RGB for Color Check and Comparison. - let r = parseInt(currentColor.slice(1, 3), 16), - g = parseInt(currentColor.slice(3, 5), 16), - b = parseInt(currentColor.slice(5, 7), 16); + // const changeColor = (colorRangePickerEvent?: any) => { + // let currentColor: any = colorRangePickerEvent.target.value; + // localStorage.setItem(`color`, JSON.stringify(currentColor)); + // setColor(currentColor); - let luminance = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); - if (luminance > 128) { - // Background is light, set text color to black - setDark(false); - } else { - // Background is dark, set text color to white - setDark(true); - } - } + // let r = parseInt(currentColor.slice(1, 3), 16), + // g = parseInt(currentColor.slice(3, 5), 16), + // b = parseInt(currentColor.slice(5, 7), 16); - // const addOrUpdateUser = async (id: any, user: User) => { - // setDoc(doc(db, `users`, id), { ...user, id }).then(newSub => { - // localStorage.setItem(`user`, JSON.stringify({ ...user, id })); - // setUser({ ...user, id }); - // setUpdates(updates + 1); - // return newSub; - // }).catch(error => console.log(error)); + // let luminance = (0.2126 * r) + (0.7152 * g) + (0.0722 * b); + // if (luminance > 128) { + // setDark(false); + // } else { + // setDark(true); + // } // } const authForm = (e?: any) => { e.preventDefault(); - let formFields = e.target.children; + + let form = e?.target; + let formFields = form?.children; let clicked = e?.nativeEvent?.submitter; let email = formFields?.email?.value ?? `email`; let password = formFields?.password?.value ?? `pass`; + const signInUser = (usr: User) => { + localStorage.setItem(`user`, JSON.stringify(usr)); + setAuthState(`Sign Out`); + setUser(usr); + if (isValid(usr?.boards)) { + setBoards(usr?.boards); + } + } + + const onSignOut = () => { + setUser(null); + setAuthState(`Next`); + setEmailField(false); + setUpdates(updates + 1); + localStorage.removeItem(`user`); + let hasLocalBoards = localStorage.getItem(`local_boards`); + if (hasLocalBoards) { + let localBoards = JSON.parse(hasLocalBoards); + setBoards(localBoards); + } else { + setBoards([]); + } + } + + const onSignIn = (email, password) => { + if (useDatabase == true) { + signInWithEmailAndPassword(auth, email, password).then((userCredential: any) => { + if (userCredential != null) { + let existingUser = users.find(usr => usr?.email?.toLowerCase() == email?.toLowerCase()); + if (existingUser != null) { + signInUser(existingUser); + toast.success(`Successfully Signed In`); + } else { + setEmailField(true); + setAuthState(`Sign Up`); + } + } + }).catch((error) => { + const errorCode = error.code; + const errorMessage = error.message; + if (errorMessage) { + toast.error(renderErrorMessage(errorMessage)); + console.log(`Error Signing In`, { + error, + errorCode, + errorMessage + }); + } + return; + }); + } else { + toast.error(`Database not connected or not being used`); + } + } + + const onSignUp = (email, password) => { + if (useDatabase == true) { + createUserWithEmailAndPassword(auth, email, password).then(async (userCredential: any) => { + if (userCredential != null) { + let { + uid, + photoURL: avatar, + displayName: name, + accessToken: token, + phoneNumber: phone, + isAnonymous: anonymous, + emailVerified: verified, + } = userCredential?.user; + + let highestRank = await findHighestNumberInArrayByKey(users, `rank`); + let userData = { + uid, + name, + email, + phone, + avatar, + rank: highestRank + 1, + auth: { + token, + verified, + anonymous, + } + } + + let cleanedUser = removeNullAndUndefinedProperties(userData); + let newUser = new User(cleanedUser); + + await addUserToDatabase(newUser).then(() => { + toast.success(`Signed Up & In as: ${newUser?.name}`); + console.log(`New User`, newUser); + signInUser(newUser); + form.reset(); + }); + } else { + toast.error(`Error on Sign Up`); + } + }).catch((error) => { + console.log(`Error Signing Up`, error); + const errorMessage = error.message; + if (errorMessage) { + toast.error(renderErrorMessage(errorMessage)); + } else { + toast.error(`Error Signing Up`); + } + return; + }); + } else { + toast.error(`Database not connected or not being used`); + } + } + switch(clicked?.value) { default: console.log(`Clicked Value`, clicked?.value); @@ -94,46 +199,14 @@ export default function Form(props?: any) { setAuthState(`Sign Up`); } setEmailField(true); - // toast.error(`Authentication Not Implemented Yet`); - // getDocs(collection(db, `users`)).then((snapshot) => { - // let latestUsers = snapshot.docs.map((doc: any) => doc.data()).sort((a: any, b: any) => b?.highScore - a?.highScore); - // let macthingEmails = latestUsers.filter((usr: any) => usr?.email.toLowerCase() == email.toLowerCase()); - // setUsers(latestUsers); - // let arr = []; - // setEmailField(true); - // if (arr.length > 0) { - // // localStorage.setItem(`account`, JSON.stringify(macthingEmails[0])); - // setAuthState(`Sign In`); - // } else { - // setAuthState(`Sign Up`); - // } - // }); - // showAlert(`Whoah There!`,
- //

- // Ok so, I haven't quite put user authentication in yet, I will soon! For now, this is not supported yet, apologies! - //

- //
, `420px`, `auto`); break; case `Back`: - setUpdates(updates+1); + setUpdates(updates + 1); setAuthState(`Next`); setEmailField(false); break; case `Sign Out`: - setLists([]); - setUser(null); - setHighScore(0); - setAuthState(`Next`); - setEmailField(false); - setUpdates(updates+1); - setContent(defaultContent); - // localStorage.removeItem(`user`); - // localStorage.removeItem(`users`); - // localStorage.removeItem(`lists`); - // localStorage.removeItem(`score`); - // localStorage.removeItem(`health`); - // localStorage.removeItem(`account`); - // localStorage.removeItem(`highScore`); + onSignOut(); break; case `Save`: let emptyFields = []; @@ -160,30 +233,16 @@ export default function Form(props?: any) { return {[input.id]: input.value} } }))); - // addOrUpdateUser(user?.id, updatedUser); } break; case `Sign In`: - let storedScore = JSON.parse(localStorage.getItem(`score`) as any); - let existingAccount = JSON.parse(localStorage.getItem(`account`) as any); - let listsToSet = existingAccount?.lists || JSON.parse(localStorage.getItem(`lists`) as any) || []; - let scoreToSet = Math.floor(existingAccount?.highScore > storedScore ? existingAccount?.highScore : storedScore); - if (password == ``) { - showAlert(`Password Required`); - } else { // Successful Sign In - if (password == existingAccount?.password) { - setFocus(false); - setLists(listsToSet); - setAuthState(`Sign Out`); - setUser(existingAccount); - setHighScore(scoreToSet); - setContent(existingAccount?.bio); - setColor((existingAccount?.color || `#000000`)); - // addOrUpdateUser(existingAccount?.id, {...existingAccount, highScore: scoreToSet, lastSignin: formatDate(new Date())}); - getDocs(collection(db, `users`)).then((snapshot) => setUsers(snapshot.docs.map((doc: any) => doc.data()).sort((a: any, b: any) => b?.highScore - a?.highScore))); + toast.error(`Password Required`); + } else { + if (password?.length >= 6) { + onSignIn(email, password); } else { - showAlert(`Invalid Password`); + toast.error(`Password must be 6 characters or greater`); } } break; @@ -191,49 +250,12 @@ export default function Form(props?: any) { if (password == ``) { toast.error(`Password Required`); } else { - if (password?.length > 6) { - setFocus(false); - // setAuthState(`Signed Up`); - dev() && console.log(`Usr`, { email, password }); - toast.info(`Sign Up is In Development`); + if (password?.length >= 6) { + onSignUp(email, password); } else { toast.error(`Password must be 6 characters or greater`); } } - // getDocs(collection(db, `users`)).then((snapshot) => { - // let latestUsers = snapshot.docs.map((doc: any) => doc.data()); - // let macthingEmails = latestUsers.filter((usr: any) => usr?.email.toLowerCase() == email.toLowerCase()); - // setUsers(latestUsers); - // if (macthingEmails.length > 0) { - // localStorage.setItem(`account`, JSON.stringify(macthingEmails[0])); - // setAuthState(`Sign In`); - // } else { - // setAuthState(`Sign Up`); - // } - // let storedHighScore = JSON.parse(localStorage.getItem(`highScore`) as any); - // let potentialUser = { - // id: users.length + 1, - // bio: ``, - // color: ``, - // number: 0, - // status: ``, - // name: name, - // email: email, - // roles: [`user`], - // password: password, - // lists: defaultLists, - // updated: formatDate(new Date()), - // lastSignin: formatDate(new Date()), - // registered: formatDate(new Date()), - // highScore: Math.floor(storedHighScore) || 0, - // }; - - // let uuid = genUUID(latestUsers, potentialUser); - // // addOrUpdateUser(uuid, potentialUser); - // getDocs(collection(db, `users`)).then((snapshot) => setUsers(snapshot.docs.map((doc: any) => doc.data()).sort((a: any, b: any) => b?.highScore - a?.highScore))); - // setAuthState(`Sign Out`); - // } - // }); break; }; } @@ -246,17 +268,26 @@ export default function Form(props?: any) { return <>
+ {!user && } - {!user && emailField && } - {user && window?.location?.href?.includes(`profile`) && } - {user && window?.location?.href?.includes(`profile`) && } - {user && window?.location?.href?.includes(`profile`) && } - {user && window?.location?.href?.includes(`profile`) && } - {user && window?.location?.href?.includes(`profile`) && } - {user && window?.location?.href?.includes(`profile`) && changeColor(e)} defaultValue={color} />} + {!user && emailField && } + + {(!navForm && user != null) ? <> + {window?.location?.href?.includes(`profile`) ? <> + + + + + + {/* changeColor(e)} defaultValue={color} /> */} + + : <>} + : <>} + + {(authState == `Sign In` || authState == `Sign Up`) && } - {user && window?.location?.href?.includes(`profile`) && } +
} \ No newline at end of file diff --git a/components/profile/profile.tsx b/components/profile/profile.tsx new file mode 100644 index 0000000..0689d41 --- /dev/null +++ b/components/profile/profile.tsx @@ -0,0 +1,110 @@ +import Form from '../form'; +import AuthState from '../auth-state'; +import { useRouter } from 'next/router'; +import { User } from '../../shared/models/User'; +import { dev, StateContext } from '../../pages/_app'; +import { stringMatch } from '../../shared/constants'; +import { useContext, useEffect, useState } from 'react'; + +export default function Profile({ }) { + let router = useRouter(); + let { id } = router.query; + let { user, users } = useContext(StateContext); + + let [originalQuery, setOriginalQuery] = useState(``); + let [profile, setProfile] = useState(user); + let [profileLoading, setProfileLoading] = useState(true); + + const profileDataComponent = (profile: User) => { + return <> +

- Rank: {profile?.rank}

+

- Friends: {profile?.data?.friendIDs?.length}

+

- Role: {profile?.role}

+

- Created: {profile?.meta?.created}

+

- Email: {profile?.email}

+
+

- Boards: {profile?.data?.boardIDs?.length}

+

- Lists: {profile?.data?.listIDs?.length}

+

- Items: {profile?.data?.itemIDs?.length}

+

- Tasks: {profile?.data?.taskIDs?.length}

+ + } + + useEffect(() => { + if (id) { + let quer = id?.toString(); + setOriginalQuery(quer); + let query = quer?.toLowerCase(); + if (users.length > 0) { + let userQuery = users.find((usr: User) => ( + stringMatch(usr?.name, query) + || stringMatch(usr?.id, query) + || stringMatch(usr?.uid, query) + || stringMatch(usr?.uuid, query) + || stringMatch(usr?.title, query) + || stringMatch(usr?.email, query) + || stringMatch(usr?.rank?.toString(), query) + )) + if (userQuery) { + const onFoundUser = () => { + setProfile(userQuery); + setProfileLoading(false); + console.log(`Found User for Query "${quer}"`, userQuery); + } + if (profile != null) { + if (stringMatch(profile?.id, userQuery?.id)) { + console.log(`User is Profile Query "${quer}"`); + } else onFoundUser(); + } else onFoundUser(); + } else { + console.log(`Cannot Find User for Query "${quer}"`); + setProfileLoading(false); + } + } + } + }, [user, users]) + + return <> +
+
+ {id ? <> + {profile != null ? <> +

{profile?.name}

+ : <> + {profileLoading ? <> +

Loading Profile "{id}"

+ : <> +

Cannot Find Profile "{originalQuery}"

+ } + } + : <> + {user != null ? <> + + : <> +

Sign In to View your Profile

+ } + } +
+
+ {id ? <> + {profile != null ? <> + {profileDataComponent(profile)} + : <> + {/* Empty */} + } + : <> +
+ {user != null ? <> + {dev() &&
} +
+ {profileDataComponent(user)} +
+ : <> + {/* Empty */} + } +
+ } +
+
+ +} \ No newline at end of file diff --git a/firebase.ts b/firebase.ts index 1e31868..4d30ede 100644 --- a/firebase.ts +++ b/firebase.ts @@ -1,8 +1,8 @@ -import { dev } from './pages/_app'; +import { dev, formatDate } from './pages/_app'; import { User } from './shared/models/User'; import { initializeApp } from 'firebase/app'; import { GoogleAuthProvider, getAuth } from 'firebase/auth'; -import { doc, getFirestore, setDoc } from 'firebase/firestore'; +import { doc, getFirestore, setDoc, updateDoc } from 'firebase/firestore'; export enum Environments { beta = `beta_`, @@ -62,10 +62,31 @@ export const addUserToDatabase = async (usr: User) => { try { const userReference = await doc(db, usersTable, usr?.id).withConverter(userConverter); await setDoc(userReference, usr as User); - dev() && console.log(`Added User "${usr?.name}" to Database`); } catch (error) { dev() && console.log(`Error Adding User to Database ${usersTable}`, error); } } +export const updateUserFields = async (userID: string, updates: Partial, logResult = true) => { + try { + const userRef = await doc(db, usersTable, userID).withConverter(userConverter); + await updateDoc(userRef, updates); + if (logResult) console.log(`User Fields Updated in Database`, updates); + } catch (error) { + console.log(`Error Updating User ${userID} Fields`, { error, updates }); + } +} + +export const updateUserFieldsInDatabase = async (userID: string, updates: Partial, logResult = true) => { + const now = formatDate(new Date()); + const fields = { ...updates, meta: { updated: now } }; + try { + const userRef = await doc(db, usersTable, userID).withConverter(userConverter); + await updateDoc(userRef, fields); + if (logResult) console.log(`User Fields Updated in Database`, fields); + } catch (error) { + console.log(`Error Updating User ${userID} Fields`, { error, fields }); + } +} + export default firebaseApp; \ No newline at end of file diff --git a/main.scss b/main.scss index 8195cdb..8e561bc 100644 --- a/main.scss +++ b/main.scss @@ -157,8 +157,8 @@ } aside { + background: var(--darkBG) !important; border-right: solid 1px var(--bg); - background: black !important; } body { @@ -564,18 +564,12 @@ input { } @media (min-width: 768px) { - aside { + aside, .nextra-toc { width: auto !important; display: none !important; } } -// @media (min-width: 850px) { -// aside { -// display: none !important; -// } -// } - @media (max-width: 850px) { nav { * { diff --git a/pages/_app.js b/pages/_app.js index c4ee5bf..c727067 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -2,8 +2,10 @@ import '../main.scss'; import 'react-toastify/dist/ReactToastify.css'; import ReactDOM from 'react-dom/client'; +import { User } from '../shared/models/User'; import { db, usersTable } from '../firebase'; import { dbBoards } from '../shared/database'; +import { isValid } from '../shared/constants'; import { ToastContainer } from 'react-toastify'; import { AnimatePresence, motion } from 'framer-motion'; import { collection, onSnapshot } from 'firebase/firestore'; @@ -722,17 +724,29 @@ export default function ProductIVF({ Component, pageProps, router }) { let hasStoredUser = localStorage.getItem(`user`); if (hasStoredUser) { let storedUser = JSON.parse(hasStoredUser); - dev() && console.log(`Use Stored User`, storedUser); - setUser(storedUser); + let thisUser = new User(storedUser); + dev() && console.log(`User Still Signed In`, thisUser); + setAuthState(`Sign Out`); + setUser(thisUser); + if (isValid(thisUser?.boards)) { + setBoards(thisUser?.boards); + } } const usersDatabase = collection(db, usersTable); const usersDatabaseRealtimeListener = onSnapshot(usersDatabase, snapshot => { setUsersLoading(true); let usersFromDB = []; - snapshot.forEach((doc) => usersFromDB.push({ ...doc.data() })); + snapshot.forEach((doc) => usersFromDB.push(new User({ ...doc.data() }))); usersFromDB = usersFromDB.sort((a, b) => a?.rank - b?.rank); setUsers(usersFromDB); + if (user != null) { + let thisUser = usersFromDB.find(usr => usr?.id == user?.id); + if (thisUser) { + setUser(thisUser); + } + } + dev() && console.log(`Users Update from Database`, usersFromDB); }, error => { console.log(`Error on Get Task(s) from Database`, error); }, complete => { @@ -791,7 +805,13 @@ export default function ProductIVF({ Component, pageProps, router }) { if (cachedBoards && cachedBoards?.length > 0) { setBoards(cachedBoards); } else { - setBoards(dbBoards); + let hasLocalBoards = localStorage.getItem(`local_boards`); + if (hasLocalBoards) { + let localBoards = JSON.parse(hasLocalBoards); + setBoards(localBoards); + } else { + setBoards([]); + } } let toc = document.querySelector(`.nextra-toc`); diff --git a/pages/profile/[id].mdx b/pages/profile/[id].mdx new file mode 100644 index 0000000..caf0944 --- /dev/null +++ b/pages/profile/[id].mdx @@ -0,0 +1,5 @@ +import Profile from '../../components/profile/profile' + +# Profile + + \ No newline at end of file diff --git a/pages/profile/_meta.json b/pages/profile/_meta.json new file mode 100644 index 0000000..d1c13c6 --- /dev/null +++ b/pages/profile/_meta.json @@ -0,0 +1,20 @@ +{ + "[id]": { + "title": "Profile", + "type": "page", + "display": "hidden", + "theme": { + "pagination": false, + "layout": "full" + } + }, + "index": { + "title": "Profile", + "type": "page", + "display": "hidden", + "theme": { + "pagination": false, + "layout": "full" + } + } +} \ No newline at end of file diff --git a/pages/profile/index.mdx b/pages/profile/index.mdx new file mode 100644 index 0000000..caf0944 --- /dev/null +++ b/pages/profile/index.mdx @@ -0,0 +1,5 @@ +import Profile from '../../components/profile/profile' + +# Profile + + \ No newline at end of file diff --git a/shared/ID.ts b/shared/ID.ts index de15e70..7b9b0e5 100644 --- a/shared/ID.ts +++ b/shared/ID.ts @@ -36,8 +36,9 @@ export const getIDParts = () => { return { uuid, date }; } -export const genID = (type: Types = Types.Data, position = 1, name): ID => { +export const genID = (type: Types = Types.Data, position = 1, name, injectedUID?): ID => { let { uuid, date } = getIDParts(); + uuid = injectedUID ? injectedUID : uuid; let title = `${type} ${position} ${name}`; let idString = `${title} ${stringNoSpaces(date)} ${uuid}`; let id = stringNoSpaces(idString); diff --git a/shared/constants.ts b/shared/constants.ts index 5f4b543..2e09219 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -1,4 +1,5 @@ export const removeExtraSpacesFromString = (string: string) => string.trim().replace(/\s+/g, ` `); +export const stringMatch = (string: string, check: string): boolean => string?.toLowerCase()?.includes(check?.toLowerCase()); export const stringNoSpaces = (string: string) => string?.replaceAll(/[\s,:/]/g, `_`)?.replaceAll(/[\s,:/]/g, `-`).replaceAll(/-/g, `_`); export const nameFields = { @@ -15,6 +16,39 @@ export const forceFieldBlurOnPressEnter = (e: any) => { } } +export const removeNullAndUndefinedProperties = (object) => { + return Object.entries(object).reduce((accumulator, [key, value]) => { + if (value !== null && value !== undefined) { + accumulator[key] = value; + } + return accumulator; + }, {}); +} + +export const combineArraysByKey = (data: T[], key: keyof T): any[] => { + return data.reduce((combined, item) => { + const arrayToCombine = item[key]; + if (Array.isArray(arrayToCombine)) { + return combined.concat(arrayToCombine); + } + return combined; + }, [] as any[]); +} + +export const findHighestNumberInArrayByKey = async ( arrayOfObjects: any[], key: string ): Promise => { + try { + const filteredNumbers = arrayOfObjects + .map(obj => obj[key]) + .filter(value => typeof value === `number`); + if (filteredNumbers.length === 0) return 0; + const highestNumber = Math.max(...filteredNumbers); + return highestNumber; + } catch (error) { + console.log(`Error while finding the highest number for key "${key}"`, error); + return 0; + } +} + export const setMaxLengthOnField = (e: any, maxLength) => { const target = e.target as HTMLSpanElement; if (target.innerText.length > maxLength) { @@ -56,4 +90,20 @@ export const formatDateNoSpaces = (date: any = new Date()) => { let completedDate = strTime + ` ` + (date.getMonth() + 1) + `/` + date.getDate() + `/` + date.getFullYear(); completedDate = strTimeNoSpaces + `_` + (date.getMonth() + 1) + `-` + date.getDate() + `-` + date.getFullYear(); return completedDate; +} + +export const countPropertiesInObject = (obj) => { + let count = 0; + if (typeof obj === `object` && obj !== null) { + for (const key in obj) { + count++; + count += countPropertiesInObject(obj[key]); + } + if (Array.isArray(obj)) { + obj.forEach(item => { + count += countPropertiesInObject(item); + }); + } + } + return count; } \ No newline at end of file diff --git a/shared/models/User.ts b/shared/models/User.ts index 55f6184..fba7aad 100644 --- a/shared/models/User.ts +++ b/shared/models/User.ts @@ -1,7 +1,7 @@ import { genID } from '../ID'; -import { isValid } from '../constants'; import { Types } from '../types/types'; import { capWords } from '../../pages/_app'; +import { countPropertiesInObject, isValid } from '../constants'; export enum Providers { Google = `Google` , @@ -57,13 +57,18 @@ export class User { title?: string; password?: string; + phone: any; + avatar: any; name!: string; rank: number = 1; email: string = ``; + properties: number; type: Types = Types.User; role = ROLES.Subscriber.name; + boards?: any[]; + data = { taskIDs: [], itemIDs: [], @@ -80,16 +85,23 @@ export class User { provider: Providers.Firebase, } + auth = { + token: undefined, + verified: undefined, + anonymous: undefined, + } + constructor(data: Partial) { Object.assign(this, data); if (isValid(this.email) && !isValid(this.name)) this.name = capWords(this.email.split(`@`)[0]); this.A = this.name; - let ID = genID(Types.User, undefined, this.name); + let ID = genID(Types.User, undefined, this.name, this.uid); let { id, date, title, uuid } = ID; if (!isValid(this.id)) this.id = id; if (!isValid(this.uuid)) this.uuid = uuid; if (!isValid(this.title)) this.title = title; if (!isValid(this.meta.created)) this.meta.created = date; if (!isValid(this.meta.updated)) this.meta.updated = date; + if (!isValid(this.properties)) this.properties = countPropertiesInObject(this) + 1; } } \ No newline at end of file diff --git a/styles/_utility.scss b/styles/_utility.scss index 5e9c73f..3722be7 100644 --- a/styles/_utility.scss +++ b/styles/_utility.scss @@ -6,4 +6,20 @@ &.boardIndexAndTitle { --fullWidth: 83% !important; } +} + +article { + &:has(main.nx-w-full) { + padding-top: 2px; + padding-left: max(env(safe-area-inset-left), 1.5rem); + padding-right: max(env(safe-area-inset-right), 1.5rem); + main { + padding: 0; + div { + &.nx-mt-16, &.nx-mb-8 { + display: none; + } + } + } + } } \ No newline at end of file diff --git a/theme.config.tsx b/theme.config.tsx index 2896cdc..1d9a12e 100644 --- a/theme.config.tsx +++ b/theme.config.tsx @@ -42,7 +42,7 @@ const config: DocsThemeConfig = { extraContent:
- +
, },