No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Convierte tus certificados en títulos universitarios en USA

Antes: $249

Currency
$209

Paga en 4 cuotas sin intereses

Paga en 4 cuotas sin intereses
Suscríbete

Termina en:

17 Días
15 Hrs
52 Min
1 Seg

Últimos detalles del proyecto

13/15
Recursos

Aportes 8

Preguntas 1

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

añadí opacidad al botón de agregar pasajeros cuando no tengamos ningun pasajero agregado y el destino seleccionado al mostrar el resumen del ticket
.
.
src/Machines/bookingMachine.js:

import { createMachine, assign } from "xstate";
import { fetchCountries } from '../Utils/api'

const fillCountries = {
  initial: 'loading',
  states: {
    loading: {
      invoke: {
        id: 'getCountries',
        src: ()=> fetchCountries,
        onDone: {
          target: 'success',
          actions: assign({
            countries: (context,event)=> event.data 
          })
        },
        onError: {
          target: 'failure',
          actions: assign({
            error: 'Fallo el request'
          })
        }
      }
    },
    success: {},
    failure: {
      on: {
        RETRY: { target: 'loading'}
      }
    }
  }
}

const bookingMachine = createMachine({
  id: "buy plane tickets",
  initial: "initial",
  context: {
    passengers: [],
    selectedCountry: '',
    countries: [],
    error: '',
  },
  states: {
    initial: {
      on: {
        START: { 
          target: 'search',
        },
      },
    },
    search: {
      on: {
        CONTINUE: {
          target: 'passengers',
          actions: 'choiceSelectedCountry',
        },
        CANCEL: 'initial',
      },
      ...fillCountries,
    },
    tickets: {
      after: {
        5000: {
          target: 'initial',
          actions: 'cleanContext'
        }
      }
    },
    passengers: {
      on: {
        DONE: {
          target: "tickets",
          cond: 'moreThanOnePassenger'
        },
        CANCEL: {
          target: 'initial',
          actions: 'cleanContext'
        },
        ADD: {
          target: 'passengers',
          actions: 'addPassenger'
        }
      },
    },
  },
}, 
{
  actions: {
    addPassenger: assign(
      (context, event)=> context.passengers.push(event.newPassenger) 
    ),
    choiceSelectedCountry: assign({
      selectedCountry: (context, event) => event.selectedCountry
    }),
    cleanContext: assign({
      selectedCountry: '',
      passengers: [],
    })
  },
  guards: {
    moreThanOnePassenger: (context)=> {
      return context.passengers.length > 0
    }
  }
});

export default bookingMachine;

src/Containers/StepsLayout.js:

import React from 'react';
import { Welcome } from '../Components/Welcome';
import { Search } from '../Components/Search';
import { Passengers } from '../Components/Passengers';
import { Tickets } from '../Components/Tickets';
import './StepsLayout.css';

export const StepsLayout = ({ state, send }) => {
  const renderContent = () => {
    if (state.matches('initial')) return <Welcome send={send} /> 
    if (state.matches('search')) return <Search state={state} send={send} /> 
    if (state.matches('tickets')) return <Tickets context={state.context} send={send} /> 
    if (state.matches('passengers')) return <Passengers state={state} send={send} /> 
    return null 
  };

  return (
    <div className='StepsLayout'>
      {renderContent()}
    </div>
  );
}; 

src/Components/Tickets.js:

import React from 'react';
import './Tickets.css';

export const Tickets = ({ send, context }) => {
  const finish = () => {
    send('FINISH')
  };

  return (
    <div className='Tickets'>
      <p className='Tickets-description description'>Gracias por volar con book a fly 💚</p>
      <div className='Tickets-ticket'>
        <div className='Tickets-country'>{context.selectedCountry}</div>
        <div className='Tickets-passengers'>
          <span></span>
          {context.passengers.map((passenger, idx) => (
            <p key={idx}>{passenger}</p>
          ))}
        </div>
      </div>
      <button onClick={finish} className='Tickets-finalizar button'>Finalizar</button>
    </div>
  );
}; 

src/Components/Passengers.js:

import React, { useState } from 'react';
import './Passengers.css';

