From 77d6b56bcb60af68a186e8f98e57d04f8f6c6a36 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 08:38:34 +0000 Subject: [PATCH 1/4] Changes Co-authored-by: akshayamaram <141863638+akshayamaram@users.noreply.github.com> --- src/lib/quotes.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/lib/quotes.ts diff --git a/src/lib/quotes.ts b/src/lib/quotes.ts new file mode 100644 index 0000000..cbfe93b --- /dev/null +++ b/src/lib/quotes.ts @@ -0,0 +1,70 @@ +// daily quotes + milestone quotes. engineering / aerospace / stoic flavor. + +export const DAILY_QUOTES: string[] = [ + "the engineer's first duty is to the truth, not to the deadline.", + "a problem well stated is a problem half solved.", + "every equation you understand is a small piece of the universe you own.", + "rockets don't care how you feel. show up anyway.", + "the day you stop learning is the day the field leaves you behind.", + "first principles. always.", + "you don't rise to your goals. you fall to your habits.", + "the gap between knowing and doing is closed only by reps.", + "drag is real. so is lift. choose what you generate today.", + "no one ever drowned in sweat.", + "small daily improvements compound into stunning results.", + "the syllabus is finite. your time is too. respect both.", + "discipline is choosing between what you want now and what you want most.", + "if you can't explain it simply, you don't understand it well enough.", + "study like the exam is tomorrow. live like it isn't.", + "the expert in anything was once a beginner who refused to quit.", + "you are not behind. you are exactly where consistency would have put you.", + "the only easy day was yesterday.", + "motivation gets you started. systems keep you going.", + "don't break the chain.", + "an hour of focused work beats a day of distracted study.", + "the calm sea never made a skilled sailor.", + "feynman: the first principle is that you must not fool yourself.", + "von braun: research is what i'm doing when i don't know what i'm doing.", + "kalam: dream is not what you see in sleep. it is what doesn't let you sleep.", + "edison: genius is one percent inspiration, ninety-nine percent perspiration.", + "every topic you skip today will find you in the exam hall.", + "you don't need more time. you need fewer tabs.", + "thrust over drag. effort over excuse.", + "the syllabus shrinks every time you sit down. open it.", +]; + +export const MILESTONE_QUOTES: Record<25 | 50 | 75 | 100, string[]> = { + 25: [ + "a quarter of the way. the takeoff roll is the loudest part.", + "twenty-five percent. momentum is a quiet thing until it isn't.", + "the first quarter is the hardest. you cleared it.", + ], + 50: [ + "halfway. the view from here is the rest of the climb.", + "fifty percent. you are no longer starting. you are continuing.", + "the second half is paid for by the first. keep going.", + ], + 75: [ + "three quarters. the runway is behind you now.", + "seventy-five percent. closer to done than to start.", + "you are in the final stretch. don't decelerate.", + ], + 100: [ + "complete. a section closed is a small forever.", + "one hundred percent. carry this feeling into the next.", + "done. now revise. then move.", + ], +}; + +// pick a deterministic daily quote so the same quote shows all day. +export function getDailyQuote(date = new Date()): { quote: string; key: string } { + const key = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + let h = 0; + for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0; + return { quote: DAILY_QUOTES[h % DAILY_QUOTES.length], key }; +} + +export function pickMilestoneQuote(threshold: 25 | 50 | 75 | 100): string { + const arr = MILESTONE_QUOTES[threshold]; + return arr[Math.floor(Math.random() * arr.length)]; +} From bc8c9849ea9c3ded911ba7df95625e68f0598dec Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 08:39:02 +0000 Subject: [PATCH 2/4] Changes Co-authored-by: akshayamaram <141863638+akshayamaram@users.noreply.github.com> --- src/features/motivation/DailyMail.tsx | 80 +++++++++++++++++++++ src/features/motivation/MilestoneRibbon.tsx | 58 +++++++++++++++ src/features/motivation/PickupBanner.tsx | 41 +++++++++++ src/lib/storage.ts | 3 + 4 files changed, 182 insertions(+) create mode 100644 src/features/motivation/DailyMail.tsx create mode 100644 src/features/motivation/MilestoneRibbon.tsx create mode 100644 src/features/motivation/PickupBanner.tsx diff --git a/src/features/motivation/DailyMail.tsx b/src/features/motivation/DailyMail.tsx new file mode 100644 index 0000000..93b5cf3 --- /dev/null +++ b/src/features/motivation/DailyMail.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { getDailyQuote } from "@/lib/quotes"; +import { STORAGE_KEYS } from "@/lib/storage"; + +export function DailyMail() { + const [open, setOpen] = useState(false); + const [unread, setUnread] = useState(false); + const [data, setData] = useState<{ quote: string; key: string } | null>(null); + + useEffect(() => { + if (typeof window === "undefined") return; + const d = getDailyQuote(); + setData(d); + try { + const last = localStorage.getItem(STORAGE_KEYS.dailyMailOpened); + setUnread(last !== d.key); + } catch { + setUnread(true); + } + }, []); + + const openMail = () => { + setOpen(true); + if (data) { + try { + localStorage.setItem(STORAGE_KEYS.dailyMailOpened, data.key); + } catch {} + setUnread(false); + } + }; + + return ( + <> + + + {open && data && ( +
setOpen(false)} + > +
e.stopPropagation()} + > +
+
+ letter · {data.key} +
+ +
+
+ "{data.quote}" +
+
+ — a note for today +
+
+
+ )} + + ); +} diff --git a/src/features/motivation/MilestoneRibbon.tsx b/src/features/motivation/MilestoneRibbon.tsx new file mode 100644 index 0000000..6553726 --- /dev/null +++ b/src/features/motivation/MilestoneRibbon.tsx @@ -0,0 +1,58 @@ +import { useEffect, useRef, useState } from "react"; +import { gsap } from "gsap"; +import { pickMilestoneQuote } from "@/lib/quotes"; + +export type MilestoneEvent = { + id: string; // unique key to retrigger + section: string; + threshold: 25 | 50 | 75 | 100; +}; + +export function MilestoneRibbon({ event }: { event: MilestoneEvent | null }) { + const [visible, setVisible] = useState(null); + const [quote, setQuote] = useState(""); + const ref = useRef(null); + + useEffect(() => { + if (!event) return; + setQuote(pickMilestoneQuote(event.threshold)); + setVisible(event); + }, [event]); + + useEffect(() => { + if (!visible || !ref.current) return; + const el = ref.current; + gsap.fromTo( + el, + { x: 80, opacity: 0 }, + { x: 0, opacity: 1, duration: 0.7, ease: "power3.out" }, + ); + const t = setTimeout(() => { + gsap.to(el, { + x: 80, + opacity: 0, + duration: 0.6, + ease: "power2.in", + onComplete: () => setVisible(null), + }); + }, 5200); + return () => clearTimeout(t); + }, [visible]); + + if (!visible) return null; + + return ( +
+
+ {visible.threshold === 100 ? "section complete" : `${visible.threshold}% · ${visible.section}`} +
+
+ {quote} +
+
+ ); +} diff --git a/src/features/motivation/PickupBanner.tsx b/src/features/motivation/PickupBanner.tsx new file mode 100644 index 0000000..1e3e4da --- /dev/null +++ b/src/features/motivation/PickupBanner.tsx @@ -0,0 +1,41 @@ +import { useEffect, useRef, useState } from "react"; +import { gsap } from "gsap"; + +export function PickupBanner({ label }: { label: string | null }) { + const [visible, setVisible] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!label) return; + setVisible(true); + }, [label]); + + useEffect(() => { + if (!visible || !ref.current) return; + const el = ref.current; + gsap.fromTo(el, { y: -10, opacity: 0 }, { y: 0, opacity: 1, duration: 0.6, ease: "power2.out" }); + const t = setTimeout(() => { + gsap.to(el, { + opacity: 0, + duration: 0.8, + ease: "power2.in", + onComplete: () => setVisible(false), + }); + }, 4200); + return () => clearTimeout(t); + }, [visible]); + + if (!visible || !label) return null; + + return ( +
+
+ picking up where you left off · {label} +
+
+ ); +} diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 5b1793c..4b57dc7 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -12,6 +12,9 @@ export const STORAGE_KEYS = { theme: "gate-ae-theme-v1", tourDone: "gate-ae-tour-done-v1", feedbackCooldown: "gate-ae-feedback-cooldown-v1", + milestones: "gate-ae-milestones-v1", + lastTouched: "gate-ae-last-touched-v1", + dailyMailOpened: "gate-ae-daily-mail-v1", } as const; export function loadJSON(key: string, fallback: T): T { From 26a9bc38d38bf3adb4adb7c3ecceb21e6f7f3736 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 08:39:38 +0000 Subject: [PATCH 3/4] Changes Co-authored-by: akshayamaram <141863638+akshayamaram@users.noreply.github.com> --- src/components/layout/AppHeader.tsx | 2 + src/routes/index.tsx | 99 +++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/src/components/layout/AppHeader.tsx b/src/components/layout/AppHeader.tsx index b7ba481..83b42f6 100644 --- a/src/components/layout/AppHeader.tsx +++ b/src/components/layout/AppHeader.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { STORAGE_KEYS } from "@/lib/storage"; +import { DailyMail } from "@/features/motivation/DailyMail"; export function AppHeader({ done, @@ -61,6 +62,7 @@ export function AppHeader({
{done}/{total} topics
+