Compare commits

..

No commits in common. "implementation" and "main" have entirely different histories.

11 changed files with 740 additions and 910 deletions

View file

@ -1 +0,0 @@
nodejs 22.16.0

1252
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,11 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"react": "^19.1.0", "react": "^18.2.0",
"react-dom": "^19.1.0", "react-dom": "^18.2.0",
"react-scripts": "5.0.1" "react-scripts": "5.0.1"
}, },
"scripts": { "scripts": {
@ -33,9 +33,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"html-react-parser": "^5.2.5",
"react-imask": "^7.6.1"
} }
} }

View file

@ -2,19 +2,13 @@ import skaBand from "./band-json/ska-band.json";
import kpopBand from "./band-json/kpop-band.json"; import kpopBand from "./band-json/kpop-band.json";
import punkBand from "./band-json/punk-band.json"; import punkBand from "./band-json/punk-band.json";
import BandHeader from "./BandHeader";
import BandDetails from "./BandDetails";
import BandForm from "./BandForm"; import BandForm from "./BandForm";
function App() { function App() {
const bands = [skaBand, kpopBand, punkBand]; const bands = [skaBand, kpopBand, punkBand];
return ( return (
<div className="App wrapper"> <div className="App">
<BandHeader band={bands[0]} /> <BandForm band={bands[0]} />
<main className="content">
<BandDetails band={bands[0]} />
<BandForm band={bands[0]} />
</main>
</div> </div>
); );
} }

View file

@ -1,13 +0,0 @@
import parse from 'html-react-parser';
function BandDetails({ band }) {
return (
<div className="details">
<img src={band.imgUrl} alt="" />
{parse(band.description_blurb)}
</div>
);
}
export default BandDetails;

View file

@ -1,168 +1,13 @@
import { useState, useRef } from 'react';
import { IMaskInput } from 'react-imask';
const CentsInADollar = 100;
const DefaultTotal = 0;
function deepClone(data) {
// TODO: replace with structuredClone?
return JSON.parse(
JSON.stringify(
data
)
);
}
function costToReadable(cost) {
return cost / CentsInADollar;
}
function submitPurchase(data) {
// TODO: Send to backend
console.log(data);
}
function BandForm({ band }) { function BandForm({ band }) {
const formRef = useRef(null);
const [total, setTotal] = useState(DefaultTotal);
const [formIsPending, setFormIsPending] = useState(false);
function normalizePurchaseData(formData) {
const data = deepClone(Object.fromEntries(formData));
delete data.ticketQuantity;
data.tickets = formData.getAll('ticketQuantity').map((quantity, index) => {
const {cost, name} = band.ticketTypes[index];
return {
name,
quantity,
totalCost: cost * quantity,
}
});
return data;
}
function purchase(formData) {
if (formRef.current.checkValidity() === false) {
console.warn('Form is invalid');
return;
}
setFormIsPending(true);
submitPurchase(normalizePurchaseData(formData));
}
function ticketQuantityChanged() {
const formData = new FormData(formRef.current);
const ticketQuantities = formData.getAll('ticketQuantity');
const total = ticketQuantities.reduce((total, quantity, index) => {
return total += (band.ticketTypes[index].cost * quantity);
}, 0)
setTotal(total > 0 ? total : 0)
}
return ( return (
<form className="form" action={purchase} ref={formRef}> <div>
<h2>Select Tickets</h2> <h1>{band.name}</h1>
{band.ticketTypes.map((ticket) => (
{band.ticketTypes.map(({type, name, description, cost}, index) => ( <p>
<div className="ticketWrapper" key={name}> {ticket.name} - {ticket.description}
<div className="ticket"> </p>
<div className="ticketDetails">
<h3 id={name}>{name}</h3>
<p>{description}</p>
<h4>${costToReadable(cost)}</h4>
</div>
<div>
{/* TODO: describedby cost and description */}
<input
labelledby={name}
defaultValue="0"
name="ticketQuantity"
id={name}
type="number"
step="1"
min="0"
onChange={ticketQuantityChanged}
/>
</div>
</div>
<hr />
</div>
))} ))}
</div>
<div className="total">
<h3>Total</h3>
<p aria-live="polite">${costToReadable(total)}</p>
</div>
<div className="information">
<div className="form-row">
<input
required
name="first_name"
label="First Name"
placeholder="First Name"
type="text"
/>
<input
required
name="last_name"
label="Last Name"
placeholder="Last Name"
type="text"
/>
</div>
{/* TODO: third party library address verification */}
<input
required
name="address"
label="Address"
placeholder="Address"
type="text"
/>
</div>
<fieldset className="payment">
<legend>Payment Details</legend>
{/* TODO: regex pattern */}
<IMaskInput
required
mask="0000 0000 0000 0000"
name="cardNumber"
label="Card Number"
placeholder="0000 0000 0000 0000"
type="text"
></IMaskInput>
<div className="form-row">
{/* TODO: regex pattern */}
<IMaskInput
required
mask="00 / 00"
pattern="\d*"
name="cardExpiration"
label="Card Expiration (MM/YY)"
placeholder="MM / YY"
type="text"
></IMaskInput>
{/* TODO: regex pattern */}
<IMaskInput
required
mask={Number}
name="cardCVV"
label="Card CVV"
placeholder="CVV"
type="text"
></IMaskInput>
</div>
</fieldset>
<button type="submit" disabled={formIsPending}>Get Tickets</button>
</form>
); );
} }

