Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend-dummy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,15 @@ This backend does not use a real database. However, there is a file named users.
- **200 OK:** Login successful. The user's session cookie is set.
- **400 Bad Request:** The request body is missing required fields (email or password).
- **401 Unauthorized:** The provided email or password does not match any existing user.

- [POST] users/signUp

**Description**

Endpoint to create a new user.

**Responses**

- **200 OK:** The user has been created successfully.
- **400 Bad Request:** The request body is missing required fields.
- **412 Precondition Failed:** A user with the provided email already exists.
36 changes: 34 additions & 2 deletions backend-dummy/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ router.post("/login", function (req, res, next) {
if (!email || !password) {
return res
.status(400)
.json({ status: "error", message: "Invalid form submission" });
.json({ status: "error", message: "Invalid form submission", code: 400 });
}
const user = users.find(
(user) => user.email === email && user.password === password,
);
if (!user) {
return res
.status(401)
.json({ status: "error", message: "Invalid credentials" });
.json({ status: "error", message: "Invalid credentials", code: 401 });
}
const id = randomUUID();
user["id"] = id;
Expand All @@ -37,4 +37,36 @@ router.post("/login", function (req, res, next) {
return res.json({ status: "success", message: "Login success" });
});

router.post("/signUp", function (req, res, next) {
const { name, email, password } = req.body;
if (!email || !password || !name) {
return res
.status(400)
.json({ status: "error", message: "Invalid form submission", code: 400 });
}
const user = users.find((user) => user.email === email);
if (user) {
return res.status(412).json({
status: "error",
message: "User with this email already exists",
code: 412,
});
}
const id = randomUUID();
const newUser = {
name,
email,
password,
id: randomUUID(),
};
res.cookie("cookie-id", id, {
maxAge: 900000,
httpOnly: true,
secure: true,
});
users.push(newUser);
fs.writeFileSync("users.json", `{"users":${JSON.stringify(users)}}`);
return res.json({ status: "success", message: "User created successfully" });
});

module.exports = router;
3 changes: 3 additions & 0 deletions src/common/text-field/text-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface TextFieldProps {
status?: TextFieldStatus;
type?: HTMLInputTypeAttribute;
value?: string;
onBlur?: () => void;
}

export const TextField = ({
Expand All @@ -42,6 +43,7 @@ export const TextField = ({
status = TextFieldStatus.default,
type = "text",
value,
onBlur,
}: TextFieldProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const focusOnInput = () => {
Expand Down Expand Up @@ -91,6 +93,7 @@ export const TextField = ({
ref={inputRef}
type={type}
value={value}
onBlur={onBlur}
/>
</div>
{helperText && (
Expand Down
1 change: 1 addition & 0 deletions src/networking/api-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const API_ROUTES = {
EXAMPLE: "/example",
LOGIN: "users/login",
SIGN_UP: "users/signUp",
};

export { API_ROUTES };
14 changes: 11 additions & 3 deletions src/networking/controllers/users.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { ApiService } from "networking/api-service";
import { API_ROUTES } from "networking/api-routes";
import { serializeSignUp } from "networking/serializers/users";
import { serializeLogin, serializeSignUp } from "networking/serializers/users";

const login = async (email: string, password: string) => {
const serializeCredentials = serializeSignUp(email, password);
const serializeCredentials = serializeLogin(email, password);
const response = await ApiService.post(API_ROUTES.LOGIN, {
body: JSON.stringify(serializeCredentials),
});
return response;
};
export { login };

const signUp = async (email: string, password: string, name: string) => {
const serializeCredentials = serializeSignUp(email, password, name);
const response = await ApiService.post(API_ROUTES.SIGN_UP, {
body: JSON.stringify(serializeCredentials),
});
return response;
};
export { login, signUp };
10 changes: 10 additions & 0 deletions src/networking/serializers/users.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
export const serializeSignUp = (
email: string,
password: string,
name: string,
): signUpCredentials => ({
email,
password,
name,
});

export const serializeLogin = (
email: string,
password: string,
): loginCredentials => ({
email,
password,
});
6 changes: 5 additions & 1 deletion src/networking/types/user.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
interface signUpCredentials {
interface loginCredentials {
email: string;
password: string;
}

interface signUpCredentials extends loginCredentials {
name: string;
}
3 changes: 3 additions & 0 deletions src/pages/sign-up/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { SignUp } from "./sign-up";

export { SignUp };
32 changes: 32 additions & 0 deletions src/pages/sign-up/sign-up.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@use "../../assets/stylesheets/text-styles.scss";
@import "../../assets/stylesheets/colors";

.container {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
padding: 0 30px;
}

.form {
max-width: 400px;
width: 100%;
padding: 30px;
border: 1px solid $text-neutral-20;
border-radius: 5px;
}

.field {
margin-bottom: 20px;
}

.submitButton {
width: 150px;
margin: 0 auto;
}

.error {
color: $primary-color-40;
}
113 changes: 113 additions & 0 deletions src/pages/sign-up/sign-up.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { TextField } from "common/text-field";
import styles from "./sign-up.module.scss";
import { useState } from "react";
import { Button } from "common/button";
import { signUp } from "networking/controllers/users";
import { useNavigate } from "react-router-dom";
import { ErrorStatus, type ApiError } from "networking/api-error";

const SignUp = () => {
const navigate = useNavigate();
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [repeatPassword, setRepeatPassword] = useState<string>("");
const [name, setName] = useState<string>("");
const [error, setError] = useState<boolean>(false);
const [userAlreadyExists, setUserAlreadyExists] = useState<boolean>(false);
const [passwordError, setPasswordError] = useState<boolean>(false);
const formValid =
!!name && !!email && !!password && password === repeatPassword;

const handleSignUp = async () => {
try {
await signUp(email, password, name);
navigate("/");
} catch (e) {
const err = e as ApiError;
if (err.code === ErrorStatus.PreconditionFailed) {
setUserAlreadyExists(true);
} else {
setError(true);
}
}
};
const doSignUp = () => {
handleSignUp().catch(() => {
setError(true);
});
};
return (
<div className={styles.container}>
<form className={styles.form}>
<TextField
className={styles.field}
label="Name"
name="name"
onChange={(e) => {
setName(e.target.value);
}}
/>
<TextField
className={styles.field}
label="Email"
name="email"
onChange={(e) => {
setEmail(e.target.value);
}}
/>
<TextField
className={styles.field}
label="Password"
name="password"
type="password"
onChange={(e) => {
setPassword(e.target.value);
}}
onBlur={() => {
if (repeatPassword && password !== repeatPassword) {
setPasswordError(true);
} else {
setPasswordError(false);
}
}}
/>
<TextField
className={styles.field}
label="Repeat Password"
name="Repeat password"
type="password"
onChange={(e) => {
setRepeatPassword(e.target.value);
}}
onBlur={() => {
if (password !== repeatPassword) {
setPasswordError(true);
} else {
setPasswordError(false);
}
}}
/>
{error && (
<p className={styles.error}>
Something went wrong. Please try again.
</p>
)}
{passwordError && (
<p className={styles.error}>Passwords do not match.</p>
)}
{userAlreadyExists && (
<p className={styles.error}>A user with that email already exists.</p>
)}
<Button
className={styles.submitButton}
disabled={!formValid}
onClick={doSignUp}
>
Create
</Button>
</form>
</div>
);
};

export { SignUp };
2 changes: 2 additions & 0 deletions src/routes/route-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Login } from "pages/login/login";
import { Home } from "pages/home";
import { About } from "pages/about";
import { NotFound } from "pages/not-found";
import { SignUp } from "pages/sign-up/sign-up";
import { RouteName } from "./routes";

// NOTE: this object is needed to avoid circular dependencies.
Expand All @@ -12,6 +13,7 @@ const RouteComponent = {
[RouteName.About]: About,
[RouteName.Login]: Login,
[RouteName.NotFound]: NotFound,
[RouteName.SignUp]: SignUp,
};

export { RouteComponent };
6 changes: 6 additions & 0 deletions src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum RouteName {
About = "about",
Login = "login",
NotFound = "notFound",
SignUp = "signUp",
}

export interface Route {
Expand Down Expand Up @@ -61,6 +62,11 @@ const ROUTES = [
path: "/login",
exact: true,
},
{
name: RouteName.SignUp,
path: "/signUp",
exact: true,
},
{
name: RouteName.NotFound,
path: "*",
Expand Down