diff --git a/.stylelintrc.json b/.stylelintrc.json index 4448120a..5a21bb89 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -2,5 +2,8 @@ "extends": [ "stylelint-config-standard-scss", "stylelint-config-prettier-scss" - ] + ], + "rules": { + "declaration-block-no-redundant-longhand-properties": null + } } diff --git a/cypress/e2e/Home.cy.ts b/cypress/e2e/HomePage.cy.ts similarity index 100% rename from cypress/e2e/Home.cy.ts rename to cypress/e2e/HomePage.cy.ts diff --git a/cypress/e2e/LocationPage.cy.ts b/cypress/e2e/LocationPage.cy.ts new file mode 100644 index 00000000..f04e42b0 --- /dev/null +++ b/cypress/e2e/LocationPage.cy.ts @@ -0,0 +1,32 @@ +/// + +export {}; + +context('Given a user is on the Location page', () => { + const baseUrl = Cypress.config('baseUrl'); + context('When the user clicks the "Back to home" link', () => { + specify('Then the user is directed to the Home page', () => { + cy.visit('/'); + cy.findByRole('heading', { + name: /Basil's on Market/, + }).click(); + cy.findByRole('link', { + name: /Back to home/, + }).click(); + cy.url().should('eq', baseUrl); + }); + }); + + context('When the user clicks the "Add a review" link', () => { + specify('Then the user is directed to the Home page', () => { + cy.visit('/'); + cy.findByRole('heading', { + name: /Basil's on Market/, + }).click(); + cy.findByRole('link', { + name: /Add a review/, + }).click(); + cy.url().should('eq', `${baseUrl}reviews/new`); + }); + }); +}); diff --git a/index.html b/index.html index 9fd09439..e3b22be2 100644 --- a/index.html +++ b/index.html @@ -14,63 +14,63 @@ - - - + + + { { {location.name} @@ -39,11 +40,11 @@ const LocationHeading = ({ location }: { location: Location }) => { const LocationAddress = ({ location }: { location: Location }) => { return ( -
+

{location.address}

