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 (
+
+ );
+ }
+
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 }) => {
))}
-
+
Confirm
-
+
);
};
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({
))}
- Cancel
- Copy
+
+ Cancel
+
+ Copy
@@ -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) => (
- setActiveView(view)}
>
{view}
-
+
))}
-
{
if (showForm) {
// Cancel form
@@ -496,11 +482,11 @@ export default function MilestoneTimeline({
}}
>
{showForm ? 'Cancel' : '+ New Milestone'}
-
+
+ {/* CREATE/EDIT FORM */}
{showForm && (
))}
-
-
+
+ Add Impact
-
+
)}
@@ -631,16 +610,15 @@ export default function MilestoneTimeline({
-
+
{editingMilestone ? 'Update' : 'Add'} Milestone
-
+
)}
- {/* 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({
)}
-
{
setShowTaskForm(showTaskForm === m.id ? null : m.id);
setNewTask({ title: '', description: '', due_date: '' });
}}
>
{showTaskForm === m.id ? 'Cancel Task' : 'Add Task'}
-
+
- handleEditMilestone(m)}>Edit
- setCopyWizardMilestone(m)}
- >
+ handleEditMilestone(m)}>Edit
+ setCopyWizardMilestone(m)}>
Copy
-
-
+ handleDeleteMilestone(m)}
>
Delete
-
+
{showTaskForm === m.id && (
@@ -718,7 +691,7 @@ export default function MilestoneTimeline({
value={newTask.due_date}
onChange={(e) => setNewTask({ ...newTask, due_date: e.target.value })}
/>
-
addTask(m.id)}>Save Task
+
addTask(m.id)}>Save Task
)}
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 && (
-
{
// Example action
console.log('User confirmed new career path:', pendingCareerForModal);
@@ -529,7 +529,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
className="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded"
>
Confirm Career Change to {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 */}
-
+ Add Scenario
-
+
);
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