Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Navigate, Route, Routes} from "react-router-dom";
import LandingPage from "./pages/LandingPage";
import SignUpPage from "./pages/SignUp";
import SignInPage from "./pages/SignIn";
import DashboardPage from "./pages/DashboardPage.tsx";
import ResourcePage from "./pages/ResourcePage";
import LibraryPage from "./pages/LibraryPage";
import FlashcardsPage from "./pages/FlashcardsPage";
Expand All @@ -16,14 +17,14 @@ const RequireAuth = ({ children }: { children: ReactElement }) => {
return children;
};


function App() {
return (
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/" element={<SignUpPage />} />
<Route path="/signup" element={<SignUpPage />} />
<Route path="/signin" element={<SignInPage />} />
<Route path="/dashboard" element={<DashboardPage />}/>
<Route path="/library" element={
<RequireAuth>
<LibraryPage />
Expand All @@ -34,6 +35,7 @@ function App() {
<FlashcardsPage />
</RequireAuth>
} />

<Route path="/resources/:id" element={
<RequireAuth>
<ResourcePage />
Expand All @@ -44,6 +46,4 @@ function App() {
);
}

export default App;


export default App;
Binary file added frontend/src/assets/icons/CloseIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/icons/DocStatIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/icons/DocumentStudyIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/icons/FlashStatIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/icons/FlashcardStudyIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/icons/QuizStudyIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/icons/QuizzesStatIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/icons/SummaryStudyIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 105 additions & 0 deletions frontend/src/components/dashboard/SetGoalModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useState } from "react";
import closeIcon from "../../assets/icons/CloseIcon.png";


type Props = {
initialValue: number;
onUpdateGoal: (minutes: number) => void;
onClose: () => void;
};

export default function SetGoalModal({ initialValue, onUpdateGoal, onClose }: Props) {
const [value, setValue] = useState<string>(
initialValue ? String(initialValue) : ""
);

const isValid: boolean = Number(value) > 0;

function handleSave(): void {
if (!isValid) return;
onUpdateGoal(Number(value));
onClose();
}

return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Overlay */}
<div
className="absolute inset-0 bg-black/68 backdrop-blur-sm"
onClick={onClose}
/>

{/* Modal */}
<div className="flex flex-col justify-center relative z-10 w-[676px] max-w-[90%] h-[355px] max-h-[90%] rounded-2xl bg-white p-10 gap-3 shadow-lg">
{/* Close button */}
<button
onClick={onClose}
className="absolute right-4 top-4 rounded-full p-1 text-black hover:bg-gray-100 cursor-pointer"
aria-label="Close"
>
<img src={closeIcon} alt="Close" className="h-6 w-6 object-contain"/>
</button>

{/* Title */}
<h3 className="mb-2 text-4xl font-bold">
Set Your Daily Study Goal
</h3>

{/* Description */}
<p className="mb-4 text-base text-gray-600">
How many minutes would you like to study each day?
</p>

{/* Input */}
<input
type="number"
min={5}
step={5}
placeholder="Daily Goal (minutes)"
value={value}
onChange={(e): void => setValue(e.target.value)}
className="
mb-6 w-3/5 rounded-3xl border border-gray-200
px-5 py-3 text-sm
outline-none
focus:border-[var(--color-primary)]
focus:ring-2 focus:ring-[var(--color-primary)]/20"
/>

{/* Actions */}
<div className="flex items-center gap-3">
<button
onClick={handleSave}
disabled={!isValid}
className="
h-[46px] w-[120px]
rounded-[20px]
bg-[var(--color-primary)]
text-base font-semibold text-white
disabled:opacity-50 disabled:cursor-not-allowed
cursor-pointer"
>
Save Goal
</button>

<button
onClick={onClose}
className="
h-[46px] w-[96px] rounded-[20px]
px-4 text-base font-semibold
text-[var(--color-primary)]
border border-[var(--color-primary)]
cursor-pointer"
>
Cancel
</button>
</div>

{/* Helper text */}
<p className="mt-4 text-xs text-gray-400">
* You can update this goal anytime.
</p>
</div>
</div>
);
}
50 changes: 50 additions & 0 deletions frontend/src/components/dashboard/StatCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import docIcon from "../../assets/icons/DocStatIcon.png";
import flashcardsIcon from "../../assets/icons/FlashStatIcon.png";
import quizzesIcon from "../../assets/icons/QuizzesStatIcon.png";


interface StatCardProps {
type: "documents" | "flashcards" | "quizzes";
value: number;
}


export default function StatCard({ type, value }: StatCardProps) {
const titleMap = {
documents: "Documents",
flashcards: "Flashcards",
quizzes: "Quizzes",
};

const colorMap = {
documents: "bg-[var(--color-primary)] text-white",
flashcards: "bg-[var(--color-primary-hover)] text-white",
quizzes: "bg-[var(--color-surface)] text-black",
};

const iconMap = {
documents: docIcon,
flashcards: flashcardsIcon,
quizzes: quizzesIcon,
};


return (
<div
className={`w-[419px] h-[132px] rounded-xl shadow ${colorMap[type]} flex items-center justify-between px-6`}
>
{/*Left-side - Value */}
<div className="flex flex-row items-baseline gap-2">
<div className="font-bold text-[64px]">{value}</div>
<div className="font-bold text-[20px]">{titleMap[type]}</div>
</div>

{/* Right-side - Icon */}
<img
src={iconMap[type]}
alt={titleMap[type]}
className="w-[122px] h-[120px] object-contain"
/>
</div>
);
}
196 changes: 196 additions & 0 deletions frontend/src/components/dashboard/StudyHighlights.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import SetGoalModal from "../../components/dashboard/SetGoalModal.tsx";
import React, { useState } from "react";

