Changeset 42da64d for frontend


Ignore:
Timestamp:
05/13/26 00:35:13 (8 weeks ago)
Author:
Andrej <asumanovski@…>
Branches:
master
Children:
447c39f
Parents:
a8381b1
Message:

Fixed auth issue

Location:
frontend/src
Files:
5 added
13 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/App.jsx

    ra8381b1 r42da64d  
    11import { Navigate, Route, Routes } from "react-router-dom";
     2import { clearAuthSession, getAuthToken, isTokenExpired } from "./utils/authSession";
    23
    34import LandingPage from "./pages/LandingPage/LandingPage.jsx";
     
    2324
    2425function 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  }
    2631  if (!token) return <Navigate to="/login" replace />;
    2732  return children;
  • frontend/src/api/axios.js

    ra8381b1 r42da64d  
    11import axios from "axios";
     2import { getAuthToken, isTokenExpired, logoutAndRedirect } from "../utils/authSession";
    23
    34const api = axios.create({
     
    67
    78api.interceptors.request.use((config) => {
    8   const token = localStorage.getItem("authToken");
     9  const token = getAuthToken();
    910  if (token) {
     11    if (isTokenExpired(token)) {
     12      logoutAndRedirect("/dashboard");
     13      return Promise.reject(new Error("Authentication session expired"));
     14    }
    1015    config.headers = config.headers ?? {};
    1116    config.headers.Authorization = `Bearer ${token}`;
     
    1419});
    1520
     21api.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
    1632export default api;
  • frontend/src/pages/Dashboard/DashboardLayout.jsx

    ra8381b1 r42da64d  
    66import DashboardSidebar from "./components/DashboardSidebar.jsx";
    77import DashboardTopbar from "./components/DashboardTopbar.jsx";
     8import { clearAuthSession } from "../../utils/authSession";
    89
    910const DashboardLayout = () => {
     
    2223
    2324  const onLogout = () => {
    24     localStorage.removeItem("authToken");
    25     localStorage.removeItem("authUser");
     25    clearAuthSession();
    2626    navigate("/", { replace: true });
    2727  };
  • frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx

    ra8381b1 r42da64d  
    214214        onAddAsset={() => navigate("/dashboard/investing/assets/new")}
    215215      />
    216       <InvestingProgressCard assets={graphAssets} />
     216       <InvestingProgressCard assets={graphAssets} quotesBySymbol={quotesBySymbol} />
    217217
    218218      {quotesError ? (
  • frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx

    ra8381b1 r42da64d  
    33import TimeRangeToggle from "../../../../../components/graphs/TimeRangeToggle.jsx";
    44import PercentChangeAreaChart from "../../../../../components/graphs/PercentChangeAreaChart.jsx";
    5 import { percentChangeSeries, sumByTimeBucket } from "../../../../../utils/timeSeries.js";
    6 
    7 export default function InvestingProgressCard({ assets }) {
     5import { formatBucketLabel } from "../../../../../utils/timeSeries.js";
     6
     7function 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
     51export default function InvestingProgressCard({ assets, quotesBySymbol = {} }) {
    852  const [range, setRange] = useState("weekly");
    953
    1054  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]);
    22122
    23123  const latest = points?.length ? points[points.length - 1] : null;
    24124  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);
    26140
    27141  return (
     
    29143      <div className="card-body">
    30144        <div className="flex items-center justify-between gap-4">
    31           <h2 className="card-title">Progress</h2>
     145          <h2 className="card-title">Portfolio Value</h2>
    32146          <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>
    37160            <TimeRangeToggle value={range} onChange={setRange} />
    38161          </div>
     
    40163
    41164        <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 ? (
    43166            <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.
    45168            </div>
    46169          ) : (
     
    49172              granularity={range}
    50173              height={260}
    51               positiveColor="#14b8a6"
     174              positiveColor="#10b981"
    52175            />
    53176          )}
    54177        </div>
    55178
    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>
    58200        </div>
    59201      </div>
  • frontend/src/pages/LandingPage/components/Benefits.jsx

    ra8381b1 r42da64d  
    4343  return (
    4444    <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">
    4646        <div className="text-center mb-16 md:mb-20">
    4747          <p className="font-semibold text-sm md:text-base mb-3 md:mb-4">
    4848            Why Trekr
    4949          </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">
    5151            Designed for serious progress
    5252          </h2>
  • frontend/src/pages/LandingPage/components/Faq.jsx

    ra8381b1 r42da64d  
    66  AccordionTrigger,
    77} from "@relume_io/relume-ui";
     8
     9import { FaqDefaults } from "./Faq.defaults";
    810
    911export const Faq = (props) => {
     
    1618    button,
    1719  } = {
    18     ...Faq1Defaults,
     20    ...FaqDefaults,
    1921    ...props,
    2022  };
     
    2426      className="px-[5%] py-16 md:py-24 lg:py-28 flex items-center justify-center"
    2527    >
    26       <div className="container max-w-lg">
     28      <div className="mx-auto w-full max-w-2xl">
    2729        <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">
    2931            {heading}
    3032          </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>
    3236        </div>
    3337        <Accordion type="multiple">
     
    4751            {footerHeading}
    4852          </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>
    5056          <div className="mt-6 md:mt-8">
    5157            <Button {...button}>{button.title}</Button>
     
    5662  );
    5763};
    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  
    4646      className="px-[5%] py-16 md:py-24 lg:py-28 bg-base-200"
    4747    >
    48       <div className="container">
     48      <div className="mx-auto w-full max-w-6xl">
    4949        <div className="text-center mb-16 md:mb-20">
    5050          <p className="font-semibold text-sm md:text-base mb-3 md:mb-4">
    5151            What you can track
    5252          </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">
    5454            Everything in one dashboard
    5555          </h2>
  • frontend/src/pages/LandingPage/components/Footer.jsx

    ra8381b1 r42da64d  
    44const Footer = () => {
    55  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">
    77      <aside>
    88        <img src={logo} alt="Trekr" className="h-10 w-auto" />
  • frontend/src/pages/LandingPage/components/Hero.jsx

    ra8381b1 r42da64d  
    1919  return (
    2020    <div
    21       className="hero min-h-screen"
     21      className="hero min-h-[calc(100vh-4rem)] bg-cover bg-center"
    2222      style={{
    2323        backgroundImage:
     
    2626    >
    2727      <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">
    3131            Trekr — track progress, every day
    3232          </h1>
    33           <p className="mb-5">
     33          <p className="mb-6 text-sm sm:text-base md:text-lg opacity-90">
    3434            One self-improvement hub: workouts, weight & nutrition, finances &
    3535            budgeting, investing, and daily discipline tasks. Set goals, log
     
    3737          </p>
    3838          <Link
    39             className="btn btn-primary !text-white"
     39            className="btn btn-primary !text-white btn-sm sm:btn-md md:btn-lg"
    4040            to={isAuthed ? "/dashboard" : "/register"}
    4141          >
  • frontend/src/pages/LandingPage/components/HowItWorks.jsx

    ra8381b1 r42da64d  
    11import { Button } from "@relume_io/relume-ui";
    2 import { RxChevronRight } from "react-icons/rx";
     2import { HowItWorksDefaults } from "./HowItWorks.defaults.jsx";
    33
    44export const HowItWorks = (props) => {
     
    99  return (
    1010    <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">
    1212        <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">
    1414            <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">
    1616              {heading}
    1717            </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>
    1921            <div className="mt-6 flex flex-wrap items-center gap-4 md:mt-8">
    2022              {buttons.map((button, index) => (
     
    2830            <img
    2931              src={image.src}
    30               className="w-full object-cover"
     32              className="w-full rounded-2xl object-cover"
    3133              alt={image.alt}
    3234            />
     
    3840};
    3941
    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  
    2727
    2828  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">
    3131        <Link
    3232          className="btn btn-ghost text-primary-content"
     
    3737        </Link>
    3838      </div>
    39       <div className="navbar-end">
     39      <div className="navbar-end flex-none">
    4040        {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">
    4242            <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"
    4444              to="/dashboard"
    4545            >
     
    4747            </Link>
    4848            <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"
    5050              onClick={onLogout}
    5151            >
     
    5555        ) : (
    5656          <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"
    5858            to="/login"
    5959          >
  • frontend/src/pages/LandingPage/components/Who.jsx

    ra8381b1 r42da64d  
    11import { Button } from "@relume_io/relume-ui";
    2 import { RxChevronRight } from "react-icons/rx";
     2import { WhoDefaults } from "./Who.defaults.jsx";
    33
    44export const Who = (props) => {
     
    99  return (
    1010    <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">
    1212        <div className="grid grid-cols-1 gap-y-12 md:grid-cols-2 md:items-center md:gap-x-12 lg:gap-x-20">
    1313          <div className="order-2 md:order-1">
    1414            <img
    1515              src={image.src}
    16               className="w-full object-cover"
     16              className="w-full rounded-2xl object-cover"
    1717              alt={image.alt}
    1818            />
    1919          </div>
    20           <div className="order-1 lg:order-2">
     20          <div className="order-1 lg:order-2 text-center md:text-left">
    2121            <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">
    2323              {heading}
    2424            </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>
    2628            <div className="mt-6 flex flex-wrap gap-4 md:mt-8">
    2729              {buttons.map((button, index) => (
     
    3840};
    3941
    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.