Repository: DFIC-Quant-Fund/Fund-Monitor-Web
Pull request #21: Routing on Holdings Page
Author: login: Tyler-Yue
Labels: name: priority
Created: 2025-03-24T01:54:24Z | Merged: 2025-03-25T21:22:27Z

Closes #14 

Added functionality as discussed in issue 14

Linked issue #14: Holdings page route
The page url should be indicative of the data we view:

instead of:
https://fund.degrootefinance.com/holdings

replace by:
https://fund.degrootefinance.com/holdings?date=2024-01-01&portfolio=Core


Diff:
diff --git a/README.md b/README.md
index 3e7d2ca..677d8fc 100644
--- a/README.md
+++ b/README.md
@@ -11,3 +11,4 @@ yarn dev
 ```
 
 Open [http://localhost:3000](http://localhost:3000) to see build.
+
diff --git a/src/app/holdings/page.tsx b/src/app/holdings/page.tsx
index 1ea8ef1..7d56511 100644
--- a/src/app/holdings/page.tsx
+++ b/src/app/holdings/page.tsx
@@ -1,6 +1,7 @@
 'use client';
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, Suspense } from 'react';
 import { API_BASE_URL } from '../../utils/apiBase';
+import { useSearchParams, useRouter } from 'next/navigation';
 
 // Interface for Holdings Data
 interface HoldingData {
@@ -40,10 +41,17 @@ interface HoldingsApiResponse {
     success: boolean;
 }
 
-export default function Holdings() {
+function HoldingsContent() {
+    // Getting parameters from url
+    const searchParams = useSearchParams();
+    const router = useRouter();
+    const urlDate = searchParams.get('date');
+    const urlPortfolio = searchParams.get('portfolio');
+
+    const baseDate = new Date(Date.now() - 86400000).toLocaleDateString('en-CA');
     const [holdingsData, setHoldingsData] = useState<HoldingData[]>([]);
-    const [selectedPortfolio, setSelectedPortfolio] = useState('core');
-    const [selectedDate, setSelectedDate] = useState(new Date(Date.now() - 86400000).toLocaleDateString('en-CA'));
+    const [selectedPortfolio, setSelectedPortfolio] = useState(urlPortfolio || 'core');
+    const [selectedDate, setSelectedDate] = useState(urlDate || baseDate);
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState('');
     const [exchangeRatesData, setExchangeRatesData] = useState<ExchangeRates | null>(null);
@@ -51,6 +59,33 @@ export default function Holdings() {
 
     const STARTING_VALUE = 101644.99;
 
+    // Update url when selection changes
+    const updateURL = useCallback((date: string, portfolio: string) => {
+        const params = new URLSearchParams();
+        params.set('date', date);
+        params.set('portfolio', portfolio);
+        router.push(`/holdings?${params.toString()}`, { scroll: false });
+    }, [router]);
+
+    // Updates url when there are missing params
+    useEffect(() => {
+        if (!urlDate || !urlPortfolio) {
+          updateURL(selectedDate, selectedPortfolio);
+        }
+    }, [urlDate, urlPortfolio, selectedDate, selectedPortfolio, updateURL]);
+
+    // Update on change to portfolio
+    const onPortfolioChange = (newPortfolio: string) => {
+        setSelectedPortfolio(newPortfolio);
+        updateURL(selectedDate, newPortfolio);
+    };
+
+    // Update on change to date
+    const onDateChange = (newDate: string) => {
+        setSelectedDate(newDate);
+        updateURL(newDate, selectedPortfolio);
+    };
+
     // Fetch exchange rates
     const fetchExchangeRates = useCallback(async () => {
         try {
@@ -94,7 +129,7 @@ export default function Holdings() {
     useEffect(() => {
         fetchExchangeRates();
         fetchData();
-    }, [fetchExchangeRates, fetchData]);
+    }, [urlDate, urlPortfolio, fetchExchangeRates, fetchData]);
 
     // Convert holdings market values and price to CAD
     const totalPortfolioValue = holdingsData.reduce((acc, row) => {
@@ -146,7 +181,7 @@ export default function Holdings() {
                     <div className="flex flex-col md:flex-row gap-4 mt-4 md:mt-0">
                         <select
                             value={selectedPortfolio}
-                            onChange={(e) => setSelectedPortfolio(e.target.value)}
+                            onChange={(e) => onPortfolioChange(e.target.value)}
                             className="border px-3 py-2 rounded shadow text-black"
                         >
                             <option value="core">Core Portfolio</option>
@@ -155,7 +190,7 @@ export default function Holdings() {
                         <input
                             type="date"
                             value={selectedDate}
-                            onChange={(e) => setSelectedDate(e.target.value)}
+                            onChange={(e) => onDateChange(e.target.value)}
                             min="1900-01-01"
                             max="2100-12-31"
                             className="border px-3 py-2 rounded shadow text-black"
@@ -260,3 +295,11 @@ export default function Holdings() {
         </div>
     );
 }
+export default function HoldingsPage() {
+    return (
+        <Suspense fallback={<div>Loading...</div>}>
+            <HoldingsContent />
+        </Suspense>
+    );
+}
+
diff --git a/src/app/performance/page.tsx b/src/app/performance/page.tsx
index 0bbe41e..b718ecf 100644
--- a/src/app/performance/page.tsx
+++ b/src/app/performance/page.tsx
@@ -1,5 +1,6 @@
 'use client';
-import { useState, useEffect, useCallback } from 'react';
+import {useState, useEffect, useCallback, Suspense} from 'react';
+import { useSearchParams, useRouter } from 'next/navigation';
 import { API_BASE_URL } from '../../utils/apiBase';
 
 interface PerformanceData {
@@ -17,12 +18,38 @@ interface ApiResponse {
     success: boolean;
 }
 
-export default function Performance() {
+function Performance() {
+
+    const searchParams = useSearchParams();
+    const router = useRouter();
+    const urlDate = searchParams.get('date')
+    const baseDate = new Date().toLocaleDateString('en-CA')
+
     const [performanceData, setPerformanceData] = useState<PerformanceData[]>([]);
-    const [selectedDate, setSelectedDate] = useState(new Date().toLocaleDateString('en-CA'));
+    const [selectedDate, setSelectedDate] = useState(urlDate || baseDate);
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState('');
-    
+
+    // Update url when selection changes
+    const updateURL = useCallback((date: string) => {
+        const params = new URLSearchParams();
+        params.set('date', date);
+        router.push(`/performance?${params.toString()}`, { scroll: false });
+    }, [router]);
+
+    // Updates url when no params are present
+    useEffect(() => {
+        if (!urlDate) {
+          updateURL(selectedDate);
+        }
+    }, [urlDate, selectedDate, updateURL]);
+
+    // Update on change to date
+    const onDateChange = (newDate: string) => {
+        setSelectedDate(newDate);
+        updateURL(newDate);
+    };
+
     const fetchData = useCallback(async () => {
         setLoading(true);
         setError('');
@@ -89,7 +116,7 @@ export default function Performance() {
                         <input 
                             type="date" 
                             value={selectedDate} 
-                            onChange={(e) => setSelectedDate(e.target.value)}
+                            onChange={(e) => onDateChange(e.target.value)}
                             min="1900-01-01" 
                             max="2100-12-31"
                             className="border px-3 py-2 rounded shadow text-black"
@@ -142,3 +169,11 @@ export default function Performance() {
         </div>
     );
 }
+
+export default function PerformancePage(){
+    return(
+        <Suspense fallback={<div>Loading...</div>}>
+            <Performance />
+        </Suspense>
+    )
+}


Reviews:
user: login: CameronBeneteau
body: Looks awesome @TylerY-16 😎 Please see the discussion comment for you and @OlegGlo. @TylerY-16 Can you also do this for date in `/performance`? This would be great to have on both pages for consistency.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T19:22:21Z
user: login: Tyler-Yue
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:37:35Z
user: login: OlegGlo
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T21:45:43Z
user: login: CameronBeneteau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T22:02:40Z
user: login: CameronBeneteau
body: Looks great. Curious if this logic could be shared between pages. Fine for now but just some food for thought.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T05:32:26Z
user: login: OlegGlo
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T21:20:58Z

Review comments:
diff_hunk: @@ -40,17 +41,50 @@ interface HoldingsApiResponse {
     success: boolean;
 }
 
-export default function Holdings() {
+function HoldingsContent() {
+    // Getting parameters from url
+    const searchParams = useSearchParams();
+    const router = useRouter();
+    const urlDate = searchParams.get('date');
+    const urlPortfolio = searchParams.get('portfolio');
+
+    const baseDate = new Date(Date.now() - 86400000).toLocaleDateString('en-CA');
     const [holdingsData, setHoldingsData] = useState<HoldingData[]>([]);
-    const [selectedPortfolio, setSelectedPortfolio] = useState('core');
-    const [selectedDate, setSelectedDate] = useState(new Date(Date.now() - 86400000).toLocaleDateString('en-CA'));
+    const [selectedPortfolio, setSelectedPortfolio] = useState(urlPortfolio || 'core');
+    const [selectedDate, setSelectedDate] = useState(urlDate || baseDate);
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState('');
     const [exchangeRatesData, setExchangeRatesData] = useState<ExchangeRates | null>(null);
     const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'fund', direction: 'desc' });
 
     const STARTING_VALUE = 101644.99;
 
+    // Update url when selection changes
+    const updateURL = useCallback((date: string, portfolio: string) => {
+        const params = new URLSearchParams();
+        params.set('date', date);
+        params.set('portfolio', portfolio);
+        router.push(`/holdings?${params.toString()}`, { scroll: false });
+    }, [router]);
+
+    useEffect(() => {
+        if (!urlDate || !urlPortfolio) {
+          updateURL(selectedDate, selectedPortfolio);
+        }
+    }, [urlDate, urlPortfolio, selectedDate, selectedPortfolio, updateURL]);
path: src/app/holdings/page.tsx
user: login: CameronBeneteau
body: @OlegGlo @TylerY-16 I'm just thinking from a UX perspective, would it be better to remove this `useEffect` so the base page initially loads to just `/holdings` with the default date/portfolio? That way it's easier to bookmark and force the current date on future visits. Obviously any portfolio/date selections would update the URL in case someone wants to share it and people can still navigate to `/holdings?portfolio=XXXX&date=XXXX`. Try removing this `useEffect` and let me know what you think. Good either way as long as the page is easy for people to use.
created_at: 2025-03-24T19:19:32Z
author_association: MEMBER
diff_hunk: @@ -40,17 +41,50 @@ interface HoldingsApiResponse {
     success: boolean;
 }
 
-export default function Holdings() {
+function HoldingsContent() {
+    // Getting parameters from url
+    const searchParams = useSearchParams();
+    const router = useRouter();
+    const urlDate = searchParams.get('date');
+    const urlPortfolio = searchParams.get('portfolio');
+
+    const baseDate = new Date(Date.now() - 86400000).toLocaleDateString('en-CA');
     const [holdingsData, setHoldingsData] = useState<HoldingData[]>([]);
-    const [selectedPortfolio, setSelectedPortfolio] = useState('core');
-    const [selectedDate, setSelectedDate] = useState(new Date(Date.now() - 86400000).toLocaleDateString('en-CA'));
+    const [selectedPortfolio, setSelectedPortfolio] = useState(urlPortfolio || 'core');
+    const [selectedDate, setSelectedDate] = useState(urlDate || baseDate);
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState('');
     const [exchangeRatesData, setExchangeRatesData] = useState<ExchangeRates | null>(null);
     const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'fund', direction: 'desc' });
 
     const STARTING_VALUE = 101644.99;
 
+    // Update url when selection changes
+    const updateURL = useCallback((date: string, portfolio: string) => {
+        const params = new URLSearchParams();
+        params.set('date', date);
+        params.set('portfolio', portfolio);
+        router.push(`/holdings?${params.toString()}`, { scroll: false });
+    }, [router]);
+
+    useEffect(() => {
+        if (!urlDate || !urlPortfolio) {
+          updateURL(selectedDate, selectedPortfolio);
+        }
+    }, [urlDate, urlPortfolio, selectedDate, selectedPortfolio, updateURL]);
path: src/app/holdings/page.tsx
user: login: Tyler-Yue
body: My thought process was that having the parameters always in the url guarantees that the date and portfolio are specified whenever someone copies the url to share it, I can change it if you think it’s better the other way. 
@CameronBeneteau 
created_at: 2025-03-24T21:37:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,17 +41,50 @@ interface HoldingsApiResponse {
     success: boolean;
 }
 
-export default function Holdings() {
+function HoldingsContent() {
+    // Getting parameters from url
+    const searchParams = useSearchParams();
+    const router = useRouter();
+    const urlDate = searchParams.get('date');
+    const urlPortfolio = searchParams.get('portfolio');
+
+    const baseDate = new Date(Date.now() - 86400000).toLocaleDateString('en-CA');
     const [holdingsData, setHoldingsData] = useState<HoldingData[]>([]);
-    const [selectedPortfolio, setSelectedPortfolio] = useState('core');
-    const [selectedDate, setSelectedDate] = useState(new Date(Date.now() - 86400000).toLocaleDateString('en-CA'));
+    const [selectedPortfolio, setSelectedPortfolio] = useState(urlPortfolio || 'core');
+    const [selectedDate, setSelectedDate] = useState(urlDate || baseDate);
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState('');
     const [exchangeRatesData, setExchangeRatesData] = useState<ExchangeRates | null>(null);
     const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'fund', direction: 'desc' });
 
     const STARTING_VALUE = 101644.99;
 
+    // Update url when selection changes
+    const updateURL = useCallback((date: string, portfolio: string) => {
+        const params = new URLSearchParams();
+        params.set('date', date);
+        params.set('portfolio', portfolio);
+        router.push(`/holdings?${params.toString()}`, { scroll: false });
+    }, [router]);
+
+    useEffect(() => {
+        if (!urlDate || !urlPortfolio) {
+          updateURL(selectedDate, selectedPortfolio);
+        }
+    }, [urlDate, urlPortfolio, selectedDate, selectedPortfolio, updateURL]);
path: src/app/holdings/page.tsx
user: login: OlegGlo
body: I like the current version. Also makes more sense from UX when sharing. The default /holdings is the one getting bookmarked anyways. 
created_at: 2025-03-24T21:45:43Z
author_association: MEMBER
diff_hunk: @@ -40,17 +41,50 @@ interface HoldingsApiResponse {
     success: boolean;
 }
 
-export default function Holdings() {
+function HoldingsContent() {
+    // Getting parameters from url
+    const searchParams = useSearchParams();
+    const router = useRouter();
+    const urlDate = searchParams.get('date');
+    const urlPortfolio = searchParams.get('portfolio');
+
+    const baseDate = new Date(Date.now() - 86400000).toLocaleDateString('en-CA');
     const [holdingsData, setHoldingsData] = useState<HoldingData[]>([]);
-    const [selectedPortfolio, setSelectedPortfolio] = useState('core');
-    const [selectedDate, setSelectedDate] = useState(new Date(Date.now() - 86400000).toLocaleDateString('en-CA'));
+    const [selectedPortfolio, setSelectedPortfolio] = useState(urlPortfolio || 'core');
+    const [selectedDate, setSelectedDate] = useState(urlDate || baseDate);
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState('');
     const [exchangeRatesData, setExchangeRatesData] = useState<ExchangeRates | null>(null);
     const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'fund', direction: 'desc' });
 
     const STARTING_VALUE = 101644.99;
 
+    // Update url when selection changes
+    const updateURL = useCallback((date: string, portfolio: string) => {
+        const params = new URLSearchParams();
+        params.set('date', date);
+        params.set('portfolio', portfolio);
+        router.push(`/holdings?${params.toString()}`, { scroll: false });
+    }, [router]);
+
+    useEffect(() => {
+        if (!urlDate || !urlPortfolio) {
+          updateURL(selectedDate, selectedPortfolio);
+        }
+    }, [urlDate, urlPortfolio, selectedDate, selectedPortfolio, updateURL]);
path: src/app/holdings/page.tsx
user: login: CameronBeneteau
body: As long as people know how to modify the bookmark to just `/holdings` then I'm good with this 👍
created_at: 2025-03-24T22:02:40Z
author_association: MEMBER

PR comments:
user: login: netlify[bot]
created_at: 2025-03-24T01:54:36Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *dfic-fund* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 8e7cba16b7242d13a9b887beb009ca948a6c7f7a |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/dfic-fund/deploys/67e2297f4712a00008b8a4f4 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-21--dfic-fund.netlify.app](https://deploy-preview-21--dfic-fund.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTIxLS1kZmljLWZ1bmQubmV0bGlmeS5hcHAifQ.q04zwzHon-2Ma4hJ4IkD4kH17vMXlWOSBEOPrbjRuYg)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [dfic-fund Preview](https://deploy-preview-21--dfic-fund.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/dfic-fund/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: OlegGlo
created_at: 2025-03-24T04:58:16Z
body: Run "npm run build" and fix the errors. Other than that, looks good. 
author_association: MEMBER