- Timestamp:
- 05/13/26 00:35:13 (8 weeks ago)
- Branches:
- master
- Children:
- 447c39f
- Parents:
- a8381b1
- Location:
- frontend/src
- Files:
-
- 5 added
- 13 edited
-
App.jsx (modified) (2 diffs)
-
api/axios.js (modified) (3 diffs)
-
pages/Dashboard/DashboardLayout.jsx (modified) (2 diffs)
-
pages/Dashboard/pages/Investing/InvestingTracking.jsx (modified) (1 diff)
-
pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx (modified) (4 diffs)
-
pages/LandingPage/components/Benefits.jsx (modified) (1 diff)
-
pages/LandingPage/components/Faq.defaults.js (added)
-
pages/LandingPage/components/Faq.jsx (modified) (5 diffs)
-
pages/LandingPage/components/Features.jsx (modified) (1 diff)
-
pages/LandingPage/components/Footer.jsx (modified) (1 diff)
-
pages/LandingPage/components/Hero.jsx (modified) (3 diffs)
-
pages/LandingPage/components/HowItWorks.defaults.js (added)
-
pages/LandingPage/components/HowItWorks.defaults.jsx (added)
-
pages/LandingPage/components/HowItWorks.jsx (modified) (4 diffs)
-
pages/LandingPage/components/NavbarLanding.jsx (modified) (4 diffs)
-
pages/LandingPage/components/Who.defaults.jsx (added)
-
pages/LandingPage/components/Who.jsx (modified) (3 diffs)
-
utils/authSession.js (added)
Legend:
- Unmodified
- Added
- Removed
-
frontend/src/App.jsx
ra8381b1 r42da64d 1 1 import { Navigate, Route, Routes } from "react-router-dom"; 2 import { clearAuthSession, getAuthToken, isTokenExpired } from "./utils/authSession"; 2 3 3 4 import LandingPage from "./pages/LandingPage/LandingPage.jsx"; … … 23 24 24 25 function RequireAuth({ children }) { 25 const token = localStorage.getItem("authToken"); 26 const token = getAuthToken(); 27 if (token && isTokenExpired(token)) { 28 clearAuthSession(); 29 return <Navigate to="/login" replace />; 30 } 26 31 if (!token) return <Navigate to="/login" replace />; 27 32 return children; -
frontend/src/api/axios.js
ra8381b1 r42da64d 1 1 import axios from "axios"; 2 import { getAuthToken, isTokenExpired, logoutAndRedirect } from "../utils/authSession"; 2 3 3 4 const api = axios.create({ … … 6 7 7 8 api.interceptors.request.use((config) => { 8 const token = localStorage.getItem("authToken");9 const token = getAuthToken(); 9 10 if (token) { 11 if (isTokenExpired(token)) { 12 logoutAndRedirect("/dashboard"); 13 return Promise.reject(new Error("Authentication session expired")); 14 } 10 15 config.headers = config.headers ?? {}; 11 16 config.headers.Authorization = `Bearer ${token}`; … … 14 19 }); 15 20 21 api.interceptors.response.use( 22 (response) => response, 23 (error) => { 24 const status = error?.response?.status; 25 if (status === 401 || status === 403) { 26 logoutAndRedirect("/dashboard"); 27 } 28 return Promise.reject(error); 29 }, 30 ); 31 16 32 export default api; -
frontend/src/pages/Dashboard/DashboardLayout.jsx
ra8381b1 r42da64d 6 6 import DashboardSidebar from "./components/DashboardSidebar.jsx"; 7 7 import DashboardTopbar from "./components/DashboardTopbar.jsx"; 8 import { clearAuthSession } from "../../utils/authSession"; 8 9 9 10 const DashboardLayout = () => { … … 22 23 23 24 const onLogout = () => { 24 localStorage.removeItem("authToken"); 25 localStorage.removeItem("authUser"); 25 clearAuthSession(); 26 26 navigate("/", { replace: true }); 27 27 }; -
frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx
ra8381b1 r42da64d 214 214 onAddAsset={() => navigate("/dashboard/investing/assets/new")} 215 215 /> 216 <InvestingProgressCard assets={graphAssets} />216 <InvestingProgressCard assets={graphAssets} quotesBySymbol={quotesBySymbol} /> 217 217 218 218 {quotesError ? ( -
frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx
ra8381b1 r42da64d 3 3 import TimeRangeToggle from "../../../../../components/graphs/TimeRangeToggle.jsx"; 4 4 import PercentChangeAreaChart from "../../../../../components/graphs/PercentChangeAreaChart.jsx"; 5 import { percentChangeSeries, sumByTimeBucket } from "../../../../../utils/timeSeries.js"; 6 7 export default function InvestingProgressCard({ assets }) { 5 import { formatBucketLabel } from "../../../../../utils/timeSeries.js"; 6 7 function bucketKey(date, granularity) { 8 const d = new Date(date); 9 if (Number.isNaN(d.getTime())) return null; 10 11 function startOfDay(d) { 12 return new Date(d.getFullYear(), d.getMonth(), d.getDate()); 13 } 14 15 function startOfISOWeek(d) { 16 const date = startOfDay(d); 17 const day = date.getDay(); 18 const diff = (day === 0 ? -6 : 1) - day; 19 date.setDate(date.getDate() + diff); 20 return startOfDay(date); 21 } 22 23 function startOfMonth(d) { 24 return new Date(d.getFullYear(), d.getMonth(), 1); 25 } 26 27 function startOfYear(d) { 28 return new Date(d.getFullYear(), 0, 1); 29 } 30 31 let s; 32 switch (granularity) { 33 case "daily": 34 s = startOfDay(d); 35 break; 36 case "weekly": 37 s = startOfISOWeek(d); 38 break; 39 case "monthly": 40 s = startOfMonth(d); 41 break; 42 case "yearly": 43 s = startOfYear(d); 44 break; 45 default: 46 s = startOfDay(d); 47 } 48 return s.getTime(); 49 } 50 51 export default function InvestingProgressCard({ assets, quotesBySymbol = {} }) { 8 52 const [range, setRange] = useState("weekly"); 9 53 10 54 const points = useMemo(() => { 11 // Option B: "net invested momentum". 12 // Since we only have assets (with buy date/price/quantity) and no transaction history, 13 // we treat each asset as a one-time investment at buyDate. 14 const buckets = sumByTimeBucket( 15 assets, 16 range, 17 (a) => a.buyDate, 18 (a) => (Number(a?.quantity) || 0) * (Number(a?.buyPrice) || 0), 19 ); 20 return percentChangeSeries(buckets); 21 }, [assets, range]); 55 // Calculate portfolio percentage gain/loss over time using current prices 56 // Display cumulative portfolio value and percentage change between time buckets 57 58 if (!assets || assets.length === 0) { 59 return []; 60 } 61 62 // Group assets by buy date bucket 63 const buckets = new Map(); 64 for (const asset of assets) { 65 if (asset?.buyDate) { 66 const ts = bucketKey(asset.buyDate, range); 67 if (ts !== null) { 68 if (!buckets.has(ts)) { 69 buckets.set(ts, []); 70 } 71 buckets.get(ts).push(asset); 72 } 73 } 74 } 75 76 // Sort buckets chronologically 77 const sortedBuckets = Array.from(buckets.entries()).sort((a, b) => a[0] - b[0]); 78 79 // Calculate cumulative portfolio value at each bucket 80 const points = []; 81 let cumulativeAssets = []; 82 83 for (const [ts, assetsInBucket] of sortedBuckets) { 84 cumulativeAssets = [...cumulativeAssets, ...assetsInBucket]; 85 86 let totalInvested = 0; 87 let totalCurrentValue = 0; 88 89 for (const asset of cumulativeAssets) { 90 const qty = Number(asset?.quantity) || 0; 91 const buyPrice = Number(asset?.buyPrice) || 0; 92 const ticker = String(asset?.tickerSymbol || "").toUpperCase(); 93 const currentPrice = quotesBySymbol[ticker] || buyPrice; 94 95 totalInvested += qty * buyPrice; 96 totalCurrentValue += qty * currentPrice; 97 } 98 99 if (totalInvested > 0) { 100 points.push({ 101 ts, 102 invested: totalInvested, 103 current: totalCurrentValue, 104 }); 105 } 106 } 107 108 // Calculate percentage gain from invested to current value 109 const result = []; 110 for (const p of points) { 111 const percentageGain = ((p.current - p.invested) / p.invested) * 100; 112 result.push({ 113 ts: p.ts, 114 value: percentageGain, 115 base: p.current, 116 invested: p.invested, 117 }); 118 } 119 120 return result; 121 }, [assets, range, quotesBySymbol]); 22 122 23 123 const latest = points?.length ? points[points.length - 1] : null; 24 124 const latestPct = latest ? Number(latest.value ?? 0) : 0; 25 const latestBase = latest ? Number(latest.base ?? 0) : 0; 125 const latestCurrentValue = latest ? Number(latest.base ?? 0) : 0; 126 const latestInvested = latest ? Number(latest.invested ?? 0) : 0; 127 128 const totalCurrentPortfolioValue = assets.reduce((sum, asset) => { 129 const qty = Number(asset?.quantity) || 0; 130 const ticker = String(asset?.tickerSymbol || "").toUpperCase(); 131 const currentPrice = quotesBySymbol[ticker] || Number(asset?.buyPrice) || 0; 132 return sum + qty * currentPrice; 133 }, 0); 134 135 const totalInvestedAmount = assets.reduce((sum, asset) => { 136 const qty = Number(asset?.quantity) || 0; 137 const buyPrice = Number(asset?.buyPrice) || 0; 138 return sum + qty * buyPrice; 139 }, 0); 26 140 27 141 return ( … … 29 143 <div className="card-body"> 30 144 <div className="flex items-center justify-between gap-4"> 31 <h2 className="card-title">P rogress</h2>145 <h2 className="card-title">Portfolio Value</h2> 32 146 <div className="flex items-center gap-3"> 33 <span className="badge badge-ghost"> 34 {latestPct >= 0 ? "+" : ""} 35 {latestPct.toFixed(1)}% (last) 36 </span> 147 <div className="flex flex-col items-end gap-1"> 148 <span className="text-sm font-semibold"> 149 ${totalCurrentPortfolioValue.toFixed(2)} 150 </span> 151 <span 152 className={`text-xs font-bold ${ 153 latestPct >= 0 ? "text-success" : "text-error" 154 }`} 155 > 156 {latestPct >= 0 ? "+" : ""} 157 {latestPct.toFixed(2)}% 158 </span> 159 </div> 37 160 <TimeRangeToggle value={range} onChange={setRange} /> 38 161 </div> … … 40 163 41 164 <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100 p-2"> 42 {points.length < 2? (165 {points.length < 1 ? ( 43 166 <div className="flex h-65 items-center justify-center text-sm opacity-70"> 44 Add at least 2 investments to see % change.167 Add investments to see portfolio growth. 45 168 </div> 46 169 ) : ( … … 49 172 granularity={range} 50 173 height={260} 51 positiveColor="#1 4b8a6"174 positiveColor="#10b981" 52 175 /> 53 176 )} 54 177 </div> 55 178 56 <div className="mt-2 text-xs opacity-70"> 57 Showing percentage change in invested amount per {range} bucket. Latest bucket invested: {Math.round(latestBase)}. 179 <div className="mt-3 grid grid-cols-3 gap-2 text-xs"> 180 <div className="rounded bg-base-300 p-2"> 181 <div className="opacity-70">Invested</div> 182 <div className="font-semibold">${totalInvestedAmount.toFixed(2)}</div> 183 </div> 184 <div className="rounded bg-base-300 p-2"> 185 <div className="opacity-70">Current Value</div> 186 <div className="font-semibold">${totalCurrentPortfolioValue.toFixed(2)}</div> 187 </div> 188 <div className="rounded bg-base-300 p-2"> 189 <div className="opacity-70">Gain/Loss</div> 190 <div 191 className={`font-semibold ${ 192 totalCurrentPortfolioValue >= totalInvestedAmount 193 ? "text-success" 194 : "text-error" 195 }`} 196 > 197 ${(totalCurrentPortfolioValue - totalInvestedAmount).toFixed(2)} 198 </div> 199 </div> 58 200 </div> 59 201 </div> -
frontend/src/pages/LandingPage/components/Benefits.jsx
ra8381b1 r42da64d 43 43 return ( 44 44 <section id="benefits" className="px-[5%] py-16 md:py-24 lg:py-28"> 45 <div className=" container">45 <div className="mx-auto w-full max-w-6xl"> 46 46 <div className="text-center mb-16 md:mb-20"> 47 47 <p className="font-semibold text-sm md:text-base mb-3 md:mb-4"> 48 48 Why Trekr 49 49 </p> 50 <h2 className=" text-4xl md:text-6xl lg:text-7xl font-bold mb-6">50 <h2 className="font-bold mb-6 text-3xl sm:text-4xl md:text-5xl lg:text-6xl leading-tight"> 51 51 Designed for serious progress 52 52 </h2> -
frontend/src/pages/LandingPage/components/Faq.jsx
ra8381b1 r42da64d 6 6 AccordionTrigger, 7 7 } from "@relume_io/relume-ui"; 8 9 import { FaqDefaults } from "./Faq.defaults"; 8 10 9 11 export const Faq = (props) => { … … 16 18 button, 17 19 } = { 18 ...Faq 1Defaults,20 ...FaqDefaults, 19 21 ...props, 20 22 }; … … 24 26 className="px-[5%] py-16 md:py-24 lg:py-28 flex items-center justify-center" 25 27 > 26 <div className=" container max-w-lg">28 <div className="mx-auto w-full max-w-2xl"> 27 29 <div className="rb-12 mb-12 text-center md:mb-18 lg:mb-20"> 28 <h2 className="rb-5 mb-5 text-5xl font-bold md:mb-6 md:text-7xl lg:text-8xl">30 <h2 className="rb-5 mb-5 font-bold text-4xl sm:text-5xl md:mb-6 md:text-6xl lg:text-7xl leading-tight"> 29 31 {heading} 30 32 </h2> 31 <p className="md:text-md">{description}</p> 33 <p className="text-sm sm:text-base md:text-lg opacity-80"> 34 {description} 35 </p> 32 36 </div> 33 37 <Accordion type="multiple"> … … 47 51 {footerHeading} 48 52 </h4> 49 <p className="md:text-md">{footerDescription}</p> 53 <p className="text-sm sm:text-base md:text-lg opacity-80"> 54 {footerDescription} 55 </p> 50 56 <div className="mt-6 md:mt-8"> 51 57 <Button {...button}>{button.title}</Button> … … 56 62 ); 57 63 }; 58 59 export const Faq1Defaults = {60 heading: "Frequently asked questions",61 description:62 "Quick answers on how Trekr helps you track self-improvement across multiple areas — in one place.",63 questions: [64 {65 title: "What can I track in Trekr?",66 answer:67 "Workouts (type and duration), weight & nutrition (goal, calories), finances & budgeting (income and expenses), investing (portfolio and returns), and discipline (daily tasks you can check off).",68 },69 {70 title: "Do I need multiple apps to do all this?",71 answer:72 "No — Trekr is designed to unify the most important self-improvement categories so you can get a clear overview without switching between tools.",73 },74 {75 title: "How do goals and progress tracking work?",76 answer:77 "You set a goal (for example: target weight or monthly budget), then log daily/weekly data. Trekr helps you see where you are vs. the goal and how you're trending over time.",78 },79 {80 title: "Where is my data stored?",81 answer:82 "Your entries are stored in a database and linked to your user profile so you can track progress consistently over time.",83 },84 {85 title: "Is Trekr a mobile app?",86 answer:87 "Right now Trekr is a web app. The goal is a fast, simple experience on both mobile and desktop in the browser.",88 },89 ],90 footerHeading: "Still have questions?",91 footerDescription: "Send us a message and we’ll help you get started.",92 button: {93 title: "Contact",94 variant: "secondary",95 },96 }; -
frontend/src/pages/LandingPage/components/Features.jsx
ra8381b1 r42da64d 46 46 className="px-[5%] py-16 md:py-24 lg:py-28 bg-base-200" 47 47 > 48 <div className=" container">48 <div className="mx-auto w-full max-w-6xl"> 49 49 <div className="text-center mb-16 md:mb-20"> 50 50 <p className="font-semibold text-sm md:text-base mb-3 md:mb-4"> 51 51 What you can track 52 52 </p> 53 <h2 className=" text-4xl md:text-6xl lg:text-7xl font-bold mb-6">53 <h2 className="font-bold mb-6 text-3xl sm:text-4xl md:text-5xl lg:text-6xl leading-tight"> 54 54 Everything in one dashboard 55 55 </h2> -
frontend/src/pages/LandingPage/components/Footer.jsx
ra8381b1 r42da64d 4 4 const Footer = () => { 5 5 return ( 6 <footer className="footer sm:footer-horizontal bg-neutral text-neutral-content p- 10">6 <footer className="footer sm:footer-horizontal bg-neutral text-neutral-content p-8 sm:p-10"> 7 7 <aside> 8 8 <img src={logo} alt="Trekr" className="h-10 w-auto" /> -
frontend/src/pages/LandingPage/components/Hero.jsx
ra8381b1 r42da64d 19 19 return ( 20 20 <div 21 className="hero min-h- screen"21 className="hero min-h-[calc(100vh-4rem)] bg-cover bg-center" 22 22 style={{ 23 23 backgroundImage: … … 26 26 > 27 27 <div className="hero-overlay"></div> 28 <div className="hero-content text-neutral-content text-center ">29 <div className=" max-w-md">30 <h1 className="mb-5 text-5xl font-bold">28 <div className="hero-content text-neutral-content text-center px-4 py-16 sm:py-24"> 29 <div className="w-full max-w-2xl"> 30 <h1 className="mb-5 font-bold text-3xl sm:text-4xl md:text-5xl lg:text-6xl leading-tight"> 31 31 Trekr — track progress, every day 32 32 </h1> 33 <p className="mb- 5">33 <p className="mb-6 text-sm sm:text-base md:text-lg opacity-90"> 34 34 One self-improvement hub: workouts, weight & nutrition, finances & 35 35 budgeting, investing, and daily discipline tasks. Set goals, log … … 37 37 </p> 38 38 <Link 39 className="btn btn-primary !text-white "39 className="btn btn-primary !text-white btn-sm sm:btn-md md:btn-lg" 40 40 to={isAuthed ? "/dashboard" : "/register"} 41 41 > -
frontend/src/pages/LandingPage/components/HowItWorks.jsx
ra8381b1 r42da64d 1 1 import { Button } from "@relume_io/relume-ui"; 2 import { RxChevronRight } from "react-icons/rx";2 import { HowItWorksDefaults } from "./HowItWorks.defaults.jsx"; 3 3 4 4 export const HowItWorks = (props) => { … … 9 9 return ( 10 10 <section id="relume" className="px-[5%] py-16 md:py-24 lg:py-28"> 11 <div className=" container">11 <div className="mx-auto w-full max-w-6xl"> 12 12 <div className="grid grid-cols-1 gap-y-12 md:grid-cols-2 md:items-center md:gap-x-12 lg:gap-x-20"> 13 <div >13 <div className="text-center md:text-left"> 14 14 <p className="mb-3 font-semibold md:mb-4">{tagline}</p> 15 <h1 className="rb-5 mb-5 text-5xl font-bold md:mb-6 md:text-7xl lg:text-8xl">15 <h1 className="rb-5 mb-5 font-bold text-4xl sm:text-5xl md:mb-6 md:text-6xl lg:text-7xl leading-tight"> 16 16 {heading} 17 17 </h1> 18 <p className="md:text-md">{description}</p> 18 <p className="text-sm sm:text-base md:text-lg opacity-80"> 19 {description} 20 </p> 19 21 <div className="mt-6 flex flex-wrap items-center gap-4 md:mt-8"> 20 22 {buttons.map((button, index) => ( … … 28 30 <img 29 31 src={image.src} 30 className="w-full object-cover"32 className="w-full rounded-2xl object-cover" 31 33 alt={image.alt} 32 34 /> … … 38 40 }; 39 41 40 export const HowItWorksDefaults = { 41 tagline: "How it works", 42 heading: "Log it. Track it. Improve it.", 43 description: 44 "Log what matters to your goals — workouts, weight/calories, budgets, investments, and daily tasks. Trekr gives you a clear view of trends over time so you can stay consistent and keep moving forward.", 45 buttons: [ 46 { title: "Start free", variant: "secondary" }, 47 { 48 title: "See a quick overview", 49 variant: "link", 50 size: "link", 51 iconRight: <RxChevronRight />, 52 }, 53 ], 54 image: { 55 src: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=1600&q=80", 56 alt: "Progress and goals overview on a laptop", 57 }, 58 }; 42 -
frontend/src/pages/LandingPage/components/NavbarLanding.jsx
ra8381b1 r42da64d 27 27 28 28 return ( 29 <div className="navbar bg-primary text-primary-content shadow-sm ">30 <div className="navbar-start ">29 <div className="navbar bg-primary text-primary-content shadow-sm px-4 sm:px-6"> 30 <div className="navbar-start flex-1 min-w-0"> 31 31 <Link 32 32 className="btn btn-ghost text-primary-content" … … 37 37 </Link> 38 38 </div> 39 <div className="navbar-end ">39 <div className="navbar-end flex-none"> 40 40 {isAuthed ? ( 41 <div className="flex items-center gap-2 mr-6">41 <div className="flex flex-wrap items-center justify-end gap-2 sm:gap-3"> 42 42 <Link 43 className="btn btn- outline border-primary-content text-primary-content"43 className="btn btn-sm sm:btn-md btn-outline border-primary-content text-primary-content" 44 44 to="/dashboard" 45 45 > … … 47 47 </Link> 48 48 <button 49 className="btn btn- outline border-primary-content text-primary-content"49 className="btn btn-sm sm:btn-md btn-outline border-primary-content text-primary-content" 50 50 onClick={onLogout} 51 51 > … … 55 55 ) : ( 56 56 <Link 57 className="btn btn- tertiary text-blue-200! px-8 mr-6"57 className="btn btn-sm sm:btn-md btn-tertiary text-blue-200! px-6 sm:px-8" 58 58 to="/login" 59 59 > -
frontend/src/pages/LandingPage/components/Who.jsx
ra8381b1 r42da64d 1 1 import { Button } from "@relume_io/relume-ui"; 2 import { RxChevronRight } from "react-icons/rx";2 import { WhoDefaults } from "./Who.defaults.jsx"; 3 3 4 4 export const Who = (props) => { … … 9 9 return ( 10 10 <section id="relume" className="px-[5%] py-16 md:py-24 lg:py-28"> 11 <div className=" container">11 <div className="mx-auto w-full max-w-6xl"> 12 12 <div className="grid grid-cols-1 gap-y-12 md:grid-cols-2 md:items-center md:gap-x-12 lg:gap-x-20"> 13 13 <div className="order-2 md:order-1"> 14 14 <img 15 15 src={image.src} 16 className="w-full object-cover"16 className="w-full rounded-2xl object-cover" 17 17 alt={image.alt} 18 18 /> 19 19 </div> 20 <div className="order-1 lg:order-2 ">20 <div className="order-1 lg:order-2 text-center md:text-left"> 21 21 <p className="mb-3 font-semibold md:mb-4">{tagline}</p> 22 <h2 className="rb-5 mb-5 text-5xl font-bold md:mb-6 md:text-7xl lg:text-8xl">22 <h2 className="rb-5 mb-5 font-bold text-4xl sm:text-5xl md:mb-6 md:text-6xl lg:text-7xl leading-tight"> 23 23 {heading} 24 24 </h2> 25 <p className="md:text-md">{description}</p> 25 <p className="text-sm sm:text-base md:text-lg opacity-80"> 26 {description} 27 </p> 26 28 <div className="mt-6 flex flex-wrap gap-4 md:mt-8"> 27 29 {buttons.map((button, index) => ( … … 38 40 }; 39 41 40 export const WhoDefaults = {41 tagline: "Who it's for",42 heading: "Anyone working on themselves",43 description:44 "Whether you're training regularly, trying to manage weight and nutrition, organizing your budget, tracking investments, or building discipline — Trekr brings your key habits and metrics together in one place.",45 buttons: [46 { title: "Create an account", variant: "secondary" },47 {48 title: "Learn more",49 variant: "link",50 size: "link",51 iconRight: <RxChevronRight />,52 },53 ],54 image: {55 src: "https://images.unsplash.com/photo-1554284126-aa88f22d8b74?auto=format&fit=crop&w=1600&q=80",56 alt: "Planning habits and routine",57 },58 };
Note:
See TracChangeset
for help on using the changeset viewer.
