Final Project
This semester, we've learned some great tools for web development. Now, we will put them all together to make an amazing final project!
Outline
This project will involve building a React web app complete with a connection to a Firestore database.
You will be working in groups of 3 or 4 to complete this final project. Your method of collaboration is up to you, but using GitHub is strongly advised.
Requirements
* Firebase authentication system
* Firestore database connection on Frontend
    * Create, Read, Update, Delete operations
* Use conditional rendering (at least once)
* Use ES6 syntax (includes arrow functions, spreading, destructuring)
* Use Functional programming (map/filter/reduce)
* Use React Hooks
* Use TypeScript (and code is well typed, so no any)
* Use Next.js with at least 2 pages
Suggested Project Ideas
* Picnic items
* Clicker game (more involved)
* Blog
* Forum
* Wiki
* Songs browsing website
* Social network
* School management DB
You are not limited to these ideas, they are just some things we've seen in the past that might help you get started!
Milestones
The project will be divided into a number of milestones to help you progress in a reasonable way.
Your remaining slip days are still valid for this. The group's allotted slip days is the max of your group members'. You are allowed to use at most 3 slip days per each of Milestones 1, 2, 3.
Team Matching Form
Due: April 19 by 11:59 PM (no slip days)
Please fill out this team matching form by Wednesday, April 19th. This is a hard deadline because we want to assign group members the same day so you have enough time to work on Milestone 1.
Milestone 1
Due: April 23 by 11:59 PM
Let's get started early! You'll want to really think about what you want to build and how you want to build it. This milestone is intended to help you get started on that.
Steps:
1. Create a mock of your web app. This can be a picture, or multiple pictures (handdrawn or created from a design program such as Figma etc.). Your mock should include sketches of the different pages or views in your application. You will attach these pictures to the document in step 2.
2. Create a proposal for your web app. This should be a pdf file at least 200 words long. It should contain:
* A description that outlines what your project will be about, what it does, etc.
* Technical considerations! For instance, how will you structure your database schema! (What collections will you need, how will they be related, and what documents will they store).
* A link to your GitHub repo link, if you are using GitHub (highly recommended!). Make sure the repo is public!
* The mockups you created in step 1.
Submission
Submit the PDF document to CMSX by the deadline!
Milestone 2
Due: May 3 by 11:59 PM
Time to properly get started coding!
Get started implementing the frontend, using the skills we learned in class (Next.js, CSS, etc.).
To get started, you may need to reference the materials we covered in Lecture 3.
Use hardcoded data for now as necessary just to get the layout done. These can be default values embedded in your frontend code. You will connect it to the database in the subsequent milestones.
Submit a zip file of your project to CMSX by the deadline! Along with your code, include a README containing a description of the project, a list of group members and netIDs, a link to the deployed site, a link to the GitHub repo if you used GitHub, and anything else that you think is important for us to know. This is separate from the proposal you submitted in Milestone 1.
Milestone 3
Due: May 7 by 11:59 PM
Now, it's time to connect your frontend to your backend, using the skills we learned in class (Firebase, Firestore, etc.).
Connect the app to Firebase by replacing hardcoded data with database calls.
Also work on having an authentication system set up using Firebase Auth.
Submit a zip file of your project to CMSX by the deadline! Include all the same things as in Milestone 2.
Final Project
Due: May 10 by 11:59 PM
This is the last milestone!
Finish up the authentication system on your site and deploy your website.

tip
Need help? Come to office hours and we can help you debug. Also feel free to post questions on Ed!

