diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6987fd6..689018c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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"; @@ -16,7 +17,6 @@ const RequireAuth = ({ children }: { children: ReactElement }) => { return children; }; - function App() { return ( @@ -24,6 +24,7 @@ function App() { } /> } /> } /> + }/> @@ -34,6 +35,7 @@ function App() { } /> + @@ -44,6 +46,4 @@ function App() { ); } -export default App; - - +export default App; \ No newline at end of file diff --git a/frontend/src/assets/icons/CloseIcon.png b/frontend/src/assets/icons/CloseIcon.png new file mode 100644 index 0000000..2d033aa Binary files /dev/null and b/frontend/src/assets/icons/CloseIcon.png differ diff --git a/frontend/src/assets/icons/DocStatIcon.png b/frontend/src/assets/icons/DocStatIcon.png new file mode 100644 index 0000000..e8dd72e Binary files /dev/null and b/frontend/src/assets/icons/DocStatIcon.png differ diff --git a/frontend/src/assets/icons/DocumentStudyIcon.png b/frontend/src/assets/icons/DocumentStudyIcon.png new file mode 100644 index 0000000..a232c19 Binary files /dev/null and b/frontend/src/assets/icons/DocumentStudyIcon.png differ diff --git a/frontend/src/assets/icons/FlashStatIcon.png b/frontend/src/assets/icons/FlashStatIcon.png new file mode 100644 index 0000000..2a5c3ff Binary files /dev/null and b/frontend/src/assets/icons/FlashStatIcon.png differ diff --git a/frontend/src/assets/icons/FlashcardStudyIcon.png b/frontend/src/assets/icons/FlashcardStudyIcon.png new file mode 100644 index 0000000..40cd146 Binary files /dev/null and b/frontend/src/assets/icons/FlashcardStudyIcon.png differ diff --git a/frontend/src/assets/icons/QuizStudyIcon.png b/frontend/src/assets/icons/QuizStudyIcon.png new file mode 100644 index 0000000..d3a84ce Binary files /dev/null and b/frontend/src/assets/icons/QuizStudyIcon.png differ diff --git a/frontend/src/assets/icons/QuizzesStatIcon.png b/frontend/src/assets/icons/QuizzesStatIcon.png new file mode 100644 index 0000000..1339f12 Binary files /dev/null and b/frontend/src/assets/icons/QuizzesStatIcon.png differ diff --git a/frontend/src/assets/icons/SummaryStudyIcon.png b/frontend/src/assets/icons/SummaryStudyIcon.png new file mode 100644 index 0000000..190a35f Binary files /dev/null and b/frontend/src/assets/icons/SummaryStudyIcon.png differ diff --git a/frontend/src/components/dashboard/SetGoalModal.tsx b/frontend/src/components/dashboard/SetGoalModal.tsx new file mode 100644 index 0000000..0943061 --- /dev/null +++ b/frontend/src/components/dashboard/SetGoalModal.tsx @@ -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( + initialValue ? String(initialValue) : "" + ); + + const isValid: boolean = Number(value) > 0; + + function handleSave(): void { + if (!isValid) return; + onUpdateGoal(Number(value)); + onClose(); + } + + return ( +
+ {/* Overlay */} +
+ + {/* Modal */} +
+ {/* Close button */} + + + {/* Title */} +

+ Set Your Daily Study Goal +

+ + {/* Description */} +

+ How many minutes would you like to study each day? +

+ + {/* Input */} + 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 */} +
+ + + +
+ + {/* Helper text */} +

