Skip to content

Commit 8b0b29f

Browse files
authored
Merge pull request #23 from BUMETCS673/james-dev2
Adding RegisterForm, RegisterForm.test, and index (app.tsx additions too)
2 parents cd4a158 + 75389a8 commit 8b0b29f

34 files changed

+5762
-102
lines changed

code/frontend/package-lock.json

Lines changed: 1363 additions & 73 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

code/frontend/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
},
1919
"devDependencies": {
2020
"@eslint/js": "^9.33.0",
21-
"@types/react": "^19.1.10",
22-
"@types/react-dom": "^19.1.7",
21+
"@types/react": "^19.1.13",
22+
"@types/react-dom": "^19.1.9",
2323
"@vitejs/plugin-react": "^5.0.0",
2424
"eslint": "^9.33.0",
2525
"eslint-plugin-react-hooks": "^5.2.0",
@@ -29,4 +29,4 @@
2929
"typescript-eslint": "^8.39.1",
3030
"vite": "^7.1.2"
3131
}
32-
}
32+
}

code/frontend/public/logo.png

435 KB
Loading

code/frontend/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { BrowserRouter, Routes, Route } from 'react-router-dom';
2+
import LoginForm from './components/loginAndRegistration/LoginForm';
3+
import './index.css';
24

