diff --git a/new-dti-website/components/courses/DDProjects.tsx b/new-dti-website/components/courses/DDProjects.tsx new file mode 100644 index 000000000..f3fe78184 --- /dev/null +++ b/new-dti-website/components/courses/DDProjects.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; + +// *INTERFACE +interface DDProjectsProps { + title: string; + description: string; + imageSrc: string; +} + +/** + * `DDProjects` Component - Displays information about past student Projects within Trends :) + * + * @remarks + * This component is used to present information about student projects, including the title, + * description, and an image representing the project that they did in Trends class. There is an interactive expand/collapse + * functionality, allowing the user to toggle additional details with a smooth transition effect. + * The card's background color changes based on its state (open/closed). The component is also responsive to screen size. + * + * The component is designed to receive data via props and a json object. + * + * @param props - Contains: + * - `title`: The title of the project, displayed prominently at the top of the card. + * - `description`: A brief description of the project, revealed when the card is expanded. + * - `imageSrc`: The URL for the image that represents the project, displayed in the expanded view. + */ +export default function DDProjects({ title, description, imageSrc }: DDProjectsProps) { + const [isOpen, setIsOpen] = useState(false); + + const toggleCard = () => { + setIsOpen(!isOpen); + }; + + return ( +
+
+

+ {title} +

+ +
+ + {/* Smooth transition for the Additional Content onClick */} +
+
+

{description}

+ {title} +
+
+
+ ); +} diff --git a/new-dti-website/components/courses/Experiences.tsx b/new-dti-website/components/courses/Experiences.tsx index 7dc824f9e..ecf61e248 100644 --- a/new-dti-website/components/courses/Experiences.tsx +++ b/new-dti-website/components/courses/Experiences.tsx @@ -1,12 +1,27 @@ import React from 'react'; import Image from 'next/image'; +// * INTERFACE interface IconProps { icon: string; title: string; description: string; } +/** + * `Experiences` Component - Displays key experiences for students in Trends :) + * + * @remarks + * This component is used to highlight three core experiences students will get from Trends, + * including best practices, deploying web applications, and completing a final project. Each experience consists + * of an icon, a title, and a brief description. The component adapts its layout based on screen size, with responsive + * text sizes and image scaling. + * + * @param props - Contains: + * - `icon`: The URL path to the icon image associated with the experience. + * - `title`: The title of the key experience. + * - `description`: A short description about the past student's projects. + */ export default function Experiences({ icon, title, description }: IconProps) { return ( <> @@ -18,11 +33,11 @@ export default function Experiences({ icon, title, description }: IconProps) { height={150} alt={icon} unoptimized - className="w-24 md:w-[35%]" + className="w-24 md:w-[30%]" /> -
{title}
+
{title}
-
{description}
+
{description}
); diff --git a/new-dti-website/components/courses/Projects.tsx b/new-dti-website/components/courses/Projects.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/new-dti-website/components/courses/TestimonialCard.tsx b/new-dti-website/components/courses/TestimonialCard.tsx new file mode 100644 index 000000000..8471bdd8c --- /dev/null +++ b/new-dti-website/components/courses/TestimonialCard.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +// * INTERFACE +interface TestimonialCardProps { + description: string; + name: string; + semesterTaken: string; +} + +/** + * `TestimonialCard` Component - Displays a single testimonial from a student who has done Trends. + * + * @remarks + * This component is used to present testimonials from students about their experiences taking the course. It showcases + * a brief description of the testimonial, the name of the student (or anonymous), and the semester in which they took the class. + * + * @param props - Contains: + * - `description`: The student's written testimonial about the course. + * - `name`: The name of the student who gave the testimonial, or "Anonymous" if the student prefers not to disclose their name. + * - `semesterTaken`: The semester when the student took the course. + */ +export default function TestimonialCard({ + description, + name, + semesterTaken +}: TestimonialCardProps) { + return ( +
+
❛❛
+

{description}

