Skip to content
Snippets Groups Projects
Commit 1c77a841 authored by hannahl8's avatar hannahl8
Browse files

add discussion page with comments

remove unnecesary props
create context for login and discussions
parent ebd5b2f5
No related branches found
No related tags found
No related merge requests found
Pipeline #10731 failed
Showing
with 301 additions and 196 deletions
......@@ -9,7 +9,7 @@ import OpeningsPage from "./pages/OpeningsPage.tsx";
import DiscussionPage from "./pages/DiscussionPage.tsx";
import ProfilePage from "./pages/ProfilePage.tsx";
import './App.css'
import {Route, Routes, useNavigate} from "react-router-dom";
import {Route, Routes} from "react-router-dom";
import {
discussionPagePath,
labsPagePath,
......@@ -19,34 +19,21 @@ import {
signUpStudentPagePath,
welcomePagePath
} from "./utils.ts";
import {useState} from "react";
import DiscussionItemPage from "./pages/DiscussionItemPage.tsx";
export default function App() {
const [isLoggedIn, setIsLoggedIn] = useState(!!sessionStorage.getItem('token'));
const navigate = useNavigate();
const handleLogin = (token: string) => {
sessionStorage.setItem('token', token);
setIsLoggedIn(true);
};
const handleLogout = () => {
sessionStorage.removeItem('token');
setIsLoggedIn(false);
navigate(`${loginPagePath}`);
};
return (
<div className="app">
<AppHeader isLoggedIn={isLoggedIn} onLogout={handleLogout}/>
<AppHeader/>
<Routes>
<Route path={welcomePagePath} element={<WelcomePage isLoggedIn={isLoggedIn}/>}/>
<Route path={welcomePagePath} element={<WelcomePage/>}/>
<Route path={signUpStudentPagePath} element={<SignUpStudentPage/>}/>
<Route path={signUpProfessorPagePath} element={<SignUpProfessorPage/>}/>
<Route path={loginPagePath} element={<LoginPage onLogin={handleLogin}/>}/>
<Route path={loginPagePath} element={<LoginPage/>}/>
<Route path={labsPagePath} element={<LabsPage/>}/>
<Route path={openingsPagePath} element={<OpeningsPage/>}/>
<Route path={discussionPagePath} element={<DiscussionPage/>}/>
<Route path={`${discussionPagePath}/:id`} element={<DiscussionItemPage />} />
<Route path={profilePagePath} element={<ProfilePage/>}/>
</Routes>
<AppFooter/>
......
......@@ -8,9 +8,10 @@ import {
profilePagePath,
welcomePagePath
} from "../utils.ts";
import {AppHeaderProps} from "../types.ts";
import {useLoginContext} from "../contexts/LoginContext.tsx";
export default function AppHeader({isLoggedIn, onLogout}: AppHeaderProps) {
export default function AppHeader() {
const {isLoggedIn, onLogout} = useLoginContext();
return (
<header className="header container">
<section className="logo-and-title">
......@@ -26,14 +27,25 @@ export default function AppHeader({isLoggedIn, onLogout}: AppHeaderProps) {
</Link>
</section>
<section className="labs-openings-discussion-profile-login">
<Link to={labsPagePath}><button className="button"><i className="fa-solid fa-flask"></i> LABS</button></Link>
<Link to={openingsPagePath}><button className="button"><i className="fa-solid fa-door-open"></i> OPENINGS</button></Link>
<Link to={discussionPagePath}><button className="button"><i className="fa-solid fa-comments"></i> DISCUSSION</button></Link>
<Link to={profilePagePath}><button className="button"><i className="fa-solid fa-user"></i> PROFILE</button></Link>
<Link to={labsPagePath}>
<button className="button"><i className="fa-solid fa-flask"></i> LABS</button>
</Link>
<Link to={openingsPagePath}>
<button className="button"><i className="fa-solid fa-door-open"></i> OPENINGS</button>
</Link>
<Link to={discussionPagePath}>
<button className="button"><i className="fa-solid fa-comments"></i> DISCUSSION</button>
</Link>
<Link to={profilePagePath}>
<button className="button"><i className="fa-solid fa-user"></i> PROFILE</button>
</Link>
{isLoggedIn ? (
<button className="button" onClick={onLogout}><i className="fa-solid fa-sign-out"></i> LOGOUT</button>
<button className="button" onClick={onLogout}><i className="fa-solid fa-sign-out"></i> LOGOUT
</button>
) : (
<Link to={loginPagePath}><button className="button"><i className="fa-solid fa-sign-in"></i> LOGIN</button></Link>
<Link to={loginPagePath}>
<button className="button"><i className="fa-solid fa-sign-in"></i> LOGIN</button>
</Link>
)}
</section>
</header>
......
.comment {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 20px;
gap: 10px;
border-bottom: 1px solid var(--default-text-color);
border-radius: 5px;
width: 100%;
}
\ No newline at end of file
import {CommentItem} from "../types.ts";
import {dateTimeFormatOptions} from "../utils.ts";
import "./CommentListItem.css"
export default function CommentListItem(props: { comment: CommentItem }) {
const id = props.comment.id;
const content = props.comment.content;
const createdAt = props.comment.createdAt;
const student = props.comment.student;
const createdAtDate = new Date(createdAt);
const createdAtString = createdAtDate.toLocaleString('en-US', dateTimeFormatOptions);
return (
<div key={id} className="comment">
<p className='comment-content'>{content}</p>
<p className='comment-details'>Posted by {student} on {createdAtString} </p>
</div>
);
}
\ No newline at end of file
import {getDiscussions} from "../data.ts";
import DiscussionListItem from "./DiscussionListItem";
import './DiscussionList.css';
import {useDiscussionContext} from "../contexts/DiscussionContext.tsx";
export default function DiscussionList() {
const {discussions} = useDiscussionContext();
const discussionList = getDiscussions().map((discussion) => (
<DiscussionListItem discussion={discussion}/>
const discussionList = discussions.map((discussion) => (
<DiscussionListItem key={discussion.id} discussion={discussion}/>
));
return <ul id="discussion-posts">{discussionList}</ul>
......
......@@ -15,6 +15,11 @@
text-decoration: underline;
}
.discussion-title:hover {
color: var(--primary-color);
cursor: pointer;
}
.discussion-post .discussion-author-details {
font-size: 1rem;
font-style: italic;
......
import {Link} from "react-router-dom";
import {DiscussionItem, ProfessorUser, StudentUser} from "../types.ts";
import {discussionPagePath, profilePagePath} from "../utils.ts";
import {Link, useNavigate} from "react-router-dom";
import {DiscussionItem} from "../types.ts";
import {dateTimeFormatOptions, discussionPagePath, profilePagePath} from "../utils.ts";
import "./DiscussionListItem.css";
function isStudentUser(author: StudentUser | ProfessorUser): author is StudentUser {
return (author as StudentUser).studentType !== undefined;
}
function humanizeDate(dateString: string) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
export default function DiscussionListItem(props: {
discussion: DiscussionItem;
}) {
export default function DiscussionListItem(props: { discussion: DiscussionItem }) {
const id = props.discussion.id;
const title = props.discussion.title;
const author = props.discussion.author;
const content = props.discussion.content;
const dateCreated = humanizeDate(props.discussion.dateCreated);
const comments = props.discussion.comments;
const likes = props.discussion.likes;
const emailFirstPart = props.discussion.author.email.split('@')[0];
const numberOfComments = props.discussion.numberOfComments;
const createdAt = props.discussion.createdAt;
const labName = props.discussion.labName;
const student = props.discussion.student;
const authorDetails = `${student.year} | ${student.aboutMe} | ${labName}`;
const emailFirstPart = props.discussion.student.email.split('@')[0];
const createdAtDate = new Date(createdAt);
const createdAtString = createdAtDate.toLocaleString('en-US', dateTimeFormatOptions);
let authorDetails;
if (isStudentUser(author)) {
authorDetails = `${author.studentType} | ${author.major.name} | ${author.studentDescription}`;
} else {
authorDetails = `${author.professorType} | ${author.researchLabCollege?.name}`;
}
const navigate = useNavigate();
const handleTitleClick = () => {
navigate(`${discussionPagePath}/${id}`, {state: {discussion: props.discussion}});
};
return (
<li key={id} className="discussion-post">
<Link to={`${discussionPagePath}/${id}`}>
<h1 className="discussion-title">{title}</h1>
</Link>
<h1 className="discussion-title" onClick={handleTitleClick}>{title}</h1>
<p className="discussion-author-details">{authorDetails}</p>
<p className="discussion-content">{content}</p>
<p className="discussion-details">
Posted by <Link
to={`${profilePagePath}/${emailFirstPart}`}>{emailFirstPart}</Link> on {dateCreated} | <i
className="fa-regular fa-comment"></i> {comments} | <i
className="fa-regular fa-thumbs-up"></i> {likes}
to={`${profilePagePath}/${emailFirstPart}`}>{emailFirstPart}</Link> on {createdAtString} | {numberOfComments} comments
</p>
</li>
);
}
\ No newline at end of file
import {DiscussionItem} from "../types.ts";
import React, {createContext, useContext, useEffect, useState} from "react";
import {fetchDiscussions} from "../services.ts";
import {useLoginContext} from "./LoginContext.tsx";
interface DiscussionContextType {
discussions: DiscussionItem[]
}
const DiscussionContext = createContext<DiscussionContextType>({
discussions: []
});
export const DiscussionProvider = ({children}: React.PropsWithChildren) => {
const {isLoggedIn} = useLoginContext();
const [discussions, setDiscussions] = useState<DiscussionItem[]>([]);
useEffect(() => {
if (!isLoggedIn) {
return;
}
const token = sessionStorage.getItem('token');
if (!token) {
console.log("No token found");
return;
}
fetchDiscussions(token)
.then((data) => setDiscussions(data))
.catch(console.error)
}, [isLoggedIn]);
return (
<DiscussionContext.Provider value={{discussions}}>
{children}
</DiscussionContext.Provider>
);
}
export const useDiscussionContext = () => {
const context = useContext(DiscussionContext);
if (!context) {
throw new Error('useMajorContext must be used within a MajorProvider');
}
return context;
}
\ No newline at end of file
import React, {createContext, useState, useContext} from 'react';
import {useNavigate} from "react-router-dom";
import {loginPagePath} from "../utils.ts";
interface LoginContextType {
isLoggedIn: boolean;
setIsLoggedIn: React.Dispatch<React.SetStateAction<boolean>>;
onLogin: (token: string) => void;
onLogout: () => void;
}
const LoginContext = createContext<LoginContextType>({
isLoggedIn: false,
setIsLoggedIn: () => null,
onLogin: () => null,
onLogout: () => null,
});
export const LoginProvider = ({children}: React.PropsWithChildren) => {
const navigate = useNavigate();
const [isLoggedIn, setIsLoggedIn] = useState(!!sessionStorage.getItem('token'));
const onLogin = (token: string) => {
sessionStorage.setItem('token', token);
setIsLoggedIn(true);
};
const onLogout = () => {
sessionStorage.removeItem('token');
setIsLoggedIn(false);
navigate(loginPagePath)
};
return (
<LoginContext.Provider value={{isLoggedIn, setIsLoggedIn, onLogin, onLogout}}>
{children}
</LoginContext.Provider>
);
};
export const useLoginContext = () => {
const context = useContext(LoginContext);
if (!context) {
throw new Error('useLoginContext must be used within a LoginProvider');
}
return context;
}
\ No newline at end of file
import sourceData from "./discussion.json";
import {DiscussionItem} from "./types.ts";
export function getDiscussions(): DiscussionItem[] {
return sourceData.discussions as DiscussionItem[];
}
\ No newline at end of file
{
"discussions": [
{
"id": 1,
"title": "Looking for a College of Engineering Research position",
"content": "Hello everyone, I hope you're all doing well. My name is Harold, and I'm currently an undergraduate student majoring in Aerospace Engineering at Virginia Tech. I'm reaching out here because I'm really passionate about aerospace research and am actively looking for a research position to gain practical experience and contribute to ongoing projects. A bit about my background: I've developed a solid foundation in key aerospace subjects like fluid dynamics, propulsion systems, and structural analysis through my coursework. I've particularly enjoyed projects related to aerodynamics and aircraft design, which have sparked my interest in research and innovation. I have experience with software tools such as MATLAB, ANSYS, and SolidWorks, and I thrive in collaborative environments. I'm eager to apply my skills, learn new ones, and contribute to meaningful research in the aerospace field.",
"author": {
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "johndoe@example.com",
"studentType": "Undergraduate",
"major": {
"id": 1,
"name": "Aerospace Engineering"
},
"studentDescription": "Actively Seeking Research Opportunity"
},
"dateCreated": "2023-07-12T00:00:00Z",
"comments": 12,
"likes": 10
},
{
"id": 2,
"title": "Recent Advances in Aerodynamics and Flow Control - Research Update",
"content": "I am pleased to share with you some exciting updates from our latest research efforts in the field of aerodynamics and flow control. Over the past year, our team at Virginia Tech has been working diligently on several innovative projects, and I am thrilled to present some of our key accomplishments:\nDevelopment of Active Flow Control Mechanisms: We have successfully designed and tested a novel active flow control mechanism that significantly reduces drag on aircraft wings. This new mechanism employs a combination of synthetic jet actuators and surface plasma discharges to manipulate boundary layer behavior, leading to improved aerodynamic efficiency. Our initial wind tunnel tests have shown a reduction in drag by up to 15%, which has the potential to enhance fuel efficiency in commercial aircraft significantly.",
"author": {
"id": 1,
"firstName": "Steve",
"lastName": "Brown",
"email": "robertbrown@example.com",
"professorType": "Principal Investigator",
"researchLabName": "Aerodynamics Engineering Lab",
"researchLabUrl": "http://example.com",
"researchLabPrincipalInvestigator": "Dr. Robert Brown",
"researchLabCollege": {
"id": 1,
"name": "College of Engineering"
},
"researchLabDescription": "Our lab focuses on the future of Electrical Engineering."
},
"dateCreated": "2023-07-28T00:00:00Z",
"comments": 29,
"likes": 150
},
{
"id": 3,
"title": "Exciting Milestones in My Biomedical Engineering Research Journey",
"content": "I hope you're all doing well! My name is Hassan, and I am a graduate researcher in Biomedical Engineering at Virginia Tech. I am excited to share some of the accomplishments from my recent research projects in the field of biomedical engineering.\nDevelopment of Biocompatible Materials for Implants: One of my key projects involved the development of novel biocompatible materials for medical implants. Using advanced polymer synthesis techniques, I have created materials that exhibit enhanced biocompatibility and mechanical properties. These materials have the potential to improve the longevity and performance of implants, benefiting patients with conditions requiring long-term medical devices.",
"author": {
"id": 2,
"firstName": "Alice",
"lastName": "Johnson",
"email": "alicejohnson@example.com",
"studentType": "Graduate (Master's)",
"major": {
"id": 2,
"name": "Biomedical Engineering"
},
"studentDescription": "Current Researcher Receiving Credit"
},
"dateCreated": "2023-07-31T00:00:00Z",
"comments": 12,
"likes": 60
}
]
}
\ No newline at end of file
......@@ -5,12 +5,18 @@ import './index.css'
import {BrowserRouter} from "react-router-dom";
import "./assets/global.css";
import {MajorProvider} from "./contexts/MajorContext.tsx";
import {LoginProvider} from "./contexts/LoginContext.tsx";
import {DiscussionProvider} from "./contexts/DiscussionContext.tsx";
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
< BrowserRouter basename={import.meta.env.BASE_URL}>
<MajorProvider>
<App/>
<LoginProvider>
<DiscussionProvider>
<App/>
</DiscussionProvider>
</LoginProvider>
</MajorProvider>
</BrowserRouter>
</React.StrictMode>,
......
.comments-div {
margin: 1em 3em;
display: flex;
flex-direction: column;
align-items: flex-start;
}
\ No newline at end of file
import {useLocation} from "react-router-dom";
import CommentListItem from "../components/CommentListItem.tsx";
import {CommentItem} from "../types.ts";
import DiscussionListItem from "../components/DiscussionListItem.tsx";
import "./DiscussionItemPage.css"
export default function DiscussionItemPage() {
const location = useLocation();
const discussion = location.state.discussion;
const comments = discussion.comments;
const commentsList = comments.length > 0 ? comments.map((comment: CommentItem) => (
<CommentListItem key={comment.id} comment={comment}/>
)) : "No comments yet!";
return (
<div className='discussion-item-page center-content'>
<DiscussionListItem discussion={discussion}/>
<div className='comments-div'>
<h2>Comments</h2>
<ul className='comments-list'>{commentsList}</ul>
</div>
</div>
);
}
\ No newline at end of file
import './DiscussionPage.css'
import DiscussionList from "../components/DiscussionList.tsx";
import {Link} from "react-router-dom";
import {discussionPagePath} from "../utils.ts";
import {Link, useNavigate} from "react-router-dom";
import {discussionPagePath, loginPagePath} from "../utils.ts";
import {useEffect} from "react";
import {useLoginContext} from "../contexts/LoginContext.tsx";
export default function DiscussionPage() {
const {isLoggedIn} = useLoginContext();
const navigate = useNavigate();
useEffect(() => {
if (!isLoggedIn) {
navigate(loginPagePath);
}
}, [isLoggedIn, navigate]);
return (
<div className='discussion-page center-content'>
<section className='discussion-header'>
......
......@@ -2,9 +2,11 @@ import './LoginPage.css'
import {Link, useNavigate} from "react-router-dom";
import React, {useState} from "react";
import {loginAPI, signUpProfessorPagePath, signUpStudentPagePath, welcomePagePath} from "../utils.ts";
import {LoginProps} from "../types.ts";
import {useLoginContext} from "../contexts/LoginContext.tsx";
export default function LoginPage({onLogin}: LoginProps) {
export default function LoginPage() {
const {onLogin} = useLoginContext();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
......@@ -30,7 +32,6 @@ export default function LoginPage({onLogin}: LoginProps) {
body: JSON.stringify({username: email, password: password}),
})
.then(response => {
console.log("Response:", response);
if (!response.ok) {
throw new Error('Login failed');
}
......@@ -46,7 +47,6 @@ export default function LoginPage({onLogin}: LoginProps) {
})
.then(data => {
if (data.success) {
console.log('Login successful');
navigate(`${welcomePagePath}`); // We can change this
} else {
alert('Login failed: ' + data.message);
......
......@@ -5,9 +5,11 @@ import {
labsPagePath, loginPagePath,
openingsPagePath
} from "../utils.ts";
import {WelcomeProps} from "../types.ts";
import {useLoginContext} from "../contexts/LoginContext.tsx";
export default function WelcomePage({isLoggedIn}: WelcomeProps) {
export default function WelcomePage() {
const {isLoggedIn} = useLoginContext();
return (
<div className="welcome-page center-content">
{isLoggedIn ? (
......
import axios from "axios";
import {CollegeItem, MajorItem, ResearchLabItem} from "./types";
import {collegesAPI, labsAPI, majorsAPI} from "./utils.ts";
export const baseUrl = 'http://localhost:8080/api'
import {CollegeItem, DiscussionItem, MajorItem, ResearchLabItem} from "./types";
import {collegesAPI, discussionsAPI, labsAPI, majorsAPI} from "./utils.ts";
export const fetchResearchLabs = async (): Promise<ResearchLabItem[]> => {
const response = await axios.get(`${labsAPI}`);
return response.data as ResearchLabItem[];
}
export const fetchDiscussions = async (token: string): Promise<DiscussionItem[]> => {
const response = await axios.get(`${discussionsAPI}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
return response.data as DiscussionItem[];
}
export const fetchColleges = async (): Promise<CollegeItem[]> => {
const response = await axios.get(`${collegesAPI}`);
return response.data as CollegeItem[];
......
export interface StudentUser {
id: number;
firstName: string;
lastName: string;
email: string;
year: string; // Undergraduate or Graduate
aboutMe: string; // "Actively Seeking Research Opportunities" or "Current Researcher Receiving Credit"
major: MajorItem; // Major
}
export interface ProfessorUser {
id: number;
firstName: string;
lastName: string;
email: string;
college: CollegeItem;
isPending: boolean;
professorType: string; // Principal Investigator or Professor
researchLabName?: string;
researchLabUrl?: string;
researchLabCollege?: CollegeItem;
researchLabDescription?: string;
}
export interface ResearchLabItem {
id: number;
name: string;
url: string;
principleInvestigator: string;
major: string;
college: string;
description: string;
}
......@@ -16,20 +41,28 @@ export interface CollegeItem {
export interface MajorItem {
id: number;
name: string;
college: string;
}
export interface WelcomeProps {
isLoggedIn: boolean;
export interface DiscussionItem {
id: number;
title: string;
content: string;
numberOfComments: number;
createdAt: Date;
labName: string;
student: StudentUser;
comments: CommentItem[];
}
export interface AppHeaderProps {
isLoggedIn: boolean;
onLogout: () => void;
export interface CommentItem {
id: number;
content: string;
createdAt: Date;
student: string;
}
export interface LoginProps {
onLogin: (token: string) => void;
}
// remove PROPS if not necessary
export interface ResearchLabProps {
researchLabs: ResearchLabItem[];
......@@ -37,36 +70,4 @@ export interface ResearchLabProps {
export interface CollegeProps {
colleges: CollegeItem[];
}
export interface DiscussionItem {
id: number;
title: string;
content: string;
author: StudentUser | ProfessorUser;
dateCreated: string;
comments?: number;
likes?: number;
}
export interface StudentUser {
id: number;
firstName: string;
lastName: string;
email: string;
studentType: string; // Undergraduate or Graduate
major: MajorItem; // Major
studentDescription: string; // "Actively Seeking Research Opportunities" or "Current Researcher Receiving Credit"
}
export interface ProfessorUser {
id: number;
firstName: string;
lastName: string;
email: string;
professorType: string; // Principal Investigator or Professor
researchLabName?: string;
researchLabUrl?: string;
researchLabCollege?: CollegeItem;
researchLabDescription?: string;
}
}
\ No newline at end of file
......@@ -4,10 +4,10 @@ export const baseUrl = 'http://localhost:8080/api'
export const loginAPI = `${baseUrl}/login`;
export const labsAPI = `${baseUrl}/labs`;
export const discussionsAPI = `${baseUrl}/discussions`;
export const collegesAPI = `${baseUrl}/colleges`;
export const majorsAPI = `${baseUrl}/majors`;
// ROUTES
export const welcomePagePath = '/';
export const signUpStudentPagePath = '/signup/student';
......@@ -16,4 +16,12 @@ export const loginPagePath = '/login';
export const labsPagePath = '/labs';
export const openingsPagePath = '/openings';
export const discussionPagePath = '/discussion';
export const profilePagePath = '/profile';
\ No newline at end of file
export const profilePagePath = '/profile';
export const dateTimeFormatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment