diff --git a/backend/server.js b/backend/server.js
index 23795be..758d4c2 100755
--- a/backend/server.js
+++ b/backend/server.js
@@ -180,8 +180,8 @@ app.post('/api/register', async (req, res) => {
// 2) Insert into user_auth, referencing user_profile.id
const authQuery = `
- INSERT INTO user_auth (id, username, hashed_password)
- VALUES (?, ?, ?)
+ INSERT INTO user_auth (user_id, username, hashed_password)
+ VALUES (?, ?, ?)
`;
pool.query(
authQuery,
@@ -219,7 +219,7 @@ app.post('/api/register', async (req, res) => {
* Body: { username, password }
* Returns JWT signed with user_profile.id
*/
-app.post('/api/signin', (req, res) => {
+app.post('/api/signin', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res
@@ -227,25 +227,28 @@ app.post('/api/signin', (req, res) => {
.json({ error: 'Both username and password are required' });
}
+ // SELECT only the columns you actually have:
+ // 'ua.id' is user_auth's primary key,
+ // 'ua.user_id' references user_profile.id,
+ // and we alias user_profile.id as profileId for clarity.
const query = `
SELECT
- user_auth.id,
- user_auth.hashed_password,
- user_profile.firstname,
- user_profile.lastname,
- user_profile.email,
- user_profile.zipcode,
- user_profile.state,
- user_profile.area,
- user_profile.is_premium,
- user_profile.is_pro_premium,
- user_profile.career_situation,
- user_profile.career_priorities,
- user_profile.career_list
- FROM user_auth
- LEFT JOIN user_profile ON user_auth.id = user_profile.id
- WHERE user_auth.username = ?
+ ua.id AS authId,
+ ua.user_id AS userProfileId,
+ ua.hashed_password,
+ up.firstname,
+ up.lastname,
+ up.email,
+ up.zipcode,
+ up.state,
+ up.area,
+ up.career_situation
+ FROM user_auth ua
+ LEFT JOIN user_profile up
+ ON ua.user_id = up.id
+ WHERE ua.username = ?
`;
+
pool.query(query, [username], async (err, results) => {
if (err) {
console.error('Error querying user_auth:', err.message);
@@ -259,22 +262,26 @@ app.post('/api/signin', (req, res) => {
}
const row = results[0];
- // Compare password
+
+ // Compare password with bcrypt
const isMatch = await bcrypt.compare(password, row.hashed_password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid username or password' });
}
- // The user_profile id is stored in user_auth.id
- const token = jwt.sign({ id: row.id }, SECRET_KEY, {
+ // IMPORTANT: Use 'row.userProfileId' (from user_profile.id) in the token
+ // so your '/api/user-profile' can decode it and do SELECT * FROM user_profile WHERE id=?
+ const token = jwt.sign({ id: row.userProfileId }, SECRET_KEY, {
expiresIn: '2h',
});
- // Return the user info + token
+ // Return user info + token
+ // 'authId' is user_auth's PK, but typically you won't need it on the client
+ // 'row.userProfileId' is the actual user_profile.id
res.status(200).json({
message: 'Login successful',
token,
- id: row.id, // The user_profile.id
+ id: row.userProfileId, // This is user_profile.id (important if your frontend needs it)
user: {
firstname: row.firstname,
lastname: row.lastname,
@@ -282,16 +289,14 @@ app.post('/api/signin', (req, res) => {
zipcode: row.zipcode,
state: row.state,
area: row.area,
- is_premium: row.is_premium,
- is_pro_premium: row.is_pro_premium,
career_situation: row.career_situation,
- career_priorities: row.career_priorities,
- career_list: row.career_list,
},
});
});
});
+
+
/* ------------------------------------------------------------------
CHECK USERNAME (MySQL)
------------------------------------------------------------------ */
diff --git a/src/components/ExpensesWizard.js b/src/components/ExpensesWizard.js
new file mode 100644
index 0000000..27e6966
--- /dev/null
+++ b/src/components/ExpensesWizard.js
@@ -0,0 +1,129 @@
+import React, { useState } from 'react';
+import { Button } from './ui/button.js';
+
+
+function ExpensesWizard({ onClose, onExpensesCalculated }) {
+ const [housing, setHousing] = useState('');
+ const [utilities, setUtilities] = useState('');
+ const [groceries, setGroceries] = useState('');
+ const [transportation, setTransportation] = useState('');
+ const [insurance, setInsurance] = useState('');
+ const [misc, setMisc] = useState('');
+
+ const calculateTotal = () => {
+ const sum =
+ (parseFloat(housing) || 0) +
+ (parseFloat(utilities) || 0) +
+ (parseFloat(groceries) || 0) +
+ (parseFloat(transportation) || 0) +
+ (parseFloat(insurance) || 0) +
+ (parseFloat(misc) || 0);
+
+ return sum;
+ };
+
+ const handleFinish = () => {
+ const total = calculateTotal();
+ onExpensesCalculated(total);
+ onClose();
+ };
+
+ const totalExpenses = calculateTotal();
+
+ return (
+
+
Monthly Expenses Wizard
+
+ Enter approximate amounts for each category below. We'll sum them up to estimate
+ your monthly expenses.
+
+
+
+
+ setHousing(e.target.value)}
+ placeholder="e.g. 1500"
+ />
+
+
+
+
+ setUtilities(e.target.value)}
+ placeholder="Water, electricity, gas, etc. (e.g. 200)"
+ />
+
+
+
+
+ setGroceries(e.target.value)}
+ placeholder="Groceries, dining out, etc. (e.g. 300)"
+ />
+
+
+
+
+ setTransportation(e.target.value)}
+ placeholder="Car payment, gas, train, bus, uber fare e.g. 500"
+ />
+
+
+
+
+ setInsurance(e.target.value)}
+ placeholder="Car, house, rental, health insurance not deducted from paycheck (e.g. 200)"
+ />
+
+
+
+
+ setMisc(e.target.value)}
+ placeholder="Subscriptions, Phone, any recurring cost not covered elsewhere e.g. 250"
+ />
+
+
+{/* Show the user the current total */}
+
+ Current Total: ${totalExpenses.toFixed(2)}
+
+
+
+
+
+
+
+ );
+}
+
+export default ExpensesWizard;
diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js
index 93771bf..97f2b59 100644
--- a/src/components/PremiumOnboarding/CollegeOnboarding.js
+++ b/src/components/PremiumOnboarding/CollegeOnboarding.js
@@ -475,7 +475,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerProfileId
diff --git a/src/components/PremiumOnboarding/FinancialOnboarding.js b/src/components/PremiumOnboarding/FinancialOnboarding.js
index 295f8cf..5b2b0ab 100644
--- a/src/components/PremiumOnboarding/FinancialOnboarding.js
+++ b/src/components/PremiumOnboarding/FinancialOnboarding.js
@@ -1,7 +1,10 @@
// FinancialOnboarding.js
-import React from 'react';
+import React, { useState } from 'react';
+import Modal from '../ui/modal.js';
+import ExpensesWizard from '../../components/ExpensesWizard.js'; // path to your wizard
+import { Button } from '../../components/ui/button.js'; // using your Tailwind-based button
-const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = false }) => {
+const FinancialOnboarding = ({ nextStep, prevStep, data, setData }) => {
const {
currently_working = '',
current_salary = 0,
@@ -14,19 +17,37 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
emergency_contribution = 0,
extra_cash_emergency_pct = "",
extra_cash_retirement_pct = "",
- planned_monthly_expenses = '',
- planned_monthly_debt_payments = '',
- planned_monthly_retirement_contribution = '',
- planned_monthly_emergency_contribution = '',
- planned_surplus_emergency_pct = '',
- planned_surplus_retirement_pct = '',
- planned_additional_income = ''
} = data;
- const handleChange = (e) => {
- const { name, value } = e.target;
- let val = parseFloat(value) || 0;
+ const [showExpensesWizard, setShowExpensesWizard] = useState(false);
+ const handleNeedHelpExpenses = () => {
+ setShowExpensesWizard(true);
+ };
+
+ const handleExpensesCalculated = (total) => {
+ setData(prev => ({
+ ...prev,
+ monthly_expenses: total
+ }));
+ };
+
+ const infoIcon = (msg) => (
+
+ i
+
+ );
+
+
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ let val = parseFloat(value) || 0;
+ if (type === 'checkbox') {
+ val = checked;
+ }
if (name === 'extra_cash_emergency_pct') {
val = Math.min(Math.max(val, 0), 100);
setData(prevData => ({
@@ -46,6 +67,11 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
}
};
+ const handleSubmit = () => {
+ // Move to next step
+ nextStep();
+ };
+
return (
Financial Details
@@ -53,11 +79,13 @@ const FinancialOnboarding = ({ nextStep, prevStep, data, setData, isEditMode = f
{currently_working === 'yes' && (
-
+
-
+
-
-
+
+
+
-
+
-
+
-
+
-
+
-
+
- {/* Only show the planned overrides if isEditMode is true */}
- {isEditMode && (
-
- )}
-
+
+ {showExpensesWizard && (
+ setShowExpensesWizard(false)}>
+ setShowExpensesWizard(false)}
+ onExpensesCalculated={handleExpensesCalculated}
+ />
+
+ )}
+
);
diff --git a/src/components/PremiumOnboarding/OnboardingContainer.js b/src/components/PremiumOnboarding/OnboardingContainer.js
index 37cb6d1..8b1e061 100644
--- a/src/components/PremiumOnboarding/OnboardingContainer.js
+++ b/src/components/PremiumOnboarding/OnboardingContainer.js
@@ -1,4 +1,3 @@
-// OnboardingContainer.js
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import PremiumWelcome from './PremiumWelcome.js';
@@ -12,41 +11,62 @@ import authFetch from '../../utils/authFetch.js';
const OnboardingContainer = () => {
console.log('OnboardingContainer MOUNT');
+ const navigate = useNavigate();
+
+ // 1. Local state for multi-step onboarding
const [step, setStep] = useState(0);
const [careerData, setCareerData] = useState({});
const [financialData, setFinancialData] = useState({});
const [collegeData, setCollegeData] = useState({});
- const navigate = useNavigate();
- const nextStep = () => setStep(step + 1);
- const prevStep = () => setStep(step - 1);
+ // 2. On mount, check if localStorage has onboarding data
+ useEffect(() => {
+ const stored = localStorage.getItem('premiumOnboardingState');
+ if (stored) {
+ try {
+ const parsed = JSON.parse(stored);
+ // Restore step and data if they exist
+ if (parsed.step !== undefined) setStep(parsed.step);
+ if (parsed.careerData) setCareerData(parsed.careerData);
+ if (parsed.financialData) setFinancialData(parsed.financialData);
+ if (parsed.collegeData) setCollegeData(parsed.collegeData);
+ } catch (err) {
+ console.warn('Failed to parse premiumOnboardingState:', err);
+ }
+ }
+ }, []);
+
+ // 3. Whenever any key pieces of state change, save to localStorage
+ useEffect(() => {
+ const stateToStore = {
+ step,
+ careerData,
+ financialData,
+ collegeData
+ };
+ localStorage.setItem('premiumOnboardingState', JSON.stringify(stateToStore));
+ }, [step, careerData, financialData, collegeData]);
+
+ // Move user to next or previous step
+ const nextStep = () => setStep((prev) => prev + 1);
+ const prevStep = () => setStep((prev) => prev - 1);
function parseFloatOrNull(value) {
- // If user left it blank ("" or undefined), treat it as NULL.
- if (value == null || value === '') {
- return null;
+ if (value == null || value === '') {
+ return null;
+ }
+ const parsed = parseFloat(value);
+ return isNaN(parsed) ? null : parsed;
}
- const parsed = parseFloat(value);
- // If parseFloat can't parse, also return null
- return isNaN(parsed) ? null : parsed;
-}
console.log('Final collegeData in OnboardingContainer:', collegeData);
- // Final “all done” submission when user finishes the last step
+ // 4. Final “all done” submission
const handleFinalSubmit = async () => {
try {
- // Build a scenarioPayload that includes optional planned_* fields:
const scenarioPayload = {
...careerData,
- planned_monthly_expenses: parseFloatOrNull(careerData.planned_monthly_expenses),
- planned_monthly_debt_payments: parseFloatOrNull(careerData.planned_monthly_debt_payments),
- planned_monthly_retirement_contribution: parseFloatOrNull(careerData.planned_monthly_retirement_contribution),
- planned_monthly_emergency_contribution: parseFloatOrNull(careerData.planned_monthly_emergency_contribution),
- planned_surplus_emergency_pct: parseFloatOrNull(careerData.planned_surplus_emergency_pct),
- planned_surplus_retirement_pct: parseFloatOrNull(careerData.planned_surplus_retirement_pct),
- planned_additional_income: parseFloatOrNull(careerData.planned_additional_income),
-};
+ };
// 1) POST career-profile (scenario)
const careerRes = await authFetch('/api/premium/career-profile', {
@@ -56,7 +76,7 @@ const OnboardingContainer = () => {
});
if (!careerRes.ok) throw new Error('Failed to save career profile');
const careerJson = await careerRes.json();
- const { career_profile_id } = careerJson; // <-- Renamed from career_profile_id
+ const { career_profile_id } = careerJson;
if (!career_profile_id) {
throw new Error('No career_profile_id returned by server');
}
@@ -69,34 +89,38 @@ const OnboardingContainer = () => {
});
if (!financialRes.ok) throw new Error('Failed to save financial profile');
- // 3) Only do college-profile if user is "currently_enrolled" or "prospective_student"
- if (
- careerData.college_enrollment_status === 'currently_enrolled' ||
- careerData.college_enrollment_status === 'prospective_student'
- ) {
- const mergedCollege = {
- ...collegeData,
- career_profile_id,
- college_enrollment_status: careerData.college_enrollment_status,
- };
- const collegeRes = await authFetch('/api/premium/college-profile', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(mergedCollege),
- });
- if (!collegeRes.ok) throw new Error('Failed to save college profile');
- } else {
- console.log('Skipping college-profile upsert because user is not enrolled/planning.');
- }
+ // 3) Possibly POST college-profile
+ if (
+ careerData.college_enrollment_status === 'currently_enrolled' ||
+ careerData.college_enrollment_status === 'prospective_student'
+ ) {
+ const mergedCollege = {
+ ...collegeData,
+ career_profile_id,
+ college_enrollment_status: careerData.college_enrollment_status,
+ };
+ const collegeRes = await authFetch('/api/premium/college-profile', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(mergedCollege),
+ });
+ if (!collegeRes.ok) throw new Error('Failed to save college profile');
+ } else {
+ console.log('Skipping college-profile upsert because user is not enrolled/planning.');
+ }
- // Done => navigate
- navigate('/milestone-tracker');
+ // 4) Clear localStorage so next onboarding starts fresh (optional)
+ localStorage.removeItem('premiumOnboardingState');
+
+ // 5) Navigate away
+ navigate('/milestone-tracker');
} catch (err) {
console.error(err);
- // (optionally show error to user)
+ // Optionally show error to user
}
};
+ // 5. Array of steps
const onboardingSteps = [
,
@@ -121,7 +145,6 @@ const OnboardingContainer = () => {
nextStep={nextStep}
data={{
...collegeData,
- // keep enrollment status from careerData if relevant:
college_enrollment_status: careerData.college_enrollment_status,
}}
setData={setCollegeData}
diff --git a/src/components/PremiumOnboarding/ReviewPage.js b/src/components/PremiumOnboarding/ReviewPage.js
index e7c12d1..c39ad7d 100644
--- a/src/components/PremiumOnboarding/ReviewPage.js
+++ b/src/components/PremiumOnboarding/ReviewPage.js
@@ -87,19 +87,26 @@ function ReviewPage({
{/* --- COLLEGE SECTION --- */}
{inOrPlanningCollege && (
-
-
College Info
-
College Name: {collegeData.college_name || 'N/A'}
-
Major: {collegeData.major || 'N/A'}
- {/* If you have these fields, show them if they're meaningful */}
- {collegeData.tuition != null && (
-
Tuition (calculated): {formatNum(collegeData.tuition)}
- )}
- {collegeData.program_length != null && (
-
Program Length (years): {formatNum(collegeData.program_length)}
- )}
-
- )}
+
+
College Info
+
+
College Name
+
Major
+
Program Type
+
Tuition (calculated)
+
Program Length (years)
+
Credit Hours Per Year
+
Credit Hours Required
+
Hours Completed
+
Is In State?
+
Loan Deferral Until Graduation?
+
Annual Financial Aid
+
Existing College Debt
+
Extra Monthly Payment
+
Expected Graduation
+
Expected Salary
+
+)}
{/* --- ACTION BUTTONS --- */}