3-
function App() {
5+
export default function App() {
46
return (
57
<BrowserRouter>
8+
<LoginForm />
69
<Routes>
710
<Route path="/myJobs" element={<div>My Jobs Page</div>} />
811
<Route path="/" element={<div>Main Page</div>} />
912
</Routes>
1013
</BrowserRouter>
1114
);
1215
}
13-
14-
export default App;
File renamed without changes.

code/frontend/src/components/asideAndToggler/themeToggler.tsx

Whitespace-only changes.

code/frontend/src/components/jobsList/JobsCard.tsx

Whitespace-only changes.

code/frontend/src/components/jobsList/JobsViewList.tsx

Whitespace-only changes.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React, { useState } from "react";
2+
import { Link } from "react-router-dom"; // Link to navigate to Register page
3+
import { isRequired, isEmail } from "./validation"; // keep path simple (no .ts extension)
4+
5+
/**
6+
* @typedef {Object} LoginValues
7+
* @property {string} email
8+
* @property {string} password
9+
*/
10+
type LoginValues = {
11+
email: string;
12+
password: string;
13+
};
14+
15+
type Props = {
16+
// Optional: parent can pass a submit handler
17+
onSubmit?: (values: LoginValues) => void;
18+
// Toggle submit button visibility (button creation is in a different story)
19+
showSubmitButton?: boolean;
20+
};
21+
22+
// Login form with email + password fields, Bootstrap styling, and basic validation
23+
const LoginForm: React.FC<Props> = ({ onSubmit, showSubmitButton = false }) => {
24+
// Controlled state for form fields
25+
const [values, setValues] = useState<LoginValues>({ email: "", password: "" });
26+
27+
// Track which fields the user has interacted with
28+
// Used to show errors only after user touches/leaves a field
29+
const [touched, setTouched] = useState<Record<keyof LoginValues, boolean>>({
30+
email: false,
31+
password: false,
32+
});
33+
34+
// Validation rules (errors show if not empty/invalid email)
35+
const errors = {
36+
email:
37+
!isRequired(values.email) ? "Email is required" :
38+
!isEmail(values.email) ? "Enter a valid email" : "",
39+
password: !isRequired(values.password) ? "Password is required" : "",
40+
};
41+
42+
const hasError = (field: keyof LoginValues) => touched[field] && !!errors[field];
43+
44+
// Update field values
45+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
46+
setValues({ ...values, [e.target.name]: e.target.value });
47+
};
48+
49+
// Mark field as "touched" on blur
50+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
51+
setTouched({ ...touched, [e.target.name]: true });
52+
};
53+
54+
// Prevent submission if validation fails
55+
const handleSubmit = (e: React.FormEvent) => {
56+
e.preventDefault();
57+
setTouched({ email: true, password: true });
58+
const valid = !errors.email && !errors.password;
59+
if (valid && onSubmit) onSubmit(values);
60+
};
61+
62+
return (
63+
// Center the card regardless of parent layout; prevent squish
64+
<div
65+
className="d-flex align-items-center justify-content-center"
66+
style={{ minHeight: "100vh", width: "100vw", padding: "1rem" }}
67+
>
68+
{/* Subtle card container around the form */}
69+
<div className="card shadow-sm" style={{ width: 380, maxWidth: "100%" }}>
70+
<div className="card-body">
71+
{/* App/logo image at top (served from /public) */}
72+
<img
73+
src="/logo.png"
74+
alt="JobTracker"
75+
className="img-fluid w-100 mb-3"
76+
style={{ maxHeight: 64, objectFit: "contain" }}
77+
/>
78+
79+
<h1 className="h4 text-center mb-4">Login</h1>
80+
81+
<form className="w-100" noValidate onSubmit={handleSubmit}>
82+
<div className="mb-3">
83+
<label htmlFor="loginEmail" className="form-label">Email</label>
84+
<input
85+
id="loginEmail"
86+
name="email"
87+
type="email"
88+
className={`form-control ${hasError("email") ? "is-invalid" : ""}`}
89+
value={values.email}
90+
onChange={handleChange}
91+
onBlur={handleBlur}
92+
placeholder="name@example.com"
93+
required
94+
/>
95+
{hasError("email") && <div className="invalid-feedback">{errors.email}</div>}
96+
</div>
97+
98+
<div className="mb-3">
99+
<label htmlFor="loginPassword" className="form-label">Password</label>
100+
<input
101+
id="loginPassword"
102+
name="password"
103+
type="password"
104+
className={`form-control ${hasError("password") ? "is-invalid" : ""}`}
105+
value={values.password}
106+
onChange={handleChange}
107+
onBlur={handleBlur}
108+
placeholder="••••••••"
109+
required
110+
/>
111+
{hasError("password") && (
112+
<div className="invalid-feedback">{errors.password}</div>
113+
)}
114+
</div>
115+
116+
{/* Black, full-width login button (rendered only if allowed by story flag) */}
117+
{showSubmitButton && (
118+
<button type="submit" className="btn btn-dark w-100">Login</button>
119+
)}
120+
</form>
121+
122+
{/* Small footer text with link to Register page */}
123+
<div className="text-center mt-3 mb-0 small">
124+
Don’t have an account?{' '}
125+
<Link to="/register">Register</Link>
126+
</div>
127+
</div>
128+
</div>
129+
</div>
130+
);
131+
};
132+
133+
export default LoginForm;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React, { useState } from "react";
2+
import { Link } from "react-router-dom"; // Link to navigate to Login page
3+
import { isRequired, isEmail } from "./validation"; // keep path simple (no .ts extension)
4+
5+
type RegisterValues = { name: string; email: string; password: string };
6+
type Props = {
7+
onSubmit?: (values: RegisterValues) => void;
8+
showSubmitButton?: boolean;
9+
};
10+
11+
// Register form with name, email, and password inputs
12+
// Mirrors LoginForm but adds "name" field
13+
const RegisterForm: React.FC<Props> = ({ onSubmit, showSubmitButton = false }) => {
14+
const [values, setValues] = useState<RegisterValues>({ name: "", email: "", password: "" });
15+
16+
// Track touched fields for showing validation feedback
17+
const [touched, setTouched] = useState<Record<keyof RegisterValues, boolean>>({
18+
name: false,
19+
email: false,
20+
password: false,
21+
});
22+
23+
// Validation: all fields required, email must match format
24+
const errors = {
25+
name: !isRequired(values.name) ? "Name is required" : "",
26+
email:
27+
!isRequired(values.email) ? "Email is required" :
28+
!isEmail(values.email) ? "Enter a valid email" : "",
29+
password: !isRequired(values.password) ? "Password is required" : "",
30+
};
31+
32+
const hasError = (field: keyof RegisterValues) => touched[field] && !!errors[field];
33+
34+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35+
setValues({ ...values, [e.target.name]: e.target.value });
36+
};
37+
38+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
39+
setTouched({ ...touched, [e.target.name]: true as any });
40+
};
41+
42+
const handleSubmit = (e: React.FormEvent) => {
43+
e.preventDefault();
44+
setTouched({ name: true, email: true, password: true });
45+
const valid = !errors.name && !errors.email && !errors.password;
46+
if (valid && onSubmit) onSubmit(values);
47+
};
48+
49+
return (
50+
// Center the card regardless of parent layout; prevent squish
51+
<div
52+
className="d-flex align-items-center justify-content-center"
53+
style={{ minHeight: "100vh", width: "100vw", padding: "1rem" }}
54+
>
55+
{/* Subtle card container around the form */}
56+
<div className="card shadow-sm" style={{ width: 380, maxWidth: "100%" }}>
57+
<div className="card-body">
58+
{/* App/logo image at top (served from /public) */}
59+
<img
60+
src="/logo.png"
61+
alt="JobTracker"
62+
className="img-fluid w-100 mb-3"
63+
style={{ maxHeight: 64, objectFit: "contain" }}
64+
/>
65+
66+
<h2 className="h4 text-center mb-4">Register</h2>
67+
68+
<form className="w-100" noValidate onSubmit={handleSubmit}>
69+
<div className="mb-3">
70+
<label htmlFor="regName" className="form-label">Name</label>
71+
<input
72+
id="regName"
73+
name="name"
74+
type="text"
75+
className={`form-control ${hasError("name") ? "is-invalid" : ""}`}
76+
value={values.name}
77+
onChange={handleChange}
78+
onBlur={handleBlur}
79+
placeholder="Your full name"
80+
required
81+
/>
82+
{hasError("name") && <div className="invalid-feedback">{errors.name}</div>}
83+
</div>
84+
85+
<div className="mb-3">
86+
<label htmlFor="regEmail" className="form-label">Email</label>
87+
<input
88+
id="regEmail"
89+
name="email"
90+
type="email"
91+
className={`form-control ${hasError("email") ? "is-invalid" : ""}`}
92+
value={values.email}
93+
onChange={handleChange}
94+
onBlur={handleBlur}
95+
placeholder="name@example.com"
96+
required
97+
/>
98+
{hasError("email") && <div className="invalid-feedback">{errors.email}</div>}
99+
</div>
100+
101+
<div className="mb-3">
102+
<label htmlFor="regPassword" className="form-label">Password</label>
103+
<input
104+
id="regPassword"
105+
name="password"
106+
type="password"
107+
className={`form-control ${hasError("password") ? "is-invalid" : ""}`}
108+
value={values.password}
109+
onChange={handleChange}
110+
onBlur={handleBlur}
111+
placeholder="Create a password"
112+
required
113+
/>
114+
{hasError("password") && (
115+
<div className="invalid-feedback">{errors.password}</div>
116+
)}
117+
</div>
118+
119+
{/* Black, full-width register button (rendered only if allowed by story flag) */}
120+
{showSubmitButton && (
121+
<button type="submit" className="btn btn-dark w-100">Register</button>
122+
)}
123+
</form>
124+
125+
{/* Small footer text with link to Login page */}
126+
<p className="text-center mt-3 mb-0 small">
127+
Already have an account?{" "}
128+
<Link to="/login">Login</Link>
129+
</p>
130+
</div>
131+
</div>
132+
</div>
133+
);
134+
};
135+
136+
export default RegisterForm;

0 commit comments

Comments
 (0)