Skip to content

Commit

Permalink
Merge pull request #12 from TritonSE/feature/christensophia/team-page
Browse files Browse the repository at this point in the history
Feature/christensophia/team page
  • Loading branch information
syz16 authored Feb 11, 2024
2 parents 2e50468 + e8b25c2 commit 2982966
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 3 deletions.
3 changes: 2 additions & 1 deletion backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "dotenv/config";
import cors from "cors";
import express, { NextFunction, Request, Response } from "express";
import { isHttpError } from "http-errors";
import memberRoutes from "src/routes/members";

const app = express();

Expand All @@ -24,7 +25,7 @@ app.use(
);

// Routes ( e.g. app.use("/api/task", taskRoutes); )

app.use("/api/member", memberRoutes);
/**
* Error handler; all errors thrown by server are handled here.
* Explicit typings required here because TypeScript cannot infer the argument types.
Expand Down
36 changes: 36 additions & 0 deletions backend/src/controllers/member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { RequestHandler } from "express";
import MemberModel from "src/models/member";
import { Types } from "mongoose";

export const createMember: RequestHandler = async (req, res, next) => {
const { name, role, profilePictureURL } = req.body;
try {
const member = await MemberModel.create({
name: name,
role: role,
profilePictureURL: profilePictureURL,
});
res.status(201).json(member);
} catch (error) {
next(error);
}
};

export const getMember: RequestHandler = async (req, res, next) => {
const { id } = req.params;
try {
const member = Types.ObjectId.isValid(id) ? await MemberModel.findById(id) : null;
res.status(200).json(member);
} catch (error) {
next(error);
}
};

export const getAllMembers: RequestHandler = async (req, res, next) => {
try {
const members = await MemberModel.find({});
res.status(200).json(members);
} catch (error) {
next(error);
}
};
11 changes: 11 additions & 0 deletions backend/src/models/member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { InferSchemaType, Schema, model } from "mongoose";

const memberSchema = new Schema({
name: { type: String, required: true },
role: { type: String, required: true },
profilePictureURL: { type: String },
});

type Member = InferSchemaType<typeof memberSchema>;

export default model<Member>("Member", memberSchema);
9 changes: 9 additions & 0 deletions backend/src/routes/members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import express from "express";
import * as MembersController from "src/controllers/member";

const router = express.Router();

router.get("/get", MembersController.getAllMembers);
router.post("/post", MembersController.createMember);

export default router;
50 changes: 50 additions & 0 deletions backend/src/validators/member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { body } from "express-validator";

const makeIDValidator = () =>
body("_id")
.exists()
.withMessage("_id is required")
.bail()
.isMongoId()
.withMessage("_id must be a MongoDB object ID");
const makeNameValidator = () =>
body("name")
// title must exist, if not this message will be displayed
.exists()
.withMessage("name is required")
// bail prevents the remainder of the validation chain for this field from being executed if
// there was an error
.bail()
.isString()
.withMessage("name must be a string")
.bail()
.notEmpty()
.withMessage("name cannot be empty");
const makeRoleValidator = () =>
body("role")
// title must exist, if not this message will be displayed
.exists()
.withMessage("role is required")
// bail prevents the remainder of the validation chain for this field from being executed if
// there was an error
.bail()
.isString()
.withMessage("role must be a string")
.bail()
.notEmpty()
.withMessage("role cannot be empty");
const makeProfilePictureURLValidator = () =>
body("profilePictureURL").optional().isString().withMessage("profilePictureURL must be a string");

export const createMember = [
makeNameValidator(),
makeRoleValidator(),
makeProfilePictureURLValidator(),
];

export const updateTask = [
makeIDValidator(),
makeNameValidator(),
makeRoleValidator(),
makeProfilePictureURLValidator(),
];
6 changes: 5 additions & 1 deletion frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
images: {
domains: ["images.fineartamerica.com"],
},
};

module.exports = nextConfig;
Binary file added frontend/public/image 18.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/volunteer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions frontend/src/api/member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { get, handleAPIError } from "./requests";

import type { APIResult } from "./requests";
export type Member = {
_id: string;
name: string;
role: string;
profilePictureURL?: string;
};

export async function getAllMembers(): Promise<APIResult<Member[]>> {
try {
const response = await get(`/api/member/get`);
console.log(response);
const json = (await response.json()) as Member[];
console.log(json);
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}
5 changes: 4 additions & 1 deletion frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-start-rgb: 255, 255, 255;
--background-end-rgb: 255, 255, 255;
--color-primary-purple: #694c97;
--font-small-subtitle: normal 700 28px/150% "Roboto Slab";
--font-body: normal 400 20px/120% "Open Sans";
}

@media (prefers-color-scheme: dark) {
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/app/team/page.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:[email protected]&display=swap");

.description {
text-align: left;
font-family: "Open Sans";
font-size: 24px;
font-style: normal;
font-weight: 400;
line-height: 32px; /* 133.333% */
}

