Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
import { Link, Route, Routes, useParams } from 'react-router';
import { Route, Routes } from 'react-router';
import SiteHeader from './components/SiteHeader';
import ExploreByTaste from './components/ExploreByTaste';
import WineCatalog from './components/WineCatalog';
import FindWineByFood from './components/FindWineByFood';

/**
* NB: Temporary placeholder. The detail page is not built yet.
* "View wine details" on Explore by taste links here, so the link
* already works. Replace this with the real detail page component.
*/
function WineDetailsPlaceholder() {
const { id } = useParams();
return (
<main>
<h1>Wine details</h1>
<p>The detail page for wine {id} is not built yet.</p>
<Link to="/">← Back to Explore by taste</Link>
</main>
);
}
import WineDetailsPage from './components/WineDetailsPage';

/**
* Root component for Wine not?
Expand All @@ -27,7 +12,7 @@ function WineDetailsPlaceholder() {
* - / Explore by taste (front page)
* - /food Find wine by food
* - /wines the wine catalog
* - /wines/:id detail page (placeholder for now)
* - /wines/:id detail page for one wine
*
* Each page has its own <main> and one <h1>, so the heading hierarchy is
* correct on every page.
Expand All @@ -48,7 +33,7 @@ export default function App() {
}
/>
<Route path="/food" element={<FindWineByFood />} />
<Route path="/wines/:id" element={<WineDetailsPlaceholder />} />
<Route path="/wines/:id" element={<WineDetailsPage />} />
</Routes>
</>
);
Expand Down
4 changes: 0 additions & 4 deletions client/src/components/FoodWineCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ export default function FoodWineCard({ wine }: { wine: FoodWine }) {

<div className="food-card-body">
<h3>
{/*
NB: The detail page (/wines/:id) is not built yet. Until then this
link opens a placeholder page, see App.tsx.
*/}
<Link to={`/wines/${wine.id}`}>{wine.name}</Link>
</h3>
<p className="food-winery">{wine.winery}</p>
Expand Down
4 changes: 0 additions & 4 deletions client/src/components/SelectedWine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,6 @@ export default function SelectedWine({ wine }: Props) {
)}
</div>

{/*
NB: The detail page (/wines/:id) is not built yet. Until then this
link opens a placeholder page, see App.tsx.
*/}
<Link className="taste-button" to={`/wines/${wine.id}`}>
View wine details →
</Link>
Expand Down
242 changes: 242 additions & 0 deletions client/src/components/WineDetailsPage.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Detail page for one wine (/wines/:id).
* Uses the colour variables from index.css, so the page matches the rest
* of the app and keeps the contrast checked there (WCAG AA).
*/

.wine-details-page {
max-width: 60rem;
}

.wine-details-back {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 44px; /* large enough to hit on a touch screen */
padding: 0 1rem;
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-surface);
color: var(--color-text);
font: inherit;
cursor: pointer;
}

.wine-details-back:hover {
background: var(--color-panel);
}

/* One column on phones, two from tablet width */
.wine-details {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem;
margin-top: 1.25rem;
}

@media (min-width: 48rem) {
.wine-details {
grid-template-columns: 1fr 1fr;
}

.wine-details-header {
grid-column: 1 / -1;
}
}

/* Top: bottle to the left, name and rating to the right */
.wine-details-header {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto;
column-gap: 1.5rem;
align-items: center;
padding: 1.5rem;
border-radius: 1rem;
background: var(--color-panel);
border-left: 0.5rem solid var(--type-color, var(--color-accent));
}

.wine-details-header > :not(img) {
grid-column: 2;
}

.wine-details-image {
grid-column: 1;
grid-row: 1 / span 4;
height: 11rem;
width: auto;
object-fit: contain;
}

/* Colour stripe per wine type. Decorative only: the type is also text. */
.wine-details[data-wine-type='red'] {
--type-color: var(--wine-red);
}
.wine-details[data-wine-type='white'] {
--type-color: var(--wine-white);
}
.wine-details[data-wine-type='rosé'] {
--type-color: var(--wine-rose);
}
.wine-details[data-wine-type='sparkling'] {
--type-color: var(--wine-sparkling);
}
.wine-details[data-wine-type^='dessert'] {
--type-color: var(--wine-dessert);
}

.wine-details-type {
margin: 0;
color: var(--color-accent);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}

.wine-details-header h1 {
margin: 0.25rem 0 0.4rem;
font-family: var(--font-display);
font-size: clamp(1.75rem, 4vw, 2.5rem);
line-height: 1.15;
}

/* The h1 gets focus from code on load; no outline needed there */
.wine-details-header h1:focus {
outline: none;
}

.wine-details-winery {
margin: 0;
color: var(--color-muted);
font-size: 1.1rem;
}

.wine-details-rating {
margin: 0.75rem 0 0;
font-weight: 700;
}

.wine-details-rating small {
color: var(--color-muted);
font-weight: 400;
}

.wine-details-star {
color: var(--color-star);
}

/* The four boxes: about, taste, food, vintages */
.wine-details-card {
padding: 1.25rem 1.5rem;
border: 1px solid var(--color-border);
border-radius: 1rem;
background: var(--color-surface);
}

.wine-details-card h2 {
margin: 0 0 1rem;
font-size: 1.15rem;
}

/* Label to the left, value to the right */
.wine-details-facts {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.6rem 1.25rem;
margin: 0;
}

.wine-details-facts dt {
color: var(--color-muted);
}

.wine-details-facts dd {
margin: 0;
font-weight: 600;
overflow-wrap: anywhere;
}

.wine-details-facts a {
color: var(--color-accent);
}

.wine-details-scale {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem 0.75rem;
}

.wine-details-scale meter {
width: 7rem;
height: 0.6rem;
}

.wine-details-scale meter::-webkit-meter-bar {
border: none;
border-radius: 999px;
background: var(--color-sunk);
}

.wine-details-scale meter::-webkit-meter-optimum-value {
border-radius: 999px;
background: var(--color-accent);
}

.wine-details-scale meter::-moz-meter-bar {
border-radius: 999px;
background: var(--color-accent);
}

/* Food pairings and vintages as small pills */
.wine-details-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
}

.wine-details-tags li {
padding: 0.3rem 0.75rem;
border-radius: 999px;
background: var(--color-panel);
font-size: 0.9rem;
}

.wine-details-more {
margin-top: 0.75rem;
}

.wine-details-more summary {
margin-bottom: 0.75rem;
color: var(--color-accent);
font-weight: 600;
cursor: pointer;
}

.wine-details-muted {
margin: 0;
color: var(--color-muted);
}

/* Phones: bottle above the text instead of beside it */
@media (max-width: 36rem) {
.wine-details-header {
grid-template-columns: 1fr;
}

.wine-details-header > :not(img),
.wine-details-image {
grid-column: 1;
grid-row: auto;
}

.wine-details-image {
height: 8rem;
justify-self: center;
}
}
Loading