+ * You can update this goal anytime. +

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/dashboard/StatCard.tsx b/frontend/src/components/dashboard/StatCard.tsx new file mode 100644 index 0000000..0dc3e0e --- /dev/null +++ b/frontend/src/components/dashboard/StatCard.tsx @@ -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 ( +
+ {/*Left-side - Value */} +
+
{value}
+
{titleMap[type]}
+
+ + {/* Right-side - Icon */} + {titleMap[type]} +
+ ); +} diff --git a/frontend/src/components/dashboard/StudyHighlights.tsx b/frontend/src/components/dashboard/StudyHighlights.tsx new file mode 100644 index 0000000..5581237 --- /dev/null +++ b/frontend/src/components/dashboard/StudyHighlights.tsx @@ -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>; +}; + +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 ( +
+
+
+

Study Highlights

+ + {isGoalOpen && ( + { + setTodayStats((prev: TodayStats) => ({ + ...prev, + goalMinutes: minutes, + })); + }} + onClose={(): void => setIsGoalOpen(false)} + /> + )} + +
+ + {/* Weekly Activity */} +
+
+ Weekly Activity (last 7 days) +
+ +
+ + {/* Ring */} +
+ {/* Gray base ring */} +
+ + {/* Colored segments */} +
{ + 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%)", + }} + /> +
+ + {/* Legend */} +
+ {weeklyItems.map((item: WeeklyItem) => ( +
+ + + {Math.round(item.percent)}% {item.label} + +
+ ))} +
+
+
+ + {/* Today */} +
+
Today
+ + {/* Time Studied */} +
+ {/* Left text */} +
+ Time Studied +
+ + {/* Progress block */} +
+ {/* Min labels */} +
+ {today.studiedMinutes} min + {today.goalMinutes} min +
+ + {/* Progress bar */} +
+
+
+
+
+ + + {/* Stats */} +
+
Flashcards Reviewed {today.flashcardsReviewed} cards
+
Quizzes Completed {today.quizzesCompleted} quiz
+
+
+
+
+

Short study sessions improve retention

+
+
+ ); +} diff --git a/frontend/src/components/dashboard/StudyHistory.tsx b/frontend/src/components/dashboard/StudyHistory.tsx new file mode 100644 index 0000000..b8502a2 --- /dev/null +++ b/frontend/src/components/dashboard/StudyHistory.tsx @@ -0,0 +1,180 @@ +// import { useNavigate } from "react-router-dom"; +import documentStudyIcon from "../../assets/icons/DocumentStudyIcon.png" +import flashcardStudyIcon from "../../assets/icons/FlashcardStudyIcon.png" +import quizStudyIcon from "../../assets/icons/QuizStudyIcon.png" +import summaryStudyIcon from "../../assets/icons/SummaryStudyIcon.png" +// import { getActivityLog } from "../../api/apiClient.ts" + + + +export type ActivityLogItem = { + id: string; + type: "resource_uploaded" | "summary_created" | "flashcards_created" | "quiz_created"; + resourceId: string; + resourceTitle: string; + createdAt: string; +}; + +type HistoryItem = { + id: string; + type: ActivityLogItem["type"]; + title: string; + resourceTitle: string; + subtitle: string; + resourceId: string; + icon: string; +}; + + +type TabKey = "resource" | "summary" | "flashcards" | "quizzes"; + + +const activityConfig = { + resource_uploaded: { + title: "Document Uploaded", + icon: documentStudyIcon, + }, + summary_created: { + title: "Summary Created", + icon: summaryStudyIcon, + }, + flashcards_created: { + title: "Flashcards Created", + icon: flashcardStudyIcon, + }, + quiz_created: { + title: "Quiz Created", + icon: quizStudyIcon, + }, +} as const; + +const tabMap: Record = { + resource_uploaded: "resource", + summary_created: "summary", + flashcards_created: "flashcards", + quiz_created: "quizzes", +}; + + + +function mapActivityToHistory(item: ActivityLogItem): HistoryItem { + const config = activityConfig[item.type]; + + function pluralize(value: number, unit: string): string { + return value === 1 ? unit : `${unit}s`; + } + + function formatTimeAgo(date: string): string { + const diff: number = Math.floor( + (Date.now() - new Date(date).getTime()) / 1000 + ); + + if (diff < 60) { + const seconds: number = diff; + return `${seconds} ${pluralize(seconds, "second")} ago`; + } + + if (diff < 3600) { + const minutes: number = Math.floor(diff / 60); + return `${minutes} ${pluralize(minutes, "minute")} ago`; + } + + if (diff < 86400) { + const hours: number = Math.floor(diff / 3600); + return `${hours} ${pluralize(hours, "hour")} ago`; + } + + const days: number = Math.floor(diff / 86400); + return `${days} ${pluralize(days, "day")} ago`; + } + + return { + id: item.id, + type: item.type, + title: config.title, + resourceTitle: item.resourceTitle, + subtitle: formatTimeAgo(item.createdAt), + resourceId: item.resourceId, + icon: config.icon, + }; +} + +interface StudyHistoryProps { + data: ActivityLogItem[]; +} + +export default function StudyHistory({ data }: StudyHistoryProps) { + // const navigate = useNavigate(); + const items: HistoryItem[] = data.map(mapActivityToHistory); + + const handleClick = (item: HistoryItem): void => { + if (!item.resourceId) { + alert("No resourceId available!"); + return; + } + alert(`Navigate to resource: ${item.resourceId}\nTab: ${tabMap[item.type]}`); + }; + + /* Backend integration (when API is ready) + + const handleClick = (item: HistoryItem) => { + if (!item.resourceId) return; + navigate(`/resources/${item.resourceId}`, { state: { activeTab: tabMap[item.type] } }); + }; + + const [apiData, setApiData] = useState([]); + + useEffect(() => { + getActivityLog() + .then(setApiData) + .catch((err) => { + console.error("Failed to load activity log", err); + }); + }, []); + + const items = apiData.map(mapActivityToHistory); + */ + + return ( +
+

