Removed UserID since ChatGPT can't code two similar variables

This commit is contained in:
Josh 2025-05-22 14:26:19 +00:00
parent 491fadbefd
commit d4919c31a5
6 changed files with 445 additions and 241 deletions

View File

@ -5,17 +5,13 @@ import helmet from 'helmet';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import fs from 'fs';
import path from 'path';
import bodyParser from 'body-parser';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken'; // For token-based authentication
import mysql from 'mysql2';
import sqlite3 from 'sqlite3';
import sqlite3 from 'sqlite3';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -33,7 +29,6 @@ const DB_USER = process.env.DB_USER || 'sqluser';
const DB_PASSWORD = process.env.DB_PASSWORD || '';
const DB_NAME = process.env.DB_NAME || 'user_profile_db';
// Create a MySQL pool for user_profile data
const pool = mysql.createPool({
host: DB_HOST,
@ -41,7 +36,7 @@ const pool = mysql.createPool({
user: DB_USER,
password: DB_PASSWORD,
database: DB_NAME,
connectionLimit: 10, // optional
connectionLimit: 10,
});
// Test a quick query (optional)
@ -55,7 +50,13 @@ pool.query('SELECT 1', (err) => {
const app = express();
const PORT = 5000;
const allowedOrigins = ['http://localhost:3000', 'http://34.16.120.118:3000', 'https://dev1.aptivaai.com'];
// Allowed origins for CORS
const allowedOrigins = [
'http://localhost:3000',
'http://34.16.120.118:3000',
'https://dev1.aptivaai.com',
];
app.disable('x-powered-by');
app.use(bodyParser.json());
@ -79,7 +80,13 @@ app.use(
}
},
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Authorization', 'Content-Type', 'Accept', 'Origin', 'X-Requested-With'],
allowedHeaders: [
'Authorization',
'Content-Type',
'Accept',
'Origin',
'X-Requested-With',
],
credentials: true,
})
);
@ -88,15 +95,21 @@ app.use(
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With');
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Content-Type, Accept, Origin, X-Requested-With'
);
res.status(200).end();
});
// Add HTTP headers for security and caching
// Add HTTP headers for security
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
res.setHeader('Content-Security-Policy', "default-src 'self';");
res.removeHeader('X-Powered-By');
next();
@ -108,11 +121,16 @@ app.use((req, res, next) => {
next();
});
// =============== USER REGISTRATION (MySQL) ===============
// /api/register
/* ------------------------------------------------------------------
USER REGISTRATION (MySQL)
------------------------------------------------------------------ */
/**
* POST /api/register
* Body:
* username, password, firstname, lastname, email, zipcode, state, area, career_situation
*/
app.post('/api/register', async (req, res) => {
const {
userId, // random ID from the front end
username,
password,
firstname,
@ -121,48 +139,70 @@ app.post('/api/register', async (req, res) => {
zipcode,
state,
area,
career_situation
career_situation,
} = req.body;
if (
!username ||
!password ||
!firstname ||
!lastname ||
!email ||
!zipcode ||
!state ||
!area
) {
return res.status(400).json({ error: 'Missing required fields.' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
// Insert row in user_profile, storing both user_id (random) and letting id auto-increment
// 1) Insert into user_profile
const profileQuery = `
INSERT INTO user_profile
(user_id, firstname, lastname, email, zipcode, state, area, career_situation)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
(firstname, lastname, email, zipcode, state, area, career_situation)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
pool.query(
profileQuery,
[userId, firstname, lastname, email, zipcode, state, area, career_situation],
[firstname, lastname, email, zipcode, state, area, career_situation],
(errProfile, resultProfile) => {
if (errProfile) {
console.error('Error inserting user_profile:', errProfile.message);
return res.status(500).json({ error: 'Failed to create user profile' });
return res
.status(500)
.json({ error: 'Failed to create user profile' });
}
const newProfileAutoId = resultProfile.insertId; // auto-increment PK
const newProfileId = resultProfile.insertId; // auto-increment PK
// Insert into user_auth, referencing the auto-increment PK
// 2) Insert into user_auth, referencing user_profile.id
const authQuery = `
INSERT INTO user_auth (user_id, username, hashed_password)
VALUES (?, ?, ?)
`;
pool.query(authQuery, [newProfileAutoId, username, hashedPassword], (errAuth) => {
if (errAuth) {
console.error('Error inserting user_auth:', errAuth.message);
if (errAuth.code === 'ER_DUP_ENTRY') {
return res.status(400).json({ error: 'Username already exists' });
pool.query(
authQuery,
[newProfileId, username, hashedPassword],
(errAuth) => {
if (errAuth) {
console.error('Error inserting user_auth:', errAuth.message);
if (errAuth.code === 'ER_DUP_ENTRY') {
return res
.status(400)
.json({ error: 'Username already exists' });
}
return res.status(500).json({ error: 'Failed to register user' });
}
return res.status(500).json({ error: 'Failed to register user' });
return res.status(201).json({
message: 'User registered successfully',
profileId: newProfileId, // the user_profile.id
});
}
return res.status(201).json({
message: 'User registered successfully',
dbId: newProfileAutoId, // the auto-increment PK
customId: userId, // the random ID
});
});
);
}
);
} catch (err) {
@ -171,36 +211,47 @@ app.post('/api/register', async (req, res) => {
}
});
// =============== SIGN-IN (MySQL) ===============
app.post('/api/signin', async (req, res) => {
/* ------------------------------------------------------------------
SIGN-IN (MySQL)
------------------------------------------------------------------ */
/**
* POST /api/signin
* Body: { username, password }
* Returns JWT signed with user_profile.id
*/
app.post('/api/signin', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Both username and password are required' });
return res
.status(400)
.json({ error: 'Both username and password are required' });
}
const query = `
SELECT
user_auth.user_id,
user_auth.hashed_password,
user_profile.id,
SELECT
user_auth.user_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.email,
user_profile.firstname,
user_profile.lastname,
user_profile.career_priorities,
user_profile.career_list
FROM user_auth
LEFT JOIN user_profile ON user_auth.user_id = user_profile.id
LEFT JOIN user_profile ON user_auth.user_id = user_profile.id
WHERE user_auth.username = ?
`;
pool.query(query, [username], async (err, results) => {
if (err) {
console.error('Error querying user_auth:', err.message);
return res.status(500).json({ error: 'Failed to query user authentication data' });
return res
.status(500)
.json({ error: 'Failed to query user authentication data' });
}
if (!results || results.length === 0) {
@ -208,22 +259,29 @@ app.post('/api/signin', async (req, res) => {
}
const row = results[0];
// Compare password
const isMatch = await bcrypt.compare(password, row.hashed_password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid username or password' });
}
const token = jwt.sign({ userId: row.id }, SECRET_KEY, { expiresIn: '2h' });
// The user_profile id is stored in user_auth.user_id
const token = jwt.sign({ id: row.user_id }, SECRET_KEY, {
expiresIn: '2h',
});
// Return the user info + token
res.status(200).json({
message: 'Login successful',
token,
userId: row.id,
userId: row.user_id, // The user_profile.id
user: {
user_id: row.user_id,
firstname: row.firstname,
lastname: row.lastname,
email: row.email,
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,
@ -234,7 +292,9 @@ app.post('/api/signin', async (req, res) => {
});
});
// =============== CHECK USERNAME (MySQL) ===============
/* ------------------------------------------------------------------
CHECK USERNAME (MySQL)
------------------------------------------------------------------ */
app.get('/api/check-username/:username', (req, res) => {
const { username } = req.params;
const query = `SELECT username FROM user_auth WHERE username = ?`;
@ -251,17 +311,27 @@ app.get('/api/check-username/:username', (req, res) => {
});
});
// =============== UPSERT USER PROFILE (MySQL) ===============
/* ------------------------------------------------------------------
UPSERT USER PROFILE (MySQL)
------------------------------------------------------------------ */
/**
* POST /api/user-profile
* Headers: { Authorization: Bearer <token> }
* Body: { firstName, lastName, email, zipCode, state, area, ... }
*
* If user_profile row exists (id = token.id), update
* else insert
*/
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;
let profileId;
try {
const decoded = jwt.verify(token, SECRET_KEY);
userId = decoded.userId;
profileId = decoded.id; // user_profile.id from sign-in
} catch (error) {
console.error('JWT verification failed:', error);
return res.status(401).json({ error: 'Invalid or expired token' });
@ -280,110 +350,134 @@ app.post('/api/user-profile', (req, res) => {
career_list,
} = req.body;
// Check existing profile
pool.query(`SELECT * FROM user_profile WHERE user_id = ?`, [userId], (err, results) => {
if (err) {
console.error('Error checking profile:', err.message);
return res.status(500).json({ error: 'Database error' });
// Check if profile row exists
pool.query(
`SELECT * FROM user_profile WHERE id = ?`,
[profileId],
(err, results) => {
if (err) {
console.error('Error checking profile:', err.message);
return res.status(500).json({ error: 'Database error' });
}
const existingRow = results && results.length > 0 ? results[0] : null;
// If creating a brand-new profile, ensure required fields
if (
!existingRow &&
(!firstName || !lastName || !email || !zipCode || !state || !area)
) {
return res
.status(400)
.json({ error: 'All fields are required for initial profile creation.' });
}
const finalAnswers =
interest_inventory_answers !== undefined
? interest_inventory_answers
: existingRow?.interest_inventory_answers || null;
const finalCareerPriorities =
career_priorities !== undefined
? career_priorities
: existingRow?.career_priorities || null;
const finalCareerList =
career_list !== undefined
? career_list
: existingRow?.career_list || null;
if (existingRow) {
// Update the existing user_profile
const updateQuery = `
UPDATE user_profile
SET firstname = ?, lastname = ?, email = ?, zipcode = ?, state = ?, area = ?,
career_situation = ?, interest_inventory_answers = ?, career_priorities = ?,
career_list = ?
WHERE id = ?
`;
const params = [
firstName || existingRow.firstname,
lastName || existingRow.lastname,
email || existingRow.email,
zipCode || existingRow.zipcode,
state || existingRow.state,
area || existingRow.area,
careerSituation || existingRow.career_situation,
finalAnswers,
finalCareerPriorities,
finalCareerList,
profileId,
];
pool.query(updateQuery, params, (err2) => {
if (err2) {
console.error('Update error:', err2.message);
return res
.status(500)
.json({ error: 'Failed to update user profile' });
}
return res
.status(200)
.json({ message: 'User profile updated successfully' });
});
} else {
// Insert a new profile (the user_auth record exists, but the user_profile row is missing)
const insertQuery = `
INSERT INTO user_profile
(id, firstname, lastname, email, zipcode, state, area, career_situation,
interest_inventory_answers, career_priorities, career_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
profileId, // Force the row's primary key to match the existing user ID
firstName,
lastName,
email,
zipCode,
state,
area,
careerSituation || null,
finalAnswers,
finalCareerPriorities,
finalCareerList,
];
pool.query(insertQuery, params, (err3) => {
if (err3) {
console.error('Insert error:', err3.message);
return res
.status(500)
.json({ error: 'Failed to create user profile' });
}
return res
.status(201)
.json({ message: 'User profile created successfully', id: profileId });
});
}
}
const existingRow = results && results.length > 0 ? results[0] : null;
if (!existingRow && (!firstName || !lastName || !email || !zipCode || !state || !area)) {
return res.status(400).json({ error: 'All fields are required for initial profile creation.' });
}
const finalAnswers =
interest_inventory_answers !== undefined
? interest_inventory_answers
: existingRow?.interest_inventory_answers || null;
const finalCareerPriorities =
career_priorities !== undefined
? career_priorities
: existingRow?.career_priorities || null;
const finalCareerList =
career_list !== undefined
? career_list
: existingRow?.career_list || null;
if (existingRow) {
// Update
const updateQuery = `
UPDATE user_profile
SET firstname = ?, lastname = ?, email = ?, zipcode = ?, state = ?, area = ?, career_situation = ?,
interest_inventory_answers = ?, career_priorities = ?, career_list = ?
WHERE user_id = ?
`;
const params = [
firstName || existingRow.firstname,
lastName || existingRow.lastname,
email || existingRow.email,
zipCode || existingRow.zipcode,
state || existingRow.state,
area || existingRow.area,
careerSituation || existingRow.career_situation,
finalAnswers,
finalCareerPriorities,
finalCareerList,
userId
];
pool.query(updateQuery, params, (err2, result2) => {
if (err2) {
console.error('Update error:', err2.message);
return res.status(500).json({ error: 'Failed to update user profile' });
}
res.status(200).json({ message: 'User profile updated successfully' });
});
} else {
// Insert new profile
const insertQuery = `
INSERT INTO user_profile
(user_id, firstname, lastname, email, zipcode, state, area, career_situation,
interest_inventory_answers, career_priorities, career_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
userId,
firstName,
lastName,
email,
zipCode,
state,
area,
careerSituation || null,
finalAnswers,
finalCareerPriorities,
finalCareerList,
];
pool.query(insertQuery, params, (err3, result3) => {
if (err3) {
console.error('Insert error:', err3.message);
return res.status(500).json({ error: 'Failed to create user profile' });
}
res.status(201).json({ message: 'User profile created successfully', id: result3.insertId });
});
}
});
);
});
// =============== FETCH USER PROFILE (MySQL) ===============
/* ------------------------------------------------------------------
FETCH USER PROFILE (MySQL)
------------------------------------------------------------------ */
app.get('/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;
let profileId;
try {
const decoded = jwt.verify(token, SECRET_KEY);
userId = decoded.userId;
profileId = decoded.id; // user_profile.id
} catch (error) {
console.error('Error verifying token:', error.message);
return res.status(401).json({ error: 'Invalid or expired token' });
}
const query = 'SELECT * FROM user_profile WHERE user_id = ?';
pool.query(query, [userId], (err, results) => {
const query = 'SELECT * FROM user_profile WHERE id = ?';
pool.query(query, [profileId], (err, results) => {
if (err) {
console.error('Error fetching user profile:', err.message);
return res.status(500).json({ error: 'Internal server error' });
@ -395,20 +489,28 @@ app.get('/api/user-profile', (req, res) => {
});
});
// =============== SALARY_INFO REMAINS IN SQLITE ===============
/* ------------------------------------------------------------------
SALARY_INFO REMAINS IN SQLITE
------------------------------------------------------------------ */
app.get('/api/areas', (req, res) => {
const { state } = req.query;
if (!state) {
return res.status(400).json({ error: 'State parameter is required' });
}
const salaryDbPath = path.resolve('/home/jcoakley/aptiva-dev1-app/salary_info.db');
const salaryDb = new sqlite3.Database(salaryDbPath, sqlite3.OPEN_READONLY, (err) => {
if (err) {
console.error('Error connecting to database:', err.message);
return res.status(500).json({ error: 'Failed to connect to database' });
const salaryDbPath = path.resolve(
'/home/jcoakley/aptiva-dev1-app/salary_info.db'
);
const salaryDb = new sqlite3.Database(
salaryDbPath,
sqlite3.OPEN_READONLY,
(err) => {
if (err) {
console.error('Error connecting to database:', err.message);
return res.status(500).json({ error: 'Failed to connect to database' });
}
}
});
);
const query = `SELECT DISTINCT AREA_TITLE FROM salary_data WHERE PRIM_STATE = ?`;
salaryDb.all(query, [state], (err, rows) => {
@ -428,30 +530,29 @@ app.get('/api/areas', (req, res) => {
});
/* ------------------------------------------------------------------
PREMIUM UPGRADE ENDPOINT
------------------------------------------------------------------ */
PREMIUM UPGRADE ENDPOINT
------------------------------------------------------------------ */
app.post('/api/activate-premium', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authorization token is required' });
}
let userId; // Will hold the auto-increment "id" from user_profile
let profileId;
try {
const decoded = jwt.verify(token, SECRET_KEY);
userId = decoded.userId; // => user_profile.id (auto-inc PK)
profileId = decoded.id;
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
// Use MySQL pool.query instead of db.run (which is for SQLite)
const query = `
UPDATE user_profile
SET is_premium = 1,
is_pro_premium = 1
WHERE id = ?
`;
pool.query(query, [userId], (err, results) => {
pool.query(query, [profileId], (err) => {
if (err) {
console.error('Error updating premium status:', err.message);
return res.status(500).json({ error: 'Failed to activate premium' });
@ -460,7 +561,9 @@ app.post('/api/activate-premium', (req, res) => {
});
});
// =============== START SERVER ===============
/* ------------------------------------------------------------------
START SERVER
------------------------------------------------------------------ */
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

View File

@ -39,6 +39,13 @@ body, #root {
color: white;
}
.dialog-overlay,
[data-state="open"].dialog-overlay {
background-color: transparent !important;
pointer-events: none !important;
z-index: 0 !important;
}
/* Animation for the logo */
.App-logo {
height: 40vmin;

View File

@ -249,9 +249,14 @@ function CareerExplorer() {
if (profileData.career_list) {
setCareerList(JSON.parse(profileData.career_list));
}
} else {
setShowModal(true);
}
if (!profileData.career_priorities) {
setShowModal(true);
}
} else {
setShowModal(true);
}
} catch (err) {
console.error('Error fetching user profile:', err);
setShowModal(true);

View File

@ -406,7 +406,7 @@ function EducationalProgramsPage() {
<span className="relative inline-block">
<span>Why no courses?</span>
<span
className="ml-1 inline-flex h-2.5 w-2.5 items-center justify-center
className="top-0 left-0 -translate-y-3/4 translate-x-1/8 ml-.5 inline-flex h-2.5 w-2.5 items-center justify-center
rounded-full bg-blue-500 text-[10px] font-italics text-white cursor-help"
title="Abilities are more innate in nature, and difficult to offer courses for them."
>

View File

@ -48,8 +48,7 @@ function SignUp() {
const [areas, setAreas] = useState([]);
const [error, setError] = useState('');
const [loadingAreas, setLoadingAreas] = useState(false);
const [frontendUserId] = useState(() => Math.floor(Math.random() * 1000000000));
// new states
const [showCareerSituations, setShowCareerSituations] = useState(false);
const [selectedSituation, setSelectedSituation] = useState(null);
@ -172,7 +171,6 @@ function SignUp() {
try {
console.log("Payload sent to backend:", {
userId: frontendUserId,
username, password, firstname, lastname, email, zipcode, state, area,
career_situation: selectedSituation.id
});
@ -181,7 +179,6 @@ function SignUp() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: frontendUserId,
username,
password,
firstname,
@ -212,83 +209,154 @@ function SignUp() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
{!showCareerSituations ? (
<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>
{error && (
<div className="mb-4 p-2 text-sm text-red-600 bg-red-100 rounded">
{error}
</div>
)}
<form className="space-y-3" onSubmit={handleNextClick}>
<input className="w-full px-3 py-2 border 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 rounded-md" placeholder="Retype Password" type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(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 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="Email" value={email} onChange={(e) => setEmail(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 rounded-md" value={state} onChange={(e) => setState(e.target.value)}><option value="">Select State</option>
{states.map((s) => (
<option key={s.code} value={s.code}>{s.name}</option>
))}
</select>
<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>
{areas.map((a, i) => (
<option key={i} value={a}>{a}</option>
))}
</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">
Next
</Button>
</form>
</>
) : (
<>
<h2 className="mb-4 text-xl font-semibold text-center">Where are you in your career journey right now?</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)}
/>
</>
<h2 className="mb-4 text-2xl font-semibold text-center">Sign Up</h2>
{error && (
<div className="mb-4 p-2 text-sm text-red-600 bg-red-100 rounded">
{error}
</div>
)}
<form className="space-y-3" onSubmit={handleNextClick}>
<input
className="w-full px-3 py-2 border 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 rounded-md"
placeholder="Retype Password"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(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 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="Email"
value={email}
onChange={(e) => setEmail(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 rounded-md"
value={state}
onChange={(e) => setState(e.target.value)}
>
<option value="">Select State</option>
{states.map((s) => (
<option key={s.code} value={s.code}>
{s.name}
</option>
))}
</select>
<div className="relative w-full">
<label>
<span
title="Selecting an Area will allow us to provide regional salary data for your career choices."
className="absolute top-2 right-2 translate-x-7 -translate-y-3
inline-flex h-4 w-4 items-center justify-center
rounded-full bg-blue-500 text-xs font-bold text-white cursor-help"
>
i
</span>
</label>
<select
id="area"
className="w-full px-3 py-2 border rounded-md"
value={area}
onChange={(e) => setArea(e.target.value)}
disabled={loadingAreas}
>
<option value="">Select Area</option>
{areas.map((a, i) => (
<option key={i} value={a}>
{a}
</option>
))}
</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">
Next
</Button>
</form>
</div>
</div>
);
) : (
<div className="w-full max-w-screen-lg mx-auto bg-white p-6 rounded-lg shadow-lg">
<h2 className="mb-4 text-xl font-semibold text-center">
Where are you in your career journey right now?
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 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>
</div>
)}
{showPrompt && (
<PromptModal
open={true}
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>
);
}
export default SignUp;

View File

@ -0,0 +1,21 @@
// FadingPromptModal.jsx
import React from 'react';
import PromptModal from './PromptModal.js';
export default function FadingPromptModal({ open, ...props }) {
return (
<div
className={`
fixed inset-0 z-50
flex items-center justify-center
transition-opacity duration-300
${open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
`}
>
<div className="absolute inset-0 bg-gray-800 bg-opacity-50 z-10" />
<div className="relative z-20">
<PromptModal open={open} {...props} />
</div>
</div>
);
}