diff --git a/examples/7guis-flight-booker-react/README.md b/examples/7guis-flight-booker-react/README.md index 3fa1c7e9a8..4ce848b2f5 100644 --- a/examples/7guis-flight-booker-react/README.md +++ b/examples/7guis-flight-booker-react/README.md @@ -11,4 +11,6 @@ Visit the [7GUIs project](https://eugenkiss.github.io/7guis/tasks#flight/ "Fligh ## Screenshots -![App Screenshot](public/flight-booker.jpg) +![App Screenshot](public/flight-booker.png) + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/statelyai/xstate/tree/main/examples/7guis-flight-booker-react) diff --git a/examples/7guis-flight-booker-react/package-lock.json b/examples/7guis-flight-booker-react/package-lock.json index c681a7faca..652717a43b 100644 --- a/examples/7guis-flight-booker-react/package-lock.json +++ b/examples/7guis-flight-booker-react/package-lock.json @@ -9,9 +9,9 @@ "version": "0.0.0", "dependencies": { "@xstate/react": "^4.1.0", - "lucide-react": "^0.366.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "vite-tsconfig-paths": "^4.3.2", "xstate": "^5.10.0" }, "devDependencies": { @@ -1234,7 +1234,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "devOptional": true }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -1732,7 +1732,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1785,7 +1784,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -2346,6 +2345,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2578,14 +2582,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "0.366.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.366.0.tgz", - "integrity": "sha512-iUOsp/35wOkrgEzigZlZI/OhVxQZ8CmxjebdIjfSDzNBmrNYjQfKSpeKderaEFfGt3OycF1BE+wLlaqWRuoh4w==", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2626,14 +2622,13 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -2767,7 +2762,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "devOptional": true }, "node_modules/picomatch": { "version": "2.3.1", @@ -2785,7 +2780,7 @@ "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -2917,7 +2912,7 @@ "version": "4.14.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -3045,7 +3040,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -3125,6 +3120,25 @@ "typescript": ">=4.2.0" } }, + "node_modules/tsconfck": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", + "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3153,7 +3167,7 @@ "version": "5.4.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3226,7 +3240,7 @@ "version": "5.2.8", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", - "dev": true, + "devOptional": true, "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -3277,6 +3291,24 @@ } } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/examples/7guis-flight-booker-react/package.json b/examples/7guis-flight-booker-react/package.json index dceb4e4480..f042caed58 100644 --- a/examples/7guis-flight-booker-react/package.json +++ b/examples/7guis-flight-booker-react/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@xstate/react": "^4.1.0", - "lucide-react": "^0.366.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "vite-tsconfig-paths": "^4.3.2", "xstate": "^5.10.0" }, "devDependencies": { diff --git a/examples/7guis-flight-booker-react/public/flight-booker.png b/examples/7guis-flight-booker-react/public/flight-booker.png new file mode 100644 index 0000000000..f72fefdd83 Binary files /dev/null and b/examples/7guis-flight-booker-react/public/flight-booker.png differ diff --git a/examples/7guis-flight-booker-react/src/App.tsx b/examples/7guis-flight-booker-react/src/App.tsx index a36f575b06..83b93f531e 100644 --- a/examples/7guis-flight-booker-react/src/App.tsx +++ b/examples/7guis-flight-booker-react/src/App.tsx @@ -1,79 +1,57 @@ -import { FlightContext } from "./machine"; -import { DateInput } from "./DateInput"; -import { useRef, useState } from "react"; -import { X } from "lucide-react"; +import FlightContext from "./machines/flightMachine"; +import { BookButton, Header } from "./components"; +import { DateSelector, TripSelector } from "./components"; +import { TODAY } from "./utils"; -const Flight = () => { - const dialogRef = useRef(null); +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 { startDate, returnDate } = state.context; - const isValidDate = startDate && (!returnDate || returnDate >= startDate); - const canSubmit = state.matches("booking") && isValidDate; - - const trip = state.matches({ booking: "roundTrip" }) ? "roundTrip" : "oneWay"; + const isValidDepartDate = departDate >= TODAY; + const isValidReturnDate = returnDate >= departDate; return ( -
- - -
-

- Book Flight - -

-
-
- - - send({ type: "CHANGE_START_DATE", value }) - } - label="Start date" - /> - - send({ type: "CHANGE_RETURN_DATE", value }) - } - disabled={trip === "oneWay" || !startDate} - label="Return date" - /> - - -
-
+
+
Book Flight
+ + ) => + send({ + type: "CHANGE_DEPART_DATE", + value: e.currentTarget.value, + }) + } + /> + ) => + send({ + type: "CHANGE_RETURN_DATE", + value: e.currentTarget.value, + }) + } + /> + +
); -}; - -const App = () => { - return ; -}; - -export default App; +} diff --git a/examples/7guis-flight-booker-react/src/DateInput.tsx b/examples/7guis-flight-booker-react/src/DateInput.tsx deleted file mode 100644 index 62e1de0a6c..0000000000 --- a/examples/7guis-flight-booker-react/src/DateInput.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export const DateInput: React.FC<{ - value?: string | Date; - onChange: (value: string) => void; - disabled?: boolean; - label: string; -}> = ({ value, onChange, disabled, label }) => { - return ( -
- -
- ); -}; diff --git a/examples/7guis-flight-booker-react/src/components/BookButton.tsx b/examples/7guis-flight-booker-react/src/components/BookButton.tsx new file mode 100644 index 0000000000..b77805526a --- /dev/null +++ b/examples/7guis-flight-booker-react/src/components/BookButton.tsx @@ -0,0 +1,39 @@ +import FlightContext from "../machines/flightMachine"; + +type Props = { + isBooking: boolean; + isBooked: boolean; + eventType: EventType; +}; + +export default function BookButton({ eventType, isBooking, isBooked }: Props) { + const { send } = FlightContext.useActorRef(); + const state = FlightContext.useSelector((state) => state); + const isValidDepartDate = state.can({ type: eventType }); + const canBook = !isBooked && isValidDepartDate; + + const bookFlight = () => send({ type: eventType }); + + const successMessage = ( + <> +

You booked a flight!

+

+ Departs: {state.context.departDate} +

+

+ Returns: {state.context.returnDate} +

+ + ); + + return ( + <> + + {isBooking ?

Booking...

: successMessage} +
+ + + ); +} diff --git a/examples/7guis-flight-booker-react/src/components/DateInput.tsx b/examples/7guis-flight-booker-react/src/components/DateInput.tsx new file mode 100644 index 0000000000..b5d58f1d35 --- /dev/null +++ b/examples/7guis-flight-booker-react/src/components/DateInput.tsx @@ -0,0 +1,14 @@ +type Props = { + isValidDate: boolean; +} & Input; + +function DateInput({ isValidDate, ...props }: Props) { + return ( + + ); +} + +export default DateInput; diff --git a/examples/7guis-flight-booker-react/src/components/Header.tsx b/examples/7guis-flight-booker-react/src/components/Header.tsx new file mode 100644 index 0000000000..d949256c8a --- /dev/null +++ b/examples/7guis-flight-booker-react/src/components/Header.tsx @@ -0,0 +1,8 @@ +function Header({ children }: { children: React.ReactNode }) { + return ( +
+

{children}

+
+ ); +} +export default Header; diff --git a/examples/7guis-flight-booker-react/src/components/TripSelector.tsx b/examples/7guis-flight-booker-react/src/components/TripSelector.tsx new file mode 100644 index 0000000000..51e21f0b30 --- /dev/null +++ b/examples/7guis-flight-booker-react/src/components/TripSelector.tsx @@ -0,0 +1,27 @@ +import FlightContext from "../machines/flightMachine"; + +export default function TripSelector({ + isBooking, + isBooked, + tripType, + ...props +}: TripSelectorProps) { + const { send } = FlightContext.useActorRef(); + + return ( + + ); +} diff --git a/examples/7guis-flight-booker-react/src/components/index.ts b/examples/7guis-flight-booker-react/src/components/index.ts new file mode 100644 index 0000000000..e58f05e36c --- /dev/null +++ b/examples/7guis-flight-booker-react/src/components/index.ts @@ -0,0 +1,6 @@ +import Header from "./Header"; +import BookButton from "./BookButton"; +import DateSelector from "./DateInput"; +import TripSelector from "./TripSelector"; + +export { Header, BookButton, DateSelector, TripSelector }; diff --git a/examples/7guis-flight-booker-react/src/index.css b/examples/7guis-flight-booker-react/src/index.css deleted file mode 100644 index 51df6499b2..0000000000 --- a/examples/7guis-flight-booker-react/src/index.css +++ /dev/null @@ -1,116 +0,0 @@ -* { - box-sizing: border-box; -} -:root { - --white: rgba(255, 255, 255, 0.87); - --inner-shadow: rgba(0, 0, 0, 0.15) 0px 3px 3px 0px inset; - - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: var(--white); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.visually-hidden { - clip: rect(0 0 0 0); - clip-path: inset(50%); - height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; - width: 1px; -} - -body { - margin: 0; - display: grid; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - margin: 0 0 12px; - border-bottom: 1px solid var(--white); - display: flex; - gap: 8px; -} - -dialog { - background: #333; - border: none; - border-radius: 8px; - max-width: 200px; - & .close { - border-radius: 50%; - width: 32px; - height: 32px; - padding: 0; - display: grid; - place-items: center; - background: var(--white); - color: #808080; - } -} - -select { - padding: 8px; - border-radius: 8px; - margin-bottom: 16px; -} - -.text-input { - padding-bottom: 16px; - & input { - background: #222; - width: 100%; - padding: 8px; - border: none; - border-radius: 8px; - box-shadow: var(--inner-shadow); - &:disabled { - opacity: 0.5; - } - } -} -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -input:focus { - outline: none; -} -button:focus-visible { - outline: 12px auto -webkit-focus-ring-color; -} - -.open { - position: absolute; - width: 140px; - left: calc(50% - 70px); -} - -.error { - background: hsla(1, 100%, 38%, 0.5); - padding: 6px 12px; - border-radius: 9.5px; - margin-bottom: 12px; -} diff --git a/examples/7guis-flight-booker-react/src/machine.ts b/examples/7guis-flight-booker-react/src/machine.ts deleted file mode 100644 index bed7fef1cc..0000000000 --- a/examples/7guis-flight-booker-react/src/machine.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { setup, assign, assertEvent } from "xstate"; -import { createActorContext } from "@xstate/react"; - -const TODAY = new Date().toISOString().split("T")[0]; - -type FlightData = { - startDate?: string; - returnDate?: string; -}; -export const flightBookerMachine = setup({ - types: { - context: {} as FlightData, - events: {} as - | { type: "BOOK" } - | { type: "CHANGE_TRIP" } - | { type: "CHANGE_START_DATE"; value: string } - | { type: "CHANGE_RETURN_DATE"; value: string }, - }, - actions: { - setStartDate: assign(({ event }) => { - assertEvent(event, "CHANGE_START_DATE"); - return { startDate: event.value }; - }), - setReturnDate: assign(({ event }) => { - assertEvent(event, "CHANGE_RETURN_DATE"); - return { returnDate: event.value }; - }), - }, - guards: {}, -}).createMachine({ - context: { - startDate: TODAY, - returnDate: TODAY, - }, - id: "flightBookerMachine", - initial: "booking", - states: { - booking: { - initial: "oneWay", - on: { - CHANGE_START_DATE: { - actions: "setStartDate", - }, - BOOK: { - target: "booked", - }, - }, - states: { - oneWay: { - on: { - CHANGE_TRIP: { - target: "roundTrip", - }, - }, - }, - roundTrip: { - on: { - CHANGE_TRIP: { - target: "oneWay", - }, - CHANGE_RETURN_DATE: { - actions: "setReturnDate", - }, - }, - }, - }, - }, - booked: { - type: "final", - }, - }, -}); - -export const FlightContext = createActorContext(flightBookerMachine); diff --git a/examples/7guis-flight-booker-react/src/machines/flightMachine.ts b/examples/7guis-flight-booker-react/src/machines/flightMachine.ts new file mode 100644 index 0000000000..27af7bbf66 --- /dev/null +++ b/examples/7guis-flight-booker-react/src/machines/flightMachine.ts @@ -0,0 +1,107 @@ +import { setup, assign, assertEvent, fromPromise } from "xstate"; +import { createActorContext } from "@xstate/react"; +import { TODAY, TOMORROW } from "../utils"; +import { sleep } from "../utils"; + +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 }, + }, + actions: { + setDepartDate: assign(({ event }) => { + assertEvent(event, "CHANGE_DEPART_DATE"); + return { departDate: event.value }; + }), + setReturnDate: assign(({ event }) => { + assertEvent(event, "CHANGE_RETURN_DATE"); + return { returnDate: event.value }; + }), + }, + actors: { + Booker: fromPromise(() => { + return sleep(2000); + }), + }, + guards: { + "isValidDepartDate?": ({ context: { departDate } }) => { + return departDate >= TODAY; + }, + "isValidReturnDate?": ({ context: { departDate, returnDate } }) => { + return departDate >= TODAY && returnDate > departDate; + }, + }, +}).createMachine({ + id: "flightBookerMachine", + context: { + departDate: TODAY, + returnDate: TOMORROW, + }, + initial: "scheduling", + states: { + scheduling: { + initial: "oneWay", + on: { + CHANGE_DEPART_DATE: { + actions: { + type: "setDepartDate", + }, + }, + }, + states: { + oneWay: { + on: { + CHANGE_TRIP_TYPE: { + target: "roundTrip", + }, + BOOK_DEPART: { + target: "#flightBookerMachine.booking", + guard: { + type: "isValidDepartDate?", + }, + }, + }, + }, + roundTrip: { + on: { + CHANGE_TRIP_TYPE: { + target: "oneWay", + }, + CHANGE_RETURN_DATE: { + actions: { + type: "setReturnDate", + }, + }, + BOOK_RETURN: { + target: "#flightBookerMachine.booking", + guard: { + type: "isValidReturnDate?", + }, + }, + }, + }, + }, + }, + booking: { + invoke: { + src: "Booker", + onDone: { + target: "booked", + }, + onError: { + target: "scheduling", + }, + }, + }, + booked: { + type: "final", + }, + }, +}); + +export default createActorContext(flightBookerMachine); diff --git a/examples/7guis-flight-booker-react/src/main.tsx b/examples/7guis-flight-booker-react/src/main.tsx index 4fe57a2e55..6e821ac917 100644 --- a/examples/7guis-flight-booker-react/src/main.tsx +++ b/examples/7guis-flight-booker-react/src/main.tsx @@ -1,8 +1,10 @@ +import "./styles/reset.css"; +import "./styles/styles.css"; + import React from "react"; import ReactDOM from "react-dom/client"; -import { FlightContext } from "./machine.ts"; +import FlightContext from "./machines/flightMachine.ts"; import App from "./App.tsx"; -import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/examples/7guis-flight-booker-react/src/styles/reset.css b/examples/7guis-flight-booker-react/src/styles/reset.css new file mode 100644 index 0000000000..691da330e1 --- /dev/null +++ b/examples/7guis-flight-booker-react/src/styles/reset.css @@ -0,0 +1,144 @@ +/* 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; +} + +* { + box-sizing: border-box; +} + +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/examples/7guis-flight-booker-react/src/styles/styles.css b/examples/7guis-flight-booker-react/src/styles/styles.css new file mode 100644 index 0000000000..d3ccf47631 --- /dev/null +++ b/examples/7guis-flight-booker-react/src/styles/styles.css @@ -0,0 +1,135 @@ +/**************************** +------- VARIABLES -------- +****************************/ +&:root { + /* colors */ + --white: hsl(0 0% 95%); + --black: hsl(0 0% 15%); + --gray: hsl(0 10% 70%); + --lightgray: hsl(0 20% 80%); + + /* shadows */ + --box-border: var(--gray) 0px 0px 0px 2px inset, var(--black) 0px 0px 0px 2px; + --inner-shadow: inset 0 1px 2px hsl(0 0% 0%/0.8), 0 -1px 1px var(--gray), + 0 1px 0 var(--gray); + --outer-shadow: 0.3px 0.5px 0.7px var(--black), + 0.8px 1.6px 2px -0.8px var(--black), 2.1px 4.1px 5.2px -1.7px var(--black), + 5px 10px 12.6px -2.5px var(--black); +} + +/**************************** +------- UNIVERSAL -------- +****************************/ +* { + transition: box-shadow 0.1s; + border: none; + font-size: 1rem; + border-radius: 4px; + padding: 8px; + width: 100%; + outline-offset: 2px; + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } +} + +/**************************** +----- NATIVE ELEMENTS ----- +****************************/ +body { + display: grid; + min-height: 100vh; + place-content: center; + background: var(--black); + & h1 { + margin-top: -4px; + color: var(--black); + font-size: 2rem; + font-weight: 700; + letter-spacing: -0.05em; + padding-bottom: 6px; + border-radius: 0; + border-bottom: 1px solid var(--black); + } +} + +main { + display: flex; + flex-direction: column; + gap: 8px; + padding: 16px; + background: var(--gray); + & .error { + background: red; + font-weight: 700; + color: white !important; + } +} + +dialog { + width: fit-content; + background: var(--gray); + padding: 12px 24px; + box-shadow: var(--outer-shadow); + font-weight: 700; + transition: background 0.5s; + & h2 { + color: var(--black); + font-size: 1.5rem; + margin-bottom: 8px; + padding: 8px 0; + border-bottom: 1px solid var(--black); + border-radius: 0; + } + & p { + margin: 8px 0; + font-weight: normal; + & span { + font-weight: 700; + } + } +} + +input[type="date"] { + cursor: pointer; + box-shadow: var(--inner-shadow); + color: var(--black); + border-bottom: 1px solid var(--lightgray); + &:disabled { + cursor: not-allowed; + } +} + +select { + cursor: pointer; + color: var(--white); + background: var(--black); + &:hover { + box-shadow: var(--box-border); + } +} + +button { + cursor: pointer; + color: var(--white); + font-weight: 500; + font-family: inherit; + background-color: var(--black); + &:hover:not(:disabled) { + box-shadow: var(--box-border); + } +} + +/**************************** +---------- UTILS ---------- +****************************/ +.visually-hidden { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} diff --git a/examples/7guis-flight-booker-react/src/utils/index.ts b/examples/7guis-flight-booker-react/src/utils/index.ts new file mode 100644 index 0000000000..3cccd39d84 --- /dev/null +++ b/examples/7guis-flight-booker-react/src/utils/index.ts @@ -0,0 +1,11 @@ +// CONSTANTS +export const TODAY = new Date().toISOString().split("T")[0]; + +export const TOMORROW = new Date(Date.now() + 86400000) + .toISOString() + .split("T")[0]; + +// Emulate async operation +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/examples/7guis-flight-booker-react/tsconfig.json b/examples/7guis-flight-booker-react/tsconfig.json index a7fc6fbf23..e3c54abb2a 100644 --- a/examples/7guis-flight-booker-react/tsconfig.json +++ b/examples/7guis-flight-booker-react/tsconfig.json @@ -2,10 +2,16 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "*": ["./*.d.ts"], + "@/*": ["./src/*", "./dist/*", ""] + }, + /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, @@ -20,6 +26,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/examples/7guis-flight-booker-react/types.d.ts b/examples/7guis-flight-booker-react/types.d.ts new file mode 100644 index 0000000000..3766666baf --- /dev/null +++ b/examples/7guis-flight-booker-react/types.d.ts @@ -0,0 +1,18 @@ +import React from "react"; + +declare global { + type Input = React.InputHTMLAttributes; + type Select = React.SelectHTMLAttributes; + + type EventType = "BOOK_DEPART" | "BOOK_RETURN"; + + type FlightData = { + departDate: string; + returnDate: string; + }; + + type TripSelectorProps = { + isBooked: boolean; + tripType: "oneWay" | "roundTrip"; + } & React.InputHTMLAttributes; +} diff --git a/examples/7guis-flight-booker-react/vite.config.ts b/examples/7guis-flight-booker-react/vite.config.ts index 5a33944a9b..2db9f2c987 100644 --- a/examples/7guis-flight-booker-react/vite.config.ts +++ b/examples/7guis-flight-booker-react/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), tsconfigPaths()], +});