Skip to content

Commit

Permalink
Example: 7GUIs-flightBooker-react
Browse files Browse the repository at this point in the history
Fix return flight showing on one-way itinerary.
  • Loading branch information
stevebarakat committed May 19, 2024
1 parent 01cfc0a commit 809e342
Show file tree
Hide file tree
Showing 12 changed files with 755 additions and 176 deletions.
584 changes: 564 additions & 20 deletions examples/7guis-flight-booker-react/package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions examples/7guis-flight-booker-react/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "7guis-flight-booker-react",
"name": "fb",
"private": true,
"version": "0.0.0",
"type": "module",
Expand All @@ -10,13 +10,15 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.5",
"@xstate/react": "^4.1.0",
"date-fns": "^3.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vite-tsconfig-paths": "^4.3.2",
"xstate": "^5.10.0"
"xstate": "^5.9.1"
},
"devDependencies": {
"@types/node": "^20.12.10",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
Expand All @@ -26,6 +28,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"typescript": "^5.2.2",
"vite": "^5.2.0"
"vite": "^5.2.0",
"vite-tsconfig-paths": "^4.3.2"
}
}
77 changes: 50 additions & 27 deletions examples/7guis-flight-booker-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,80 @@
import FlightContext from './machines/flightMachine';
import { BookButton, Header } from './components';
import { DateSelector, TripSelector } from './components';
import { TODAY } from './utils';
import FlightContext, { TODAY } from "./machines/flightMachine";
import { BookButton, Header } from "./components";
import { DateSelector, TripSelector } from "./components";
import { format } from "date-fns";

const dateFormat = "EEEE MMMM do, yyyy";

export default function App() {
const { send } = FlightContext.useActorRef();
const state = FlightContext.useSelector((state) => state);
const { departDate, returnDate } = state.context;
const isRoundTrip = state.matches({ scheduling: 'roundTrip' });
const isBooking = state.matches('booking');
const isBooked = state.matches('booked');
const isRoundTrip = state.context.tripType === "roundTrip";
const isBooking = state.matches("booking");
const isBooked = state.matches("booked");

const isValidDepartDate = departDate >= TODAY;
const isValidReturnDate = returnDate >= departDate;

return (
<main>
const successMessage = (
<>
<Header>Booked!</Header>
<p>
You booked a <b>{isRoundTrip ? "round trip" : "one way"}</b> flight.
</p>
<p>
<b>Departs:</b> {format(departDate, dateFormat)}
</p>
{isRoundTrip && (
<p>
<b>Returns:</b> {format(returnDate, dateFormat)}
</p>
)}
</>
);

const ui = (
<>
<Header>Book Flight</Header>
<TripSelector
id="Trip Type"
isBooking={isBooking}
isBooked={isBooked}
tripType={isRoundTrip ? 'roundTrip' : 'oneWay'}
tripType={isRoundTrip ? "roundTrip" : "oneWay"}
/>
<DateSelector
id="Depart Date"
value={departDate}
isValidDate={isValidDepartDate}
disabled={isBooking || isBooked}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
send({
type: 'CHANGE_DEPART_DATE',
value: e.currentTarget.value
})
}
/>
<DateSelector
id="Return Date"
value={returnDate}
isValidDate={isValidReturnDate}
disabled={!isRoundTrip}
disabled={isBooking}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
send({
type: 'CHANGE_RETURN_DATE',
value: e.currentTarget.value
type: "CHANGE_DEPART_DATE",
value: e.currentTarget.value,
})
}
/>
{isRoundTrip && (
<DateSelector
id="Return Date"
value={returnDate}
isValidDate={isValidReturnDate}
disabled={!isRoundTrip || isBooking}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
send({
type: "CHANGE_RETURN_DATE",
value: e.currentTarget.value,
})
}
/>
)}
<BookButton
eventType={isRoundTrip ? 'BOOK_RETURN' : 'BOOK_DEPART'}
eventType={isRoundTrip ? "BOOK_RETURN" : "BOOK_DEPART"}
isBooking={isBooking}
isBooked={isBooked}
/>
</main>
</>
);

return <main>{isBooked ? successMessage : ui}</main>;
}
25 changes: 4 additions & 21 deletions examples/7guis-flight-booker-react/src/components/BookButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import FlightContext from '../machines/flightMachine';
import FlightContext from "../machines/flightMachine";

