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

frontend stuff done #72

Merged
merged 10 commits into from
Dec 7, 2024
1 change: 1 addition & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ApplicationsModule } from './applications/applications.module';
username: process.env.NX_DB_USERNAME,
password: process.env.NX_DB_PASSWORD,
autoLoadEntities: true,
database: process.env.NX_DB_DATABASE || 'c4c-ops',
// entities: [join(__dirname, '**/**.entity.{ts,js}')],
// Setting synchronize: true shouldn't be used in production - otherwise you can lose production data
synchronize: true,
Expand Down
20 changes: 12 additions & 8 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,21 @@ export class AuthController {

// TODO will be deprecated if we use Google OAuth
@Post('/verify')
verifyUser(@Body() body: VerifyUserRequestDTO): void {
try {
this.authService.verifyUser(body.email, String(body.verificationCode));
} catch (e) {
throw new BadRequestException(e.message);
}
async verifyUser(@Body() body: VerifyUserRequestDTO) {
return await this.authService
.verifyUser(body.email, String(body.verificationCode))
.catch((err) => {
throw new BadRequestException(err.message);
});
}

@Post('/signin')
signin(@Body() signInDto: SignInRequestDto): Promise<SignInResponseDto> {
return this.authService.signin(signInDto);
async signin(
@Body() signInDto: SignInRequestDto,
): Promise<SignInResponseDto> {
return await this.authService.signin(signInDto).catch((err) => {
throw new UnauthorizedException(err.message);
});
}

@Post('/delete/:userId')
Expand Down
22 changes: 14 additions & 8 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,22 @@ export class AuthService {

verifyUser(email: string, verificationCode: string): Promise<unknown> {
return new Promise((resolve, reject) => {
return new CognitoUser({
const cognitoUser = new CognitoUser({
Username: email,
Pool: this.userPool,
}).confirmRegistration(verificationCode, true, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});

return cognitoUser.confirmRegistration(
verificationCode,
true,
(err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
},
);
});
}

Expand All @@ -108,7 +114,7 @@ export class AuthService {

const cognitoUser = new CognitoUser(userData);

return new Promise<SignInResponseDto>((resolve, reject) => {
return new Promise((resolve, reject) => {
return cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
resolve({
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/auth/dtos/verify-user.request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IsEmail, IsNumber } from 'class-validator';
import { IsEmail, IsNumberString } from 'class-validator';

export class VerifyUserRequestDTO {
@IsEmail()
email: string;

@IsNumber()
@IsNumberString()
verificationCode: number;
}
18 changes: 14 additions & 4 deletions apps/backend/src/interceptors/current-user.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,23 @@ export class CurrentUserInterceptor implements NestInterceptor {
const userEmail = cognitoUserAttributes.find(
(attribute) => attribute.Name === 'email',
).Value;
const users = await this.usersService.findByEmail(userEmail);
const name = cognitoUserAttributes
.find((attribute) => attribute.Name === 'name')
.Value.split(' ');

if (users.length > 0) {
const user = users[0];
const [firstName, lastName] = [name[0], name.at(-1)];

request.user = user;
// check if the cognito user has a corresponding user in the database
const users = await this.usersService.findByEmail(userEmail);
let user = null;
if (users.length > 0) {
// if the user exists, use the user from the database
user = users[0];
} else {
// if the user does not exist, create a new user in the database
user = await this.usersService.create(userEmail, firstName, lastName);
}
request.user = user;
}

return handler.handle();
Expand Down
5 changes: 3 additions & 2 deletions apps/backend/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ export class UsersService {
return user;
}

findByEmail(email: string): Promise<User[]> {
return this.usersRepository.find({
async findByEmail(email: string): Promise<User[]> {
const users = await this.usersRepository.find({
where: { email },
relations: ['applications'],
});
return users;
}

async updateUser(
Expand Down
29 changes: 27 additions & 2 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ import axios, { type AxiosInstance, AxiosRequestConfig } from 'axios';
import type {
Application,
applicationRow,
ApplicationStage,
} from '@components/ApplicationTables';

const defaultBaseUrl =
import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';

type SubmitReviewRequest = {
applicantId: number;
stage: ApplicationStage;
rating: number;
content: string;
};

export class ApiClient {
private axiosInstance: AxiosInstance;

Expand Down Expand Up @@ -46,6 +55,17 @@ export class ApiClient {
})) as Promise<string>;
}

public async submitReview(
accessToken: string,
reviewData: SubmitReviewRequest,
): Promise<void> {
return this.post('/api/reviews', reviewData, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
}) as Promise<void>;
}

private async get(
path: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -56,9 +76,14 @@ export class ApiClient {
.then((response) => response.data);
}

private async post(path: string, body: unknown): Promise<unknown> {
private async post(
path: string,
body: unknown,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
headers: AxiosRequestConfig<any> | undefined = undefined,
): Promise<unknown> {
return this.axiosInstance
.post(path, body)
.post(path, body, headers)
.then((response) => response.data);
}

Expand Down
4 changes: 0 additions & 4 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ const router = createBrowserRouter([
]);

export const App: React.FC = () => {
useEffect(() => {
apiClient.getHello().then((res) => console.log(res));
}, []);

return <RouterProvider router={router} />;
};

Expand Down
106 changes: 96 additions & 10 deletions apps/frontend/src/components/ApplicationTables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import {
ListItemText,
ListItemIcon,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Rating,
} from '@mui/material';
import { useEffect, useState, useRef } from 'react';
import apiClient from '@api/apiClient';
import { DoneOutline } from '@mui/icons-material';

enum ApplicationStage {
export enum ApplicationStage {
RESUME = 'RESUME',
INTERVIEW = 'INTERVIEW',
ACCEPTED = 'ACCEPTED',
Expand Down Expand Up @@ -67,17 +72,18 @@ export type Application = {
response: Response[];
numApps: number;
};
const TODAY = new Date();

const getCurrentSemester = (): Semester => {
const month: number = new Date().getMonth();
const month: number = TODAY.getMonth();
if (month >= 1 && month <= 7) {
return Semester.FALL; // We will be recruiting for the fall semester during Feb - Aug
}
return Semester.SPRING; // We will be recruiting for the spring semester during Sep - Jan
};

const getCurrentYear = (): number => {
return new Date().getFullYear();
return TODAY.getFullYear();
};

export function ApplicationTable() {
Expand All @@ -91,6 +97,41 @@ export function ApplicationTable() {
const [selectedApplication, setSelectedApplication] =
useState<Application | null>(null);

const [openReviewModal, setOpenReviewModal] = useState(false);
const [reviewComment, setReviewComment] = useState('');
const [reviewRating, setReviewRating] = useState<number>(0);

const handleOpenReviewModal = () => {
setOpenReviewModal(true);
};

const handleCloseReviewModal = () => {
setOpenReviewModal(false);
setReviewComment('');
};
const stageToSubmit = selectedApplication?.stage || ApplicationStage.ACCEPTED;

const handleReviewSubmit = async () => {
if (!selectedUser || reviewRating === 0 || !reviewComment) {
alert('Please select a user, provide a rating, and add a comment.');
return;
}

try {
await apiClient.submitReview(accessToken, {
applicantId: selectedUser.userId,
stage: stageToSubmit,
rating: reviewRating,
content: reviewComment,
});
alert('Review submitted successfully!');
handleCloseReviewModal();
} catch (error) {
console.error('Error submitting review:', error);
alert('Failed to submit review.');
}
};

const fetchData = async () => {
const data = await apiClient.getAllApplications(accessToken);
// Each application needs an id for the DataGrid to work
Expand All @@ -102,9 +143,19 @@ export function ApplicationTable() {
}
};

// const getApplication = async (userId: number) => {
// const application = await apiClient.getApplication(accessToken, userId);
// setSelectedApplication(application);
// };

const getApplication = async (userId: number) => {
const application = await apiClient.getApplication(accessToken, userId);
setSelectedApplication(application);
try {
const application = await apiClient.getApplication(accessToken, userId);
setSelectedApplication(application);
} catch (error) {
console.error('Error fetching application:', error);
alert('Failed to fetch application details.');
}
};

const getFullName = async () => {
Expand All @@ -113,10 +164,10 @@ export function ApplicationTable() {

useEffect(() => {
// Access token comes from OAuth redirect uri https://frontend.com/#access_token=access_token
const hash = window.location.hash;
const accessTokenMatch = hash.match(/access_token=([^&]*)/);
const urlParams = new URLSearchParams(window.location.hash.substring(1));
const accessTokenMatch = urlParams.get('access_token');
if (accessTokenMatch) {
setAccessToken(accessTokenMatch[1]);
setAccessToken(accessTokenMatch);
}
isPageRendered.current = false;
}, []);
Expand Down Expand Up @@ -258,10 +309,45 @@ export function ApplicationTable() {
}}
>
<Typography variant="body1">Reviews: None</Typography>
<Button variant="contained" size="small">
<Button
variant="contained"
size="small"
onClick={handleOpenReviewModal}
>
Start Review
</Button>
</Stack>
<Dialog open={openReviewModal} onClose={handleCloseReviewModal}>
<DialogTitle>Write Review</DialogTitle>
<DialogContent>
<Stack direction="row" alignItems="center" spacing={2} mb={2}>
<Typography variant="body1">Rating:</Typography>
<Rating
name="review-rating"
value={reviewRating}
onChange={(_, value) => setReviewRating(value || 0)}
precision={1}
/>
</Stack>
<TextField
autoFocus
margin="dense"
id="review"
label="Review Comments"
type="text"
fullWidth
multiline
rows={4}
variant="outlined"
value={reviewComment}
onChange={(e) => setReviewComment(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseReviewModal}>Cancel</Button>
<Button onClick={handleReviewSubmit}>Submit Review</Button>
</DialogActions>
</Dialog>
</>
) : null}
</Container>
Expand Down
Loading