From f9674643d52221860e6415af51115bbd54437bab Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 12 May 2025 17:06:12 +0000 Subject: [PATCH] Added the 4 user journeys, landing pages, and navigation from SignIn. --- backend/server.js | 176 ++++++-------------- src/App.js | 8 + src/components/EnhancingLanding.js | 26 +++ src/components/PlanningLanding.js | 47 ++++++ src/components/PreparingLanding.js | 26 +++ src/components/RetirementLanding.js | 26 +++ src/components/SignIn.js | 17 +- src/components/SignUp.js | 239 ++++++++++++++++++++++------ src/components/ui/PromptModal.js | 27 ++++ src/components/ui/SituationCard.js | 20 +++ src/components/ui/dialog.js | 18 ++- user_profile.db | Bin 110592 -> 110592 bytes 12 files changed, 448 insertions(+), 182 deletions(-) create mode 100644 src/components/EnhancingLanding.js create mode 100644 src/components/PlanningLanding.js create mode 100644 src/components/PreparingLanding.js create mode 100644 src/components/RetirementLanding.js create mode 100644 src/components/ui/PromptModal.js create mode 100644 src/components/ui/SituationCard.js diff --git a/backend/server.js b/backend/server.js index 35c0643..a917724 100755 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,7 @@ import express from 'express'; import axios from 'axios'; import cors from 'cors'; +import helmet from 'helmet'; import dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import path from 'path'; @@ -35,6 +36,11 @@ const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'h app.disable('x-powered-by'); app.use(bodyParser.json()); app.use(express.json()); +app.use(helmet({ + contentSecurityPolicy: false, + crossOriginEmbedderPolicy: false, + }) +); // Enable CORS with dynamic origin checking app.use( @@ -71,6 +77,13 @@ app.use((req, res, next) => { next(); }); +// Force Content-Type to application/json on all responses +app.use((req, res, next) => { + res.setHeader('Content-Type', 'application/json'); + next(); +}); + + // Route for user registration app.post('/api/register', async (req, res) => { const { @@ -82,12 +95,13 @@ app.post('/api/register', async (req, res) => { email, zipcode, state, - area + area, + career_situation } = req.body; - // Validate all required fields - if (!userId || !username || !password || !firstname || !lastname || !email || !zipcode || !state || !area) { - return res.status(400).json({ error: 'All fields are required' }); + // Validate all required fields, including career_situation + if (!userId || !username || !password || !firstname || !lastname || !email || !zipcode || !state || !area || !career_situation) { + return res.status(400).json({ error: 'All fields including career_situation are required' }); } try { @@ -98,6 +112,7 @@ app.post('/api/register', async (req, res) => { INSERT INTO user_auth (user_id, username, hashed_password) VALUES (?, ?, ?) `; + db.run(authQuery, [userId, username, hashedPassword], function (err) { if (err) { console.error('Error inserting into user_auth:', err.message); @@ -107,143 +122,28 @@ app.post('/api/register', async (req, res) => { return res.status(500).json({ error: 'Failed to register user' }); } - // Insert into user_profile with actual provided values + // Insert into user_profile including career_situation const profileQuery = ` - INSERT INTO user_profile (user_id, firstname, lastname, email, zipcode, state, area) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO user_profile (user_id, firstname, lastname, email, zipcode, state, area, career_situation) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; - db.run(profileQuery, [userId, firstname, lastname, email, zipcode, state, area], (err) => { + + db.run(profileQuery, [userId, firstname, lastname, email, zipcode, state, area, career_situation], (err) => { if (err) { console.error('Error inserting into user_profile:', err.message); return res.status(500).json({ error: 'Failed to create user profile' }); } - res.status(201).json({ message: 'User registered successfully', userId }); + return res.status(201).json({ message: 'User registered successfully', userId }); }); }); } catch (error) { console.error('Error during registration:', error.message); - res.status(500).json({ error: 'Internal server error' }); + return res.status(500).json({ error: 'Internal server error' }); } }); -// Route to save or update user profile -app.post('/api/user-profile', (req, res) => { - const token = req.headers.authorization?.split(' ')[1]; - if (!token) { - return res.status(401).json({ error: 'Authorization token is required' }); - } - - let userId; - try { - const decoded = jwt.verify(token, SECRET_KEY); - userId = decoded.userId; - } catch (error) { - return res.status(401).json({ error: 'Invalid or expired token' }); - } - - const { - firstName, - lastName, - email, - zipCode, - state, - area, - careerSituation, - interest_inventory_answers, - } = req.body; - - if (!firstName || !lastName || !email || !zipCode || !state || !area) { - return res.status(400).json({ error: 'All fields are required (except IIA)' }); - } - - // 1) Check if we have an existing user_profile row for this userId - const checkQuery = `SELECT * FROM user_profile WHERE user_id = ?`; - db.get(checkQuery, [userId], (err, existingRow) => { - if (err) { - console.error('Error checking profile:', err.message); - return res.status(500).json({ error: 'Database error' }); - } - - // 2) If interest_inventory_answers was omitted in the request, - // keep the old value from existingRow. - let finalAnswers; - if (existingRow) { - // If the row exists, keep or replace answers: - finalAnswers = - interest_inventory_answers === undefined - ? existingRow.interest_inventory_answers - : interest_inventory_answers; - } else { - // If no row yet, use whatever is passed or null - finalAnswers = interest_inventory_answers || null; - } - - if (existingRow) { - // 3) Update - const updateQuery = ` - UPDATE user_profile - SET firstname = ?, - lastname = ?, - email = ?, - zipcode = ?, - state = ?, - area = ?, - career_situation = ?, - interest_inventory_answers = ? - WHERE user_id = ? - `; - const params = [ - firstName, - lastName, - email, - zipCode, - state, - area, - careerSituation || null, - finalAnswers, - userId, - ]; - db.run(updateQuery, params, function (err) { - if (err) { - console.error('Error updating profile:', err.message); - return res.status(500).json({ error: 'Failed to update user profile' }); - } - return res.status(200).json({ message: 'User profile updated successfully' }); - }); - } else { - // 4) Insert new - const insertQuery = ` - INSERT INTO user_profile - (firstname, lastname, email, zipcode, state, area, career_situation, interest_inventory_answers, user_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - const params = [ - firstName, - lastName, - email, - zipCode, - state, - area, - careerSituation || null, - finalAnswers, - userId, - ]; - db.run(insertQuery, params, function (err) { - if (err) { - console.error('Error inserting profile:', err.message); - return res.status(500).json({ error: 'Failed to create user profile' }); - } - return res - .status(201) - .json({ message: 'User profile created successfully', id: this.lastID }); - }); - } - }); -}); - - // Route for login app.post('/api/login', (req, res) => { const { username, password } = req.body; @@ -290,6 +190,8 @@ app.post('/api/signin', async (req, res) => { user_auth.hashed_password, user_profile.zipcode, user_profile.is_premium, + user_profile.is_pro_premium, + user_profile.career_situation, user_profile.email, user_profile.firstname, user_profile.lastname @@ -334,11 +236,33 @@ app.post('/api/signin', async (req, res) => { email: row.email, zipcode: row.zipcode, is_premium: row.is_premium, + is_pro_premium: row.is_pro_premium, + career_situation: row.career_situation, } }); }); }); +/// Check if username already exists +app.get('/api/check-username/:username', (req, res) => { + const { username } = req.params; + + const query = `SELECT username FROM user_auth WHERE username = ?`; + + db.get(query, [username], (err, row) => { + if (err) { + console.error('Error checking username:', err.message); + return res.status(500).json({ error: 'Database error' }); + } + + if (row) { + res.status(200).json({ exists: true }); + } else { + res.status(200).json({ exists: false }); + } + }); +}); + // Route to fetch user profile diff --git a/src/App.js b/src/App.js index 2d8f92d..6977d38 100644 --- a/src/App.js +++ b/src/App.js @@ -15,6 +15,10 @@ import SessionExpiredHandler from './components/SessionExpiredHandler.js'; import GettingStarted from './components/GettingStarted.js'; import SignIn from './components/SignIn.js'; import SignUp from './components/SignUp.js'; +import PlanningLanding from './components/PlanningLanding.js'; +import PreparingLanding from './components/PreparingLanding.js'; +import EnhancingLanding from './components/EnhancingLanding.js'; +import RetirementLanding from './components/RetirementLanding.js'; import InterestInventory from './components/InterestInventory.js'; import Dashboard from './components/Dashboard.js'; import UserProfile from './components/UserProfile.js'; @@ -231,6 +235,10 @@ function App() { } /> } /> } /> + } /> + } /> + } /> + } /> {/* Premium-only routes */} +
+

