Compare commits

..

11 commits

Author SHA1 Message Date
Ava Gaiety W
3f45514fd8 normalize and output data for server 2025-06-14 16:50:17 -06:00
Ava Gaiety W
71c24d1cec moved invalid comments 2025-06-14 16:35:00 -06:00
Ava Gaiety W
4b9e0627be payment info gathering 2025-06-14 16:34:18 -06:00
Ava Gaiety W
80625a82ed fix npm install version issues 2025-06-14 16:03:33 -06:00
Ava Gaiety W
0592650985 personal info form 2025-06-14 16:00:25 -06:00
Ava Gaiety W
746534d4d3 total calc 2025-06-14 15:52:33 -06:00
Ava Gaiety W
46a18112cc upgrade react and react-dom 2025-06-14 14:28:13 -06:00
Ava Gaiety W
8a684ba8a2 fix key in loop 2025-06-14 14:27:36 -06:00
Ava Gaiety W
d770642d7d list ticket details, begin on form 2025-06-14 14:22:21 -06:00
Ava Gaiety W
c02139b1c9 css reset, basic layout, split into components 2025-06-14 14:08:15 -06:00
Ava Gaiety W
f595d0ac8f .tool-versions file for nodejs ease of install (asdf, mise) 2025-06-14 13:31:56 -06:00
11 changed files with 910 additions and 740 deletions

1
.tool-versions Normal file
View file

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

View file

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

13
src/BandDetails.js Normal file
View file

@ -0,0 +1,13 @@
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,13 +1,168 @@
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 }) {
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 (
<div>
<h1>{band.name}</h1>
{band.ticketTypes.map((ticket) => (
<p>
{ticket.name} - {ticket.description}
</p>
<form className="form" action={purchase} ref={formRef}>
<h2>Select Tickets</h2>
{band.ticketTypes.map(({type, name, description, cost}, index) => (
<div className="ticketWrapper" key={name}>
<div className="ticket">
<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>
);
}

13
src/BandHeader.js Normal file
View file

@ -0,0 +1,13 @@
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",
"date": 1683644012000,
"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",
"ticketTypes": [
{

View file

@ -1,13 +1,120 @@
body {
margin: 0;
@import "./reset.css";
@import "./variables.css";
* {
box-sizing: border-box;
}
html {
color: var(--color-text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: var(--spacing-lg);
font-size: var(--font-base);
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
input {
width: 100%;
padding: 1rem;
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);
}
}
}

48
src/reset.css Normal file
View file

@ -0,0 +1,48 @@
/* 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;
}

13
src/variables.css Normal file
View file

@ -0,0 +1,13 @@
: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;
}