{location.phone && (

- + {location.phone}

diff --git a/src/components/home/LocationCards.tsx b/src/components/HomePage/LocationCards.tsx similarity index 72% rename from src/components/home/LocationCards.tsx rename to src/components/HomePage/LocationCards.tsx index 740c18d1..ded2841f 100644 --- a/src/components/home/LocationCards.tsx +++ b/src/components/HomePage/LocationCards.tsx @@ -1,11 +1,11 @@ import { LocationCard } from './LocationCard'; -import type { Location } from '../../types/sparkeats'; +import type { Locations } from '../../types/sparkeats'; -export function LocationCards({ locations }: { locations: Location[] }) { +export function LocationCards({ locations }: { locations: Locations }) { return (
    - {locations.map((location) => ( + {Object.values(locations).map((location) => ( ))}
diff --git a/src/components/home/HomePage.tsx b/src/components/HomePage/index.tsx similarity index 100% rename from src/components/home/HomePage.tsx rename to src/components/HomePage/index.tsx diff --git a/src/components/LocationPage/LocationDetails.tsx b/src/components/LocationPage/LocationDetails.tsx new file mode 100644 index 00000000..fa52cad7 --- /dev/null +++ b/src/components/LocationPage/LocationDetails.tsx @@ -0,0 +1,37 @@ +import { Location } from '../../types/sparkeats'; + +function LocationAddress({ location }: { location: Location }) { + return ( + <> +
+

{location?.address}

+
+ {location?.phone && ( +

+ + {location?.phone} + +

+ )} + {location?.url && ( +

+ + Visit Site + +

+ )} + + ); +} + +export function LocationDetails({ location }: { location: Location }) { + return ( +
+
+

{location.reviewCountText}

+
Average Stars
+
+ +
+ ); +} diff --git a/src/components/LocationPage/LocationHeader.tsx b/src/components/LocationPage/LocationHeader.tsx new file mode 100644 index 00000000..0ee60dd8 --- /dev/null +++ b/src/components/LocationPage/LocationHeader.tsx @@ -0,0 +1,15 @@ +import { Location } from '../../types/sparkeats'; + +export function LocationHeader({ location }: { location: Location }) { + return ( +
+
+

{location.name}

+

+ {location.city}, {location.region} +

+
+
+
+ ); +} diff --git a/src/components/LocationPage/LocationReviews.tsx b/src/components/LocationPage/LocationReviews.tsx new file mode 100644 index 00000000..d6dac5c8 --- /dev/null +++ b/src/components/LocationPage/LocationReviews.tsx @@ -0,0 +1,48 @@ +import { Link } from 'react-router-dom'; +import { Location, Review } from '../../types/sparkeats'; + +export function LocationReviews({ + location, + reviews = [], +}: { + location: Location; + reviews: Review[]; +}) { + return ( +
+ + Add a review + + + {reviews.map((review: Review) => ( +
+

{review.reviewerName}

+
+ {/* Created year/month/day*/} +
+
+ Stars +
+

Comments

+

{review.text}

+ {review.imageURL && ( +
+ {review.imageDescription} +
+ )} +
+ ))} +
+ ); +} diff --git a/src/components/LocationPage/index.tsx b/src/components/LocationPage/index.tsx new file mode 100644 index 00000000..09c8d01e --- /dev/null +++ b/src/components/LocationPage/index.tsx @@ -0,0 +1,30 @@ +import { useLocation as useWindowLocation } from 'react-router-dom'; +import { Link } from 'react-router-dom'; +import { LocationHeader } from './LocationHeader'; +import { LocationDetails } from './LocationDetails'; +import { LocationReviews } from './LocationReviews'; +import { useLocations } from '../../useLocations'; + +export function LocationPage() { + const { + state: { id }, + } = useWindowLocation(); + + const locations = useLocations(); + + const location = locations[id]; + + return ( +
+
+ + + Back to home + +
+ + + +
+ ); +} diff --git a/src/locations.ts b/src/locations.ts index 3f243991..6d3ba71e 100644 --- a/src/locations.ts +++ b/src/locations.ts @@ -9,7 +9,7 @@ import legacyPlaces from '../data/place.json'; import legacyReviews from '../data/review.json'; import legacyPlaceImages from '../data/placeImage.json'; import legacyReviewImages from '../data/reviewImage.json'; -import { Location, Review } from './types/sparkeats'; +import { Locations, Location, Review } from './types/sparkeats'; type LegacyPlace = { createdAt: number; @@ -47,7 +47,18 @@ type LegacyReview = { placeId: number; }; -function getImageURL( +function getReviewImageURL( + imagePath: string, + imageID: string, + legacyImages: LegacyImage[] +) { + const imageName = legacyImages.find( + (image) => image.id.toString() === imageID + )?.fd; + return imageName ? `${imagePath}${imageName}` : null; +} + +function getLocationImageURL( imagePath: string, imageID: string, legacyImages: LegacyImage[] @@ -81,7 +92,7 @@ function transformReview({ id, reviewerName, text, - imageURL: getImageURL('img/reviews/', imageID, legacyReviewImages), + imageURL: getReviewImageURL('/img/reviews/', imageID, legacyReviewImages), imageDescription: getImageDescription(reviewImageAlt), starRating, placeID, @@ -94,35 +105,58 @@ function getReviews(placeID: number) { .map(transformReview); } -function transformLocations(legacyPlaces: LegacyPlace[]): Location[] { - return legacyPlaces.map( - ({ - id, - placeName: name, - city, - state: region, - address, - phone, - placeURL: url, - placeImage: imageID, - placeImageAlt, - }) => { - return { +function getReviewCountText(reviewCount: number): string { + return reviewCount !== 1 ? `${reviewCount} Reviews` : `${reviewCount} Review`; +} + +function mapLocation( + locationMap: { [key: string]: Location }, + location: Location +): { [key: string]: Location } { + return { + [location.id]: location, + ...locationMap, + }; +} + +function transformLocations(legacyPlaces: LegacyPlace[]): Locations { + return legacyPlaces + .map( + ({ id, - name, + placeName: name, city, - region, - country: '', // TODO + state: region, address, phone, - url, - locationURL: getLocationURL(id), - imageURL: getImageURL('img/locations/', imageID, legacyPlaceImages), - imageDescription: getImageDescription(placeImageAlt), - reviews: getReviews(id), - }; - } - ); + placeURL: url, + placeImage: imageID, + placeImageAlt, + }) => { + const reviews = getReviews(id); + + return { + id, + name, + city, + region, + country: '', + address, + phone, + url, + locationURL: getLocationURL(id), + imageURL: getLocationImageURL( + '/img/locations/', + imageID, + legacyPlaceImages + ), + imageDescription: getImageDescription(placeImageAlt), + reviews, + reviewCountText: getReviewCountText(reviews.length), + }; + } + ) + .reduce(mapLocation, {}); } const locations = transformLocations(legacyPlaces); diff --git a/src/main.tsx b/src/main.tsx index 1839ecbb..2ca5c5a2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,7 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { App } from './App'; -import { HomePage } from './components/home/HomePage'; +import { HomePage } from './components/HomePage'; +import { LocationPage } from './components/LocationPage'; window.__SPARKEATS_VERSION__ = import.meta.env['VITE_SPARKEATS_VERSION']; @@ -12,7 +13,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( }> } /> - Location Page} /> + } /> New Location Page} /> New Review Page} /> diff --git a/src/scss/components/_review-overview.scss b/src/scss/components/_review-details.scss similarity index 94% rename from src/scss/components/_review-overview.scss rename to src/scss/components/_review-details.scss index 3f757eff..3be71b94 100644 --- a/src/scss/components/_review-overview.scss +++ b/src/scss/components/_review-details.scss @@ -1,12 +1,12 @@ /* stylelint-disable scss/no-global-function-names */ -.review-overview { +.review-details { box-sizing: border-box; display: flex; justify-content: space-between; flex-direction: column; background: $secondary-color; - background: url('./img/background-img.png'); + background: url('/img/background-img.png'); border: 1px solid $primary-color; box-shadow: 2px 4px 28px rgba(0 0 0 / 8%); padding: 3rem 3rem 1rem; diff --git a/src/scss/components/_review-header.scss b/src/scss/components/_review-header.scss index e55ae735..7848c0dc 100644 --- a/src/scss/components/_review-header.scss +++ b/src/scss/components/_review-header.scss @@ -1,6 +1,6 @@ .review-header { height: 22.25rem; - background: url('./img/review-header_bg.svg') no-repeat; + background: url('/img/review-header_bg.svg') no-repeat; background-size: cover; position: relative; @@ -26,7 +26,7 @@ padding: 1rem; position: absolute; background: $secondary-color; - background: url('./img/background-img.png'); + background: url('/img/background-img.png'); border: 0.0625rem solid $primary-color; bottom: 1rem; left: 1rem; diff --git a/src/scss/components/_review-nav.scss b/src/scss/components/_review-nav.scss index 85c57d08..1b63dc33 100644 --- a/src/scss/components/_review-nav.scss +++ b/src/scss/components/_review-nav.scss @@ -5,7 +5,7 @@ &__svg { height: 0.9375rem; width: 1rem; - background: url('./img/review-header_home-arrow.svg') no-repeat; + background: url('/img/review-header_home-arrow.svg') no-repeat; display: inline-block; } diff --git a/src/scss/elements/_body.scss b/src/scss/elements/_body.scss index 2e0bd5bc..5ad51946 100644 --- a/src/scss/elements/_body.scss +++ b/src/scss/elements/_body.scss @@ -1,6 +1,6 @@ body { font-family: $roboto-condensed-stack; color: $primary-color; - background: url('./img/background-img.png'); + background: url('/img/background-img.png'); background-blend-mode: darken; } diff --git a/src/scss/objects/_review-page.scss b/src/scss/objects/_review-page.scss index 5ba8d2de..2bc86d1d 100644 --- a/src/scss/objects/_review-page.scss +++ b/src/scss/objects/_review-page.scss @@ -14,11 +14,12 @@ @supports (display: grid) { display: grid; grid-column-gap: 2rem; - grid-template: - 'nav . .' auto - 'header header header' auto - 'review review overview' auto - / repeat(3, 1fr); + grid-template-columns: repeat(3, 1fr); + grid-template-rows: auto; + grid-template-areas: + 'nav . .' + 'header header header' + 'review review overview'; } @media (min-width: $bp-full-size-desktop) { @@ -56,7 +57,7 @@ } } - .review-overview { + .review-details { order: 2; @media (max-width: $bp-rating-overview-move-right) { @@ -66,7 +67,7 @@ @media (min-width: $bp-rating-overview-move-right) { width: calc(33% - 4rem); align-self: flex-start; - margin: 8.8rem 0 0 1rem; // magic number that aligns the review-overview with the top of review-submission. + margin: 8.8rem 0 0 1rem; // magic number that aligns the review-details with the top of review-submission. flex-grow: 1; order: 3; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 84e70867..d7339193 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -4,4 +4,6 @@ declare global { interface Window { __SPARKEATS_VERSION__: string; } + + type WindowLocation = Location; } diff --git a/src/types/sparkeats.ts b/src/types/sparkeats.ts index 3835af27..b9ef1014 100644 --- a/src/types/sparkeats.ts +++ b/src/types/sparkeats.ts @@ -1,3 +1,21 @@ +export type Locations = { + [key: string]: { + id: number; + name: string; + city: string; + region: string; + country: string; + address: string; + phone: string; + url: string; + locationURL: string; + imageURL: string; + imageDescription: string; + reviews: Review[]; + reviewCountText: string; + }; +}; + export type Location = { id: number; name: string; @@ -11,13 +29,14 @@ export type Location = { imageURL: string; imageDescription: string; reviews: Review[]; + reviewCountText: string; }; export type Review = { id: number; reviewerName: string; text: string; - imageURL: string; + imageURL: string | null; imageDescription: string; starRating: number; placeID: number;