Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ venv
.DS_Store
node_modules
team_logos
.venv/
backend/.venv/
2 changes: 1 addition & 1 deletion frontend/apply/src/app/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function Button({
ref={button}
disabled={disabled}
>
{buttonWidth && (
{buttonWidth !== 0 && (
<svg
className="loading-indicator"
viewBox={`0 0 ${buttonWidth + borderWidth * 2} ${buttonHeight + borderWidth * 2}`}
Expand Down
25 changes: 25 additions & 0 deletions frontend/apply/src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,30 @@
"submitApplication": "Submit Application",
"applicationSubmittedSuccess": "Your application has been submitted.",
"submitApplicationConfirmation": "Are you sure you want to submit this application? Once submitted, you will not be able to edit it anymore."
},
"registerPage": {
"registerTitle": "Register",
"username": "Username",
"usernamePlaceholder": "Choose a username",
"usernameRequired": "Username is required",
"email": "Email",
"emailPlaceholder": "your.email@example.com",
"emailRequired": "Email is required",
"password": "Password",
"passwordPlaceholder": "Enter a password",
"passwordRequired": "Password is required",
"passwordConfirmation": "Confirm password",
"passwordConfirmationPlaceholder": "Confirm your password",
"passwordConfirmationRequired": "Password confirmation is required",
"personalIdentityNumber": "Personal identity number",
"PersonNumberPlaceholder": "YYYYMMDD-XXXX",
"PersonNumberRequired": "Personal identity number is required",
"section": "Section",
"sectionPlaceholder": "Select your section",
"sectionRequired": "Section is required",
"phoneNumber": "Phone number",
"phoneNumberPlaceholder": "+46 12 345 67 89",
"phoneNumberRequired": "Phone number is required",
"registerAccount": "Create account"
}
}
25 changes: 25 additions & 0 deletions frontend/apply/src/app/i18n/locales/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,30 @@
"submitApplication": "Skicka ansökan",
"applicationSubmittedSuccess": "Din ansökan har skickats in.",
"submitApplicationConfirmation": "Är du säker på att du vill skicka in denna ansökan? När den väl är inskickad kommer du inte kunna redigera den längre."
},
"registerPage": {
"registerTitle": "Registrera",
"username": "Användarnamn",
"usernamePlaceholder": "Välj ett användarnamn",
"usernameRequired": "Användarnamn krävs",
"email": "E-post",
"emailPlaceholder": "din.epost@exempel.se",
"emailRequired": "E-post krävs",
"password": "Lösenord",
"passwordPlaceholder": "Ange ett lösenord",
"passwordRequired": "Lösenord krävs",
"passwordConfirmation": "Bekräfta lösenord",
"passwordConfirmationPlaceholder": "Bekräfta ditt lösenord",
"passwordConfirmationRequired": "Bekräftelse av lösenord krävs",
"personalIdentityNumber": "Personnummer",
"PersonNumberPlaceholder": "ÅÅÅÅMMDD-XXXX",
"PersonNumberRequired": "Personnummer krävs",
"section": "Sektion",
"sectionPlaceholder": "Välj din sektion",
"sectionRequired": "Sektion krävs",
"phoneNumber": "Telefonnummer",
"phoneNumberPlaceholder": "+46 12 345 67 89",
"phoneNumberRequired": "Telefonnummer krävs",
"registerAccount": "Skapa konto"
}
}
2 changes: 1 addition & 1 deletion frontend/apply/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function Login() {
</form>

<div className={styles.links}>
<a href="/signup" className={styles.link}>
<a href="/register" className={styles.link}>
{t("loginPage.noAccount")}
</a>
<a href="/forgot-password" className={styles.link}>
Expand Down
96 changes: 96 additions & 0 deletions frontend/apply/src/app/register/login.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
.loginContainer {
display: flex;
justify-content: end;
align-items: center;
min-height: calc(100vh - 100px);
padding: 24px 128px 24px 0;
background: url("/Uppsala_University_2023.jpg");
background-size: cover;
}

.loginCard {
background: white;
border-radius: 12px;
padding: 3rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
max-width: 870px;
width: 100%;
}

.title {
font-size: 2rem;
font-weight: 700;
color: #1a202c;
}

.form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 1.5rem;
}

.formFullWidth {
grid-column: 1 / -1;
}

.errorMessage {
background-color: #fee;
border: 1px solid #fcc;
color: #c33;
padding: 0.75rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
text-align: center;
}

.links {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e2e8f0;
}

.link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
text-align: center;
padding-left: 5rem;
transition: color 0.2s ease;
}

.link:hover {
color: #764ba2;
text-decoration: underline;
}

@media (max-width: 1000px) {
.loginContainer {
padding-right: 48px;
}
.loginCard {
max-width: 500px;
}
.form {
grid-template-columns: 1fr;
}
}

@media (max-width: 768px) {
.loginContainer {
justify-content: center;
padding: 24px 0;
}

.loginCard {
padding: 2rem;
}

.title {
font-size: 1.75rem;
}
}
171 changes: 171 additions & 0 deletions frontend/apply/src/app/register/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"use client";

import { useState } from "react";
import TextInput from "@/components/TextInput";
import styles from "../register/login.module.css";
import { useTranslation } from "react-i18next";
import "@/i18n/config";
import Button from "@/components/Button";

export default function Signup() {
const { t } = useTranslation();

const [username, setUsername] = useState("");
const [email, setEmail] = useState("");

const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");

const [personalIdentityNumber, setPersonalIdentityNumber] = useState("");
const [sectionValue, setSectionValue] = useState(""); // placeholder until dropdown/select
const [phoneNumberValue, setPhoneNumberValue] = useState("");

const [error, setError] = useState("");
const [loading, setLoading] = useState(false);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;

if (name === "username") setUsername(value);
if (name === "email") setEmail(value);

if (name === "password") setPassword(value);
if (name === "passwordConfirmation") setPasswordConfirmation(value);

if (name === "personalIdentityNumber") setPersonalIdentityNumber(value);
if (name === "section") setSectionValue(value);
if (name === "phoneNumber") setPhoneNumberValue(value);
};

// you said ignore handleSubmit logic for now
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);

