From e41bd5c4a15508dfaf0beea21c86efed3203f0d7 Mon Sep 17 00:00:00 2001 From: Devangee Jain Date: Mon, 18 May 2026 14:41:53 +0530 Subject: [PATCH 1/3] feat: add reading progress tracking --- web/package-lock.json | 12 ---- web/src/app/page.tsx | 33 +++++++++++ web/src/lib/api.ts | 135 ------------------------------------------ 3 files changed, 33 insertions(+), 147 deletions(-) delete mode 100644 web/src/lib/api.ts diff --git a/web/package-lock.json b/web/package-lock.json index 33c6ae4..8e42f07 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -72,7 +72,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1589,7 +1588,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1656,7 +1654,6 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -2182,7 +2179,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2536,7 +2532,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3166,7 +3161,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3352,7 +3346,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,7 +5647,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5664,7 +5656,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6413,7 +6404,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6576,7 +6566,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6862,7 +6851,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 2485d26..0d8f692 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,11 +1,13 @@ "use client"; + import { motion } from "framer-motion"; import { PenTool, MessageSquare, ChevronRight, Sparkles, BookOpen, Layers } from "lucide-react"; import Link from "next/link"; import TypewriterText from "@/components/TypewriterText"; import { GlassCard } from "@/components/ui/GlassCard"; import { InkButton } from "@/components/ui/InkButton"; +import { useEffect, useState } from "react"; export default function LandingPage() { const container = { @@ -19,6 +21,25 @@ export default function LandingPage() { }, }; + const [progress, setProgress] = useState(0); + useEffect(() => { + const handleScroll = () => { + const totalHeight = + document.documentElement.scrollHeight - + document.documentElement.clientHeight; + + const scrollTop = window.scrollY; + + const scrollProgress = (scrollTop / totalHeight) * 100; + + setProgress(scrollProgress); + }; + + window.addEventListener("scroll", handleScroll); + + return () => window.removeEventListener("scroll", handleScroll); +}, []); + const item = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0, transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const } }, @@ -26,6 +47,18 @@ export default function LandingPage() { return (
+ +
{ - success: boolean; - data?: T; - message?: string; - pagination?: { - page: number; - limit: number; - total: number; - totalPages: number; - }; -} - -class ApiClient { - private getHeaders() { - const token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null; - return { - 'Content-Type': 'application/json', - ...(token ? { 'Authorization': `Bearer ${token}` } : {}), - }; - } - - private async refreshTokens() { - const refreshToken = localStorage.getItem('refresh_token'); - if (!refreshToken) throw new Error('No refresh token available'); - - const res = await fetch(`${BASE_URL}/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken }), - }); - - if (!res.ok) { - localStorage.removeItem('access_token'); - localStorage.removeItem('refresh_token'); - throw new Error('Session expired'); - } - - const { data } = await res.json(); - localStorage.setItem('access_token', data.accessToken); - localStorage.setItem('refresh_token', data.refreshToken); - return data.accessToken; - } - - private isRefreshing = false; - private refreshSubscribers: ((token: string) => void)[] = []; - - private onRefreshed(token: string) { - this.refreshSubscribers.forEach((cb) => cb(token)); - this.refreshSubscribers = []; - } - - private addRefreshSubscriber(cb: (token: string) => void) { - this.refreshSubscribers.push(cb); - } - - async request(endpoint: string, options: RequestInit = {}): Promise { - const url = `${BASE_URL}${endpoint}`; - const headers = this.getHeaders(); - - let res = await fetch(url, { ...options, headers }); - - // Handle 401 Unauthorized (Token expired) - if (res.status === 401 && typeof window !== 'undefined') { - if (this.isRefreshing) { - return new Promise((resolve) => { - this.addRefreshSubscriber((token) => { - resolve(this.request(endpoint, { - ...options, - headers: { ...headers, 'Authorization': `Bearer ${token}` }, - })); - }); - }); - } - - this.isRefreshing = true; - try { - const newToken = await this.refreshTokens(); - this.isRefreshing = false; - this.onRefreshed(newToken); - - // Retry the original request - res = await fetch(url, { - ...options, - headers: { ...headers, 'Authorization': `Bearer ${newToken}` }, - }); - } catch (err) { - this.isRefreshing = false; - this.refreshSubscribers = []; - window.location.href = '/login'; - throw err; - } - } - - if (!res.ok) { - const errorData = await res.json().catch(() => ({})); - throw new Error(errorData.message || `API error: ${res.status}`); - } - - return await res.json(); - } - - get(endpoint: string) { - return this.request>(endpoint, { method: 'GET' }); - } - - post(endpoint: string, body: unknown = {}) { - return this.request>(endpoint, { - method: 'POST', - body: JSON.stringify(body), - }); - } - - put(endpoint: string, body: unknown = {}) { - return this.request>(endpoint, { - method: 'PUT', - body: JSON.stringify(body), - }); - } - - patch(endpoint: string, body: unknown = {}) { - return this.request>(endpoint, { - method: 'PATCH', - body: JSON.stringify(body), - }); - } - - delete(endpoint: string) { - return this.request>(endpoint, { method: 'DELETE' }); - } -} - -export const api = new ApiClient(); From 2d3e5b45a7cc7dc50db5917b7a5ff1813f369572 Mon Sep 17 00:00:00 2001 From: Devangee Jain Date: Mon, 18 May 2026 16:22:55 +0530 Subject: [PATCH 2/3] feat: add bookmark resume reading --- web/src/app/page.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 0d8f692..46cba14 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -21,6 +21,22 @@ export default function LandingPage() { }, }; + + useEffect(() => { + const saved = localStorage.getItem( + `reading-position-${window.location.pathname}` + ); + + if (saved) { + requestAnimationFrame(() => { + window.scrollTo({ + top: parseInt(saved), + behavior: "smooth", + }); + }); + } +}, []); + const [progress, setProgress] = useState(0); useEffect(() => { const handleScroll = () => { @@ -33,6 +49,10 @@ export default function LandingPage() { const scrollProgress = (scrollTop / totalHeight) * 100; setProgress(scrollProgress); + localStorage.setItem( + `reading-position-${window.location.pathname}`, + window.scrollY.toString() +); }; window.addEventListener("scroll", handleScroll); From 3c60b73d88f5cd9d44af6f96a32273413b817cf3 Mon Sep 17 00:00:00 2001 From: Devangee Jain Date: Mon, 18 May 2026 17:25:00 +0530 Subject: [PATCH 3/3] feat: enhance article reading experience --- web/src/app/page.tsx | 352 +++++++++++++++++++++++++------------------ 1 file changed, 207 insertions(+), 145 deletions(-) diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 46cba14..171b346 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,15 +1,26 @@ "use client"; - import { motion } from "framer-motion"; -import { PenTool, MessageSquare, ChevronRight, Sparkles, BookOpen, Layers } from "lucide-react"; +import { + PenTool, + MessageSquare, + ChevronRight, + Sparkles, + BookOpen, + Layers, +} from "lucide-react"; import Link from "next/link"; import TypewriterText from "@/components/TypewriterText"; import { GlassCard } from "@/components/ui/GlassCard"; import { InkButton } from "@/components/ui/InkButton"; import { useEffect, useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; export default function LandingPage() { + const [progress, setProgress] = useState(0); + const [fontSize, setFontSize] = useState(18); + const [eyeMode, setEyeMode] = useState(false); + const container = { hidden: { opacity: 0 }, show: { @@ -21,181 +32,232 @@ export default function LandingPage() { }, }; - - useEffect(() => { - const saved = localStorage.getItem( - `reading-position-${window.location.pathname}` - ); + const item = { + hidden: { opacity: 0, y: 20 }, + show: { + opacity: 1, + y: 0, + transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const }, + }, + }; + + // ✅ Restore scroll position + useEffect(() => { + const saved = localStorage.getItem( + `reading-position-${window.location.pathname}` + ); - if (saved) { - requestAnimationFrame(() => { - window.scrollTo({ - top: parseInt(saved), - behavior: "smooth", + if (saved) { + requestAnimationFrame(() => { + window.scrollTo({ + top: parseInt(saved), + behavior: "smooth", + }); }); - }); - } -}, []); + } + }, []); - const [progress, setProgress] = useState(0); + // ✅ Scroll tracking + save progress useEffect(() => { - const handleScroll = () => { - const totalHeight = - document.documentElement.scrollHeight - - document.documentElement.clientHeight; + const handleScroll = () => { + const totalHeight = + document.documentElement.scrollHeight - + document.documentElement.clientHeight; - const scrollTop = window.scrollY; + const scrollTop = window.scrollY; - const scrollProgress = (scrollTop / totalHeight) * 100; + setProgress((scrollTop / totalHeight) * 100); - setProgress(scrollProgress); - localStorage.setItem( - `reading-position-${window.location.pathname}`, - window.scrollY.toString() -); - }; - - window.addEventListener("scroll", handleScroll); + localStorage.setItem( + `reading-position-${window.location.pathname}`, + scrollTop.toString() + ); + }; - return () => window.removeEventListener("scroll", handleScroll); -}, []); - - const item = { - hidden: { opacity: 0, y: 20 }, - show: { opacity: 1, y: 0, transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] as const } }, - }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); return ( -
+
+ {/* Progress Bar */} +
-
- {/* Navigation / Logo Header */} - + {/* NAVBAR */} +
-
- -
- - Writers' Pub - +
+ +
+ + Writers' Pub +
-
- Studio - The Agora - Marketplace + +
+ Studio + Agora + Marketplace
+ - - Sign In + + Sign In - {/* Hero Section */} + {/* HERO */}
- - - The First Complete Writing Ecosystem - - - - -
- -
- -
- - - A premium space where writers write, readers critique, and editors select the future of publishing. - Experience a studio empowered by AI and human connection. - - - - - - Start Writing - - - - - - + + + Writing Ecosystem + + + + +
+ +
+ + + A premium writing ecosystem with readers, writers, and editors. + + + {/* TOOLBAR */} + + {/* Top */} + + + {/* Eye Mode */} + + + {/* Font Controls */} + + + + + + + Start Writing + + +
- - -
-
-
-
-
-
-
Chapter 1 — Draft
-
- -
-

The Obsidian Inkwell

-

The parchment felt warm beneath my fingertips, a stark contrast to the cold October air that seeped through the cracks of the Writers' Pub. Here, every drop of ink was a promise, and every sentence a battle won against the silence of the blank page...

-
- -
-
- {[1, 2, 3].map((i) => ( -
ED
- ))} -
-
- 3 Active Critiques -
-
- - - {/* Abstract Floating Elements */} - - - + {/* ARTICLE PREVIEW */} + + +

+ The Obsidian Inkwell +

+ +

+ The parchment felt warm beneath my fingertips, a stark contrast + to the cold October air that seeped through the cracks of the + Writers' Pub. Every drop of ink was a promise, every + sentence a battle won against silence... +

+
- {/* Feature Highlights */} - - {[ - { icon: BookOpen, title: "The Studio", desc: "A creative environment for pure writing flow." }, - { icon: MessageSquare, title: "The Agora", desc: "Share drafts for high-quality, structured human feedback." }, - { icon: Layers, title: "The Exchange", desc: "A marketplace matching writers with publishers." }, - ].map((feature, idx) => ( - -
- -
-

{feature.title}

-

{feature.desc}

-
- ))} + {/* FEATURES */} + + {[ + { + icon: BookOpen, + title: "Studio", + desc: "Write freely", + }, + { + icon: MessageSquare, + title: "Agora", + desc: "Get feedback", + }, + { + icon: Layers, + title: "Exchange", + desc: "Publish works", + }, + ].map((f, i) => ( + + +

{f.title}

+

{f.desc}

+
+ ))}