diff --git a/frontend/app/streak/page.tsx b/frontend/app/streak/page.tsx index cfe5bd1..a1bb8de 100644 --- a/frontend/app/streak/page.tsx +++ b/frontend/app/streak/page.tsx @@ -1,51 +1,488 @@ "use client"; -import { StreakScreen } from "@/components/StreakScreen"; -import { DayData } from "@/components/WeeklyCalendar"; + +import React, { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useStreak } from "@/hooks/useStreak"; -import { useMemo } from "react"; -const DAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; +export interface StreakData { + [date: string]: { + completed: boolean; + inStreak?: boolean; + missed?: boolean; + }; +} -function getWeekData(streakDates: string[]): DayData[] { - const today = new Date(); - const currentDay = today.getDay(); // 0 = Sunday, 1 = Monday, etc. - - // Build array for the current week (Sun-Sat) - return DAYS.map((day, index) => { - // Calculate date for this day of the week - const dayDate = new Date(today); - dayDate.setDate(today.getDate() - (currentDay - index)); - const dateString = dayDate.toISOString().split("T")[0]; - - return { - day, - completed: streakDates.includes(dateString), - }; - }); +export interface DayData { + day: string; + completed: boolean; } -export default function StreakPage() { - const router = useRouter(); - const { currentStreak, streakDates, isLoading } = useStreak({ autoFetch: true }); +interface StreakDayIndicatorProps { + status: "empty" | "completed" | "streak" | "missed"; + isToday?: boolean; + inStreakRun?: boolean; +} - const weekData = useMemo(() => getWeekData(streakDates), [streakDates]); +const StreakDayIndicator: React.FC = ({ + status, + isToday = false, + inStreakRun = false, +}) => { + const baseClasses = + "flex items-center justify-center w-[24px] h-[24px] md:w-[28px] md:h-[28px] rounded-full z-10 shrink-0"; - if (isLoading) { - return ( -
-
Loading streak...
+ let statusClasses = ""; + if (status === "empty") statusClasses = "bg-[#E6E6E6]/20"; + else if (status === "completed") statusClasses = "bg-[#FACC15]"; + else if (status === "streak") + statusClasses = "bg-[#FACC15] shadow-lg shadow-[#FACC15]/50"; + else if (status === "missed") statusClasses = "bg-white/30"; + + const todayClasses = isToday + ? "ring-2 ring-white ring-offset-2 ring-offset-[#050C16]" + : ""; + + return ( +
+ {inStreakRun && status === "streak" && ( +
+ )} +
+ {status === "streak" && ( + 🔥 + )}
+
+ ); +}; + +interface StreakCalendarProps { + currentMonth: Date; + streakData: StreakData; + onMonthChange?: (date: Date) => void; +} + +const StreakCalendar: React.FC = ({ + currentMonth, + streakData, + onMonthChange, +}) => { + const [selectedMonth, setSelectedMonth] = useState(currentMonth); + + const weekDays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + const monthNames = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", + ]; + + const getDaysInMonth = (date: Date) => + new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); + + const getFirstDayOfMonth = (date: Date) => { + const firstDay = new Date(date.getFullYear(), date.getMonth(), 1).getDay(); + return firstDay === 0 ? 6 : firstDay - 1; + }; + + const formatDateKey = (year: number, month: number, day: number) => + `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + + const isToday = (year: number, month: number, day: number) => { + const today = new Date(); + return ( + year === today.getFullYear() && + month === today.getMonth() && + day === today.getDate() ); + }; + + const handlePreviousMonth = () => { + const newMonth = new Date(selectedMonth.getFullYear(), selectedMonth.getMonth() - 1); + setSelectedMonth(newMonth); + onMonthChange?.(newMonth); + }; + + const handleNextMonth = () => { + const newMonth = new Date(selectedMonth.getFullYear(), selectedMonth.getMonth() + 1); + setSelectedMonth(newMonth); + onMonthChange?.(newMonth); + }; + + const renderCalendarDays = () => { + const daysInMonth = getDaysInMonth(selectedMonth); + const firstDayOfMonth = getFirstDayOfMonth(selectedMonth); + const year = selectedMonth.getFullYear(); + const month = selectedMonth.getMonth(); + const days = []; + + for (let i = 0; i < firstDayOfMonth; i++) { + days.push(
); + } + + for (let day = 1; day <= daysInMonth; day++) { + const dateKey = formatDateKey(year, month, day); + const dayData = streakData[dateKey]; + const today = isToday(year, month, day); + + let status: "empty" | "completed" | "streak" | "missed" = "empty"; + if (dayData?.missed) status = "missed"; + else if (dayData?.completed) + status = dayData?.inStreak ? "streak" : "completed"; + + days.push( +
+ + {day} + + +
+ ); + } + + return days; + }; + + return ( +
+
+ {/* Month Header */} +
+ +

+ {monthNames[selectedMonth.getMonth()].slice(0, 3).toUpperCase()}{" "} + {selectedMonth.getFullYear()} +

+ +
+ + {/* Divider */} +
+ + {/* Weekday Labels */} +
+ {weekDays.map((day) => ( +
+ + {day} + +
+ ))} +
+ + {/* Calendar Grid */} +
{renderCalendarDays()}
+
+
+ ); +}; + +interface StreakSummaryCardProps { + streakCount: number; + isActive?: boolean; +} + +const StreakSummaryCard: React.FC = ({ + streakCount, + isActive = true, +}) => { + return ( +
+
+ {/* Number badge */} +
+ + {streakCount} + +
+

+ day streak! +

+
+ + {/* Flame */} +
+ {/* Flame SVG inline since we don't have the asset in this context */} + + + + + +
+
+ ); +}; + +interface ShareStreakModalProps { + streakCount: number; + onClose: () => void; +} + +const ShareStreakModal: React.FC = ({ streakCount, onClose }) => { + const shareOptions = [ + { label: "Contacts", icon: "👤" }, + { label: "Telegram", icon: "✈️" }, + { label: "Twitter", icon: "𝕏" }, + { label: "Whatsapp", icon: "💬" }, + { label: "E-mail", icon: "✉️", highlight: true }, + { label: "More", icon: "⋯" }, + ]; + + return ( +
+ {/* Backdrop */} +
+ + {/* Share card preview */} +
+
+
+

I'm on a

+
+
+ {streakCount} +
+
+

day streak!

+

mind block

+
+
+ + + + + +
+
+
+ + {/* Bottom sheet */} +
+
+ +

Share Your Streak

+
+
+ +
+ {shareOptions.map((opt) => ( + + ))} +
+
+
+ ); +}; + +interface StreakNavbarProps { + streakCount: number; + points: number; + onShare: () => void; + onClose: () => void; +} + +const StreakNavbar: React.FC = ({ streakCount, points, onShare, onClose }) => { + return ( + + ); +}; + +// Demo streak data +const DEMO_STREAK_DATA: StreakData = (() => { + const data: StreakData = {}; + const today = new Date(); + const year = today.getFullYear(); + const month = today.getMonth(); + + // Simulate a streak run from day 14 to 20 + for (let d = 14; d <= 20; d++) { + const key = `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; + data[key] = { completed: true, inStreak: true }; + } + // And current streak (last 4 days incl today) + for (let i = 3; i >= 0; i--) { + const d = new Date(today); + d.setDate(today.getDate() - i); + const key = d.toISOString().split("T")[0]; + data[key] = { completed: true, inStreak: true }; } + // A couple of solo completed days + const solo1 = `${year}-${String(month + 1).padStart(2, "0")}-08`; + const solo2 = `${year}-${String(month + 1).padStart(2, "0")}-10`; + data[solo1] = { completed: true, inStreak: false }; + data[solo2] = { completed: true, inStreak: false }; + + return data; +})(); + +export default function StreakPage() { + const router = useRouter(); + const [showShare, setShowShare] = useState(false); + + const streakCount = 4; + const points = 1100; return ( - <> - router.push("/dashboard")} +
+ {/* Navbar */} + setShowShare(true)} + onClose={() => router.push("/dashboard")} /> - + + {/* Page Header */} +
+ +