type Props = {
isBooking: boolean;
Expand All @@ -14,26 +14,9 @@ export default function BookButton({ eventType, isBooking, isBooked }: Props) {

const bookFlight = () => send({ type: eventType });

const successMessage = (
<>
<h2>You booked a flight!</h2>
<p>
<span>Departs:</span> {state.context.departDate}
</p>
<p>
<span>Returns:</span> {state.context.returnDate}
</p>
</>
);

return (
<>
<dialog open={isBooking || isBooked}>
{isBooking ? <p>Booking...</p> : successMessage}
</dialog>
<button onClick={bookFlight} disabled={!canBook}>
{isBooking ? 'Booking' : isBooked ? 'Booked!' : 'Book'}
</button>
</>
<button onClick={bookFlight} disabled={!canBook}>
{isBooking ? "Booking..." : "Book"}
</button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function DateInput({ isValidDate, ...props }: Props) {
return (
<label>
<span className="visually-hidden">{props.id}</span>
<input type="date" {...props} className={isValidDate ? '' : 'error'} />
<input type="date" {...props} className={isValidDate ? "" : "error"} />
</label>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export default function TripSelector({
<select
disabled={isBooked || isBooking}
value={tripType}
onChange={() => {
send({ type: "CHANGE_TRIP_TYPE" });
onChange={(e) => {
const selectedValue = e.currentTarget.value as "oneWay" | "roundTrip";
send({ type: "CHANGE_TRIP_TYPE", tripType: selectedValue });
}}
{...props}
>
Expand Down
8 changes: 4 additions & 4 deletions examples/7guis-flight-booker-react/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Header from './Header';
import BookButton from './BookButton';
import DateSelector from './DateInput';
import TripSelector from './TripSelector';
import Header from "./Header";
import BookButton from "./BookButton";
import DateSelector from "./DateInput";
import TripSelector from "./TripSelector";

export { Header, BookButton, DateSelector, TripSelector };
125 changes: 72 additions & 53 deletions examples/7guis-flight-booker-react/src/machines/flightMachine.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,126 @@
import { setup, assign, assertEvent, fromPromise } from 'xstate';
import { createActorContext } from '@xstate/react';
import { TODAY, TOMORROW } from '../utils';
import { sleep } from '../utils';
import { setup, assign, assertEvent, fromPromise } from "xstate";
import { createActorContext } from "@xstate/react";
import { generateDate } from "../utils";
import { sleep } from "../utils";

export const TODAY = generateDate(0);
const TOMORROW = generateDate(1);

export const flightBookerMachine = setup({
types: {
context: {} as FlightData,
events: {} as
| { type: 'BOOK_DEPART' }
| { type: 'BOOK_RETURN' }
| { type: 'CHANGE_TRIP_TYPE' }
| { type: 'CHANGE_DEPART_DATE'; value: string }
| { type: 'CHANGE_RETURN_DATE'; value: string }
| { type: "BOOK_DEPART" }
| { type: "BOOK_RETURN" }
| { type: "CHANGE_TRIP_TYPE"; tripType: "oneWay" | "roundTrip" }
| { type: "CHANGE_DEPART_DATE"; value: string }
| { type: "CHANGE_RETURN_DATE"; value: string },
},
actions: {
setDepartDate: assign(({ event }) => {
assertEvent(event, 'CHANGE_DEPART_DATE');
assertEvent(event, "CHANGE_DEPART_DATE");
return { departDate: event.value };
}),
setReturnDate: assign(({ event }) => {
assertEvent(event, 'CHANGE_RETURN_DATE');
assertEvent(event, "CHANGE_RETURN_DATE");
return { returnDate: event.value };
})
}),
setTripType: assign(({ event }) => {
assertEvent(event, "CHANGE_TRIP_TYPE");
return { tripType: event.tripType };
}),
},
actors: {
Booker: fromPromise(() => {
return sleep(2000);
})
return sleep(1000);
}),
},
guards: {
'isValidDepartDate?': ({ context: { departDate } }) => {
"isValidDepartDate?": ({ context: { departDate } }) => {
return departDate >= TODAY;
},
'isValidReturnDate?': ({ context: { departDate, returnDate } }) => {
"isValidReturnDate?": ({ context: { departDate, returnDate } }) => {
return departDate >= TODAY && returnDate > departDate;
}
}
},
},
}).createMachine({
id: 'flightBookerMachine',
id: "flightBookerMachine",
context: {
departDate: TODAY,
returnDate: TOMORROW
returnDate: TOMORROW,
tripType: "oneWay",
},
initial: 'scheduling',
initial: "scheduling",
states: {
scheduling: {
initial: 'oneWay',
initial: "oneWay",
on: {
CHANGE_DEPART_DATE: {
actions: {
type: 'setDepartDate'
}
}
type: "setDepartDate",
},
},

BOOK_DEPART: {
target: "booking",
guard: {
type: "isValidDepartDate?",
},
},

BOOK_RETURN: {
target: "booking",
guard: {
type: "isValidReturnDate?",
},
},
},
states: {
oneWay: {
on: {
CHANGE_TRIP_TYPE: {
target: 'roundTrip'
target: "roundTrip",
actions: {
type: "setTripType",
tripType: "roundTrip",
},
},
BOOK_DEPART: {
target: '#flightBookerMachine.booking',
guard: {
type: 'isValidDepartDate?'
}
}
}
},
},
roundTrip: {
on: {
CHANGE_TRIP_TYPE: {
target: 'oneWay'
target: "oneWay",
actions: {
type: "setTripType",
tripType: "oneWay",
},
},

CHANGE_RETURN_DATE: {
actions: {
type: 'setReturnDate'
}
type: "setReturnDate",
},
},
BOOK_RETURN: {
target: '#flightBookerMachine.booking',
guard: {
type: 'isValidReturnDate?'
}
}
}
}
}
},
},
},
},
booking: {
invoke: {
src: 'Booker',
src: "Booker",
onDone: {
target: 'booked'
target: "booked",
},
onError: {
target: 'scheduling'
}
}
target: "scheduling",
},
},
},
booked: {
type: 'final'
}
}
type: "final",
},
},
});

export default createActorContext(flightBookerMachine);
Loading

0 comments on commit 809e342

Please sign in to comment.