Compare commits
No commits in common. "implementation" and "main" have entirely different histories.
implementa
...
main
11 changed files with 740 additions and 910 deletions
|
@ -1 +0,0 @@
|
|||
nodejs 22.16.0
|
1252
package-lock.json
generated
1252
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
@ -3,11 +3,11 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -33,9 +33,5 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"html-react-parser": "^5.2.5",
|
||||
"react-imask": "^7.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,13 @@ 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 wrapper">
|
||||
<BandHeader band={bands[0]} />
|
||||
<main className="content">
|
||||
<BandDetails band={bands[0]} />
|
||||
<div className="App">
|
||||
<BandForm band={bands[0]} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
165
src/BandForm.js
165
src/BandForm.js
|
@ -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 }) {
|
||||
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 (
|
||||
<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>
|
||||
<h1>{band.name}</h1>
|
||||
{band.ticketTypes.map((ticket) => (
|
||||
<p>
|
||||
{ticket.name} - {ticket.description}
|
||||
</p>
|
||||
))}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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": [
|
||||
{
|
||||
|
|
117
src/index.css
117
src/index.css
|
@ -1,120 +1,13 @@
|
|||
@import "./reset.css";
|
||||
@import "./variables.css";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
color: var(--color-text);
|
||||
body {
|
||||
margin: 0;
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Add table
Reference in a new issue