diff --git a/app/academia/components/Marks.tsx b/app/academia/components/Marks.tsx index ff7be6b5..93a92e68 100644 --- a/app/academia/components/Marks.tsx +++ b/app/academia/components/Marks.tsx @@ -4,11 +4,9 @@ import Error from "@/components/States/Error"; import Loading from "@/components/States/Loading"; import { useMarks } from "@/provider/MarksProvider"; import dynamic from "next/dynamic"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { FiInfo } from "react-icons/fi"; -import { IoRefreshOutline } from "react-icons/io5"; import NoData from "./subcomponents/NoData"; -import { useMutateAll } from "@/hooks/useMutate"; const InfoPopup = dynamic( () => import("./subcomponents/Attendance/InfoPopup").then((a) => a.default), @@ -24,20 +22,10 @@ const MarkCard = dynamic( ); export default function Marks() { - const mutate = useMutateAll(); - const { marks, isLoading, error, requestedAt } = useMarks(); + const { marks, isLoading, error, isOld } = useMarks(); const [showInfoPopup, setShowInfoPopup] = useState(false); const infoIconRef = useRef(null); - useEffect(() => { - if ( - !isLoading && - !error && - (!requestedAt || Date.now() - requestedAt > 2 * 60 * 60 * 1000) - ) - mutate({ mutateMarks: true }); - }, [error, isLoading, marks, mutate, requestedAt]); - const toggleInfoPopup = () => setShowInfoPopup((e) => !e); return ( @@ -69,14 +57,16 @@ export default function Marks() { )} - {!error && } + {!error && } {isLoading ? ( ) : error ? ( ) : marks ? ( - <> +
{marks ?.filter((a) => a.courseType === "Theory") @@ -91,7 +81,7 @@ export default function Marks() { ) .map((mark, i) => )}
- +
) : ( )} diff --git a/app/academia/components/subcomponents/Attendance/AttendanceCard.tsx b/app/academia/components/subcomponents/Attendance/AttendanceCard.tsx index d772b90a..393eb218 100644 --- a/app/academia/components/subcomponents/Attendance/AttendanceCard.tsx +++ b/app/academia/components/subcomponents/Attendance/AttendanceCard.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { AttendanceCourse } from "@/types/Attendance"; import { calculateMargin } from "@/utils/Margin"; import { useTimetable } from "@/provider/TimetableProvider"; @@ -9,10 +9,9 @@ import AttendancePill from "./AttendancePill"; import Margin from "./Margin"; import Title from "./Title"; -const Legend = dynamic( - () => import("./Legend").then((a) => a.default), - { ssr: true }, -); +const Legend = dynamic(() => import("./Legend").then((a) => a.default), { + ssr: true, +}); export default function AttendanceCard({ course, @@ -23,6 +22,7 @@ export default function AttendanceCard({ }) { const { timetable } = useTimetable(); const { day } = useDay(); + const { courseTitle, @@ -56,15 +56,17 @@ export default function AttendanceCard({
- <Margin - margin={margin} - category={category} - courseTitle={courseTitle} - countHoursPerDay={countHoursPerDay} - /> + <div> + <Margin + margin={margin} + category={category} + courseTitle={courseTitle} + countHoursPerDay={countHoursPerDay} + /> + </div> <AttendancePill present={present} absent={absent} total={total} /> <span className={`w-24 self-end justify-self-end text-right font-semibold md:self-center md:justify-self-center ${ diff --git a/app/academia/components/subcomponents/Attendance/AttendanceContent.tsx b/app/academia/components/subcomponents/Attendance/AttendanceContent.tsx index 7dfcc10f..9164ed76 100644 --- a/app/academia/components/subcomponents/Attendance/AttendanceContent.tsx +++ b/app/academia/components/subcomponents/Attendance/AttendanceContent.tsx @@ -1,34 +1,26 @@ -import React, { useEffect } from "react"; +import React from "react"; import { useAttendance } from "@/provider/AttendanceProvider"; import Error from "@/components/States/Error"; import Loading from "@/components/States/Loading"; import NoData from "../NoData"; import AttendanceList from "./Predict/AttendanceList"; import { AttendanceCourse } from "@/types/Attendance"; -import { useMutateAll } from "@/hooks/useMutate"; export default function AttendanceContent(): JSX.Element { - const { attendance, requestedAt, isLoading, error } = useAttendance(); - const mutate = useMutateAll(); - - useEffect(() => { - if ( - !isLoading && - !error && - (!requestedAt || Date.now() - requestedAt > 60 * 60 * 1000) - ) { - mutate({ mutateAttendance: true }); - } - }, [attendance, error, isLoading, mutate, requestedAt]); + const { attendance, isOld, isLoading, error } = useAttendance(); if (isLoading) return <Loading size="max" />; if (error) return <Error component="Attendance" />; if (!attendance) return <NoData component="Attendance" />; return ( - <AttendanceList - open={false} - displayedAttendance={attendance as AttendanceCourse[]} - /> + <div + className={`${isOld ? "border-light-info-color dark:border-dark-info-color" : "border-transparent"} -mx-2 rounded-2xl border-4 border-dotted`} + > + <AttendanceList + open={false} + displayedAttendance={attendance as AttendanceCourse[]} + /> + </div> ); } diff --git a/app/academia/components/subcomponents/Attendance/AttendanceHeader.tsx b/app/academia/components/subcomponents/Attendance/AttendanceHeader.tsx index a961fb93..ae37f481 100644 --- a/app/academia/components/subcomponents/Attendance/AttendanceHeader.tsx +++ b/app/academia/components/subcomponents/Attendance/AttendanceHeader.tsx @@ -31,7 +31,7 @@ export const AttendanceHeader: FC<AttendanceHeaderProps> = ({ const [showInfoPopup, setShowInfoPopup] = useState<boolean>(false); const infoIconRef = useRef<HTMLDivElement>(null); - const {error} = useAttendance() + const {error, isOld} = useAttendance() useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -92,7 +92,7 @@ export const AttendanceHeader: FC<AttendanceHeaderProps> = ({ )} </div> </div> - {!error && <Refresh type={{ mutateAttendance: true }} />} + {!error && <Refresh type={{ mutateAttendance: true }} isOld={isOld} />} </div> ); }; \ No newline at end of file diff --git a/app/academia/components/subcomponents/Attendance/InfoPopup.tsx b/app/academia/components/subcomponents/Attendance/InfoPopup.tsx index 2134541c..9ff98fb9 100644 --- a/app/academia/components/subcomponents/Attendance/InfoPopup.tsx +++ b/app/academia/components/subcomponents/Attendance/InfoPopup.tsx @@ -12,7 +12,7 @@ const InfoPopup: React.FC<InfoPopupProps> = ({ }) => ( <div style={{ WebkitBackdropFilter: "blur(10px)" }} - className={`absolute ${bottom ? "top-6" : "bottom-6"} md:-left-20 -left-28 z-10 w-48 animate-fadeIn rounded-xl bg-light-background-light bg-opacity-60 text-light-color shadow-lg backdrop-blur-sm dark:bg-dark-background-light dark:bg-opacity-70 dark:text-dark-color`} + className={`absolute ${bottom ? "top-6" : "bottom-6"} md:-left-20 -left-28 w-48 animate-fadeIn rounded-xl bg-light-background-light bg-opacity-60 text-light-color shadow-lg backdrop-blur-sm dark:bg-dark-background-light dark:bg-opacity-70 dark:text-dark-color`} > <p className="p-3 px-4 text-xs font-medium opacity-100 md:text-sm dark:opacity-70"> {text} diff --git a/app/academia/components/subcomponents/Attendance/Legend.tsx b/app/academia/components/subcomponents/Attendance/Legend.tsx index 4a5224d1..6d094c52 100644 --- a/app/academia/components/subcomponents/Attendance/Legend.tsx +++ b/app/academia/components/subcomponents/Attendance/Legend.tsx @@ -1,17 +1,53 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from "react"; +import { FiInfo } from "react-icons/fi"; +import InfoPopup from "./InfoPopup"; export default function AttendanceLegend() { + const [showInfoPopup, setShowInfoPopup] = useState<boolean>(false); + const infoIconRef = useRef<HTMLDivElement>(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + infoIconRef.current && + !infoIconRef.current.contains(event.target as Node) + ) { + setShowInfoPopup(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const toggleInfoPopup = () => setShowInfoPopup((prev) => !prev); + return ( - <div className="hidden items-center justify-between p-4 opacity-40 xl:flex"> - <div className="flex w-[300px] items-center space-x-3"> + <div className="hidden items-center justify-between p-4 xl:flex"> + <div className="flex w-[300px] items-center space-x-3 opacity-40"> <span className="text-xs font-medium text-light-color dark:text-dark-color"> Title </span> </div> - <div className="w-24 text-right text-xs"> - <span className="text-xs font-medium">Margin</span> + <div className="flex w-24 items-center justify-end gap-3 text-right text-xs"> + <span className="text-xs font-medium opacity-40">Margin</span> + <div className="relative" ref={infoIconRef}> + <FiInfo + className="cursor-help opacity-40" + onClick={toggleInfoPopup} + onMouseEnter={toggleInfoPopup} + onMouseLeave={() => setShowInfoPopup(false)} + /> + {showInfoPopup && ( + <InfoPopup + warn + text="Enter the dates you'll be absent to see a predicted attendance percentage and margin." + onClose={() => setShowInfoPopup(false)} + /> + )} + </div> </div> - <div className="flex items-center gap-3 rounded-full bg-light-background-darker p-0.5 text-xs font-medium dark:bg-dark-background-darker"> + <div className="flex items-center gap-3 rounded-full bg-light-background-darker p-0.5 text-xs font-medium opacity-40 dark:bg-dark-background-darker"> <div className="flex items-center font-medium"> <span className="rounded-l-full bg-light-success-background px-3 text-light-success-color dark:bg-dark-success-background dark:text-dark-success-color"> Present @@ -24,7 +60,7 @@ export default function AttendanceLegend() { Total </span> </div> - <span className="text-xs">Percentage</span> + <span className="text-xs opacity-40">Percentage</span> </div> ); -} \ No newline at end of file +} diff --git a/app/academia/components/subcomponents/Attendance/Predict/AttendanceList.tsx b/app/academia/components/subcomponents/Attendance/Predict/AttendanceList.tsx index f71aeefe..2a886915 100644 --- a/app/academia/components/subcomponents/Attendance/Predict/AttendanceList.tsx +++ b/app/academia/components/subcomponents/Attendance/Predict/AttendanceList.tsx @@ -14,13 +14,12 @@ interface AttendanceListProps { } export default function AttendanceList({ - open, displayedAttendance, }: AttendanceListProps) { return ( <> {displayedAttendance && ( - <div className={`-mx-3 transition duration-200`}> + <div className={`transition duration-200`}> <AttendanceCard legend course={displayedAttendance[0]} /> </div> )} @@ -31,7 +30,7 @@ export default function AttendanceList({ .map((course, index) => ( <div key={index} - className="-mx-3 my-1 rounded-xl odd:bg-light-background-normal even:bg-light-background-light odd:dark:bg-dark-background-normal even:dark:bg-transparent" + className="my-1 rounded-xl odd:bg-light-background-normal even:bg-light-background-light odd:dark:bg-dark-background-normal even:dark:bg-transparent" > <AttendanceCard course={course} /> </div> @@ -42,7 +41,7 @@ export default function AttendanceList({ <Indicator type="Practical" separator /> )} - <div className="my-4"> + <div className="mt-4"> {displayedAttendance ?.filter((a) => a.category === "Practical") .map((course, index) => ( diff --git a/components/Indicator.tsx b/components/Indicator.tsx index 9fd8b71e..f4bbcaec 100644 --- a/components/Indicator.tsx +++ b/components/Indicator.tsx @@ -10,7 +10,7 @@ export default function Indicator({ separator?: boolean }) { return separator ? ( - <div className="select-none flex w-full gap-3 items-center" aria-hidden="true" style={{ WebkitUserSelect: "none" }} > + <div className="select-none ml-3 max-w-[97%] flex w-full gap-3 items-center" aria-hidden="true" style={{ WebkitUserSelect: "none" }} > <span className={`flex items-start justify-start rounded-full text-xs font-semibold ${type === "Practical" || type === "Lab" ? "dark:text-practical text-light-success-color" : "dark:text-theory text-light-warn-color"}`} > diff --git a/components/Refresh.tsx b/components/Refresh.tsx index c97ac283..757d5ba1 100644 --- a/components/Refresh.tsx +++ b/components/Refresh.tsx @@ -2,7 +2,7 @@ import React from "react"; import { IoRefreshOutline } from "react-icons/io5"; import { MutateOptions, useMutateAll } from "@/hooks/useMutate"; -export default function Refresh({ type }: { type?: MutateOptions }) { +export default function Refresh({ type, isOld }: { type?: MutateOptions; isOld?: boolean }) { const mutateAll = useMutateAll(); const clickHandler = () => { const c = confirm( @@ -14,10 +14,10 @@ export default function Refresh({ type }: { type?: MutateOptions }) { return ( <button tabIndex={0} - className={`rounded-full p-1 text-sm text-light-color opacity-60 transition duration-200 hover:bg-light-background-dark active:-rotate-45 dark:text-dark-color dark:hover:bg-dark-background-dark`} + className={`rounded-full p-1 text-sm group ${isOld ? "bg-light-info-color dark:bg-dark-info-color dark:text-light-color text-dark-color px-2" : "hover:bg-light-background-dark text-light-color opacity-60 dark:text-dark-color dark:hover:bg-dark-background-dark"}`} onClick={clickHandler} > - <IoRefreshOutline /> + <IoRefreshOutline className="group-active:-rotate-45 transition duration-200" /> </button> ); } diff --git a/provider/AttendanceProvider.tsx b/provider/AttendanceProvider.tsx index e248b4ba..ebeec726 100644 --- a/provider/AttendanceProvider.tsx +++ b/provider/AttendanceProvider.tsx @@ -11,6 +11,7 @@ interface AttendanceContextType { attendance: AttendanceCourse[] | null; error: Error | null; requestedAt: number | null; + isOld?: boolean; isLoading: boolean; mutate: () => Promise<void | AttendanceResponse | null | undefined>; } @@ -18,6 +19,7 @@ interface AttendanceContextType { const AttendanceContext = createContext<AttendanceContextType>({ attendance: null, error: null, + isOld: false, requestedAt: null, isLoading: false, mutate: async () => {}, @@ -131,6 +133,7 @@ export function AttendanceProvider({ attendance: attendance?.attendance || null, requestedAt: attendance?.requestedAt || 0, error: error || null, + isOld: !isValidating && !error && attendance?.requestedAt ? Date.now() - attendance?.requestedAt > 2 * 60 * 60 * 1000 : false, isLoading: isValidating, mutate, }} diff --git a/provider/MarksProvider.tsx b/provider/MarksProvider.tsx index 906ca40b..0628a2f6 100644 --- a/provider/MarksProvider.tsx +++ b/provider/MarksProvider.tsx @@ -15,6 +15,7 @@ import { token } from "@/utils/Encrypt"; interface MarksContextType { marks: Mark[] | null; + isOld?: boolean; requestedAt: number | null; error: Error | null; isLoading: boolean; @@ -24,6 +25,7 @@ interface MarksContextType { const MarksContext = createContext<MarksContextType>({ marks: null, requestedAt: null, + isOld: false, error: null, isLoading: false, mutate: async () => {}, @@ -133,6 +135,7 @@ export function MarksProvider({ ) || null, requestedAt: marks?.requestedAt || 0, error: error || null, + isOld: !isValidating && !error && marks?.requestedAt ? Date.now() - marks?.requestedAt > 4 * 60 * 60 * 1000 : false, isLoading: isValidating, mutate, }} @@ -140,4 +143,4 @@ export function MarksProvider({ {children} </MarksContext.Provider> ); -} +} \ No newline at end of file