Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
22 changes: 22 additions & 0 deletions frontend-challenge/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM node:20-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci

FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev

FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build

FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["npm", "run", "start"]
88 changes: 70 additions & 18 deletions frontend-challenge/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,78 @@
# UI Code Challenge!
# Tennis court directory - frontend challenge

This small assignment will help evaluate your front end development capabilities. You will be evaluated on design choices (friction, scalability, etc), efficient and effective coding, and style.
## Overview

## Challenge
This is a two-page React/TypeScript application built within a 4-hour time frame.

Create a mobile first, two page app for reviewing tennis courts. A user should be able to see a display of courts, search for a specific court, select a court detail view, and leave a review.
- Page 1: Directory of tennis courts with search feature.
- [home page demo video](https://photos.app.goo.gl/qTM4Sj5SRdZR1B1S8)

## Rules
- Page 2: Detailed information about a selected court including description and reviews.
- [details page demo video](https://photos.app.goo.gl/vFNeMVZgW1eHq56S9)

1. Pull this repo locally and work on your own branch
2. Maximum time is 4 hours
3. You will only be evaluated on how it looks on mobile device sizes
4. No backend, all data will be mocked
5. You do not need to write tests for this exercise given the time limit
6. When you are done, submit a PR to this repo.
My approach was to build a minimal viable product (MVP) first, strictly meeting the core requirements of a 2 page application with a searchable list of all tennis courts and a details page for each with the ability to submit a review. Remaining time was allocated to implementing the review display. To ensure a complete application within the allotted 4 hours, I planned out each component in advance in a detailed visual flow chart.

## Hints
## Teck stack

- Do not use frameworks outside of the JavaScript/Typescript ecosystem
- Submissions using React Native are preferred but React, Next, or pure JavaScript are acceptable
- Bonus points if you mock > 50 courts as this will let you show off your scalable design skills
- You can use coding assistants, but include every prompt you used in your PR
- Your job is to delight users
- Frontend framework: React v?? with Typescript
- Routing: React Router
- Styling: TailwindCSS
- Build tool: vite

Good luck!
## Features

- Court directory: A homepage listyall available tennis courts with basic info.
- Search: filter list of courts by keywords present in their names.
- Court details view: provides additional information regarding an individual tennis court such as price, location and description
- Review submission: add a review for a particular court and see it instantly on the page.
- Ratings: visual breakdown of ratings and total score.

## Getting started

Run these commands in the terminal:

1. `cd frontend-challenge`
2. `cd ui-code-challenge-submission`
3. `npm install`
4. `npm run dev`

This app should be running on localhost:5173 (or as configured by Vite).

## Design decisions

### Styling choice: TailwindCSS

I initially considered React-Bootstrap, Bulma and TailwindCSS.

- React-Bootstrap: component based, robust, and well supported but less flexible.
- Bulma: lightweight and easy to use but larger file sizes for small projects.
- TailwindCSS: Choosen because it is lightweight, highly customizable, optimized for production and already included in React Router.

### Data simulation

Instead of setting up a backend or mock server, I simulated data with 2 plain typescript files:

- mockCourtData.ts: exports an array of of objects mimicking a JSON structure.
- mockReviewData.ts: exports and object where each key is a id used in courts.ts and the value is a array of objects representing reviews for that associated tennis court. This structure was chosen to reflect how this data might be stored and efficiently retrieved in a database.

### Review ratings

- The star ratings breakdown display and for each review are hardcoded static data. This is because making them dynamic would take additional time and is not a requirement for this challenge.
- The review submission form, by extension, does not include the ability to provide a rating. This prevents a useless ui element that will not match stored data.

### IDs

- Consists of hardcoded data uses sequential numbers represented as strings.
- Submitted reviews use 6 digit randomly generated numbers represented as strings to avoid duplication and tracking ids.
- For a product to be deployed id generation should happen on the backend using longer more complex UUIDs that check for conflict.

## Future Enhancements

- Dynamic ratings calculation and ability to submit a rating with a review
- Backend or API integration for persistent storage.
- More robust ID generation (UUIDs).
- Add pagination for lists of tennis courts and reviews.
- Add account authentication to prevent spamming and misuse.
- Include multiple image displays (such as carousels) to details view.
- Include website and contact info in details view.
- And much more!
81 changes: 81 additions & 0 deletions frontend-challenge/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Logs
Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostics reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
.lcov

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency
node_modules/
jspm_packages/
/.pnp
/.pnp.js

# Production
/build

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test
.env.production
.env.local
.env.development.local
.env.test.local
.env.production.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# misc
.DS_Store
28 changes: 28 additions & 0 deletions frontend-challenge/app/DetailsPage/DetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* DetailsPage component displays detailed information about a specific court
*
* @param {string} courtId - The ID of the court to display details for.
* @returns {JSX.Element} The details page component.
* @public
*/

import { Navigate } from "react-router";

import { ItemDetails } from "./ItemDetails";
import { ReviewSection } from "../ReviewSection/ReviewSection";
import { data } from "../../data/mockCourtData";

export function DetailsPage({ courtId }: { courtId: string }) {
const courtData = data.tennisCourts.find((el) => el.id === courtId);

if (!courtData) {
return <Navigate to="/404" replace />;
}

return (
<div className="max-w-[85rem] px-4 sm:px-6 lg:px-8 mx-auto">
<ItemDetails courtData={courtData} />
<ReviewSection courtId={courtId} />
</div>
);
}
81 changes: 81 additions & 0 deletions frontend-challenge/app/DetailsPage/ItemDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* A React component that displays detailed information about a tennis court.
*
* @param {TennisCourtType} courtData - The data for the tennis court to display details for.
* @returns {JSX.Element} - styled section with details of specified tennis court.
* @public
*/

import { type TennisCourtType } from "../../data/mockCourtData";

export function ItemDetails({ courtData }: { courtData: TennisCourtType }) {
return (
<div className="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-6">
<div className="lg:col-span-2">
<div className="py-8 lg:pe-8">
<div className="space-y-5 lg:space-y-8">
<h2 className="text-3xl font-bold lg:text-5xl dark:text-white">
{courtData.name}
</h2>
<img
className="w-full object-cover rounded-xl"
src={courtData.image}
alt="Tennis court"
/>
<dl className="flex flex-col sm:flex-row">
<dt className="min-w-40">
<span className="block text-sm text-gray-500 dark:text-neutral-500">
Price:
</span>
</dt>
<dd>
<span className="block text-sm font-medium text-gray-800 dark:text-neutral-200">
$$
</span>
</dd>
</dl>
<dl className="flex flex-col sm:flex-row gap-1">
<dt className="min-w-40">
<span className="block text-sm text-gray-500 dark:text-neutral-500">
Location:
</span>
</dt>
<dd>
<span className="block text-sm font-medium text-gray-800 dark:text-neutral-200">
{courtData.location}
</span>
</dd>
</dl>
<dl className="flex flex-col sm:flex-row gap-1">
<dt className="min-w-40">
<span className="block text-sm text-gray-500 dark:text-neutral-500">
Hours of Operation:
</span>
</dt>
<dd>
<span className="block text-sm font-medium text-gray-800 dark:text-neutral-200">
{courtData.daysOpen} * {courtData.hours}
</span>
</dd>
</dl>
<dl className="flex flex-col sm:flex-row gap-1">
<dt className="min-w-40">
<span className="block text-sm text-gray-500 dark:text-neutral-500">
Instructors Available:
</span>
</dt>
<dd>
<span className="block text-sm font-medium text-gray-800 dark:text-neutral-200">
{courtData.hasInstructors ? "Yes" : "None"}
</span>
</dd>
</dl>
<p className="text-lg text-gray-800 dark:text-neutral-200">
{courtData.description}
</p>
</div>
</div>
</div>
</div>
);
}
32 changes: 32 additions & 0 deletions frontend-challenge/app/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* The HomePage component rendering the header and body of the homepage.
*
* @param null
* @returns {JSX.Element} - The rendered HomePage component.
* @public
*/

import { useState } from "react";

import { HomePageBody } from "./HomePageBody";
import { HomePageHeader } from "./HomePageHeader";

import { data } from "../../data/mockCourtData";
import { type TennisCourtType } from "../../data/mockCourtData";

export function HomePage() {
const [tennisCourts, setTennisCourts] = useState(data.tennisCourts);

const updateTennisCourts = (value: TennisCourtType[]) =>
setTennisCourts(value);

return (
<div>
<HomePageHeader
courts={data.tennisCourts}
onUpdateCourts={updateTennisCourts}
/>
<HomePageBody courts={tennisCourts} />
</div>
);
}
24 changes: 24 additions & 0 deletions frontend-challenge/app/HomePage/HomePageBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* HomePageBody component that displays a grid of tennis court cards.
*
* @param {TennisCourtType[]} courts - Array of tennis court objects to be displayed in the body.
* @returns {JSX.Element} - A grid layout of tennis court cards or a message if no results are found.
* @public
*/

import { type TennisCourtType } from "../../data/mockCourtData";
import { HomePageCourtCard } from "./HomePageCourtCard";

export function HomePageBody({ courts }: { courts: TennisCourtType[] }) {
if (courts.length === 0) return <div>No results found</div>;

return (
<div className="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{courts.map((court, index) => (
<HomePageCourtCard key={index} court={court} />
))}
</div>
</div>
);
}
Loading