diff --git a/src/App.css b/src/App.css index f20a3b6..a3af7c6 100644 --- a/src/App.css +++ b/src/App.css @@ -257,15 +257,15 @@ --alpha-shadow-color-scroll-shaded-div: 0.3; --color-very-good: 21, 87, 37; - --color-very-good-background: 0, 255, 0, 0.2; - --color-good: 66, 77, 30; - --color-good-background: 201, 255, 0, 0.2; - --color-average: 89, 89, 25; - --color-average-background: 255, 217, 0, 0.2; - --color-bad: 104, 80, 35; - --color-bad-background: 255, 165, 0, 0.2; - --color-very-bad: 117, 45, 45; - --color-very-bad-background: 153, 1, 1, 0.2; + --color-very-good-background: 0, 255, 0, 0.2; + --color-good: 66, 77, 30; + --color-good-background: 201, 255, 0, 0.2; + --color-average: 89, 89, 25; + --color-average-background: 255, 217, 0, 0.2; + --color-bad: 104, 80, 35; + --color-bad-background: 255, 165, 0, 0.2; + --color-very-bad: 117, 45, 45; + --color-very-bad-background: 153, 1, 1, 0.2; } :root.balanced *, diff --git a/src/App.jsx b/src/App.jsx index 9b614b0..9d84a35 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -874,9 +874,6 @@ export default function App() { newGrade.subjectName = grade.libelleMatiere; newGrade.isSignificant = !grade.nonSignificatif; newGrade.examSubjectSRC = grade.uncSujet; - if (grade.id == 6291688) { - console.log(grade) - } newGrade.examSubjectSRC = grade.uncSujet === "" ? undefined : new File(grade.uncSujet, "NODEVOIR", grade.uncSujet, `sujet-${grade.devoir}-${grade.subjectCode}`, { idDevoir: grade.id }); newGrade.examCorrectionSRC = grade.uncCorrige === "" ? undefined : new File(grade.uncCorrige, "NODEVOIR", grade.uncCorrige, `correction-${grade.devoir}-${grade.subjectCode}`, { idDevoir: grade.id }); newGrade.isReal = true; @@ -1141,7 +1138,7 @@ export default function App() { function sortMessages(messages) { - const sortedMessages = messages.messages.received.map((message) => { console.log("files:", message.files); return { + const sortedMessages = messages.messages.received.map((message) => { return { date: message.date, files: structuredClone(message.files)?.map((file) => new File(file.id, file.type, file.libelle)), from: message.from, @@ -1316,7 +1313,6 @@ export default function App() { }) }) .then((response) => { - console.log(".then ~ response:", response) // GESTION DATA let statusCode = response.code; if (statusCode === 200) { @@ -1696,7 +1692,6 @@ export default function App() { ) .then((response) => response.json()) .then((response) => { - console.log(".then ~ response:", response) let code; if (accountsListState[activeAccount].firstName === "Guest") { code = 49969; @@ -1723,7 +1718,7 @@ export default function App() { async function fetchMessageContent(id, controller) { const oldSortedMessages = useUserData("sortedMessages").get(); - if (oldSortedMessages) { + if (oldSortedMessages && oldSortedMessages?.length > 0) { const targetMessageIdx = oldSortedMessages.findIndex((item) => item.id === id); if (oldSortedMessages[targetMessageIdx].content !== null) { return; diff --git a/src/components/app/Messaging/Inbox.css b/src/components/app/Messaging/Inbox.css index b5d3328..e8e4337 100644 --- a/src/components/app/Messaging/Inbox.css +++ b/src/components/app/Messaging/Inbox.css @@ -43,9 +43,24 @@ padding: 10px; border-left: 4px solid rgb(var(--text-color-alt), .5); border-bottom: 1px solid rgb(var(--text-color-alt), .5); - opacity: .6; + opacity: 0; + --opacity: .6; outline: none; cursor: pointer; + animation: jump-in 0.5s var(--timing-function-spring-effect) forwards; + animation-delay: calc(var(--order) * 60ms); +} + +@keyframes jump-in { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: var(--opacity); + transform: translateY(0); + } } #inbox .message-container:last-child { @@ -55,14 +70,14 @@ #inbox .message-container[data-read=false] { border-left: 4px solid rgb(var(--text-color-alt)); padding-left: 6px; - opacity: 1; + --opacity: 1; } #inbox .message-container:is(:hover, :focus-visible) { background-color: rgba(var(--background-color-0), .2); } #inbox .message-container.selected { - opacity: .8; + --opacity: .8; background-color: rgba(var(--background-color-focus)); } @@ -128,3 +143,14 @@ :fullscreen #inbox .message-container:last-child { border-bottom: none; } + + +#inbox .no-message-received { + color: rgb(var(--text-color-alt)); + display: flex; + height: 100%; + padding: 75px 20px; + align-items: center; + justify-content: center; + text-align: center; +} diff --git a/src/components/app/Messaging/Inbox.jsx b/src/components/app/Messaging/Inbox.jsx index f064bd7..342676b 100644 --- a/src/components/app/Messaging/Inbox.jsx +++ b/src/components/app/Messaging/Inbox.jsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useContext } from "react"; - +import { useState, useEffect, useRef, useContext } from "react"; +import ContentLoader from "react-content-loader"; import { AppContext } from "../../../App"; import "./Inbox.css"; @@ -12,31 +12,34 @@ import MarkAsUnread from "../../graphics/MarkAsUnread"; export default function Inbox({ selectedMessage, setSelectedMessage, fetchMessageMarkAsUnread }) { // States - const { useUserData } = useContext(AppContext); + const { useUserData, actualDisplayTheme, useUserSettings } = useContext(AppContext); + const settings = useUserSettings(); const [search, setSearch] = useState(""); const messages = useUserData("sortedMessages"); + const contentLoadersRandomValues = useRef({ authorWidth: Array.from({ length: 13 }, (_) => Math.round(Math.random() * 100) + 100), subjectWidth: Array.from({ length: 13 }, (_) => Math.floor(Math.random() * 150) + 150), dateWidth: Array.from({ length: 13 }, (_) => Math.floor(Math.random() * 50) + 50), containsFiles: Array.from({ length: 13 }, (_) => (Math.random() > .6)) }) + // behavior const handleClick = (message) => { setSelectedMessage(message.id); } - + const handleKeyDown = (event, msg) => { if (event.key === "Enter" || event.key === " ") { handleClick(msg); } } - + const handleMarkAsUnread = (event, msg) => { event.preventDefault(); event.stopPropagation(); const controller = new AbortController(); fetchMessageMarkAsUnread([msg.id], controller); - + if (msg.id === selectedMessage) { setSelectedMessage(null); } - + // mark as unread locally and kick the content so as to trigger a refetch the next reading (as the "mark as read" feature is trigger when fetching the message) const oldMsg = messages.get(); const msgIdx = oldMsg.findIndex((item) => item.id === msg.id); @@ -54,7 +57,7 @@ export default function Inbox({ selectedMessage, setSelectedMessage, fetchMessag let regexp; try { regexp = new RegExp(removeAccents(search.toLowerCase())); - } catch {return -1} + } catch { return -1 } const filterBy = [message.subject, message.from.name, message.content?.content, message.files?.map((file) => file.name)].flat(); for (let filter of filterBy) { if (filter) { @@ -75,8 +78,8 @@ export default function Inbox({ selectedMessage, setSelectedMessage, fetchMessag ? (messages.get().length > 0 ? <ScrollShadedDiv className="messages-container"> <ul> - {messages.get().filter(filterResearch).map((message) => <li className={"message-container" + (selectedMessage === message.id ? " selected" : "")} data-read={message.read} onClick={() => handleClick(message)} onKeyDown={(event) => handleKeyDown(event, message)} key={message.id} role="button" tabIndex={0}> - <h4 className="message-subject"><span className="author-name">{message.from.name}</span> <span className="actions"><button disabled={!message.read} onClick={(event) => handleMarkAsUnread(event, message)} className="mark-as-unread" title="Marquer comme non lu"><MarkAsUnread className="mark-as-unread-icon"/></button> {message.files?.length > 0 && <AttachmentIcon className="attachment-icon" />}</span></h4> + {messages.get().filter(filterResearch).map((message, index) => <li style={{ "--order": index }} className={"message-container" + (selectedMessage === message.id ? " selected" : "")} data-read={message.read} onClick={() => handleClick(message)} onKeyDown={(event) => handleKeyDown(event, message)} key={message.id} role="button" tabIndex={0}> + <h4 className="message-subject"><span className="author-name">{message.from.name}</span> <span className="actions"><button disabled={!message.read} onClick={(event) => handleMarkAsUnread(event, message)} className="mark-as-unread" title="Marquer comme non lu"><MarkAsUnread className="mark-as-unread-icon" /></button> {message.files?.length > 0 && <AttachmentIcon className="attachment-icon" />}</span></h4> <p className="message-author">{message.subject}</p> <p className="message-date">{(new Date(message.date)).toLocaleDateString("fr-FR", { month: "long", @@ -87,9 +90,41 @@ export default function Inbox({ selectedMessage, setSelectedMessage, fetchMessag </li>)} </ul> </ScrollShadedDiv> - : <p>Vous n'avez reçu aucun message. Profitez bien de votre isolement social ^^</p> + : <p className="no-message-received">Vous n'avez reçu aucun message. Profitez bien de votre isolement social ^^</p> ) - : <p>content-loader</p> + : <ScrollShadedDiv className="messages-container"> + <ul> + {Array.from({ length: 13 }, (_, index) => <li key={index} style={{ "--order": -69 /* skip the animation */ }} className={"message-container"}> + <h4 className="message-subject"><span className="author-name"><ContentLoader + animate={settings.get("displayMode") === "quality"} + speed={1} + backgroundColor={actualDisplayTheme === "dark" ? "#7878ae" : "#75759a"} + foregroundColor={actualDisplayTheme === "dark" ? "#9292d4" : "#9292c0"} + style={{ width: `min(${contentLoadersRandomValues.current.authorWidth[index]}px, 100%)`, maxHeight: "20px" }} + > + <rect x="0" y="0" rx="5" ry="5" width="100%" height="100%" /> + </ContentLoader></span><span className="actions">{contentLoadersRandomValues.current.containsFiles[index] && <AttachmentIcon className="attachment-icon" />}</span></h4> + <p className="message-author"><ContentLoader + animate={settings.get("displayMode") === "quality"} + speed={1} + backgroundColor={actualDisplayTheme === "dark" ? "#63638c" : "#9d9dbd"} + foregroundColor={actualDisplayTheme === "dark" ? "#7e7eb2" : "#bcbce3"} + style={{ width: `min(${contentLoadersRandomValues.current.subjectWidth[index]}px, 60%)`, maxHeight: "16px" }} + > + <rect x="0" y="0" rx="5" ry="5" width="100%" height="100%" /> + </ContentLoader></p> + <p className="message-date"><ContentLoader + animate={settings.get("displayMode") === "quality"} + speed={1} + backgroundColor={actualDisplayTheme === "dark" ? "#63638c" : "#9d9dbd"} + foregroundColor={actualDisplayTheme === "dark" ? "#7e7eb2" : "#bcbce3"} + style={{ width: `min(${contentLoadersRandomValues.current.dateWidth[index]}px, 30%)`, maxHeight: "16px" }} + > + <rect x="0" y="0" rx="5" ry="5" width="100%" height="100%" /> + </ContentLoader></p> + </li>)} + </ul> + </ScrollShadedDiv> } </div> ) diff --git a/src/components/app/Messaging/MessageReader.css b/src/components/app/Messaging/MessageReader.css index aa5cdab..1ebf66f 100644 --- a/src/components/app/Messaging/MessageReader.css +++ b/src/components/app/Messaging/MessageReader.css @@ -101,3 +101,14 @@ #message-reader .attachments-container .attachment .download-icon { height: 20px; } + +#message-reader .no-selected-message-placeholder { + color: rgb(var(--text-color-alt)); + width: 100%; + padding: 75px 20px; + position: relative; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} diff --git a/src/components/app/Messaging/MessageReader.jsx b/src/components/app/Messaging/MessageReader.jsx index 5da9e06..764f945 100644 --- a/src/components/app/Messaging/MessageReader.jsx +++ b/src/components/app/Messaging/MessageReader.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useContext } from "react"; - +import ContentLoader from "react-content-loader"; import { AppContext } from "../../../App"; import "./MessageReader.css"; @@ -12,7 +12,8 @@ import DownloadIcon from "../../graphics/DownloadIcon"; export default function MessageReader({ selectedMessage }) { // States - const { useUserData, actualDisplayTheme } = useContext(AppContext); + const { useUserData, actualDisplayTheme, useUserSettings } = useContext(AppContext); + const settings = useUserSettings(); const messages = useUserData("sortedMessages").get(); const message = messages ? messages.find((item) => item.id === selectedMessage) : null; @@ -21,8 +22,7 @@ export default function MessageReader({ selectedMessage }) { // JSX return ( <div id="message-reader"> - {selectedMessage !== null - ? message?.content + {selectedMessage !== null && messages && messages.length > 0 ? <div className="message-container"> <div className="email-header"> <p className="author">{message && message?.from?.name}</p> @@ -35,9 +35,37 @@ export default function MessageReader({ selectedMessage }) { }))}</p> </div> <hr /> - <ScrollShadedDiv className="message-content-container" key={selectedMessage}> + <ScrollShadedDiv className="message-content-container" key={message?.content ? selectedMessage + "-content" /* trigger a rerender so that the ScrollShadedDiv detect overflow and display shadows */ : selectedMessage}> + {message?.content + ? <EncodedHTMLDiv className="message-content" backgroundColor={actualDisplayTheme === "dark" ? "#303047" : "#d6d6f8"}>{message?.content && message?.content?.content}</EncodedHTMLDiv> + : <ContentLoader + className="message-content" + animate={settings.get("displayMode") === "quality"} + speed={1} + backgroundColor={actualDisplayTheme === "dark" ? "#63638c" : "#9d9dbd"} + foregroundColor={actualDisplayTheme === "dark" ? "#7e7eb2" : "#bcbce3"} + style={{ display: "block", width: "min(800px, 100%)", margin: "0 auto", height: "575px" }} + > + <rect x="0" y="0" rx="8" ry="8" width="30%" height="20px" /> + + <rect x="0" y="60" rx="8" ry="8" width="100%" height="20px" /> + <rect x="0" y="90" rx="8" ry="8" width="70%" height="20px" /> + + <rect x="0" y="150" rx="8" ry="8" width="100%" height="20px" /> + <rect x="0" y="180" rx="8" ry="8" width="100%" height="20px" /> + <rect x="0" y="210" rx="8" ry="8" width="100%" height="20px" /> + <rect x="0" y="240" rx="8" ry="8" width="50%" height="20px" /> + + <rect x="0" y="300" rx="8" ry="8" width="100%" height="20px" /> + <rect x="0" y="330" rx="8" ry="8" width="40%" height="20px" /> + + <rect x="0" y="390" rx="8" ry="8" width="40%" height="20px" /> + <rect x="0" y="420" rx="8" ry="8" width="60%" height="20px" /> + <rect x="0" y="450" rx="8" ry="8" width="30%" height="20px" /> - <EncodedHTMLDiv className="message-content" backgroundColor={actualDisplayTheme === "dark" ? "#303047" : "#d6d6f8"}>{message?.content && message?.content?.content}</EncodedHTMLDiv> + <rect x="0" y="510" rx="8" ry="8" width="20%" height="20px" /> + </ContentLoader> + } </ScrollShadedDiv> {message && (message?.files?.length > 0 ? <> @@ -51,8 +79,7 @@ export default function MessageReader({ selectedMessage }) { : null)} </div> - : <p>content-loader</p> - : <p>Sélectionnez un message dans votre boîte de réception pour le visualiser ici !</p> + : <p className="no-selected-message-placeholder">Sélectionnez un message dans votre boîte de réception pour le visualiser ici</p> } </div> ) diff --git a/src/components/app/Messaging/Messaging.jsx b/src/components/app/Messaging/Messaging.jsx index 452ac20..aceeabe 100644 --- a/src/components/app/Messaging/Messaging.jsx +++ b/src/components/app/Messaging/Messaging.jsx @@ -38,7 +38,6 @@ export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fe if (isLoggedIn) { if (messages.get() === undefined) { fetchMessages(controller); - setSelectedMessage(null); } } @@ -48,6 +47,9 @@ export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fe }, [isLoggedIn, activeAccount, messages.get()]); useEffect(() => { + if (messages.get() === undefined) { + return; + } const controller = new AbortController(); if (selectedMessage !== null) { fetchMessageContent(selectedMessage, controller); @@ -65,7 +67,7 @@ export default function Messaging({ isLoggedIn, activeAccount, fetchMessages, fe return () => { controller.abort(); } - }, [location, selectedMessage]); + }, [location, selectedMessage, messages.get()]); useEffect(() => { if (oldSelectedMessage.current !== selectedMessage) { diff --git a/src/components/graphics/Arrow.jsx b/src/components/graphics/Arrow.jsx index 39e4848..4fecfab 100644 --- a/src/components/graphics/Arrow.jsx +++ b/src/components/graphics/Arrow.jsx @@ -2,6 +2,6 @@ import "./graphics.css" export default function Arrow({ className = "", id = "", alt, ...props }) { return ( - <svg aria-label={alt} className={className} id={id} height="20px" width="20px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-62.7 -62.7 455.40 455.40" xml:space="preserve" strokeWidth="33"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path id="XMLID_337_" d="M253.858,234.26c-2.322-5.605-7.792-9.26-13.858-9.26h-60V15c0-8.284-6.716-15-15-15 c-8.284,0-15,6.716-15,15v210H90c-6.067,0-11.537,3.655-13.858,9.26c-2.321,5.605-1.038,12.057,3.252,16.347l75,75 C157.322,328.536,161.161,330,165,330s7.678-1.464,10.607-4.394l75-75C254.896,246.316,256.18,239.865,253.858,234.26z M165,293.787 L126.213,255h77.573L165,293.787z"></path> </g></svg> + <svg aria-label={alt} className={className} id={id} height="20px" width="20px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="-62.7 -62.7 455.40 455.40" xmlSpace="preserve" strokeWidth="33"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path id="XMLID_337_" d="M253.858,234.26c-2.322-5.605-7.792-9.26-13.858-9.26h-60V15c0-8.284-6.716-15-15-15 c-8.284,0-15,6.716-15,15v210H90c-6.067,0-11.537,3.655-13.858,9.26c-2.321,5.605-1.038,12.057,3.252,16.347l75,75 C157.322,328.536,161.161,330,165,330s7.678-1.464,10.607-4.394l75-75C254.896,246.316,256.18,239.865,253.858,234.26z M165,293.787 L126.213,255h77.573L165,293.787z"></path> </g></svg> ) } \ No newline at end of file