+ Enhancing Your Career +

+

+ AptivaAI helps you advance your career. Plan career milestones, enhance your skill set, optimize your resume, and prepare for promotions or transitions. +

+
+ + +
+
+ + ); +} + +export default EnhancingLanding; diff --git a/src/components/PlanningLanding.js b/src/components/PlanningLanding.js new file mode 100644 index 0000000..8b143f0 --- /dev/null +++ b/src/components/PlanningLanding.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from './ui/button.js'; + +function PlanningLanding() { + const navigate = useNavigate(); + + return ( +
+
+

+ Planning Your Career +

+

+ Discover career options that match your interests, skills, and potential. + AptivaAI helps you find your ideal career path, provides insights into the educational requirements, + expected salaries, job market trends, and more. +

+ +
+ + + + + +
+
+
+ ); +} + +export default PlanningLanding; diff --git a/src/components/PreparingLanding.js b/src/components/PreparingLanding.js new file mode 100644 index 0000000..4480537 --- /dev/null +++ b/src/components/PreparingLanding.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from './ui/button.js'; + +function PreparingLanding() { + const navigate = useNavigate(); + + return ( +
+
+

+ Preparing for Your (Next) Career +

+

+ Identify skills, education, or certifications you need, discover educational opportunities, and set clear milestones to start or transition into your next career. +

+
+ + +
+
+
+ ); +} + +export default PreparingLanding; diff --git a/src/components/RetirementLanding.js b/src/components/RetirementLanding.js new file mode 100644 index 0000000..a193343 --- /dev/null +++ b/src/components/RetirementLanding.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from './ui/button.js'; + +function RetirementLanding() { + const navigate = useNavigate(); + + return ( +
+
+

+ Retirement Planning +

+

+ Plan strategically and financially for retirement. AptivaAI provides you with clear financial projections, milestone tracking, and scenario analysis for a secure future. +

+
+ + +
+
+
+ ); +} + +export default RetirementLanding; diff --git a/src/components/SignIn.js b/src/components/SignIn.js index f784ae0..6392584 100644 --- a/src/components/SignIn.js +++ b/src/components/SignIn.js @@ -47,8 +47,21 @@ function SignIn({ setIsAuthenticated, setUser }) { setUser(user); } - // Navigate to next screen - navigate('/getting-started'); + const userCareerSituation = user.career_situation; + + const careerSituationRouteMap = { + planning: '/planning', + preparing: '/preparing', + enhancing: '/enhancing', + retirement: '/retirement', + }; + + if (careerSituationRouteMap[userCareerSituation]) { + navigate(careerSituationRouteMap[userCareerSituation]); + } else { + navigate('/getting-started'); // fallback if undefined + } + } catch (error) { setError(error.message); } diff --git a/src/components/SignUp.js b/src/components/SignUp.js index a630183..a6f81e4 100644 --- a/src/components/SignUp.js +++ b/src/components/SignUp.js @@ -1,20 +1,59 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from './ui/button.js'; +import SituationCard from './ui/SituationCard.js'; +import PromptModal from './ui/PromptModal.js'; + +const careerSituations = [ + { + id: "planning", + title: "Planning Your Career", + description: "I'm exploring options and figuring out what careers fit my interests and skills.", + route: "/planning" + }, + { + id: "preparing", + title: "Preparing for Your (Next) Career", + description: "I'm gaining education, skills, or certifications required to start or transition into a new career.", + route: "/preparing" + }, + { + id: "enhancing", + title: "Enhancing Your Career", + description: "I'm established professionally and want to advance, seek promotions, or shift roles.", + route: "/enhancing" + }, + { + id: "retirement", + title: "Retirement Planning", + description: "I'm preparing financially and strategically for retirement.", + route: "/retirement" + } +]; function SignUp() { const navigate = useNavigate(); + // existing states const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); const [firstname, setFirstname] = useState(''); const [lastname, setLastname] = useState(''); const [email, setEmail] = useState(''); + const [confirmEmail, setConfirmEmail] = useState(''); const [zipcode, setZipcode] = useState(''); const [state, setState] = useState(''); const [area, setArea] = useState(''); const [areas, setAreas] = useState([]); const [error, setError] = useState(''); + const [loadingAreas, setLoadingAreas] = useState(false); + + // new states + const [showCareerSituations, setShowCareerSituations] = useState(false); + const [selectedSituation, setSelectedSituation] = useState(null); + const [showPrompt, setShowPrompt] = useState(false); + const states = [ { name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' }, @@ -35,13 +74,15 @@ function SignUp() { { name: 'Vermont', code: 'VT' }, { name: 'Virginia', code: 'VA' }, { name: 'Washington', code: 'WA' }, { name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' }, ]; - + useEffect(() => { const fetchAreas = async () => { if (!state) { setAreas([]); return; } + + setLoadingAreas(true); // Start loading try { const res = await fetch(`/api/areas?state=${state}`); const data = await res.json(); @@ -49,97 +90,197 @@ function SignUp() { } catch (err) { console.error('Error fetching areas:', err); setAreas([]); + } finally { + setLoadingAreas(false); // Done loading } }; - + fetchAreas(); }, [state]); - - const handleSignUp = async (e) => { - e.preventDefault(); - setError(''); - + + const validateFields = async () => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; const zipRegex = /^\d{5}$/; const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/; - - if (!username || !password || !firstname || !lastname || !email || !zipcode || !state || !area) { + + if ( + !username || !password || !confirmPassword || + !firstname || !lastname || + !email || !confirmEmail || + !zipcode || !state || !area + ) { setError('All fields are required.'); - return; + return false; } + if (!emailRegex.test(email)) { setError('Enter a valid email address.'); - return; + return false; } + + if (email !== confirmEmail) { + setError('Emails do not match.'); + return false; + } + if (!zipRegex.test(zipcode)) { setError('ZIP code must be exactly 5 digits.'); - return; + return false; } + if (!passwordRegex.test(password)) { setError('Password must include at least 8 characters, one uppercase, one lowercase, one number, and one special character.'); - return; + return false; } - + + if (password !== confirmPassword) { + setError('Passwords do not match.'); + return false; + } + + // Explicitly check if username exists before proceeding try { + const res = await fetch(`/api/check-username/${encodeURIComponent(username)}`); + const data = await res.json(); + if (data.exists) { + setError('Username already exists. Please choose a different username.'); + return false; + } + } catch (err) { + console.error('Error checking username:', err); + setError('Could not verify username availability. Please try again.'); + return false; + } + + setError(''); + return true; + }; + + + const handleNextClick = async (e) => { + e.preventDefault(); + + const isValid = await validateFields(); + if (isValid) { + setShowCareerSituations(true); + } + }; + + + const handleSituationConfirm = async () => { + try { + // Verify payload clearly: + console.log("Payload sent to backend:", { + userId: Math.floor(Math.random() * 1000000000), + username, password, firstname, lastname, email, zipcode, state, area, + career_situation: selectedSituation.id + }); + const response = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: Math.floor(Math.random() * 1000000000), username, password, firstname, lastname, email, zipcode, state, area, + career_situation: selectedSituation.id }), }); - + const data = await response.json(); - + if (!response.ok) { setError(data.error || 'Registration failed. Please try again.'); + setShowPrompt(false); return; } - - navigate('/getting-started'); + + navigate(selectedSituation.route); } catch (err) { console.error(err); setError('An unexpected error occurred. Please try again later.'); + setShowPrompt(false); } }; + + return (
-