.subtitle {
font: var(--font-small-subtitle);
font-size: 32px;
line-height: 150%; /* 48px */
letter-spacing: 0.64px;
}

.text {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 47px;
margin-left: 99px;
margin-right: 99px;
margin-top: 53px;
margin-bottom: 64px;
}

.membersContainer {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
margin-left: 119px;
margin-right: 119px;
margin-bottom: 147px;
gap: 60px;
}
54 changes: 54 additions & 0 deletions frontend/src/app/team/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import React, { useEffect, useState } from "react";

import styles from "./page.module.css";

import { Member, getAllMembers } from "@/api/member";
import BackgroundHeader from "@/components/BackgroundHeader";
import MemberInfo from "@/components/MemberInfo";

export default function Team() {
const [members, setMembers] = useState<Member[]>([]);

useEffect(() => {
getAllMembers()
.then((result) => {
if (result.success) {
console.log(result.data);
setMembers(result.data);
} else {
alert(result.error);
}
})
.catch((error) => {
alert(error);
});
}, []);
return (
<main>
<BackgroundHeader
backgroundImage="/image 18.png"
header="About Us"
title="Meet Our Team"
description="Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit
sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
/>
<div className={styles.text}>
<div className={styles.subtitle}>Our Team</div>
<p className={styles.description}>
Our dedicated team @ 4 Future Leaders of Tomorrow is a non-profit charitable organization
committed in preventing and ending homelessness, hunger and disparity in underprivileged
communities. Everyone deserves a chance for a better future!. We are reaching out by
providing resources in needed communities - whether it be a delicious meal, warm clothing,
educational supplies, referrals, toys or even bus passes
</p>
</div>
<div className={styles.membersContainer}>
{members.map((member) => (
<MemberInfo key={member._id} member={member} />
))}
</div>
</main>
);
}
37 changes: 37 additions & 0 deletions frontend/src/components/BackgroundHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Image from "next/image";
import React from "react";

type BackgroundHeaderProps = {
backgroundImage: string;
header: string;
title: string;
description: string;
};

const BackgroundHeader = ({
backgroundImage,
header,
title,
description,
}: BackgroundHeaderProps) => {
return (
<div className="w-full h-screen relative">
<Image
src={backgroundImage}
alt="Background image"
layout="fill"
objectFit="cover"
className="object-cover"
priority
/>
<div className="absolute top-0 left-0 w-full h-full bg-black opacity-30"></div>
<div className="absolute bottom-20 left-20 w-1/2 h-full flex flex-col items-start justify-center ml-20">
<h2>{header}</h2>
<h1 className="text-white text-4xl py-10 font-bold">{title}</h1>
<p>{description}</p>
</div>
</div>
);
};

export default BackgroundHeader;
26 changes: 26 additions & 0 deletions frontend/src/components/MemberInfo.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:[email protected]&display=swap");

.container {
display: flex;
flex-direction: column;
align-items: center;
}

.profile {
width: 248px;
height: 249px;
border-radius: 9999px;
overflow: hidden;
margin-bottom: 23px;
}

.name {
font: var(--font-small-subtitle);
font-size: 28px;
line-height: 150%; /* 42px */
letter-spacing: 0.56px;
}

.role {
font: var(--font-body);
}
38 changes: 38 additions & 0 deletions frontend/src/components/MemberInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Image from "next/image";
import React from "react";

import styles from "./MemberInfo.module.css";

import type { Member } from "../api/member";

//Make an iterface for my props, take in a name - string, role - string, and profilePictureURL - string
type MemberInfoProps = {
member: Member | null | undefined;
};

const MemberInfo = ({ member }: MemberInfoProps) => {
if (member) {
return (
<div className={styles.container}>
<div className={styles.profile}>
{member.profilePictureURL ? (
<Image
src={member.profilePictureURL}
alt="Profile Picture"
width={248}
height={248}
priority
/>
) : (
<Image src="/volunteer.png" alt="Volunteer" width={248} height={248} priority />
)}
</div>
<h1 className={styles.name}>{member.name}</h1>
<p className={styles.role}>{member.role}</p>
</div>
);
}
return null;
};

export default MemberInfo;

0 comments on commit 2982966

Please sign in to comment.