Skip to content

Commit

Permalink
finalize variant flow
Browse files Browse the repository at this point in the history
  • Loading branch information
bcholmes committed Dec 5, 2022
1 parent 1614667 commit 86f8b23
Show file tree
Hide file tree
Showing 13 changed files with 407 additions and 139 deletions.
2 changes: 1 addition & 1 deletion client/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" type="image/png" href="images/favicon.png"/>
<link rel="shortcut icon" type="image/png" href="/images/favicon.png"/>
<title>WisCon Online Registration</title>
</head>
<body>
Expand Down
16 changes: 15 additions & 1 deletion client/src/component/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Cart extends Component {
let removeButton = this.props.edit ? (<button className="btn p-0" onClick={() => this.removeFromCart(e)}><i className="bi-trash text-danger"></i></button>) : undefined;
return (<li className="list-group-item d-flex justify-content-between lh-sm visible-on-hover pb-2" key={i}>
<div>
<h6 className="my-0">{e.offering.title}</h6>
<h6 className="my-0">{this.renderItemTitle(e)}</h6>
<small className="text-muted">{e.for}</small>
</div>
<div>
Expand Down Expand Up @@ -64,6 +64,20 @@ class Cart extends Component {
</div>);
}

renderItemTitle(item) {
if (item.values?.variantId != null) {
let title = item.offering?.title;
let variant = item.offering?.variants?.filter(v => v.id?.toString() === item.values?.variantId);
if (variant?.length) {
return title + " / " + variant[0].name;
} else {
return title;
}
} else {
return item.offering?.title;
}
}

removeFromCart(item) {
this.setState({
...this.state,
Expand Down
50 changes: 42 additions & 8 deletions client/src/component/offeringList.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import store from '../state/store';
import { fetchOfferings } from '../state/offeringActions';
import { isValidEmail } from '../util/emailUtil';
import { formatAmount } from '../util/numberUtil';
import { sdlc } from '../util/sdlcUtil';
import { isAdmin } from '../state/authActions';
import { renderPrice } from '../state/offeringFunctions';
import { renderAmountAsString, renderPrice } from '../state/offeringFunctions';

class OfferingList extends Component {

Expand Down Expand Up @@ -118,7 +117,10 @@ class OfferingList extends Component {
}

let amountEntry = undefined;
if (this.state.selectedOffering && this.state.selectedOffering.suggestedPrice == null) {
if (this.state.selectedOffering && this.state.selectedOffering.suggestedPrice == null &&
(!this.isVariantSelectionRequired() ||
(this.isNonFixedPriceVariantPresent(this.state.selectedOffering) &&
!this.isFixedPriceVariantChosen(this.state.selectedOffering)))) {
amountEntry = (<Form.Group className="mb-3" controlId="amount">
<Form.Label className="sr-only">Amount</Form.Label>
<Form.Control className={this.getErrorClass('amount')} type="text" placeholder="Amount... (e.g. 30)" value={this.getFormValue('amount')} onChange={(e) => this.setFormValue('amount', e.target.value)}/>
Expand Down Expand Up @@ -159,7 +161,7 @@ class OfferingList extends Component {
<Form.Label className="sr-only">Option</Form.Label>
<Form.Control className={this.getErrorClass('variantId')} as="select" value={this.getFormValue('variantId')} onChange={(e) => this.setFormValue("variantId", e.target.value)} key="variant">
<option></option>
{this.state.selectedOffering.variants.map((v, i) => (<option value={v.id}>{v.name + (v.suggestedPrice ? ' - ' + formatAmount(v.suggestedPrice, this.state?.selectedOffering?.currency) : '')}</option>))}
{this.state.selectedOffering.variants.map((v, i) => (<option value={v.id}>{v.name + (v.suggestedPrice != null ? ' - ' + renderAmountAsString(v.suggestedPrice, this.state?.selectedOffering?.currency) : '')}</option>))}
</Form.Control>
{this.selectedVariantDescription()}
</Form.Group>)
Expand All @@ -169,7 +171,7 @@ class OfferingList extends Component {
<Form.Label className="sr-only">Age</Form.Label>
<Form.Control className={this.getErrorClass('age')} type="text" placeholder="Age (e.g. 18 months)" value={this.getFormValue('age')} onChange={(e) => this.setFormValue("age", e.target.value)}/>
<Form.Text className="text-muted">
Please tell us how old the child is as of Memorial Day 2022.
Please tell us how old the child is as of Memorial Day 2023.
</Form.Text>
</Form.Group>) : undefined;

Expand Down Expand Up @@ -563,13 +565,32 @@ class OfferingList extends Component {
}

isVariableAmount(offering) {
if (offering) {
if (offering?.variants?.length) {
return false; // we don't consider variants to be "variable", we might consider them "choose your own price"
} else if (offering) {
return offering.minimumPrice;
} else {
return false;
}
}

isNonFixedPriceVariantPresent(offering) {
if (offering?.variants?.length) {
let variant = offering?.variants?.filter(v => v.suggestedPrice == null);
return variant?.length;
} else {
return false;
}
}
isFixedPriceVariantChosen(offering) {
if (offering?.variants?.length) {
let variant = this.findVariantById(this.state?.values?.variantId);
return variant?.suggestedPrice != null;
} else {
return false;
}
}

getFormValue(formName) {
if (this.state.values) {
return this.state.values[formName] || '';
Expand All @@ -593,6 +614,14 @@ class OfferingList extends Component {
} else {
errors[formName] = false;
}

if (formName === 'variantId') {
let variant = this.findVariantById(formValue);
if (variant?.suggestedPrice != null) {
newValue["amount"] = '' + formatAmount(variant.suggestedPrice);
}
}

this.setState({
...state,
values: newValue,
Expand All @@ -602,6 +631,11 @@ class OfferingList extends Component {

}

findVariantById(variantIdAsString) {
let variant = this.state?.selectedOffering?.variants?.filter(v => v?.id?.toString() === variantIdAsString);
return variant?.length ? variant[0] : null;
}

toNumber(value) {
let n = Number(value);
if (isNaN(n)) {
Expand Down Expand Up @@ -631,7 +665,7 @@ class OfferingList extends Component {
message = "The amount value looks a bit fishy";
} else if (value === '' || (value === 0 && offering.suggestedPrice == null)) {
message = "Please provide an amount.";
} else if (this.isVariableAmount(offering) && value < offering.minimumPrice) {
} else if (this.isVariableAmount(offering) && offering.minimumPrice != null && value < offering?.minimumPrice) {
message = "The minimum amount is " + offering.currency + " " + formatAmount(offering.minimumPrice, offering.currency);
} else if (this.isVariableAmount(offering) && offering.maximumPrice != null && value > offering.maximumPrice) {
message = "The maximum amount is " + offering.currency + " " + formatAmount(offering.maximumPrice, offering.currency);
Expand Down Expand Up @@ -751,7 +785,7 @@ class OfferingList extends Component {
}
let price = newValues.amount || 0;
if (this.isValidForm()) {
axios.post(sdlc.serverUrl('/api/order_item.php'), {
axios.post('/api/order_item.php', {
"orderId": store.getState().cart.orderId,
"for": values.for,
"itemUUID": uuid,
Expand Down
32 changes: 24 additions & 8 deletions client/src/page/adminOfferingsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import Footer from '../component/footer';
import PageHeader from '../component/pageHeader';
import { renderPrice } from '../state/offeringFunctions';
import { renderAmountAsHtml, renderAmountAsString, renderPrice } from '../state/offeringFunctions';
import { isAuthenticated } from '../util/jwtUtil';

import dayjs from "dayjs";
Expand Down Expand Up @@ -40,18 +40,14 @@ class AdminOfferingsPage extends React.Component {
<thead>
<tr>
<th>Offering</th>
<th>Price</th>
<th>Variant</th>
<th className="text-center">Price</th>
<th>Availability</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{this.props.offerings.map(o => { return (<tr key={'offering-'+o.id}>
<td>{o.title}</td>
<td>{renderPrice(o)}</td>
<td>To be provided</td>
<td></td>
</tr>); })}
{this.props.offerings.map(o => this.renderOfferingRows(o))}
</tbody>
</table>

Expand All @@ -63,6 +59,26 @@ class AdminOfferingsPage extends React.Component {
const { history } = this.props;
history.push('/');
}

renderOfferingRows(offering) {
if (offering?.variants?.length) {
return offering.variants.map(v => (<tr key={'offering-'+offering.id + '-' + v.id}>
<td>{offering.title}</td>
<td>{v.name}</td>
<td className="text-center">{renderAmountAsHtml(v.suggestedPrice, offering.currency, false)}</td>
<td>To be provided</td>
<td></td>
</tr>));
} else {
return (<tr key={'offering-'+offering.id}>
<td>{offering.title}</td>
<td></td>
<td className="text-center">{renderPrice(offering)}</td>
<td>To be provided</td>
<td></td>
</tr>);
}
}
}

function mapStateToProps(state) {
Expand Down
3 changes: 1 addition & 2 deletions client/src/state/cartActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ export const REMOVE_FROM_CART = 'REMOVE_FROM_CART';
export const CLEAR_CART = 'CLEAR_CART';
export const ADD_STRIPE_SECRET = 'ADD_STRIPE_SECRET';

export function addToCart(offering, name, values, uuid, amount, variant) {
export function addToCart(offering, name, values, uuid, amount) {
let payload = {
offering: offering,
variant: variant,
for: name,
values: values,
amount: amount,
Expand Down
41 changes: 41 additions & 0 deletions client/src/state/cartReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ADD_STRIPE_SECRET, ADD_TO_CART, CLEAR_CART, REMOVE_FROM_CART } from "./cartActions";
import { v4 as uuidv4 } from 'uuid';

const cartInitialState = createInitialCart();

function createInitialCart() {
return {
orderId: uuidv4(),
items: []
};
}

export const cart = (state = cartInitialState, action) => {
switch (action.type) {
case ADD_TO_CART:
return {
...state,
items: [
...state.items,
action.payload
]
}
case REMOVE_FROM_CART: {
let item = action.payload;
let newItemList = state.items.filter(e => e.itemUUID !== item.itemUUID);
return {
...state,
items: newItemList
}
}
case ADD_STRIPE_SECRET:
return {
...state,
clientSecret: action.payload
};
case CLEAR_CART:
return createInitialCart();
default:
return state;
}
};
60 changes: 57 additions & 3 deletions client/src/state/offeringFunctions.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,69 @@

export function renderPrice(offering) {
let price = (offering.suggestedPrice == null) ? 'Any' : (offering.suggestedPrice === 0 ? 'Free' : ('$' + offering.suggestedPrice.toFixed(0)));
let priceSuffix = isVariableAmount(offering) ? (<small className="text-muted">+/-</small>) : undefined;
let price = renderPriceAsString(offering);
let priceSuffix = isVariableAmount(offering) ? (<small className="text-muted"><sup>+</sup>&#8260;<sub>-</sub></small>) : undefined;
return (<>
<small className="text-muted fw-light"><small>{offering.currency}</small></small>{' '}
{price} {priceSuffix}
</>);
}

function renderPriceAsString(offering) {
if (isVariantsPresent(offering)) {
if (isDefaultVariantPresent(offering)) {
let defaultVariant = getDefaultVariant(offering);
return renderAmountAsString(defaultVariant.suggestedPrice, offering.currency);
} else {
return 'Varies';
}
} else {
return renderAmountAsString(offering.suggestedPrice, offering.currency);
}
}

export const renderAmountAsHtml = (amount, currency, plusMinus) => {
let priceSuffix = plusMinus ? (<small className="text-muted"><sup>+</sup>&#8260;<sub>-</sub></small>) : undefined;
return (<>
<small className="text-muted fw-light"><small>{currency}</small></small>{' '}
{renderAmountAsString(amount, currency)} {priceSuffix}
</>);
}

export const renderAmountAsString = (amount, currency) => {
let prefix = (currency === 'USD' || currency === 'CAD') ? '$' : '';
if (amount == null) {
return prefix + 'Any';
} else if (amount === 0) {
return 'Free';
} else if (typeof(amount) === "number") {
let result = amount.toFixed(2);
if (result.indexOf(".00") >= 0) {
return prefix + amount.toFixed(0);
} else {
return prefix + result;
}
} else {
return prefix + amount;
}
}

const isVariantsPresent = (offering) => {
return offering?.variants?.length;
}

const isDefaultVariantPresent = (offering) => {
return getDefaultVariant(offering) != null;
}

const getDefaultVariant = (offering) => {
let variants = offering?.variants?.filter(v => v.isDefault);
return variants?.length ? variants[0] : null;
}

const isVariableAmount = (offering) => {
if (offering) {
if (isVariantsPresent(offering)) {
return isDefaultVariantPresent(offering);
} else if (offering) {
return offering.minimumPrice;
} else {
return false;
Expand Down
Loading

0 comments on commit 86f8b23

Please sign in to comment.