Study History

+
    + {items.map((item: HistoryItem) => ( +
  • handleClick(item)} + > +
    + +
    +
    + + {item.title} + {" "} + + {item.resourceTitle} + +
    +
    {item.subtitle}
    +
    +
    +
  • + ))} +
+
+ ); +} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..59a3031 --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,126 @@ +import StatCard from "../components/dashboard/StatCard"; +import StudyHighlights from "../components/dashboard/StudyHighlights.tsx"; +import StudyHistory from "../components/dashboard/StudyHistory"; +import type { ActivityLogItem } from "../components/dashboard/StudyHistory"; +import {useState} from "react"; + + +type User = { + firstName: string; + lastName: string; +}; + +type DashboardStats = { + documents: number; + flashcards: number; + quizzes: number; +}; + + +// Mocks +const mockUser: User = { firstName: "Alena", lastName: "Petrov"}; + +const mockStats: DashboardStats = { + documents: 32, + flashcards: 120, + quizzes: 8, +}; + +const mockActivity: ActivityLogItem[] = [ + { id: "1", type: "resource_uploaded", resourceId: "res1", resourceTitle: "UX Principles", createdAt: "2026-01-16T07:00:00Z" }, + { id: "2", type: "flashcards_created", resourceId: "res2", resourceTitle: "React Basics", createdAt: "2026-01-15T13:00:00Z" }, + { id: "3", type: "quiz_created", resourceId: "res1", resourceTitle: "UX Principles", createdAt: "2026-01-14T23:00:00Z" }, + { id: "4", type: "resource_uploaded", resourceId: "res1", resourceTitle: "UX Principles", createdAt: "2026-01-14T17:00:00Z" }, + { id: "5", type: "summary_created", resourceId: "res2", resourceTitle: "React Basics", createdAt: "2026-01-13T17:00:00Z" }, + +]; + +const mockWeeklyActivity = { + flashcards: 50, + summaries: 25, + quizzes: 25, +}; + + +type TodayStats = { + studiedMinutes: number; + goalMinutes: number; + flashcardsReviewed: number; + quizzesCompleted: number; +}; + + +export default function DashboardPage() { + /** + * Auth + * TODO: replace mock user with real auth once backend is ready + */ + // const { user } = useAuth(); + // if (!user) return null; + const user = mockUser; + + /** + * Dashboard stats + * TODO: replace mock data with API call + * Endpoint: GET /api/dashboard/stats + * Expected response: + * { + * documents: number; + * flashcards: number; + * quizzes: number; + * } + */ + // const { data: stats, isLoading, error } = useDashboardStats(); + + const stats: DashboardStats = mockStats; + + + // TEMP: will replace with: + // const { data: activity } = useActivityLog(); + const activity: ActivityLogItem[] = mockActivity; + + + const [todayStats, setTodayStats] = useState({ + studiedMinutes: 25, + goalMinutes: 60, + flashcardsReviewed: 10, + quizzesCompleted: 2, + }); + + return ( +
+
+
+
+
+

+ Hi, {user.firstName} {user.lastName?.charAt(0)}. +

+
+
+ + {/* Stats */} +
+ + + +
+ + {/* Bottom grid */} +
+ + +
+ +
+
+
+
+
+ ); +} +