export const Passengers = ({ state, send }) => {
  const { passengers } = state.context
  const [value, changeValue] = useState('');

  const onChangeInput = (e) => {
    changeValue(e.target.value);
  }

  const goToTicket = ()=> {
    send('DONE')
  }

  const submit = (e) => {
    e.preventDefault();
    send('ADD', { newPassenger: value })
    changeValue('');
  }

  return (
    <form onSubmit={submit} className='Passengers'>
      <p className='Passengers-title title'>Agrega a las personas que van a volar ✈️</p>
      {passengers.map((passenger,index) => (
        <p key={`${passenger}${index}`}>{passenger}</p>
      ))}
      <input 
        id="name" 
        name="name" 
        type="text" 
        placeholder='Escribe el nombre completo' 
        required 
        value={value} 
        onChange={onChangeInput}
      />
      <div className='Passengers-buttons'>
        <button 
          className='Passengers-add button-secondary'
          type="submit"
        >
          Agregar Pasajero
        </button>
        <button
          className={`Passenger-pay button ${passengers.length === 0 ? 'Passenger-disabled' : ''}`} 
          type="button"
          onClick={goToTicket}
        >
          Ver mi ticket
        </button>
      </div>
    </form>
  );
};

src/Components/Passengers.css:

.Passengers {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 0 8px;
}

.Passengers input {
  font-size: 1rem;
  margin: 16px 0;
  border-radius: 8px;
  padding: 8px;
  border: none;
  width: 100%;
  box-sizing: border-box;
}

.Passengers-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  width: 100%;
  margin-top: 8px;
}

.Passengers-title {
  margin-bottom: 16px;
}

.Passenger-disabled {
  opacity: 0.1;
}

Lo que agregue fue un ordenamiento de la lista de los paises usando el siguiente codigo donde usamos el service para devolver la data.

countries: (context, event) => event.data.sort((a,b) => (a.name.common.localeCompare(b.name.common)))
señalar que la condición 'cond' en passengers en xState v5 se llama 'guard' y lanza un error específico para cambiarla a ese nombre ```js passengers: { on: { DONE:{ target: 'tickets', guard: 'moreThanOnePassenger' }, ```

Actualmente para este año 2024, ya esta disponible la nueva version de XState (yo estoy usando la version 5.6.2). Y hay una nueva sintaxis en varias partes del codigo, les dejo los archivos importantes del proyecto donde ha habido los mayores cambios según la nueva versión:
BookingMachine.js (actualmente se denomina a los servicios actores y son maquinas que funcionan dentro de la maquina root):

import { createMachine, assign, fromPromise } from "xstate";
import { fetchCountries } from "../Utils/api.js";

const fillCountries = {
  initial: "loading",
  states: {
    loading: {
      invoke: {
        id: 'getCountries',
        src: 'fetchCountries',
        onDone: {
          target: 'success',
          actions: assign({
            countries: ({event}) => event.output, //actualmente se envia el evento destructurado
          })
        },
        onError: {
          target: 'failure',
          actions: assign({
            error: 'Fallo el request',
          })
        }
      }
    },
    success: {},
    failure: {
      on: {
        RETRY: { target: "loading" },
      },
    },
  },
};

const bookingMachine = createMachine(
  {
    id: "buy plane tickets",
    initial: "initial",
    context: {
      passengers: [],
      selectedCountry: "",
      countries: [],
      error: '',
    },
    states: {
      initial: {
        on: {
          START: {
            target: "search",
          },
        },
      },
      search: {
        initial: "fillCountries",
        // si queremos usar maquinas de estado hijas se deben definir dentro del estado padre
        states: {
          fillCountries:{
            ...fillCountries
          }
        },
        on: {
          CONTINUE: {
            target: "passengers",
            actions: assign({
              selectedCountry: ({context, event}) => event.selectedCountry,
            }),
          },
          CANCEL: "initial",
        },
      },
      tickets: {
        after: {
          5000: {
            target: 'initial',
            actions: 'cleanContext',
          }
        },
        on: {
          FINISH: "initial",
        },
      },
      passengers: {
        on: {
          DONE: {
            target: "tickets",
            guard: "moreThanOnePassenger"
          },
          CANCEL: {
            target: "initial",
            actions: "cleanContext",
          },
          ADD: {
            target: "passengers",
            actions: assign(({context, event}) => //tambien se obtiene el context destructurando
              context.passengers.push(event.newPassenger)
            ),
          },
        },
      },
    },
  },
  {
    actions: {
      cleanContext: assign({
        selectedCountry: "",
        passengers: [],
      }),
    },
    guards: {
      moreThanOnePassenger: (context) => {
        return context.passengers.length > 0;
      }
    },
    actors:{
      // aqui definimos los servicios en este caso es un servicio basado en una promesa que toma la funcion del archivo api.js
      fetchCountries: fromPromise(async () => {
          return fetchCountries()
         })
    }
  }
);

export default bookingMachine;

search.js

import React, { useState } from 'react';
import './Search.css';