Sign Up

- - {error && ( -
- {error} -
+ {!showCareerSituations ? ( + <> +

Sign Up

+ {error && ( +
+ {error} +
+ )} +
+ setUsername(e.target.value)} /> + setPassword(e.target.value)} /> + setConfirmPassword(e.target.value)}/> + setFirstname(e.target.value)} /> + setLastname(e.target.value)} /> + setEmail(e.target.value)} /> + setConfirmEmail(e.target.value)}/> + setZipcode(e.target.value)} /> + + +
+ + {loadingAreas && ( + + Loading... + + )} +
+ + +
+ + ) : ( + <> +

Choose Your Career Stage

+
+ {careerSituations.map((situation) => ( + { + setSelectedSituation(situation); + setShowPrompt(true); + }} + /> + ))} +
+ {showPrompt &&
Modal should be open!
} + setShowPrompt(false)} + /> + )} - -
- setUsername(e.target.value)} /> - setPassword(e.target.value)} /> - setFirstname(e.target.value)} /> - setLastname(e.target.value)} /> - setEmail(e.target.value)} /> - setZipcode(e.target.value)} /> - - - - - - -
); } - -export default SignUp; +export default SignUp; \ No newline at end of file diff --git a/src/components/ui/PromptModal.js b/src/components/ui/PromptModal.js new file mode 100644 index 0000000..452c06e --- /dev/null +++ b/src/components/ui/PromptModal.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from './dialog.js'; +import { Button } from './button.js'; +import { cn } from '../../utils/cn.js'; // <-- explicitly added missing import + +const PromptModal = ({ open, title, message, confirmText, cancelText, onConfirm, onCancel }) => ( + { if (!isOpen) onCancel(); }}> + + + {title} + +
+ {message} +
+ + + + +
+
+); + +export default PromptModal; diff --git a/src/components/ui/SituationCard.js b/src/components/ui/SituationCard.js new file mode 100644 index 0000000..018208e --- /dev/null +++ b/src/components/ui/SituationCard.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { Card, CardContent } from './card.js'; +import { cn } from '../../utils/cn.js'; + +const SituationCard = ({ title, description, selected, onClick }) => ( + + +

{title}

+

{description}

+
+
+); + +export default SituationCard; diff --git a/src/components/ui/dialog.js b/src/components/ui/dialog.js index f608bc0..ef767e0 100644 --- a/src/components/ui/dialog.js +++ b/src/components/ui/dialog.js @@ -1,18 +1,26 @@ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { cn } from '../../utils/cn.js'; // <-- explicitly added missing import const Dialog = DialogPrimitive.Root; const DialogTrigger = DialogPrimitive.Trigger; const DialogContent = React.forwardRef(({ className, ...props }, ref) => { return ( - + + + + ); }); + const DialogHeader = ({ children }) => { return
{children}
; }; diff --git a/user_profile.db b/user_profile.db index 093ed49620580be31cd18a72ac8e10104ea51510..95a21ec55b5f3249f6bcca81b9a8a4747d4f9033 100644 GIT binary patch delta 467 zcmZp8z}E19ZGtpo??f4A#@>wyOZd5%`I;E`o%tT}HEnFP=HpIbfPtuqje3f)TmlT@ z;@aYZ{wy4_3>+K`93`p6CB_J1@@0tfiBMrKvU(&E&j_=2MRw9K4TE?%HX%>2>}{9E}m z_@y^C^77YvbF(q9h&Qr=Tn;u~#VAR|&_Kn+va&EOJJCF?!r3x4(8$FnHz?Fq&#W>q z&m^tfCoi$U(jd{GIKa@{B+xB2+|8o6#5*a~n-gLf$Y)@GLJf1)k95u~&Gs(O@s3Ol zEiUm1Of0EPb**wVkuC|&4=u`c&bM&%%grjvs!H_A4RuK^@iq+1oxIiG9~dT&8Th~R lzvX|tSuo%#|Kzvv96ZL{oXq0jz%b%${vN;mdpsj&006F1hF<^x delta 341 zcmZp8z}E19ZGtpo`9v9K#`28`OZd5%_*OCSJM*pLx7ye^hflsyj8%|9RN7En(4U2a zp^=q?gMq^5CWc0ahQ>ywV1|(?h-YeQYGP;#6a&&G zMnoyA-EaqoanAoTwF2-!g zn3a(Xw9CNIAw4%SGe<8uKbLj$OMMmrc@_aiW>v=0;?$z}f};Gi%$!tKUIqpRCjJZt z{;m8O8yjQzeZ5)O7+Ay`SwT)LNi8lhQZY(WF*H!g4a*NtGt9_MaSF;cb;(HdORr3E z_b;l*&h|-33GgjWOD@WHG}9|^3Nef@aSO}~Pfss2o4nQEpM#12Ed&2|{