Amigo Secreto
TLDR; Vamos a jugar amigo secreto en la oficina, pero yo trabajo remoto. Que tal si en vez de meter los papelitos con los nombres en una bolsita, creo una aplicación realtime con react.js + firebase?
Vamos a ver que resulta…
El demo está en esta pagina https://upbeat-keller-5f955d.netlify.com en caso de que quieras ver el resultado final
Primero que todo, creamos el proyecto e instalamos algunos paquetes
$ create-react-app secret-friend
$ cd secret-friend
$ npm install @react-firebase/database
$ npm install firebase
$ npm start
It’s alive!
PDTA: Importamos bootstrapcdn en el index.html de la carpeta /publish
dentro del head
tag
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
Bueno, cuando el usuario ingresa necesito solicitar el nombre de el, así que necesitamos crear el archivo src/Components/Users.js
un componente que se encargue de eso:
import React, { Component } from 'react'
export class User extends Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
}
state = {
username: "",
submited: false
}
handleSubmit = (event) => {
event.preventDefault()
if (this.state.submited)
return;
this.setState({
submited: true
})
console.log("new user added", this.state.username)
this.props.onSubmit(this.state.username)
}
handleChange = (event) => {
let username = event.target.value;
this.setState({
username
})
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label htmlFor="username" >
Player
</label>
<div>
<input
type="text"
id="username"
placeholder="Nombres"
autoComplete="off"
value={this.state.username}
onChange={this.handleChange}
disabled={this.state.submited}
>
</input>
<button
type="submit"
disabled={!this.state.username}
>
Submit
</button>
</div>
</form>
</div>
)
}
}
export default User
La idea con este componente es permitirnos registrarnos en la aplicación. Aquí hay varias cosas, pero básicamente este componente nos permitirá solicitar y devolver el nombre al componente que lo invoca de la persona que esté usando la aplicación. En este caso nos iremos al componente App.js y lo invocaremos:
import React, { Component } from 'react'
import User from './Components/User';
export default class App extends Component {
state = {
currentUser: "",
}
handleSubmit = (currentUser) => {
this.setState({
currentUser
});
localStorage.clear("user")
localStorage.setItem("user", JSON.stringify(currentUser))
}
componentDidMount() {
let userSerialized = localStorage.getItem("user")
if (userSerialized) {
var user = JSON.parse(userSerialized)
this.setState({
currentUser: user
})
}
}
render() {
return (
<div>
{!this.state.currentUser && <User
onSubmit={(currentUser) => this.handleSubmit(currentUser)}
/>}
<span className="badge badge-info">{this.state.currentUser}</span>
</div>
);
}
}
Lo que podemos ver aquí es que en cuando importamos User
le estamos pasando un evento, para saber cuando alguien haya escrito su nombre. El evento será recibido por handleSubmit que se encargará de guardar esa variable en el estado.
Adicionalmente guardaremos la información en el localStorage con el fin de que si el usuario va a refrescar la ventana, nos me permita cargar la información previamente digitada. El evento componentDidMount nos permitirá al iniciar cargar dicha información.
Al hacer un set de la propiedad this.state.currentUser el componente volverá a renderizar la aplicación, la cual evaluará varias cosas:
- Si el usuario existe, el quitará el componente
User
para evitar que vuelvan a escribir el usuario. - Mostrar un badge con el nombre de usuario actual.
Agreguemos algo de persistencia con Firebase
Para poder compartir con todos los usuarios la información, es importante conectarnos a una base de datos. En este caso vamos a usar la base de datos en tiempo real que nos provee google y que puede ser usada por medio de su SDK. Para tal razón vamos a hacer un simple step by step de como configurar firebase en react.js
Vamos al portal https://console.firebase.google.com
Creamos un nuevo proyecto. (continue, continue)
Después en el overview, hay 4 opciones (iOS, Android, Web y Unity), damos click en Web. Registramos el nombre de la App “secret-friends” y click en “Register App”. Eso nos mostrará un script parecido a esto:
<!-- The core Firebase JS SDK is always required and must be listed first --> <script src="https://www.gstatic.com/firebasejs/6.6.0/firebase-app.js"></script> <!-- TODO: Add SDKs for Firebase products that you want to use https://firebase.google.com/docs/web/setup#config-web-app --> <script> // Your web app's Firebase configuration var firebaseConfig = { apiKey: "API-KEY", authDomain: "project-name.firebaseapp.com", databaseURL: "https://project-name.firebaseio.com", projectId: "project-name", storageBucket: "", messagingSenderId: "APP-ID", appId: "ANOTHER-APP-ID" }; // Initialize Firebase firebase.initializeApp(firebaseConfig); </script>
Vamos al proyecto, creamos un archivo
src/fire.js
y allí adentro acomodamos la información anterior con la siguiente estructuraimport * as firebase from 'firebase'; const firebaseConfig = { apiKey: "API-KEY", authDomain: "project-name.firebaseapp.com", databaseURL: "https://project-name.firebaseio.com", projectId: "project-name", storageBucket: "", messagingSenderId: "APP-ID", appId: "ANOTHER-APP-ID" }; firebase.initializeApp(firebaseConfig); const database = firebase.database(); export { database, };
Todo bien allí en el projecto.
Vamos a volver al portal de Firebase y en la opción Develop/Database bajamos a la opción Or choose Realtime Database damos click en Create Database, eso abrirá un modal, allí seleccionamos la opción Start in test mode (Get set up quickly by allowing all reads and writes to your database). Esto es temporal, recuerden que como dice firebase Sus reglas de seguridad se definen como públicas, por lo que cualquiera puede robar, modificar o eliminar datos en su base de datos. Cuando se vaya a desplegar en producción, debemos cambiar esta configuración.
Todo bien hasta el momento, ahora volveremos a nuestro proyecto en React.js y vamos a crear un nuevo componente que nos permitirá traer las personas creadas desde firebase.
El componente se llamará src/Components/Participants.js
import React, { Component } from 'react'
import { database } from '../fire';
export default class Participants extends Component {
state = {
users: [],
currentUser: ""
}
componentDidMount() {
let messagesRef = database.ref('users').orderByKey().limitToLast(100);
messagesRef.on('child_added', snapshot => {
/* Update React state when message is added at Firebase Database */
let user = { text: snapshot.val(), id: snapshot.key };
this.setState(state => {
const users = state.users.concat(user);
return {
users
};
});
this.props.userUpdated(this.state.users)
})
}
render() {
return (
<div>
<ul>
{ /* Render the list of messages */
this.state.users.map(user => <li key={user.id} style={user.text === this.props.currentUser ? { color: "red" } : null}>{user.text}</li>)
}
</ul>
</div>
)
}
}
Aquí hay varias cosas, primero que todo, importamos la clase que acabamos de hacer import { database } from '../fire';
. Y vamos a hacer uso del componente componentDidMount() en el que obtendremos todos los usuarios registrados en la base de datos. En ese metodo inicialmente traerán todos los usuarios y vamos a quedar suscritos para cada vez que se agregue uno nuevo carga en este metodo permitiendonos que siempre que se agrega un nuevo usuario, este se agregará en el state en la propiedad this.state.users
this.setState(state => {
const users = state.users.concat(user);
return {
users
};
});
Esto permite que a partir de un array que actualmente está en el state, se concatena el nuevo usuario.
Despues de haber hecho esto, queremos saber en esa lista, quien soy yo? Entonces en el render vamos a hacer lo siguiente
this.state.users.map(user =>
<li
key={user.id}
style={user.text === this.props.currentUser ? { color: "red" } : null}
> {user.text}</li>)
Entonces al recorrer los usuarios, vamos a comparar cada uno con una propiedad que me pasará
Ahora para hacer uso de este componente Participants
, en el Apps
vamos a hacer las siguientes modificaciones:
// Another imports
import Participants from './Components/Participants';
export default class App extends Component {
//Another Methods
handleUsersRefresh = (users) => {
this.setState({
users
});
}
render() {
return (
<div>
{!this.state.currentUser && <User
onSubmit={(currentUser) => this.handleSubmit(currentUser)}
/>}
<span className="badge badge-info">{this.state.currentUser}</span>
<Participants
userUpdated={(userList) => this.handleUsersRefresh(userList)}
currentUser={this.state.currentUser}
/>
</div>
);
}
}
Aquí vamos a agregarle al componente Participants
2 parametros. userUpdated que es un evento que nos permitirá traer la lista de nuevos usuarios a este componente siempre que haya uno nuevo. Y el usuario actual que tenemos currentUser como parametro que se le enviará.
Ahora lo que haremos es agregar un nuevo componente que básicamente nos permitirá mostrar un mensaje de acuerdo a ciertas reglas. El componente se llamará src/Component/Status.js
import React from 'react'
export default function Status({ userLenght, handleStatus, MatchAlreadyRunned }) {
let status = ""
let canBePlayed = false
if (userLenght === 0) {
status = "Sin jugadores"
canBePlayed = false
}
else {
let par = (userLenght % 2)
if (par) {
status = "Hay una lista impar, falta alguien?"
canBePlayed = false
} else {
status = "Hay una lista par, Jugamos?"
canBePlayed = true
}
}
if (MatchAlreadyRunned) {
status = "Ya se han repartido las cartas..."
}
return (
<div>
{(<div className={`alert alert-${canBePlayed ? 'success' : 'danger'}`} role="alert">
{`${userLenght}- ${status}`}
</div>
)}
{canBePlayed && !MatchAlreadyRunned &&
<button type="button" className="btn btn-danger" onClick={handleStatus} >Repartir</button>
}
</div>
)
}
Aquí lo que básicamente estamos haciendo es pasandole a la función la cantidad de usuarios que hay (userLenght), el evento handleStatus que nos permitirá asignar el amigo secreto y MatchAlreadyRunned que nos dirá si ya se ha hecho el match de los usuarios.
Para poder usar este componente, vamos al componente Participants
y agregamos el componente Status
:
// Another imports
import Status from './Status';
export default class Participants extends Component {
state = {
users: [],
currentUser: ""
}
// Another methods
render() {
return (
<div>
<ul>
{ /* Render the list of users */ }
<Status userLenght={this.state.users.length}
handleStatus={this.props.handleStatus}
MatchAlreadyRunned={this.props.MatchAlreadyRunned}
/>
</ul>
</div>
)
}
}
Pero esos metodos van a ser implemenetados en el componente App
por tal razón, se delegará a las funciones mandadas desde el this.props.
. this.props.handleStatus y this.props.MatchAlreadyRunned
Vamos a agregar esas funciones al componente App
:
// Another Imports
import { database } from './fire';
export default class App extends Component {
state = {
currentUser: "",
users: [],
partner: ""
}
handleSubmit = (currentUser) => {
this.setState({
currentUser,
MatchAlreadyRunned: false
});
localStorage.clear("user")
localStorage.setItem("user", JSON.stringify(currentUser))
}
componentDidMount() {
// Implementación más abajo
}
func(a, b) {
return 0.5 - Math.random();
}
handleStatus = () => {
// Implementación más abajo
}
render() {
return (
<div>
{ /* Another Components */}
<Participants
userUpdated={(userList) => this.handleUsersRefresh(userList)}
currentUser={this.state.currentUser}
handleStatus={this.handleStatus}
MatchAlreadyRunned={this.state.MatchAlreadyRunned}
/>
{this.state.partner &&
<div className="alert alert-success" role="alert">
<strong >You match with {this.state.partner}</strong>
</div>
}
</div>
);
}
}
Aquí hay varias cosas, puede que no sea la forma más optima o la mejor; pero fue la solución que llegó a mi mente en su momento.
Bueno, tenemos handleStatus que es una función que me permite tomar una lista de usuarios, los desorganizo asignandolos a la variable usersShuffled, clono la lista original en la variable pendingUsersToAssign y recorro esa lista item por item diciendo:
Hey!? traeme alguien de forma random desde la lista clonada, y si es igual que el que se está evaluando, traeme otro. Si encontras uno que no sea yo, borralo de la lista clonada y metelo en la variable blendedUsers
Despues creo una lista nueva de usuarios a partir de la lista que está en this.state.users
(La original) y la recorro agregandole la propiedad partner a cada usuario con el usuario al que quedó asignado y despues agarro esa lista y le hago push a firebase permitiendo que todas las instancias de la aplicación abiertas puedan obtener esa información. En este caso será a la referencia users-match database.ref('users-match').push(newUsers);
la que acepte esa lista.
handleStatus = () => {
let { users } = this.state
let usersShuffled = users.sort(this.func);
let blendedUsers = [];
let pendingUsersToAssign = [...users];
for (let i = 0; i < usersShuffled.length; i++) {
const element = usersShuffled[i];
let partner = pendingUsersToAssign[Math.floor(Math.random() * pendingUsersToAssign.length)];
while (element.id === partner.id && pendingUsersToAssign.length > 1) {
partner = pendingUsersToAssign[Math.floor(Math.random() * pendingUsersToAssign.length)]
}
pendingUsersToAssign = pendingUsersToAssign.filter(i => i.id !== partner.id)
blendedUsers.push({
element,
partner
})
}
let newUsers = []
for (let i = 0; i < users.length; i++) {
const element = users[i];
let partner = blendedUsers.find(i => i.element.id === element.id)
newUsers.push({
id: element.id,
text: element.text,
partner: partner.partner
})
}
database.ref('users-match').push(newUsers);
}
Por tal razón hacemos uso de nuevo del metodo componentDidMount para, ademas de obtener el usuario creado, vamos a suscribirnos a firebase para que siempre que alguien haga push de algo en users-match podré obtener todos los matchs.
componentDidMount() {
//Get User from LocalStorage
let messagesRef = database.ref('users-match').orderByKey().limitToLast(100);
messagesRef.on('child_added', snapshot => {
/* Update React state when message is added at Firebase Database */
let users = { data: snapshot.val(), id: snapshot.key };
let currentUserInList = users.data.filter(i => i.text === this.state.currentUser)
this.setState({
MatchAlreadyRunned : true
})
if (currentUserInList.length > 0) {
this.setState({
partner: currentUserInList[0].partner.text
})
}
})
}
Finalmente, agarramos esa lista y filtro mi usuario a partir del this.state.currentUser
y le digo al estado que ya se ha realizado el proceso de MatchAlreadyRunned en todos los usuarios (Esto para evitar que alguien más le de click al boton del componente State
) y adicional, si está mi usuario en esa lista, actualizo mi estado en la propiedad partner. El efecto se verá en el render en la siguiente linea
{this.state.partner &&
<div className="alert alert-success" role="alert">
<strong >You match with {this.state.partner}</strong>
</div>
}
Básicamente dice que si tengo un partner asignado, me dirá quien es 😁!
Espero no haber olvidado nada. El demo está en esta pagina https://upbeat-keller-5f955d.netlify.com
PDTA: Si quieres hacerle deploy de la aplicación facilmente, sube este código a github (O tu gestor de repos favoritos) y sigue este tutorial https://dev.to/easybuoy/deploying-react-app-from-github-to-netlify-3a9j