Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/join society frontend, also improves register ux #55

Merged
merged 5 commits into from
Dec 15, 2024
Merged
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
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
Loading