export const Search = ({ state, send }) => {
  const [flight, setFlight] = useState('');

  const goToPassengers = () => {
    //actualmente los eventos se envian dentro de un objeto con la propiedad type y el payload se pone dentro del mismo objeto, no fuera de este
    send({type:'CONTINUE', selectedCountry: flight})
  }

  const handleSelectChange = (event) => {
    setFlight(event.target.value);
  };

  const options = state.context.countries;

  return (
    <div className='Search'>
      <p className='Search-title title'>Busca tu destino</p>
      <select id="country" className='Search-select' value={flight} onChange={handleSelectChange}>
        <option value="" disabled defaultValue>Escoge un país</option>
        {options.map((option) => <option value={option.name.common} key={option.name.common}>{option.name.common}</option>)}
      </select>
      <button onClick={goToPassengers} disabled={flight === ''} className='Search-continue button'>Continuar</button>
    </div>
  );
}; 

Hola a todos, dejo mi código de la clase en typescript + vite
Hice una modificacion en el evento ADD pues aparecia un bug que no permitia resetear la prop passengers de context.

passengers: {
				on: {
					DONE: {
						target: 'tickets',
						cond: 'moreThanOnePassanger',
					},
					CANCEL: {
						target: 'initial',
						actions: 'clearState',
					},
					ADD: {
						target: 'passengers',
						actions: assign({
							passengers: (context, event) => [
								...context.passengers,
								event.newPassenger,
							],
						}),
					},
				},
			},
actions: {
			clearState: assign({
				passengers: [],
				selectedCountry: '',
				countries: [],
				error: '',
			}),
		},
		guards: {
			moreThanOnePassanger: context => context.passengers.length !== 0,
		},
	},

Advertencia:
Van a detectar un error a la hora de cancelar la orden: no se borran los nombre de los pasajeros agregados.
Pienso esto se debe a como modifica la profesora dicho valor.
Para corregir dicho error lo realizo de la siguiente forma:

			ADD: {
						target: 'passengers',
						actions: assign({
							passengers: (context, event) => [
								...context.passengers,
								event.newPassenger,
							],
						}),
					},

Yo pienso al error se debe a que se debe reemplazar utilizando el assing y al hacerlo de forma directa xstate pierde el rastro dicha props.
Ademas al usar typescript puedo detectarun detalle: assing debe retornar un objeto que sea parcial o totalmente el context.
Un ejemplo de modificacion completa seria

clearState: assign({
				passengers: [],
				selectedCountry: '',
				countries: [],
				error: '',
			}),

Yo agregué la validación usando un patrón regex en el input para que solo se acepten letras y espacios para el nombre completo del pasajero al igual que el autoFocus

<input
        id='name'
        name='name'
        type='text'
        placeholder={`Enter passenger's full name`}
        required
        value={value}
        onChange={onChangeInput}
        autoFocus
        pattern='^[a-zA-Z]+(?:\s[a-zA-Z]+)*$'
      />

En Passengers.js

Si quieren usar un estado global a nivel de BaseLayouts pueden hacer un provider mediante un contexto de react, y así se evitan de estar pasando en cada flujo de componentes props de state y send.

  1. Creamos el contexto en la carpeta contexts
// src/contexts/StateContext.js
import { createContext } from 'react';

export const StateContext = createContext({
  state: null,
  send: () => {},
});
  1. Creamos el provider en la carpeta providers
// src/providers/StateProvider.jsx
import { useMachine } from '@xstate/react';
import { StateContext } from '../contexts/StateContext';
import { bookingMachine } from '../machines/bookingMachine';

const StateProvider = ({ children }) => {
  const [state, send] = useMachine(bookingMachine);
  // setState -> send: ejecutar una transición

  return (
    <StateContext.Provider value={{ state, send }}>
      {children}
    </StateContext.Provider>
  );
};

export default StateProvider;
  1. Creamos un hook para reutilizar el contexto en los componentes que quiera
// src/hooks/useStateBookingMachine.js
import { useContext } from 'react';
import { StateContext } from '../contexts/StateContext';

export const useStateBookingMachine = () => {
  return useContext(StateContext);
};
  1. Usamos el provider en el layouts donde engloobará todo nuestro flujo de máquina de estado
// src/App.jsx
import BaseLayouts from './layouts/BaseLayout';
import StateProvider from './providers/StateProvider';
import './App.css';

function App() {
  return (
    <div className="App">
      <StateProvider>
        <BaseLayouts />
      </StateProvider>
    </div>
  );
}

export default App;

Listo, ya con esto podemos usar el contexto a nivel del flujo de BaseLayouts, aquí un ejemplo de uso en el componente Search:

// src/components/Search.jsx
import { useState } from 'react';
import { useStateBookingMachine } from '../hooks/useStateBookingMachine';
import './Search.css';

const Search = () => {
  const [flight, setFlight] = useState('');

  // Aquí usamos el hook que encapsula el uso del contexto
  const { state, send } = useStateBookingMachine(); 

  // más código
  return (
    <div className="Search">
      ...
    </div>
  );

}