danger
Do not push private API keys, Firebase service accounts, or other sensitive information to GitHub. Similarly, do not push node_modules up to GitHub. If we see these in your GitHub, you will lose points.
Note that firebase configs for the frontend are fine (and should be) included in your submission.
Submit a zip file of your project to CMSX by the deadline! Include all the same things as in Milestone 2, PLUS the URL of your deployed site.
Optional Extensions
If you're looking for an added challenge, consider doing one or more of these extensions! There is no extra credit for these extensions, but it will really enrich your understanding of webdev (and make us very impressed!) :)
1. Use Material UI, Boostrap, Mantine, or another pre-fabricated component library.
2. Make your website mobile-friendly/responsive to different screen sizes. You can get started using Media Queries and Clamping in CSS, which we covered in Lecture 5.
3. Use an external API (in additional to your Firebase backend!). For instance, you could use the Spotify API to pull in song data, or the OpenWeather API to pull in weather data.
4. Implement testing using Jest and/or the React Testing Library.
5. Use Vercel's useSWR hook to implement client-side data fetching.
6. If you're a CS 3110 student, use a TypeScript Pattern-Matching Library to go further with functional programming in your frontend.
Grading
* Frontend (35%)
    * Use of ES6 & TypeScript
    * Component structure
    * Use of hooks
    * Functional programming
* Database/Backend (15%)
    * Data fetching
    * Data insertion
    * Data deletion
* Auth (10%)
    * Authentication (using Firebase auth)
* Styling (5%)
* Deployment (5%)
* Code style and file structure (5%)
* Effort (25%)
    * This may seem like an arbitrary measure, but we expect most people to get most of the points here; the only way to lose points is if it is clear that not a lot of effort has been put in or you are consistently missing milestones.
Tips for Success!
* Get in contact with your partner early!
    * Milestone 0 is intended for you to get some discussion on what you want to build before you start implementation. Make sure you are both aligned on what needs to be built to avoid issues later on. Better initial planning means less frustrations later on.
* Be realistic.
    * We know you are ambitious but also understand your own capabilities. Building something too complex may be too overwhelming. You are allowed to change ideas, but that would be time wasted on the old project.
* Use git
    * git is one of the best (and prevalent) version control systems out there. It is great for sharing code between you and your partner/team members. Please use a service like GitHub instead of emailing code back and forth to each other.
* Use branches!
    * When developing a feature, you should open up a new GitHub branch rather than committing and pushing directly to the main branch. This will allow you to develop your feature independently of the current state of main (and what your partners are doing) and only merge in when you are sure your feature is done and works.
    * Branches can also protect you from weird frustrating merge conflicts (so you can focus on developing awesome features!)
* Pair programming is fun!
    * Ideally, you should both be actively involved in the whole development process. A good way to achieve this is to step up a time to pair program and code together!



This is our web app for the final project:

Our web app is a forum for Cornell students to review dining halls on campus. This app allows users to leave ratings and reviews for different dining halls, as well as view reviews left by other users. The main page shows a gallery of the various dining halls on campus with their respective average rating, and users can click on a dining hall to view all the reviews left for that specific dining hall. The entry form page allows users to add a rating and review.

We’ll use Firestore for our database schema, and we’ll have two collections: one for dining halls and one for reviews. The dining halls collection will store each dining hall’s name, image, URL, and average rating (calculated from all the reviews left for that dining hall). Then, the reviews collection will store each review’s dining hall ID (to link the review to the correct dining hall), User ID (to link the review to the user who left it), rating (1–5 stars), and description. To link a review to the dining hall that it's for, we'll use a dining Hall ID field in the Reviews collection, and this ID will correspond to the auto-generated ID for the relevant document in the Dining Halls collection. This type of scheme will allow us to easily retrieve and display information about each dining hall and the reviews that were left for that dining hall. We can calculate the average rating by querying the Reviews collection and taking the average of all the ratings for that specific dining hall. We’ll also use Firebase authentication for user login/sign-up and also conditional rendering to display different components based on whether the user is logged in or not.


I am working on the frontend of this website. Please fix and adjust any errors in my code, organize the code, delete anything unnecessary, organize the website content, and make the frontend more aesthetically pleasing. Then, show me the new code for each file. Below are my files:


components/layout/Layout.module.css:

.container {
    display: flex;
    flex-direction: column;
    height: 100vh;
}

.header {
    background-color: #f5f5f5;
    padding: 1rem;
    display: flex;
    align-items: center;
}

.homeLink {
    text-decoration: none;
    color: #333;
    margin-left: 1rem;
}

.homeLink:hover {
    color: blue;
}

.main {
    flex: 1;
}