View file

@ -1,13 +0,0 @@
function BandHeader({ band }) {
return (
<header className="header">
<h1>{band.name}</h1>
<div className="header-details">
<time>{band.date}</time> {/* TODO: Format properly */}
<address>{band.location}</address> {/* TODO: Format properly */}
</div>
</header>
);
}
export default BandHeader;

View file

@ -3,7 +3,7 @@
"id": "flaming-potatoes", "id": "flaming-potatoes",
"date": 1683644012000, "date": 1683644012000,
"location": "Groove, 125 MacDougal St, New York, NY 10012", "location": "Groove, 125 MacDougal St, New York, NY 10012",
"description_blurb": "<p>We're the Flaming Potatoes, and once you come to this awesome small club performance, you'll be our Best Spuds!</p>", "description_blurb": "<p>We're the Flaming Potatoes, and once you come to this awesome small club performance, you'll be our Best Spuds!</p>.",
"imgUrl": "https://placehold.co/600x400/51aa97/000000", "imgUrl": "https://placehold.co/600x400/51aa97/000000",
"ticketTypes": [ "ticketTypes": [
{ {

View file

@ -1,120 +1,13 @@
@import "./reset.css"; body {
@import "./variables.css"; margin: 0;
* {
box-sizing: border-box;
}
html {
color: var(--color-text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
margin: var(--spacing-lg);
font-size: var(--font-base);
} }
input { code {
width: 100%; font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
padding: 1rem; monospace;
font-size: var(--font-base);
}
hr {
border-color: var(--color-text);
border-width: 1px;
width: 100%;
}
h1, h2 {
color: var(--color-text-darker);
font-weight: bold;
}
h1 {
font-size: var(--font-xl);
}
h2 {
font-size: var(--font-lg);
}
h3 {
font-size: var(--font-lg);
color: var(--color-text-dark);
}
h4 {
font-size: var(--font-lg);
}
.wrapper {
max-width: 1024px;
margin: 0 auto;
display: grid;
flex-direction: column;
gap: var(--spacing-lg);
}
.content {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--spacing-lg);
}
.details {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.form {
background-color: var(--color-section);
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
.ticketWrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.ticket {
display: flex;
flex-direction: row;
gap: var(--spacing-lg);
}
.ticketDetails {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.total {
display: flex;
flex-direction: row;
justify-content: space-between;
font-size: var(--font-lg);
}
.form-row {
display: flex;
flex-direction: row;
gap: var(--spacing-md);
}
.information, .payment {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
legend {
margin-bottom: var(--spacing-md);
}
}
} }

View file

@ -1,48 +0,0 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View file

@ -1,13 +0,0 @@
:root {
--spacing-md: 1rem;
--spacing-lg: 2rem;
--color-text: #7d8ca1;
--color-text-dark: #626262;
--color-text-darker: #3d4753;
--color-section: #f7f8fa;
--font-base: 16px;
--font-lg: 1.75rem;
--font-xl: 2.5rem;
}