diff --git a/src/App.js b/src/App.js index cee6881..9eb71fd 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'; import PremiumRoute from './components/PremiumRoute.js'; import SessionExpiredHandler from './components/SessionExpiredHandler.js'; @@ -19,23 +19,69 @@ function App() { const location = useLocation(); // Track whether user is authenticated - const [isAuthenticated, setIsAuthenticated] = useState(() => { - return !!localStorage.getItem('token'); - }); + const [isAuthenticated, setIsAuthenticated] = useState(false); // Track the user object, including is_premium const [user, setUser] = useState(null); - // We hide the "Upgrade to Premium" CTA if we're already on certain premium routes + // We might also track a "loading" state so we don't redirect prematurely + const [isLoading, setIsLoading] = useState(true); + + // Hide the "Upgrade to Premium" CTA on certain premium routes const premiumPaths = [ '/milestone-tracker', '/paywall', '/financial-profile', '/multi-scenario', - '/premium-onboarding' + '/premium-onboarding', ]; const showPremiumCTA = !premiumPaths.includes(location.pathname); + // On first mount, rehydrate user if there's a token + useEffect(() => { + const token = localStorage.getItem('token'); + if (!token) { + setIsLoading(false); + return; // No token => not authenticated + } + + // If we have a token, let's validate it and fetch user data + fetch('https://dev1.aptivaai.com/api/user-profile', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + .then((res) => { + if (!res.ok) { + // e.g. 401 means token invalid/expired + throw new Error('Token invalid or expired'); + } + return res.json(); + }) + .then((profile) => { + // We have a valid user profile -> set states + setUser(profile); + setIsAuthenticated(true); + }) + .catch((err) => { + console.error(err); + // If invalid, remove the token to avoid loops + localStorage.removeItem('token'); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + if (isLoading) { + // Optionally show a loading spinner or placeholder + return ( +
+

Loading...