type WeeklyActivity = {
flashcards: number;
summaries: number;
quizzes: number;
};

type TodayStats = {
studiedMinutes: number;
goalMinutes: number;
flashcardsReviewed: number;
quizzesCompleted: number;
};

type Props = {
weekly: WeeklyActivity;
today: TodayStats;
setTodayStats: React.Dispatch<React.SetStateAction<TodayStats>>;
};

type WeeklyItem = {
key: keyof WeeklyActivity;
label: string;
color: string;
value: number;
percent: number;
};


export default function StudyHighlights({ weekly, today, setTodayStats }: Props) {
const weeklyTotal: number =
weekly.flashcards + weekly.summaries + weekly.quizzes;

const flashcardsPct: number =
weeklyTotal > 0 ? (weekly.flashcards / weeklyTotal) * 100 : 0;

const summariesPct: number =
weeklyTotal > 0 ? (weekly.summaries / weeklyTotal) * 100 : 0;

const quizzesPct: number =
weeklyTotal > 0 ? (weekly.quizzes / weeklyTotal) * 100 : 0;

const progressPercent: number =
today.goalMinutes > 0
? Math.min((today.studiedMinutes / today.goalMinutes) * 100, 100)
: 0;

const weeklyItems: WeeklyItem[] = [
{ key: "flashcards", label: "Flashcards", color: "#6B53FF", value: weekly.flashcards, percent: flashcardsPct },
{ key: "summaries", label: "Summaries", color: "#9381FF", value: weekly.summaries, percent: summariesPct,},
{ key: "quizzes", label: "Quizzes", color: "#FFDD64", value: weekly.quizzes, percent: quizzesPct },
];

const GAP = 0.6;
const [isGoalOpen, setIsGoalOpen] = useState(false);


return (
<div className="flex flex-col gap-3">
<div className="rounded-2xl bg-white p-8 shadow-[var(--shadow-card)]">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold">Study Highlights</h2>
<button
className="
text-xl font-semibold
pl-5
text-[var(--color-surface)]
bg-black w-[136px] h-[39px]
rounded-[19.5px]
cursor-pointer"

onClick={(): void => setIsGoalOpen(true)}
>
Set Goal
</button>
{isGoalOpen && (
<SetGoalModal
initialValue={today.goalMinutes}
onUpdateGoal={(minutes: number): void => {
setTodayStats((prev: TodayStats) => ({
...prev,
goalMinutes: minutes,
}));
}}
onClose={(): void => setIsGoalOpen(false)}
/>
)}

</div>

{/* Weekly Activity */}
<div className="mb-8">
<div className="mb-4 text-base font-bold ">
Weekly Activity (last 7 days)
</div>

<div className="flex items-center gap-8">

{/* Ring */}
<div className="relative w-[108px] h-[108px]">
{/* Gray base ring */}
<div
className="absolute inset-0 rounded-full bg-[var(--color-surface)]"
style={{
WebkitMask:
"radial-gradient(circle, transparent 62%, black 63%)",
mask:
"radial-gradient(circle, transparent 62%, black 63%)",
}}
/>

{/* Colored segments */}
<div
className="absolute inset-0 rounded-full"
style={{
background: ((): string => {
const segments: string[] = [];
let startPercent: number = 0;

weeklyItems.forEach((item: WeeklyItem): void => {
const endPercent: number = startPercent + item.percent;
segments.push(`${item.color} ${startPercent}% ${endPercent - GAP}%`);
segments.push(`transparent ${endPercent - GAP}% ${endPercent}%`);
startPercent = endPercent;
});

return `conic-gradient(${segments.join(",")})`;
})(),
WebkitMask: "radial-gradient(circle, transparent 62%, black 63%)",
mask: "radial-gradient(circle, transparent 62%, black 63%)",
}}
/>
</div>

{/* Legend */}
<div className="space-y-4 text-sm">
{weeklyItems.map((item: WeeklyItem) => (
<div key={item.key} className="flex items-center gap-2">
<span
className="h-5 w-5 rounded-full"
style={{ backgroundColor: item.color }}
/>
<span>
{Math.round(item.percent)}% {item.label}
</span>
</div>
))}
</div>
</div>
</div>

{/* Today */}
<div>
<div className="mb-2 ml-4 text-base font-bold ">Today</div>

{/* Time Studied */}
<div className="mb-4 flex items-end gap-8">
{/* Left text */}
<div className="space-y-2 text-base">
Time Studied
</div>

{/* Progress block */}
<div className="w-[200px]">
{/* Min labels */}
<div className="mb-1 pr-4 pl-4 flex justify-between text-[10px] text-gray-400">
<span>{today.studiedMinutes} min</span>
<span>{today.goalMinutes} min</span>
</div>

{/* Progress bar */}
<div className="h-3 w-full rounded-full bg-gray-200">
<div
className="h-3 rounded-s-full bg-[var(--color-primary)]"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
</div>


{/* Stats */}
<div className="space-y-2 text-base">
<div>Flashcards Reviewed {today.flashcardsReviewed} cards</div>
<div>Quizzes Completed {today.quizzesCompleted} quiz</div>
</div>
</div>
</div>
<div className="rounded-2xl bg-white p-8 shadow-[var(--shadow-card)]">
<p className="font-bold text-[18px]">Short study sessions improve retention</p>
</div>
</div>
);
}
Loading