// minimal client-side check
if (password !== passwordConfirmation) {
setError(t("passwordsDoNotMatch"));
setLoading(false);
return;
}

// TODO: connect real register API later
setLoading(false);
// router.push("/account"); // example
};

return (
<div className={styles.loginContainer}>
<div className={styles.loginCard}>
<h1 className={styles.title}>{t("registerPage.registerTitle")}</h1>

<form onSubmit={handleSubmit} className={styles.form}>
<TextInput
required
label={t("registerPage.username")}
value={username}
onChange={handleChange}
name="username"
type="text"
placeholder={t("registerPage.usernamePlaceholder")}
error={error && username === "" ? t("registerPage.usernameRequired") : ""}
/>

<TextInput
required
label={t("registerPage.email")}
value={email}
onChange={handleChange}
name="email"
type="email"
placeholder={t("registerPage.emailPlaceholder")}
error={error && email === "" ? t("registerPage.emailRequired") : ""}
/>

<TextInput
required
label={t("registerPage.password")}
value={password}
onChange={handleChange}
name="password"
type="password"
placeholder={t("registerPage.passwordPlaceholder")}
error={error && password === "" ? t("registerPage.passwordRequired") : ""}
/>

<TextInput
required
label={t("registerPage.passwordConfirmation")}
value={passwordConfirmation}
onChange={handleChange}
name="passwordConfirmation"
type="password"
placeholder={t("registerPage.passwordConfirmationPlaceholder")}
error={
error && passwordConfirmation === ""
? t("registerPage.passwordConfirmationRequired")
: ""
}
/>

<TextInput
required
label={t("registerPage.personalIdentityNumber")}
value={personalIdentityNumber}
onChange={handleChange}
name="personalIdentityNumber"
type="text"
placeholder={t("registerPage.PersonNumberPlaceholder")}
error={
error && personalIdentityNumber === ""
? t("registerPage.PersonNumberRequired")
: ""
}
/>

{/* Placeholder until dropdown/select */}
<TextInput
required
label={t("registerPage.section")}
value={sectionValue}
onChange={handleChange}
name="section"
type="text"
placeholder={t("registerPage.sectionPlaceholder")}
error={error && sectionValue === "" ? t("registerPage.sectionRequired") : ""}
/>

<TextInput
required
label={t("registerPage.phoneNumber")}
value={phoneNumberValue}
onChange={handleChange}
name="phoneNumber"
type="tel"
placeholder={t("registerPage.phoneNumberPlaceholder")}
error={error && phoneNumberValue === "" ? t("registerPage.phoneNumberRequired") : ""}
/>

{error && <div className={styles.errorMessage}>{error}</div>}

</form>

<div className={styles.links}>
<Button
className="button activeButton"
onClick={handleSubmit}
disabled={loading}
loading={loading}
>
{t("registerPage.registerAccount")}
</Button>
<a href="/login" className={styles.link}>
{t("navbar.login")}
</a>
</div>
</div>

</div>
);
}