+
+ ); + } + return (
{/* Header */} @@ -65,13 +111,13 @@ function App() { element={ } /> } /> - {/* Paywall - open to everyone for subscription */} + {/* Paywall (accessible to all) */} } /> {/* Authenticated routes */} diff --git a/src/components/AISuggestedMilestones.js b/src/components/AISuggestedMilestones.js index 63a4f34..1609387 100644 --- a/src/components/AISuggestedMilestones.js +++ b/src/components/AISuggestedMilestones.js @@ -1,72 +1,85 @@ -// src/components/AISuggestedMilestones.js import React, { useEffect, useState } from 'react'; - +import { Button } from './ui/button.js'; const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, activeView, projectionData }) => { const [suggestedMilestones, setSuggestedMilestones] = useState([]); const [selected, setSelected] = useState([]); const [loading, setLoading] = useState(false); + // Show a warning if projectionData is not an array useEffect(() => { if (!Array.isArray(projectionData)) { console.warn('⚠️ projectionData is not an array:', projectionData); return; } - console.log('📊 projectionData sample:', projectionData.slice(0, 3)); }, [projectionData]); - - + + // Generate AI-based suggestions from projectionData useEffect(() => { if (!career || !Array.isArray(projectionData)) return; - - // Dynamically suggest milestones based on projection data const suggested = []; - - // Find salary or savings growth points from projectionData: + + // Example: if retirement crosses $50k or if loans are paid off, we suggest a milestone projectionData.forEach((monthData, index) => { - if (index === 0) return; // Skip first month for comparison + if (index === 0) return; const prevMonth = projectionData[index - 1]; - - // Example logic: suggest milestones when retirement savings hit certain thresholds - if (monthData.totalRetirementSavings >= 50000 && prevMonth.totalRetirementSavings < 50000) { + + // Retirement crossing 50k + if ( + monthData.totalRetirementSavings >= 50000 && + prevMonth.totalRetirementSavings < 50000 + ) { suggested.push({ title: `Reach $50k Retirement Savings`, date: monthData.month + '-01', - progress: 0, + progress: 0 }); } - - // Milestone when loan is paid off + + // Loan paid off if (monthData.loanBalance <= 0 && prevMonth.loanBalance > 0) { suggested.push({ title: `Student Loans Paid Off`, date: monthData.month + '-01', - progress: 0, + progress: 0 }); } }); - - // Career-based suggestions still possible (add explicitly if desired) + + // Career-based suggestions suggested.push( - { title: `Entry-Level ${career}`, date: projectionData[6]?.month + '-01' || '2025-06-01', progress: 0 }, - { title: `Mid-Level ${career}`, date: projectionData[24]?.month + '-01' || '2027-01-01', progress: 0 }, - { title: `Senior-Level ${career}`, date: projectionData[60]?.month + '-01' || '2030-01-01', progress: 0 } + { + title: `Entry-Level ${career}`, + date: projectionData[6]?.month + '-01' || '2025-06-01', + progress: 0 + }, + { + title: `Mid-Level ${career}`, + date: projectionData[24]?.month + '-01' || '2027-01-01', + progress: 0 + }, + { + title: `Senior-Level ${career}`, + date: projectionData[60]?.month + '-01' || '2030-01-01', + progress: 0 + } ); - + setSuggestedMilestones(suggested); setSelected([]); }, [career, projectionData]); - - + + // Toggle selection of a milestone const toggleSelect = (index) => { - setSelected(prev => - prev.includes(index) ? prev.filter(i => i !== index) : [...prev, index] + setSelected((prev) => + prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index] ); }; + // Confirm the selected items => POST them as new milestones const confirmSelectedMilestones = async () => { - const milestonesToSend = selected.map(index => { + const milestonesToSend = selected.map((index) => { const m = suggestedMilestones[index]; return { title: m.title, @@ -74,30 +87,30 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active date: m.date, progress: m.progress, milestone_type: activeView || 'Career', - career_path_id: careerPathId, + career_path_id: careerPathId }; }); - + try { setLoading(true); const res = await authFetch(`/api/premium/milestone`, { method: 'POST', body: JSON.stringify({ milestones: milestonesToSend }), - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' } }); - if (!res.ok) throw new Error('Failed to save selected milestones'); + const data = await res.json(); console.log('Confirmed milestones:', data); + setSelected([]); // Clear selection - window.location.reload(); + window.location.reload(); // Re-fetch or reload as needed } catch (error) { console.error('Error saving selected milestones:', error); } finally { setLoading(false); } }; - if (!suggestedMilestones.length) return null; @@ -116,9 +129,12 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active ))} - +
); }; diff --git a/src/components/CareerSearch.js b/src/components/CareerSearch.js index ad4d8eb..37e5e6d 100644 --- a/src/components/CareerSearch.js +++ b/src/components/CareerSearch.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Button } from './ui/button.js'; const CareerSearch = ({ onCareerSelected }) => { const [careerObjects, setCareerObjects] = useState([]); @@ -24,38 +25,30 @@ const CareerSearch = ({ onCareerSelected }) => { for (let k = 0; k < careersList.length; k++) { const c = careersList[k]; - // If there's a title and soc_code, store the first we encounter for that title. if (c.title && c.soc_code && c.cip_code !== undefined) { if (!uniqueByTitle.has(c.title)) { - // Add it if we haven't seen this exact title yet uniqueByTitle.set(c.title, { title: c.title, soc_code: c.soc_code, - cip_code: c.cip_code, + cip_code: c.cip_code }); } - // If you truly only want to keep the first occurrence, - // just do nothing if we see the same title again. } } } } - // Convert Map to array const dedupedArr = [...uniqueByTitle.values()]; setCareerObjects(dedupedArr); - } catch (error) { console.error('Error loading or parsing career_clusters.json:', error); } }; - fetchCareerData(); }, []); - // Called when user clicks "Confirm New Career" const handleConfirmCareer = () => { - // Find the full object by exact title match + // find the full object by exact title match const foundObj = careerObjects.find( (obj) => obj.title.toLowerCase() === searchInput.toLowerCase() ); @@ -83,9 +76,9 @@ const CareerSearch = ({ onCareerSelected }) => { ))} - + ); }; diff --git a/src/components/MilestoneTimeline.js b/src/components/MilestoneTimeline.js index 60e2ade..0daa254 100644 --- a/src/components/MilestoneTimeline.js +++ b/src/components/MilestoneTimeline.js @@ -1,5 +1,5 @@ -// src/components/MilestoneTimeline.js import React, { useEffect, useState, useCallback } from 'react'; +import { Button } from './ui/button.js'; const today = new Date(); @@ -12,7 +12,6 @@ export default function MilestoneTimeline({ }) { const [milestones, setMilestones] = useState({ Career: [], Financial: [] }); - // "new or edit" milestone form data const [newMilestone, setNewMilestone] = useState({ title: '', description: '', @@ -23,7 +22,6 @@ export default function MilestoneTimeline({ isUniversal: 0 }); const [impactsToDelete, setImpactsToDelete] = useState([]); - const [showForm, setShowForm] = useState(false); const [editingMilestone, setEditingMilestone] = useState(null); @@ -36,33 +34,23 @@ export default function MilestoneTimeline({ const [copyWizardMilestone, setCopyWizardMilestone] = useState(null); // ------------------------------------------------------------------ - // 1) HELPER FUNCTIONS (defined above usage) + // 1) HELPER: Add or remove an impact from newMilestone // ------------------------------------------------------------------ - - // Insert a new blank impact function addNewImpact() { setNewMilestone((prev) => ({ ...prev, impacts: [ ...prev.impacts, - { - impact_type: 'ONE_TIME', - direction: 'subtract', - amount: 0, - start_date: '', - end_date: '' - } + { impact_type: 'ONE_TIME', direction: 'subtract', amount: 0, start_date: '', end_date: '' } ] })); } - // Remove an impact from newMilestone.impacts function removeImpact(idx) { setNewMilestone((prev) => { const newImpacts = [...prev.impacts]; const removed = newImpacts[idx]; if (removed && removed.id) { - // queue for DB DELETE setImpactsToDelete((old) => [...old, removed.id]); } newImpacts.splice(idx, 1); @@ -70,7 +58,6 @@ export default function MilestoneTimeline({ }); } - // Update a specific impact property function updateImpact(idx, field, value) { setNewMilestone((prev) => { const newImpacts = [...prev.impacts]; @@ -92,10 +79,9 @@ export default function MilestoneTimeline({ } const data = await res.json(); if (!data.milestones) { - console.warn('No milestones field in response:', data); + console.warn('No milestones in response:', data); return; } - const categorized = { Career: [], Financial: [] }; data.milestones.forEach((m) => { if (categorized[m.milestone_type]) { @@ -104,7 +90,6 @@ export default function MilestoneTimeline({ console.warn(`Unknown milestone type: ${m.milestone_type}`); } }); - setMilestones(categorized); } catch (err) { console.error('Failed to fetch milestones:', err); @@ -116,7 +101,7 @@ export default function MilestoneTimeline({ }, [fetchMilestones]); // ------------------------------------------------------------------ - // 3) Load scenarios for copy wizard + // 3) Load Scenarios for copy wizard // ------------------------------------------------------------------ useEffect(() => { async function loadScenarios() { @@ -139,7 +124,6 @@ export default function MilestoneTimeline({ async function handleEditMilestone(m) { try { setImpactsToDelete([]); - const res = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`); if (!res.ok) { console.error('Failed to fetch milestone impacts. Status:', res.status); @@ -270,7 +254,7 @@ export default function MilestoneTimeline({ } } - // Optionally re-fetch or update local + // Re-fetch or update local await fetchMilestones(); // reset form @@ -360,7 +344,6 @@ export default function MilestoneTimeline({ // Brute force page refresh window.location.reload(); - onClose(); } catch (err) { console.error('Error copying milestone:', err); @@ -371,7 +354,9 @@ export default function MilestoneTimeline({

Copy Milestone to Other Scenarios

-

Milestone: {milestone.title}

+

+ Milestone: {milestone.title} +

{scenarios.map((s) => (
@@ -387,8 +372,10 @@ export default function MilestoneTimeline({ ))}
- - + +
@@ -425,17 +412,16 @@ export default function MilestoneTimeline({ // normal => single scenario await deleteSingleMilestone(m); } - - // done => brute force window.location.reload(); } async function deleteSingleMilestone(m) { try { - const delRes = await authFetch(`/api/premium/milestones/${m.id}`, { method: 'DELETE' }); + const delRes = await authFetch(`/api/premium/milestones/${m.id}`, { + method: 'DELETE' + }); if (!delRes.ok) { console.error('Failed to delete single milestone:', delRes.status); - return; } } catch (err) { console.error('Error removing milestone from scenario:', err); @@ -464,17 +450,17 @@ export default function MilestoneTimeline({
{['Career', 'Financial'].map((view) => ( - + ))}
- + + {/* CREATE/EDIT FORM */} {showForm && (
- {/* Title / Desc / Date / Progress */} - setNewMilestone((prev) => ({ ...prev, date: e.target.value })) - } + onChange={(e) => setNewMilestone({ ...newMilestone, date: e.target.value })} /> - {/* If Financial => newSalary + impacts */} {activeView === 'Financial' && (
updateImpact(idx, 'start_date', e.target.value)} />
+ {imp.impact_type === 'MONTHLY' && (
@@ -598,18 +581,14 @@ export default function MilestoneTimeline({
)} - +
))} - - +
)} @@ -631,16 +610,15 @@ export default function MilestoneTimeline({ - + )} - {/* Actual timeline */} + {/* TIMELINE VISUAL */}
- {milestones[activeView].map((m) => { const leftPos = calcPosition(m.date); return ( @@ -649,10 +627,7 @@ export default function MilestoneTimeline({ className="milestone-timeline-post" style={{ left: `${leftPos}%` }} > -
handleEditMilestone(m)} - /> +
handleEditMilestone(m)} />
{m.title}
{m.description &&

{m.description}

} @@ -662,6 +637,7 @@ export default function MilestoneTimeline({
{m.date}
+ {/* Tasks */} {m.tasks && m.tasks.length > 0 && (
    {m.tasks.map((t) => ( @@ -674,29 +650,26 @@ export default function MilestoneTimeline({
)} - +
- - + - +
{showTaskForm === m.id && ( @@ -718,7 +691,7 @@ export default function MilestoneTimeline({ value={newTask.due_date} onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })} /> - +
)}
diff --git a/src/components/MilestoneTracker.css b/src/components/MilestoneTracker.css index eb96e60..1e1dbb1 100644 --- a/src/components/MilestoneTracker.css +++ b/src/components/MilestoneTracker.css @@ -21,17 +21,9 @@ margin-bottom: 20px; } - .view-selector button { - padding: 10px 15px; - border: none; - cursor: pointer; - background: #4caf50; - color: white; - border-radius: 5px; - } .view-selector button.active { - background: #2e7d32; + background: #0703e2; } .timeline-container { diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index dd4b10e..ca27bc3 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -14,7 +14,7 @@ import { } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; import { Filler } from 'chart.js'; - +import { Button } from './ui/button.js'; import authFetch from '../utils/authFetch.js'; import CareerSelectDropdown from './CareerSelectDropdown.js'; import CareerSearch from './CareerSearch.js'; @@ -520,7 +520,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => { {/* Confirm new career scenario */} {pendingCareerForModal && ( - + )}
); diff --git a/src/components/MultiScenarioView.js b/src/components/MultiScenarioView.js index 84c74c2..39b2911 100644 --- a/src/components/MultiScenarioView.js +++ b/src/components/MultiScenarioView.js @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'; import authFetch from '../utils/authFetch.js'; import ScenarioContainer from './ScenarioContainer.js'; +import { Button } from './ui/button.js'; /** * MultiScenarioView @@ -248,19 +249,11 @@ export default function MultiScenarioView() { {/* Add Scenario button */}
- +
); diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js index 41eab30..c40559d 100644 --- a/src/components/ScenarioContainer.js +++ b/src/components/ScenarioContainer.js @@ -1,48 +1,45 @@ +// src/components/ScenarioContainer.js + import React, { useState, useEffect, useCallback } from 'react'; import { Line } from 'react-chartjs-2'; import { Chart as ChartJS } from 'chart.js'; import annotationPlugin from 'chartjs-plugin-annotation'; - +import { Button } from './ui/button.js'; // <-- Universal Button import authFetch from '../utils/authFetch.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import AISuggestedMilestones from './AISuggestedMilestones.js'; import ScenarioEditModal from './ScenarioEditModal.js'; -// Register the annotation plugin (though we won't use it for milestone markers). +// Register the annotation plugin ChartJS.register(annotationPlugin); /** * ScenarioContainer * ----------------- * This component: - * - Renders a ), or uses the provided `scenario` prop. + * 2) Loads the collegeProfile + milestones/impacts for that scenario. + * 3) Merges scenario + user financial data + milestone impacts → runs `simulateFinancialProjection`. + * 4) Shows a chart of net savings / retirement / loan balances over time. + * 5) Allows milestone CRUD (create, edit, delete, copy). + * 6) Offers “Clone” / “Delete” scenario callbacks from the parent. */ + export default function ScenarioContainer({ - scenario, // The scenario row from career_paths - financialProfile, // The user’s overall financial snapshot - onRemove, - onClone, - onEdit + scenario, // The scenario row from career_paths + financialProfile, // The user’s overall financial snapshot + onRemove, // Callback for deleting scenario + onClone, // Callback for cloning scenario + onEdit // (Optional) If you want a scenario editing callback }) { /************************************************************* - * 1) SCENARIO DROPDOWN: allScenarios, localScenario, etc. + * 1) SCENARIO DROPDOWN: Load, store, and let user pick *************************************************************/ const [allScenarios, setAllScenarios] = useState([]); - // We keep a local copy of scenario so you can switch from dropdown + // We keep a local copy of `scenario` so user can switch from the dropdown const [localScenario, setLocalScenario] = useState(scenario || null); - // A) On mount, fetch all scenarios to populate the dropdown + // (A) On mount, fetch all scenarios to populate the handler + // (C)