Streak

+ +
+ + {/* Main Content */} +
+ {/* Streak Summary Card */} + 0} /> + + {/* Streak Calendar Section */} +
+

+ Streak Calendar +

+ +
+ + {/* Continue Button */} + {/* */} +
+ + {/* Share Modal */} + {showShare && ( + setShowShare(false)} + /> + )} +
); -} +} \ No newline at end of file diff --git a/frontend/components/ClientLayout.tsx b/frontend/components/ClientLayout.tsx index afc94fa..18d89a7 100644 --- a/frontend/components/ClientLayout.tsx +++ b/frontend/components/ClientLayout.tsx @@ -1,34 +1,41 @@ "use client"; import { useState } from "react"; +import { usePathname } from "next/navigation"; import SideNav from "@/components/SideNav"; import { Menu } from "lucide-react"; import ErrorBoundary from "@/components/error/ErrorBoundary"; +const ROUTES_WITHOUT_SIDENAV = ["/", "/streak", "/auth/signin", "/auth/signup"]; + export default function ClientLayout({ children, }: { children: React.ReactNode; }) { const [sidebarOpen, setSidebarOpen] = useState(false); + const pathname = usePathname(); + const showSidenav = !ROUTES_WITHOUT_SIDENAV.includes(pathname); return (
- setSidebarOpen(false)} /> - + {showSidenav && ( + <> + setSidebarOpen(false)} /> + + + )}
- - {children} - + {children}
); -} +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 73cc48b..7f2d48e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@tanstack/react-query": "^5.90.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.38.0", "lucide-react": "^0.542.0", "next": "^16.1.3", "react": "19.1.0", diff --git a/package-lock.json b/package-lock.json index 693741f..6f7fb80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -730,6 +730,7 @@ "@tanstack/react-query": "^5.90.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.38.0", "lucide-react": "^0.542.0", "next": "^16.1.3", "react": "19.1.0", @@ -10071,13 +10072,13 @@ } }, "node_modules/framer-motion": { - "version": "12.34.3", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.3.tgz", - "integrity": "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==", + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", "license": "MIT", "dependencies": { - "motion-dom": "^12.34.3", - "motion-utils": "^12.29.2", + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -13722,18 +13723,18 @@ } }, "node_modules/motion-dom": { - "version": "12.34.3", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.3.tgz", - "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", "license": "MIT", "dependencies": { - "motion-utils": "^12.29.2" + "motion-utils": "^12.36.0" } }, "node_modules/motion-utils": { - "version": "12.29.2", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", - "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", "license": "MIT" }, "node_modules/ms": {