components/layout/Layout.tsx:

import React, { ReactNode } from 'react';
import Link from 'next/link';
import styles from './Layout.module.css';

interface LayoutProps {
    children: ReactNode;
}

const Layout: React.FC<LayoutProps> = ({ children }) => {
    return (
        <div className={styles.container}>
            <header className={styles.header}>
                <Link href="/">
                    <span>
                        <a className={styles.homeLink}>Home</a>
                    </span>
                </Link>
            </header>
            <main className={styles.main}>{children}</main>
        </div>
    );
};

export default Layout;


components/DiningHallCard.tsx:

import React from 'react';
import Link from 'next/link';
import styles from '../styles/dining-hall-card.module.css';

interface DiningHall {
    id: number;
    name: string;
    image: string;
    averageRating?: number;
}

const DiningHallCard: React.FC<{ diningHall: DiningHall }> = ({ diningHall }) => {
    const averageRating = diningHall.averageRating
        ? diningHall.averageRating.toFixed(1)
        : 'N/A';

    return (
        <Link href={`/dining-hall/${diningHall.id}`}>
            <div className={`${styles.card} ${styles.link}`}>
                <h2 className={styles.title}>{diningHall.name}</h2>
                <img
                    src={diningHall.image}
                    alt={`Image of ${diningHall.name}`}
                    className={styles.image}
                />
                <p className={styles.rating}>
                    Average Rating: {averageRating} / 5
                </p>
            </div>
        </Link>
    );
};

export default DiningHallCard;


components/Navbar.tsx:

import React from 'react';
import Link from 'next/link';

export default function Navbar() {
    return (
        <nav>
            <Link href="/">
                <a>Home</a>
            </Link>
            <Link href="/reviews">
                <a>Add Review</a>
            </Link>
        </
        nav>
    );
}


components/ReviewCard.module.css:

.card {
    background-color: #f5f5f5;
    border-radius: 5px;
    padding: 15px;
    margin: 15px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.user {
    font-weight: bold;
}

.rating {
    color: #f6ab2f;
}

.description {
    margin-top: 5px;
    font-size: 14px;
    color: #555;
}


components/ReviewCard.tsx:

import React from 'react';
import styles from './ReviewCard.module.css';

interface Review {
    userID: string;
    rating: number;
    description: string;
}

interface ReviewCardProps {
    review: Review;
}

const ReviewCard: React.FC<ReviewCardProps> = ({ review }) => {
    return (
        <div className={styles.card}>
            <p className={styles.user}>User: {review.userID}</p>
            <p className={styles.rating}>Rating: {review.rating}</p>
            <p className={styles.description}>{review.description}</p>
        </div>
    );
};

export default ReviewCard;


pages/dining-hall/[id].tsx:

import React from 'react';
import { useRouter } from 'next/router';
import ReviewCard from '../../components/ReviewCard';
import styles from '../../styles/dining-hall.module.css';

const diningHalls = [
    {
        id: '1',
        name: 'Dining Hall 1',
        imageURL: 'https://via.placeholder.com/150',
        avgRating: 4,
    },
    {
        id: '2',
        name: 'Dining Hall 2',
        imageURL: 'https://via.placeholder.com/150',
        avgRating: 4.2,
    },
    {
        id: '3',
        name: 'Dining Hall 3',
        imageURL: 'https://via.placeholder.com/150',
        avgRating: 3.8,
    },
];

const diningHallReviews = {
    '1': [
        { userID: 'user1', rating: 5, description: 'Great food!' },
        { userID: 'user2', rating: 3, description: 'It was okay.' },
    ],
    '2': [
        { userID: 'user3', rating: 4, description: 'Nice place!' },
        { userID: 'user4', rating: 5, description: 'Awesome food!' },
    ],
    '3': [
        { userID: 'user5', rating: 2, description: 'Not great.' },
        { userID: 'user6', rating: 3, description: 'Could be better.' },
    ],
};

export default function DiningHall() {
    const router = useRouter();
    const { id } = router.query;

    const diningHall = diningHalls.find((hall) => hall.id === id);

    if (!diningHall) {
        return <div>Loading...</div>;
    }

    const reviews = diningHallReviews[id] || [];

    return (
        <div>
            <h1>{diningHall.name}</h1>
            <img src={diningHall.imageURL} alt={diningHall.name} />
            <p>Average Rating: {diningHall.avgRating}</p>

            <div className={styles.reviews}>
                {reviews.map((review, index) => (
                    <ReviewCard key={index} review={review} />
                ))}
            </div>
        </div>
    );
}


pages/_app.tsx:

import type { AppProps } from 'next/app';
import Layout from '../components/layout/Layout';
import '../styles/globals.css';

function MyApp({ Component, pageProps }: AppProps) {
    return (
        <Layout>
            <Component {...pageProps} />
        </Layout>
    );
}

export default MyApp;
 
pages/index.tsx:

import Head from 'next/head';
import DiningHallCard from '../components/DiningHallCard';
import styles from '../styles/Home.module.css';

const diningHalls = [
    {
        id: 1,
        name: "Dining Hall 1",
        image: "/path/to/image1.jpg",
        averageRating: 4.5,
    },
    {
        id: 2,
        name: "Dining Hall 2",
        image: "/path/to/image2.jpg",
        averageRating: 3.7,
    },
    {
        id: 3,
        name: "Dining Hall 3",
        image: "/path/to/image3.jpg",
        averageRating: 4.2,
    },
];

export default function Home() {
    return (
        <div className={styles.container}>
            <Head>
                <title>CU Dining Halls</title>
                <meta name="description" content="CU Dining Halls Review App" />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <main className={styles.main}>
                <h1 className={styles.title}>CU Dining Halls</h1>

                <div className={styles.diningHalls}>
                    {diningHalls.map((diningHall) => (
                        <DiningHallCard key={diningHall.id} diningHall={diningHall} />
                    ))}
                </div>
            </main>
        </div>
    );
}


pages/reviews.tsx:

import React, { useState } from 'react';
import Navbar from '../components/Navbar';

export default function ReviewsPage() {
    const [rating, setRating] = useState(0);
    const [description, setDescription] = useState('');

    const handleSubmit = (e: { preventDefault: () => void; }) => {
        e.preventDefault();
    };

    return (
        <div>
            <Navbar />
            <form onSubmit={handleSubmit}>
                <label>
                    Rating:
                    <input
                        type="number"
                        min="1"
                        max="5"
                        value={rating}
                        onChange={(e) => setRating(parseInt(e.target.value))}
                    />
                </label>
                <label>
                    Review:
                    <textarea
                        value={description}
                        onChange={(e) => setDescription(e.target.value)}
                    ></textarea>
                </label>
                <button type="submit">Submit</button>
            </form>
        </div>
    );
}


styles/dining-hall-card.module.css:

.card {
    background-color: white;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    border-radius: 5px;
    padding: 20px;
    text-align: center;
    cursor: pointer;
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}

.card:hover {
    box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}

.image {
    width: 100%;
    height: 200px;
    object-fit: cover;
    border-radius: 5px;
    margin-bottom: 15px;
}

.title {
    margin: 0;
    margin-bottom: 10px;
    font-size: 1.5rem;
    font-weight: 500;
    color: #333;
}

.rating {
    margin: 0;
    font-size: 1rem;
    font-weight: 400;
    color: #666;
}


styles/dining-hall.module.css:

.reviews {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    grid-gap: 30px;
    margin-top: 30px;
}


styles/globals.css:

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');

html,
body {
    padding: 0;
    margin: 0;
    font-family: 'Roboto', sans-serif;
    background-color: #f8f9fa;
}

a {
    color: inherit;
    text-decoration: none;
}

* {
    box-sizing: border-box;
}


styles/Home.module.css:

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 50px;
}

.title {
    font-size: 36px;
    text-align: center;
    margin-bottom: 30px;
}

.diningHalls {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
    justify-items: center;
}


styles/index.module.css:

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 50px;
}

.title {
    font-size: 36px;
    text-align: center;
    margin-bottom: 30px;
}

.diningHalls {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
    justify-items: center;
}