Skip to content

Commit

Permalink
➡️ merge pull request #55 from devsoc-unsw/feature/joinSocietyFrontend
Browse files Browse the repository at this point in the history
feature/join society frontend, also improves register ux
  • Loading branch information
lachlanshoesmith authored Dec 15, 2024
2 parents 6b4c2bd + 6fad967 commit 812218e
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 24 deletions.
5 changes: 5 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import GenerateOTP from './GenerateOTP/GenerateOTP';
import VerifyOTP from './VerifyOTP/VerifyOTP';
import { SocietyManagementPage } from './Settings/SettingsPage/SocietyManagementPage/SocietyManagementPage';
import { CreateNewSocietyPage } from './Settings/SettingsPage/SocietyManagementPage/CreateNewSociety/CreateNewSociety';
import { SearchSocietiesPage } from './Settings/SettingsPage/SocietyManagementPage/SearchSocieties/SearchSocieties';

function App() {
const [user, setUser] = useState<User | null>(null);
Expand Down Expand Up @@ -101,6 +102,10 @@ function App() {
<Route path="events/new" element={<CreateNewEventPage />} />
<Route path="societies" element={<SocietyManagementPage />} />
<Route path="societies/new" element={<CreateNewSocietyPage />} />
<Route
path="societies/search"
element={<SearchSocietiesPage />}
/>
<Route path="discord" element={<DiscordPage />} />
</Route>
<Route path="/unauthenticated" element={<Unauthenticated />} />
Expand Down
32 changes: 16 additions & 16 deletions frontend/src/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import classes from './Login.module.css';
import { AuthScreen } from '../AuthScreen/AuthScreen';
import { TextInput, TextOptions } from '../TextInput/TextInput';
import { UserCircleIcon } from '@heroicons/react/24/outline';
import { LockClosedIcon } from '@heroicons/react/24/outline';
import { useState, FormEvent, useContext } from 'react';
import { Link } from 'react-router';
import { errorHandler, AuthError } from '../errorHandler';
import { UserContext, User } from '../UserContext/UserContext';
import classes from "./Login.module.css";
import { AuthScreen } from "../AuthScreen/AuthScreen";
import { TextInput, TextOptions } from "../TextInput/TextInput";
import { UserCircleIcon } from "@heroicons/react/24/outline";
import { LockClosedIcon } from "@heroicons/react/24/outline";
import { useState, FormEvent, useContext } from "react";
import { Link } from "react-router";
import { errorHandler, AuthError } from "../errorHandler";
import { UserContext, User } from "../UserContext/UserContext";

export default function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<AuthError | undefined>(undefined);
const [success, setSuccess] = useState<string | undefined>(undefined);
const { setUser } = useContext(UserContext);

async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const res = await fetch('http://localhost:5180/auth/login', {
method: 'POST',
const res = await fetch("http://localhost:5180/auth/login", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
credentials: 'include',
credentials: "include",
body: JSON.stringify({
username,
password,
Expand All @@ -34,7 +34,7 @@ export default function LoginPage() {
setError(errorHandler(json.error));
} else if (setUser) {
setError(undefined);
setSuccess('Logged in successfully! Redirecting...');
setSuccess("Logged in successfully! Redirecting...");
setTimeout(() => {
setUser(json as User);
}, 1000);
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/Register/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default function RegisterPage() {
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const [error, setError] = useState<AuthError | undefined>(undefined);
const [success, setSuccess] = useState<string | undefined>(undefined);
const navigate = useNavigate();

async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const res = await fetch("http://localhost:5180/auth/register", {
Expand All @@ -32,7 +34,10 @@ export default function RegisterPage() {
setError(errorHandler(json.error));
} else {
setError(undefined);
navigate("/login");
setSuccess("Signed up successfully! Redirecting to login page...");
setTimeout(() => {
navigate("/login");
}, 1000);
}
}

Expand Down Expand Up @@ -78,6 +83,7 @@ export default function RegisterPage() {
buttonText="Sign up"
onSubmit={handleSubmit}
error={error}
success={success}
/>
<div className={classes.lower} />
</main>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/Settings/SettingsNavbar/SettingsNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CalendarIcon,
FaceSmileIcon,
KeyIcon,
MagnifyingGlassIcon,
MegaphoneIcon,
StarIcon,
UserCircleIcon,
Expand Down Expand Up @@ -38,6 +39,11 @@ const rows: Row[][] = [
name: 'Create a new society',
to: '/settings/societies/new',
},
{
icon: <MagnifyingGlassIcon />,
name: 'Join a society',
to: '/settings/societies/search',
},
],
[
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.societiesList {
list-style-type: none;
}

.societiesList li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
border-radius: 32px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { UserGroupIcon } from "@heroicons/react/24/outline";
import Button from "../../../../Button/Button";
import { ButtonIcons, ButtonVariants } from "../../../../Button/ButtonTypes";
import { TextInput, TextOptions } from "../../../../TextInput/TextInput";
import { SettingsPage } from "../../SettingsPage";
import { useContext, useEffect, useState } from "react";
import { Society, UserContext } from "../../../../UserContext/UserContext";
import classes from "./SearchSocieties.module.css";

export const SearchSocietiesPage = () => {
const [societyName, setSocietyName] = useState("");
const [foundSocieties, setFoundSocieties] = useState([]);
const [error, setError] = useState("");
const { societies, setSocieties } = useContext(UserContext);
useEffect(() => {
searchSocieties("");
}, []);

const searchSocieties = async (societyName: string) => {
const allSocieties = await fetch("http://localhost:5180/societies", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
});

const societiesJson = await allSocieties.json();

const userSocieties = await fetch("http://localhost:5180/user/societies", {
method: "GET",
credentials: "include",
});

const userSocietiesJson = await userSocieties.json();
setSocieties?.(userSocietiesJson);

if (allSocieties.ok) {
setError("");
setFoundSocieties(societiesJson);
if (societyName) {
setFoundSocieties(
societiesJson.filter((society: Society) =>
society.name.toLowerCase().includes(societyName.toLowerCase())
)
);
}
} else {
setError("Failed to fetch all societies.");
}
};

const joinSociety = async (society: Society) => {
const res = await fetch("http://localhost:5180/user/society/join", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ societyId: society.id }),
});

if (res.ok) {
setSocieties?.({
joined: [...(societies?.joined ?? []), society],
administering: societies?.administering ?? [],
});
} else {
alert("Failed to join society");
}
};

return (
<SettingsPage
title="Join a society"
pageAbovePath="/settings/societies"
headerButtons={[
<TextInput
placeholder="Society name"
name="societyName"
type={TextOptions.Text}
onChange={(name) => setSocietyName(name)}
icon={<UserGroupIcon />}
error={error !== ""}
noMargin
/>,
<Button
variant={ButtonVariants.Primary}
icon={ButtonIcons.Search}
type="button"
onClick={() => searchSocieties(societyName)}
/>,
]}
>
<ul className={classes.societiesList}>
{foundSocieties.map(
(society: Society) =>
!societies?.administering.some(
(administeredSociety) => administeredSociety.name === society.name
) && (
<li key={society.id}>
<h2>{society.name}</h2>
{!societies?.joined.some(
(joinedSociety) => joinedSociety.name === society.name
) ? (
<Button
variant={ButtonVariants.Secondary}
icon={ButtonIcons.Plus}
type="button"
onClick={() => {
joinSociety(society);
}}
/>
) : (
<p>Already joined</p>
)}
</li>
)
)}
</ul>
{error && <p>{error}</p>}
</SettingsPage>
);
};
20 changes: 13 additions & 7 deletions frontend/src/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type TextInputProp = {
onChange: React.Dispatch<React.SetStateAction<string>>;
type: TextOptions;
error: boolean;
noMargin?: boolean;
textarea?: boolean;
value?: string;
};
Expand All @@ -27,18 +28,21 @@ export function TextInput(props: TextInputProp) {
const onFocus = () => setFocus(true);
const onBlur = () => setFocus(false);

function handleChange(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
function handleChange(
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) {
const value = event.target.value;
props.onChange(value);
}

return (
<div
className={`${props.className ? props.className : classes.container} ${focus ? classes.focus : ""} ${
props.error ? classes.error : ""
}`}
className={`${props.className ? props.className : classes.container} ${
focus ? classes.focus : ""
} ${props.error ? classes.error : ""}`}
style={{ marginBottom: props.noMargin ? "0" : "" }}
>
{props.textarea ?
{props.textarea ? (
<textarea
rows={6}
name={props.name}
Expand All @@ -50,7 +54,8 @@ export function TextInput(props: TextInputProp) {
autoFocus={props.autofocus}
value={props.value}
/>
: <input
) : (
<input
autoFocus={props.autofocus}
type={props.type}
name={props.name}
Expand All @@ -60,7 +65,8 @@ export function TextInput(props: TextInputProp) {
onBlur={onBlur}
className={classes.input}
onChange={handleChange}
/>}
/>
)}
<div className={classes.icon}>{props.icon && props.icon}</div>
</div>
);
Expand Down

0 comments on commit 812218e

Please sign in to comment.