+
{name}
+
{semesterTaken}
+
+ ); +} diff --git a/new-dti-website/components/courses/Testimonials.tsx b/new-dti-website/components/courses/Testimonials.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/new-dti-website/components/courses/Timeline.tsx b/new-dti-website/components/courses/Timeline.tsx index e69de29bb..3dd6c8324 100644 --- a/new-dti-website/components/courses/Timeline.tsx +++ b/new-dti-website/components/courses/Timeline.tsx @@ -0,0 +1,219 @@ +import React, { useLayoutEffect, useRef, useState } from 'react'; + +// * TYPES +export type Event = { + title: string; + date: string; + time: string; +}; + +// * INTERFACE +type TimelineProps = { + events: Event[]; + currentDate: Date; +}; + +/** + * `Timeline` Component - Displays a chronological timeline of Trends events. + * + * @remarks + * This component is used to render the timeline with all the events. The timeline automatically adjusts based on screen size + * (mobile vs. desktop) via a useEffect and displays a progress line showing how far along the current date is relative to the events. + * Each event includes a title, date, and time. The date string should be formatted with the month (abbreviated), day, and + * optionally, the time (formatted as `hh:mm AM/PM`). If no time is provided, the default will be 12:00 AM. + * If no Year is provided, the default will be the Current Year + * + * @param props - Contains: + * - `events`: An array of event objects. Each object should contain: + * - `title`: The name of the event. + * - `date`: The date of the event in the format `MMM DD, YYYY` (e.g., "Feb 19, 2024"). MONTH NOT REQUIRED TO BE ABBREVIATED both Feb and February are valid + * - `time`: (Optional) The time of the event in the format `hh:mm AM/PM` (e.g., "12:00 PM EST"). + * - `currentDate`: A `Date` object representing the current date and time, used to calculate the progress through the timeline. + */ +export default function Timeline({ events, currentDate }: TimelineProps) { + const firstEventRef = useRef(null); + const lastEventRef = useRef(null); + const [lineLength, setLineLength] = useState(0); + const [isMobile, setIsMobile] = useState(false); + + /** + * Calculates the length of the line connecting the first and last events. + * + * @remarks + * This function determines the position of the first and last event elements + * on the page and calculates the distance between them, either vertically + * (on mobile) or horizontally (on desktop). It updates the line length state accordingly. + * + * @returns A number representing the length of the line in pixels. + * + * @example + * ```ts + * calculateLineLength(); + * ``` + */ + const calculateLineLength = () => { + if (events.length === 1 || events.length === 0) { + setLineLength(0); + return; + } + if (firstEventRef.current && lastEventRef.current) { + const firstPos = firstEventRef.current.getBoundingClientRect(); + const lastPos = lastEventRef.current.getBoundingClientRect(); + + const firstCenter = { + x: firstPos.left + firstPos.width / 2, + y: firstPos.top + firstPos.height / 2 + }; + + const lastCenter = { + x: lastPos.left + lastPos.width / 2, + y: lastPos.top + lastPos.height / 2 + }; + + if (isMobile) { + const verticalDistance = Math.abs(lastCenter.y - firstCenter.y); + setLineLength(verticalDistance); + } else { + const horizontalDistance = Math.abs(lastCenter.x - firstCenter.x); + setLineLength(horizontalDistance); + } + } + }; + + /** + * Parses an event's date and time to a `Date` object. + * + * @remarks + * This function converts a date string (formatted as `MMM DD`) and an optional time string (formatted as `hh:mm AM/PM`) + * into a JavaScript `Date` object. If no time is provided, the default time is 12:00 AM. The year is automatically + * set to the current year if no year is provided, so ensure that the date values are formatted properly. + * + * @param dateStr - The date string of the event in the format `MMM DD, YYYY` (e.g., "Feb 19, 2024"). MONTH NOT REQUIRED TO BE ABBREVIATED + * @param timeStr - (Optional) The time of the event in the format `hh:mm AM/PM` (e.g., "12:00 PM EST"). + * + * @returns A `Date` object corresponding to the event's date and time. + * + * @example + * ```ts + * const eventDate = parseEventDate("Feb 19", "7:30 PM"); + * ``` + */ + const parseEventDate = (dateStr: string, timeStr: string) => { + const currentYear = new Date().getFullYear(); + let date = new Date(`${dateStr} ${currentYear} ${timeStr || '12:00 AM'}`); + return date; + }; + + /** + * Calculates the percentage of progress through the events based on the current date. + * + * @remarks + * This function sorts the events by date, calculates the total time span between the first + * and last events, and determines how far along the current date is within that time span. + * The result is a percentage that is used to fill the progress line on the timeline. + * + * @returns A number representing the percentage of progress (from 0 to 100). + * + * @example + * ```ts + * const progressPercentage = getProgressPercentage(); + * ``` + */ const getProgressPercentage = () => { + const sortedEvents = events.sort((a, b) => { + const aDate = parseEventDate(a.date, a.time); + const bDate = parseEventDate(b.date, b.time); + return aDate.getTime() - bDate.getTime(); + }); + + const firstEventDate = parseEventDate(sortedEvents[0].date, sortedEvents[0].time); + const lastEventDate = parseEventDate( + sortedEvents[sortedEvents.length - 1].date, + sortedEvents[sortedEvents.length - 1].time + ); + + const totalTimeSpan = lastEventDate.getTime() - firstEventDate.getTime(); + const timeElapsed = Math.max(0, currentDate.getTime() - firstEventDate.getTime()); + const progress = (timeElapsed / totalTimeSpan) * 100; + + return Math.min(100, Math.max(0, progress)); + }; + + const progressPercentage = getProgressPercentage(); + + // * Use Layout Effect to Handle resizing of the Timeline + useLayoutEffect(() => { + const handleResize = () => { + const mobile = window.innerWidth < 640; + setIsMobile(mobile); + console.log(isMobile); + calculateLineLength(); + console.log(lineLength); + }; + + handleResize(); + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [isMobile]); + + return ( + <> +
+
+
+
+
+ + {events.map((event, idx) => { + const eventDate = parseEventDate(event.date, event.time); + const isPast = eventDate < currentDate; + + return ( +
+ {/* Red Dot for Completed / Grey Dot for Mobile */} +
+ {/* Title and Date */} +
+

{event.title}

+
+

{event.date}

+

{event.time}

+
+
+ {/* Red Dot for Completed / Grey Dot for Tablet/Laptop */} + + ); + })} +
+ + ); +} diff --git a/new-dti-website/components/courses/data/student_projects.json b/new-dti-website/components/courses/data/student_projects.json new file mode 100644 index 000000000..4d964c053 --- /dev/null +++ b/new-dti-website/components/courses/data/student_projects.json @@ -0,0 +1,24 @@ +{ + "student_projects": [ + { + "title": "Project Name1", + "description": "This project is about blah blah blah blah blah blah blah blah blah blah blah blah blah.", + "imageSrc": "https://via.placeholder.com/400" + }, + { + "title": "Project Name2", + "description": "This project is about blah blah blah blah blah blah blah blah blah blah blah blah blah.", + "imageSrc": "https://via.placeholder.com/400" + }, + { + "title": "Project Name3", + "description": "This project is about blah blah blah blah blah blah blah blah blah blah blah blah blah.", + "imageSrc": "https://via.placeholder.com/400" + }, + { + "title": "Project Name4", + "description": "This project is about blah blah blah blah blah blah blah blah blah blah blah blah blah.", + "imageSrc": "https://via.placeholder.com/400" + } + ] +} diff --git a/new-dti-website/components/courses/data/testimonials.json b/new-dti-website/components/courses/data/testimonials.json new file mode 100644 index 000000000..671fcdf7c --- /dev/null +++ b/new-dti-website/components/courses/data/testimonials.json @@ -0,0 +1,39 @@ +{ + "testimonials": [ + { + "description": "It was so great yeah you should take it like it was crazy like all the best things in the world would want to go take the class like I feel enlightened", + "name": "NAME/ANONYMOUS 1", + "semesterTaken": "Fall 2023" + }, + { + "description": "It was so great yeah you should take it like it was crazy like all the best things in the world would want to go take the class like I feel enlightened", + "name": "NAME/ANONYMOUS 1", + "semesterTaken": "Fall 2023" + }, + { + "description": "It was so great yeah you should take it like it was crazy like all the best things in the world would want to go take the class like I feel enlightened", + "name": "NAME/ANONYMOUS 1", + "semesterTaken": "Fall 2023" + }, + { + "description": "It was so great yeah you should take it like it was crazy like all the best things in the world would want to go take the class like I feel enlightened", + "name": "NAME/ANONYMOUS 1", + "semesterTaken": "Fall 2023" + }, + { + "description": "It was so great yeah you should take it like it was crazy like all the best things in the world would want to go take the class like I feel enlightened", + "name": "NAME/ANONYMOUS 1", + "semesterTaken": "Fall 2023" + }, + { + "description": "It was so great yeah you should take it like it was crazy like all the best things in the world would want to go take the class like I feel enlightened", + "name": "NAME/ANONYMOUS 1", + "semesterTaken": "Fall 2023" + }, + { + "description": "It was so great yeah you should take it like it was crazy like all the best things in the world would want to go take the class like I feel enlightened", + "name": "NAME/ANONYMOUS 1", + "semesterTaken": "Fall 2023" + } + ] +} diff --git a/new-dti-website/components/courses/data/timeline_events.json b/new-dti-website/components/courses/data/timeline_events.json new file mode 100644 index 000000000..1d8d929a3 --- /dev/null +++ b/new-dti-website/components/courses/data/timeline_events.json @@ -0,0 +1,22 @@ +[ + { + "title": "Sign-up Deadline", + "date": "Feb 16", + "time": "12:00 PM EST" + }, + { + "title": "First Day", + "date": "Feb 19", + "time": "7:30 PM EST" + }, + { + "title": "Final Project", + "date": "April 12", + "time": "" + }, + { + "title": "Project Presentation", + "date": "November 26, 2024", + "time": "" + } +] diff --git a/new-dti-website/components/courses/data/trend_instructors.json b/new-dti-website/components/courses/data/trend_instructors.json new file mode 100644 index 000000000..833391789 --- /dev/null +++ b/new-dti-website/components/courses/data/trend_instructors.json @@ -0,0 +1,64 @@ +{ + "trend_instructors": [ + { + "lastName": "Wang", + "hometown": "", + "github": "https://github.com/oscarwang20", + "website": "", + "formerSubteams": [], + "role": "developer", + "minor": "", + "doubleMajor": "", + "netid": "ow39", + "about": "", + "linkedin": "", + "firstName": "Oscar", + "major": "", + "graduation": "", + "pronouns": "", + "subteams": ["idol"], + "roleDescription": "Developer", + "email": "ow39@cornell.edu" + }, + { + "lastName": "Wang", + "hometown": "", + "github": "https://github.com/oscarwang20", + "website": "", + "formerSubteams": [], + "role": "developer", + "minor": "", + "doubleMajor": "", + "netid": "ow39", + "about": "", + "linkedin": "", + "firstName": "Oscar", + "major": "", + "graduation": "", + "pronouns": "", + "subteams": ["idol"], + "roleDescription": "Developer", + "email": "ow39@cornell.edu" + }, + { + "lastName": "Wang", + "hometown": "", + "github": "https://github.com/oscarwang20", + "website": "", + "formerSubteams": [], + "role": "developer", + "minor": "", + "doubleMajor": "", + "netid": "ow39", + "about": "", + "linkedin": "", + "firstName": "Oscar", + "major": "", + "graduation": "", + "pronouns": "", + "subteams": ["idol"], + "roleDescription": "Developer", + "email": "ow39@cornell.edu" + } + ] +} diff --git a/new-dti-website/components/team/MemberDisplay.tsx b/new-dti-website/components/team/MemberDisplay.tsx index e55b83378..39d9f5be0 100644 --- a/new-dti-website/components/team/MemberDisplay.tsx +++ b/new-dti-website/components/team/MemberDisplay.tsx @@ -89,6 +89,7 @@ const MemberDisplay: React.FC = () => { selectedMember={selectedMember} selectedRole={selectedRole} memberDetailsRef={memberDetailsRef} + isCard={false} /> ); })} diff --git a/new-dti-website/components/team/MemberGroup.tsx b/new-dti-website/components/team/MemberGroup.tsx index 2eeb0e2c3..a377ff5ad 100644 --- a/new-dti-website/components/team/MemberGroup.tsx +++ b/new-dti-website/components/team/MemberGroup.tsx @@ -242,6 +242,7 @@ type MemberGroupProps = { selectedMember: IdolMember | undefined; selectedRole: string; memberDetailsRef: RefObject; + isCard: boolean; }; const MemberGroup: React.FC = ({ @@ -251,7 +252,8 @@ const MemberGroup: React.FC = ({ setSelectedMember, selectedMember, selectedRole, - memberDetailsRef + memberDetailsRef, + isCard }) => { const selectedMemberIndex: number = useMemo( () => (selectedMember ? members.indexOf(selectedMember) : -1), @@ -295,16 +297,9 @@ const MemberGroup: React.FC = ({ const onCloseMemberDetails = () => setSelectedMember(undefined); return ( - (selectedRole === roleName || selectedRole === 'Full Team') && ( -
-

{`${roleName} ${ - roleName !== 'Leads' ? '' : 'Team' - }`}

-

{description}

-
+ <> + {isCard ? ( +
{members.map((member, index) => ( <> = ({ ))}
-
- ) + ) : ( + (selectedRole === roleName || selectedRole === 'Full Team') && ( +
+

{`${roleName} ${ + roleName !== 'Leads' ? '' : 'Team' + }`}

+

{description}

+
+ {members.map((member, index) => ( + <> + + setSelectedMember(member === selectedMember ? undefined : member) + } + cardState={selectedMember ? index - selectedMemberIndex : undefined} + /> + {selectedMember && canInsertMemberDetails(index) && ( +
+ +
+ )} + + ))} +
+
+ ) + )} + ); }; diff --git a/new-dti-website/src/app/courses/page.tsx b/new-dti-website/src/app/courses/page.tsx index cb514a32c..ecae222fc 100644 --- a/new-dti-website/src/app/courses/page.tsx +++ b/new-dti-website/src/app/courses/page.tsx @@ -1,176 +1,293 @@ 'use client'; // *IMPORTS import Image from 'next/image'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; -// *DATA +// *IMPORT DATA import experiencesData from '../../../components/courses/data/key_experiences.json'; +import timelineData from '../../../components/courses/data/timeline_events.json'; +import testimonialData from '../../../components/courses/data/testimonials.json'; +import studentProjectData from '../../../components/courses/data/student_projects.json'; +import trendsData from '../../../components/courses/data/trend_instructors.json'; +import teamRoles from '../../../components/team/data/roles.json'; -// *COMPONENTS +// *IMPORT COMPONENTS import RedBlob from '../../../components/blob'; import Experiences from '../../../components/courses/Experiences'; +import Timeline, { Event } from '../../../components/courses/Timeline'; +import MemberGroup from '../../../components/team/MemberGroup'; +import TestimonialCard from '../../../components/courses/TestimonialCard'; +import DDProjects from '../../../components/courses/DDProjects'; -const { key_experiences } = experiencesData; +//* DATA +const key_experiences = experiencesData.key_experiences; +const timeline_events: Event[] = timelineData; +const testimonials = testimonialData.testimonials; +const student_projects = studentProjectData.student_projects; +const trend_instructors = trendsData.trend_instructors as IdolMember[]; +import { populateMembers } from '../../../src/utils'; +// * BEGIN COURSES COMPONENT export default function Courses() { const trendsLogoRef = useRef(null); + const [selectedRole, setSelectedRole] = useState('Full Team'); + const [selectedMember, setSelectedMember] = useState(undefined); + + const memberDetailsRef = useRef(null); + + const roles = populateMembers( + teamRoles as { + [key: string]: { + roleName: string; + description: string; + members: IdolMember[]; + order: string[]; + color: string; + }; + }, + trend_instructors + ); useEffect(() => { const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting && trendsLogoRef.current) { - trendsLogoRef.current.classList.add('sticker-animate'); - } + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('fade-in-animate'); + } + }); }, - { threshold: 0.5 } + { threshold: 0.9 } ); - if (trendsLogoRef.current) { - observer.observe(trendsLogoRef.current); - } + const sections = document.querySelectorAll('section'); + sections.forEach((section) => { + section.classList.add('fade-in'); + observer.observe(section); + }); return () => { - if (trendsLogoRef.current) { - observer.unobserve(trendsLogoRef.current); - } + sections.forEach((section) => { + observer.unobserve(section); + }); }; }, []); return ( <> - {/* Hero Section */} -
-
+
{ + const target = event.target as HTMLElement; + if ( + !(target.id === 'memberCard' || target.parentElement?.id === 'memberCard') && + !memberDetailsRef.current?.contains(target) + ) + setSelectedMember(undefined); + }} + > + {/* Hero Section */} +
-
-

+
+

- OUR -

-

- COURSE -

-
+ > + COURSE +

+
-
-

- Teaching the{' '} - community -

-

- A project team is meant, above all, to be a learning experience. Given our mission - of community impact, we want - to help everyone learn and grow{' '} - through our training course in{' '} - product development.{' '} -

+
+

+ Teaching the{' '} + community +

+

+ A project team is meant, above all, to be a learning experience. Given our mission + of{' '} + + community impact + + , we want to help everyone{' '} + learn and grow through + our training course in{' '} + product development.{' '} +

+
+
+
+
-
- -
-
-
- - {/* LOGO SECTION */} - + + {/* WRAPPER */} +
+ {/* LOGO SECTION */} + -
- Trends in Web Development + {/* KEY EXPERIENCES SECTION */} +
+
+ {key_experiences.map((experiences) => ( + + ))}
+
-
- Trends in Web Development in a 2-credit S/U course that showcase modern full-stack - development and best practices used within industry. We cover technologies like - TypeScript, React, Node.js, Firebase, Express and more, all of which are deployed at - scale by leading tech companies + {/* TIMELINE SECTION */} +
+
+
+
-
- - + {/* TODO: COURSE STAFF SECTION*/} +
+
+
+ Course Staff +
+
+ {Object.keys(roles).map((role) => { + const value = roles[role as Role]; + if (role === 'tpm' || role === 'dev-advisor') return <>; + return ( + + ); + })} +
-
-
- - - {/* TODO: KEY EXPERIENCES SECTION */} -
-
- {key_experiences.map((experiences) => ( - - ))} +
+ + {/* PAST STUDENT EXPERIENCES SECTION*/} +
+
+
+ Past Student Experiences +
+
+
+ {testimonials.map((testimonial, index) => ( + + ))} +
+
+
+
+ + {/* PAST STUDENT PROJECTS SECTION*/} +
+
+
+ Past Student Projects +
+
+ With the right skills, you will be able to create projects like ours. +
+
+ {student_projects.map((project) => ( + + ))} +
+
+
- - - {/* TODO: TIMELINE SECTION */} -
- - {/* TODO: COURSE STAFF SECTION*/} -
- - {/* TODO: PAST STUDENT EXPERIENCES SECTION*/} -
- - {/* TODO: PAST STUDENT PROJECTS SECTION*/} -
- - {/* STYLING SECTION */} - +
); }