Added the 4 user journeys, landing pages, and navigation from SignIn.
This commit is contained in:
parent
1d585c2041
commit
f9674643d5
@ -1,6 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import path from 'path';
|
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.disable('x-powered-by');
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
crossOriginEmbedderPolicy: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Enable CORS with dynamic origin checking
|
// Enable CORS with dynamic origin checking
|
||||||
app.use(
|
app.use(
|
||||||
@ -71,6 +77,13 @@ app.use((req, res, next) => {
|
|||||||
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
|
// Route for user registration
|
||||||
app.post('/api/register', async (req, res) => {
|
app.post('/api/register', async (req, res) => {
|
||||||
const {
|
const {
|
||||||
@ -82,12 +95,13 @@ app.post('/api/register', async (req, res) => {
|
|||||||
email,
|
email,
|
||||||
zipcode,
|
zipcode,
|
||||||
state,
|
state,
|
||||||
area
|
area,
|
||||||
|
career_situation
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Validate all required fields
|
// Validate all required fields, including career_situation
|
||||||
if (!userId || !username || !password || !firstname || !lastname || !email || !zipcode || !state || !area) {
|
if (!userId || !username || !password || !firstname || !lastname || !email || !zipcode || !state || !area || !career_situation) {
|
||||||
return res.status(400).json({ error: 'All fields are required' });
|
return res.status(400).json({ error: 'All fields including career_situation are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -98,6 +112,7 @@ app.post('/api/register', async (req, res) => {
|
|||||||
INSERT INTO user_auth (user_id, username, hashed_password)
|
INSERT INTO user_auth (user_id, username, hashed_password)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
db.run(authQuery, [userId, username, hashedPassword], function (err) {
|
db.run(authQuery, [userId, username, hashedPassword], function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error inserting into user_auth:', err.message);
|
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' });
|
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 = `
|
const profileQuery = `
|
||||||
INSERT INTO user_profile (user_id, firstname, lastname, email, zipcode, state, area)
|
INSERT INTO user_profile (user_id, firstname, lastname, email, zipcode, state, area, career_situation)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
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) {
|
if (err) {
|
||||||
console.error('Error inserting into user_profile:', err.message);
|
console.error('Error inserting into user_profile:', err.message);
|
||||||
return res.status(500).json({ error: 'Failed to create user profile' });
|
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) {
|
} catch (error) {
|
||||||
console.error('Error during registration:', error.message);
|
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
|
// Route for login
|
||||||
app.post('/api/login', (req, res) => {
|
app.post('/api/login', (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
@ -290,6 +190,8 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
user_auth.hashed_password,
|
user_auth.hashed_password,
|
||||||
user_profile.zipcode,
|
user_profile.zipcode,
|
||||||
user_profile.is_premium,
|
user_profile.is_premium,
|
||||||
|
user_profile.is_pro_premium,
|
||||||
|
user_profile.career_situation,
|
||||||
user_profile.email,
|
user_profile.email,
|
||||||
user_profile.firstname,
|
user_profile.firstname,
|
||||||
user_profile.lastname
|
user_profile.lastname
|
||||||
@ -334,11 +236,33 @@ app.post('/api/signin', async (req, res) => {
|
|||||||
email: row.email,
|
email: row.email,
|
||||||
zipcode: row.zipcode,
|
zipcode: row.zipcode,
|
||||||
is_premium: row.is_premium,
|
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
|
// Route to fetch user profile
|
||||||
|
@ -15,6 +15,10 @@ import SessionExpiredHandler from './components/SessionExpiredHandler.js';
|
|||||||
import GettingStarted from './components/GettingStarted.js';
|
import GettingStarted from './components/GettingStarted.js';
|
||||||
import SignIn from './components/SignIn.js';
|
import SignIn from './components/SignIn.js';
|
||||||
import SignUp from './components/SignUp.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 InterestInventory from './components/InterestInventory.js';
|
||||||
import Dashboard from './components/Dashboard.js';
|
import Dashboard from './components/Dashboard.js';
|
||||||
import UserProfile from './components/UserProfile.js';
|
import UserProfile from './components/UserProfile.js';
|
||||||
@ -231,6 +235,10 @@ function App() {
|
|||||||
<Route path="/interest-inventory" element={<InterestInventory />} />
|
<Route path="/interest-inventory" element={<InterestInventory />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/profile" element={<UserProfile />} />
|
<Route path="/profile" element={<UserProfile />} />
|
||||||
|
<Route path="/planning" element={<PlanningLanding />} />
|
||||||
|
<Route path="/preparing" element={<PreparingLanding />} />
|
||||||
|
<Route path="/enhancing" element={<EnhancingLanding />} />
|
||||||
|
<Route path="/retirement" element={<RetirementLanding />} />
|
||||||
|
|
||||||
{/* Premium-only routes */}
|
{/* Premium-only routes */}
|
||||||
<Route
|
<Route
|
||||||
|
26
src/components/EnhancingLanding.js
Normal file
26
src/components/EnhancingLanding.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Button } from './ui/button.js';
|
||||||
|
|
||||||
|
function EnhancingLanding() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
|
||||||
|
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-4 text-center">
|
||||||
|
Enhancing Your Career
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6 text-center">
|
||||||
|
AptivaAI helps you advance your career. Plan career milestones, enhance your skill set, optimize your resume, and prepare for promotions or transitions.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<Button onClick={() => navigate('/resume-optimizer')}>Optimize Resume</Button>
|
||||||
|
<Button onClick={() => navigate('/milestone-tracker')}>Set Career Milestones</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnhancingLanding;
|
47
src/components/PlanningLanding.js
Normal file
47
src/components/PlanningLanding.js
Normal file
@ -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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
|
||||||
|
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-4 text-center">
|
||||||
|
Planning Your Career
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6 text-center">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => navigate('/interest-inventory')}
|
||||||
|
>
|
||||||
|
Take Interest Inventory
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => navigate('/career-explorer')}
|
||||||
|
>
|
||||||
|
Explore Career Paths
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => navigate('/educational-programs')}
|
||||||
|
>
|
||||||
|
Discover Educational Programs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlanningLanding;
|
26
src/components/PreparingLanding.js
Normal file
26
src/components/PreparingLanding.js
Normal file
@ -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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
|
||||||
|
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-4 text-center">
|
||||||
|
Preparing for Your (Next) Career
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6 text-center">
|
||||||
|
Identify skills, education, or certifications you need, discover educational opportunities, and set clear milestones to start or transition into your next career.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<Button onClick={() => navigate('/interest-inventory')}>Retake Interest Inventory</Button>
|
||||||
|
<Button onClick={() => navigate('/educational-programs')}>Explore Education Options</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PreparingLanding;
|
26
src/components/RetirementLanding.js
Normal file
26
src/components/RetirementLanding.js
Normal file
@ -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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
|
||||||
|
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-4 text-center">
|
||||||
|
Retirement Planning
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6 text-center">
|
||||||
|
Plan strategically and financially for retirement. AptivaAI provides you with clear financial projections, milestone tracking, and scenario analysis for a secure future.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<Button onClick={() => navigate('/financial-profile')}>Update Financial Profile</Button>
|
||||||
|
<Button onClick={() => navigate('/milestone-tracker')}>Set Retirement Milestones</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RetirementLanding;
|
@ -47,8 +47,21 @@ function SignIn({ setIsAuthenticated, setUser }) {
|
|||||||
setUser(user);
|
setUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to next screen
|
const userCareerSituation = user.career_situation;
|
||||||
navigate('/getting-started');
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
setError(error.message);
|
setError(error.message);
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,59 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button } from './ui/button.js';
|
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() {
|
function SignUp() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// existing states
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [firstname, setFirstname] = useState('');
|
const [firstname, setFirstname] = useState('');
|
||||||
const [lastname, setLastname] = useState('');
|
const [lastname, setLastname] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
const [confirmEmail, setConfirmEmail] = useState('');
|
||||||
const [zipcode, setZipcode] = useState('');
|
const [zipcode, setZipcode] = useState('');
|
||||||
const [state, setState] = useState('');
|
const [state, setState] = useState('');
|
||||||
const [area, setArea] = useState('');
|
const [area, setArea] = useState('');
|
||||||
const [areas, setAreas] = useState([]);
|
const [areas, setAreas] = useState([]);
|
||||||
const [error, setError] = 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 = [
|
const states = [
|
||||||
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
|
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
|
||||||
@ -42,6 +81,8 @@ function SignUp() {
|
|||||||
setAreas([]);
|
setAreas([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoadingAreas(true); // Start loading
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/areas?state=${state}`);
|
const res = await fetch(`/api/areas?state=${state}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -49,44 +90,99 @@ function SignUp() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching areas:', err);
|
console.error('Error fetching areas:', err);
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingAreas(false); // Done loading
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchAreas();
|
fetchAreas();
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
const handleSignUp = async (e) => {
|
const validateFields = async () => {
|
||||||
e.preventDefault();
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
||||||
const zipRegex = /^\d{5}$/;
|
const zipRegex = /^\d{5}$/;
|
||||||
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;
|
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.');
|
setError('All fields are required.');
|
||||||
return;
|
return false;
|
||||||
}
|
|
||||||
if (!emailRegex.test(email)) {
|
|
||||||
setError('Enter a valid email address.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!zipRegex.test(zipcode)) {
|
|
||||||
setError('ZIP code must be exactly 5 digits.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!passwordRegex.test(password)) {
|
|
||||||
setError('Password must include at least 8 characters, one uppercase, one lowercase, one number, and one special character.');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
setError('Enter a valid email address.');
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordRegex.test(password)) {
|
||||||
|
setError('Password must include at least 8 characters, one uppercase, one lowercase, one number, and one special character.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly check if username exists before proceeding
|
||||||
try {
|
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', {
|
const response = await fetch('/api/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userId: Math.floor(Math.random() * 1000000000),
|
userId: Math.floor(Math.random() * 1000000000),
|
||||||
username, password, firstname, lastname, email, zipcode, state, area,
|
username, password, firstname, lastname, email, zipcode, state, area,
|
||||||
|
career_situation: selectedSituation.id
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,52 +190,97 @@ function SignUp() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setError(data.error || 'Registration failed. Please try again.');
|
setError(data.error || 'Registration failed. Please try again.');
|
||||||
|
setShowPrompt(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/getting-started');
|
navigate(selectedSituation.route);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setError('An unexpected error occurred. Please try again later.');
|
setError('An unexpected error occurred. Please try again later.');
|
||||||
|
setShowPrompt(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
|
||||||
<div className="w-full max-w-md bg-white p-6 rounded-lg shadow-lg">
|
<div className="w-full max-w-md bg-white p-6 rounded-lg shadow-lg">
|
||||||
|
{!showCareerSituations ? (
|
||||||
|
<>
|
||||||
<h2 className="mb-4 text-2xl font-semibold text-center">Sign Up</h2>
|
<h2 className="mb-4 text-2xl font-semibold text-center">Sign Up</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-2 text-sm text-red-600 bg-red-100 rounded">
|
<div className="mb-4 p-2 text-sm text-red-600 bg-red-100 rounded">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<form className="space-y-3" onSubmit={handleNextClick}>
|
||||||
<form onSubmit={handleSignUp} className="space-y-3">
|
<input className="w-full px-3 py-2 border rounded-md" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||||
<input className="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} />
|
<input type="password" className="w-full px-3 py-2 border rounded-md" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||||
<input className="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
<input className="w-full px-3 py-2 border rounded-md" placeholder="Retype Password" type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)}/>
|
||||||
<input className="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="First Name" value={firstname} onChange={(e) => setFirstname(e.target.value)} />
|
<input className="w-full px-3 py-2 border rounded-md" placeholder="First Name" value={firstname} onChange={(e) => setFirstname(e.target.value)} />
|
||||||
<input className="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Last Name" value={lastname} onChange={(e) => setLastname(e.target.value)} />
|
<input className="w-full px-3 py-2 border rounded-md" placeholder="Last Name" value={lastname} onChange={(e) => setLastname(e.target.value)} />
|
||||||
<input className="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
<input className="w-full px-3 py-2 border rounded-md" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||||
<input className="w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="Zip Code" value={zipcode} onChange={(e) => setZipcode(e.target.value)} />
|
<input className="w-full px-3 py-2 border rounded-md" placeholder="Retype Email" type="email" value={confirmEmail} onChange={(e) => setConfirmEmail(e.target.value)}/>
|
||||||
|
<input className="w-full px-3 py-2 border rounded-md" placeholder="Zip Code" value={zipcode} onChange={(e) => setZipcode(e.target.value)} />
|
||||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={state} onChange={(e) => setState(e.target.value)}>
|
<select className="w-full px-3 py-2 border rounded-md" value={state} onChange={(e) => setState(e.target.value)}><option value="">Select State</option>
|
||||||
<option value="">Select State</option>
|
{states.map((s) => (
|
||||||
{states.map((s) => <option key={s.code} value={s.code}>{s.name}</option>)}
|
<option key={s.code} value={s.code}>{s.name}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md" value={area} onChange={(e) => setArea(e.target.value)}>
|
<div className="relative w-full"><select className="w-full px-3 py-2 border rounded-md" value={area} onChange={(e) => setArea(e.target.value)} disabled={loadingAreas} // Disable select while loading
|
||||||
|
>
|
||||||
<option value="">Select Area</option>
|
<option value="">Select Area</option>
|
||||||
{areas.map((a, i) => <option key={i} value={a}>{a}</option>)}
|
{areas.map((a, i) => (
|
||||||
|
<option key={i} value={a}>{a}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{loadingAreas && (
|
||||||
|
<span className="absolute right-3 top-2.5 text-gray-400 text-sm animate-pulse">
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full">
|
<Button type="submit" className="w-full">
|
||||||
Sign Up
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="mb-4 text-xl font-semibold text-center">Choose Your Career Stage</h2>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
{careerSituations.map((situation) => (
|
||||||
|
<SituationCard
|
||||||
|
key={situation.id}
|
||||||
|
title={situation.title}
|
||||||
|
description={situation.description}
|
||||||
|
selected={selectedSituation?.id === situation.id}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSituation(situation);
|
||||||
|
setShowPrompt(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{showPrompt && <div className="mt-4 p-4 bg-yellow-200">Modal should be open!</div>}
|
||||||
|
<PromptModal
|
||||||
|
open={showPrompt}
|
||||||
|
title="Confirm Your Selection"
|
||||||
|
message={`Are you sure you want to select "${selectedSituation?.title}"? You can change this later in your profile settings.`}
|
||||||
|
confirmText="Confirm"
|
||||||
|
cancelText="Cancel"
|
||||||
|
onConfirm={handleSituationConfirm}
|
||||||
|
onCancel={() => setShowPrompt(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignUp;
|
export default SignUp;
|
27
src/components/ui/PromptModal.js
Normal file
27
src/components/ui/PromptModal.js
Normal file
@ -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 }) => (
|
||||||
|
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onCancel(); }}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className={cn("py-4 text-sm text-gray-700")}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
<DialogFooter className={cn("gap-2")}>
|
||||||
|
<Button variant="outline" onClick={onCancel}>
|
||||||
|
{cancelText || 'Cancel'}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onConfirm}>
|
||||||
|
{confirmText || 'Confirm'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PromptModal;
|
20
src/components/ui/SituationCard.js
Normal file
20
src/components/ui/SituationCard.js
Normal file
@ -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 }) => (
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer transition-shadow duration-200 hover:shadow-lg',
|
||||||
|
selected ? 'border-blue-600 ring-2 ring-blue-500' : 'border-gray-300'
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<h3 className="text-lg font-semibold">{title}</h3>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">{description}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SituationCard;
|
@ -1,18 +1,26 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { cn } from '../../utils/cn.js'; // <-- explicitly added missing import
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root;
|
const Dialog = DialogPrimitive.Root;
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
const DialogContent = React.forwardRef(({ className, ...props }, ref) => {
|
const DialogContent = React.forwardRef(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
|
<DialogPrimitive.Portal>
|
||||||
|
<DialogPrimitive.Overlay className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm" />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="fixed inset-0 bg-white shadow-lg p-4 max-w-lg mx-auto mt-20 rounded-md"
|
className={cn(
|
||||||
|
"fixed top-[50%] left-[50%] w-full max-w-md translate-x-[-50%] translate-y-[-50%] rounded-md bg-white p-6 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
</DialogPrimitive.Portal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const DialogHeader = ({ children }) => {
|
const DialogHeader = ({ children }) => {
|
||||||
return <div className="text-lg font-semibold border-b pb-2 mb-4">{children}</div>;
|
return <div className="text-lg font-semibold border-b pb-2 mb-4">{children}</div>;
|
||||||
};
|
};
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user