Maison > Article > interface Web > De zéro à vitrine : mon parcours pour créer une plateforme de location de propriétés
Code source : https://github.com/aelassas/movinin
Démo : https://movinin.dynv6.net:3004
L'idée est née d'une envie de construire sans frontières – une plateforme de location immobilière entièrement personnalisable et opérationnelle où chaque aspect est sous votre contrôle :
Voici la pile technologique qui a rendu cela possible :
Une décision clé en matière de conception a été prise d'utiliser TypeScript en raison de ses nombreux avantages. TypeScript offre un typage, des outils et une intégration puissants, ce qui donne lieu à un code de haute qualité, évolutif, plus lisible et maintenable, facile à déboguer et à tester.
J'ai choisi React pour ses puissantes capacités de rendu, MongoDB pour une modélisation flexible des données et Stripe pour le traitement sécurisé des paiements.
En choisissant cette pile, vous ne vous contentez pas de créer un site Web et une application mobile : vous investissez dans une fondation qui peut évoluer avec vos besoins, soutenue par des technologies open source robustes et une communauté de développeurs croissante.
Dans cette section, vous verrez les pages principales du frontend, le tableau de bord d'administration et l'application mobile.
Depuis le frontend, le client peut rechercher les propriétés disponibles, choisir une propriété et payer.
Ci-dessous se trouve la page principale du frontend où le client peut indiquer un point et une heure de localisation et rechercher des propriétés disponibles.
Vous trouverez ci-dessous le résultat de recherche de la page principale où le client peut choisir un bien à louer.
Ci-dessous se trouve la page où le client peut consulter les détails de la propriété :
Ci-dessous une vue des images de la propriété :
Vous trouverez ci-dessous la page de paiement où le client peut définir les options de location et le paiement. Si le client n'est pas inscrit, il peut payer et s'inscrire en même temps. Il recevra un email de confirmation et d'activation pour définir son mot de passe s'il n'est pas encore inscrit.
Vous trouverez ci-dessous la page de connexion. En production, les cookies d'authentification sont httpOnly, signés, sécurisés et stricts sur site. Ces options empêchent les attaques XSS, CSRF et MITM. Les cookies d'authentification sont également protégés contre les attaques XST via un middleware personnalisé.
Vous trouverez ci-dessous la page d'inscription.
Ci-dessous se trouve la page où le client peut voir et gérer ses réservations.
Ci-dessous se trouve la page où le client peut voir une réservation en détail.
Ci-dessous se trouve la page où le client peut voir ses notifications.
Ci-dessous se trouve la page où le client peut gérer ses paramètres.
Ci-dessous la page où le client peut modifier son mot de passe.
C'est tout. Ce sont les pages principales du frontend.
Trois types d'utilisateurs :
La plateforme est conçue pour fonctionner avec plusieurs agences. Chaque agence peut gérer ses propriétés, ses clients et ses réservations depuis le tableau de bord d'administration. La plateforme peut également fonctionner avec une seule agence.
Depuis le backend, les administrateurs peuvent créer et gérer des agences, des propriétés, des emplacements, des clients et des réservations.
Lorsque de nouvelles agences sont créées, elles reçoivent un e-mail les invitant à créer leur compte pour accéder au tableau de bord d'administration afin de pouvoir gérer leurs propriétés, leurs clients et leurs réservations.
Vous trouverez ci-dessous la page de connexion du tableau de bord d'administration.
Vous trouverez ci-dessous la page du tableau de bord où les administrateurs et les agences peuvent voir et gérer les réservations.
Si le statut d'une réservation change, le client concerné recevra une notification et un e-mail.
Ci-dessous se trouve la page où les propriétés sont affichées et peuvent être gérées.
Vous trouverez ci-dessous la page sur laquelle les administrateurs et les agences peuvent créer de nouvelles propriétés en fournissant des images et des informations sur la propriété. Pour une annulation gratuite, réglez-le à 0. Sinon, fixez le prix de l'option ou laissez-le vide si vous ne souhaitez pas l'inclure.
Vous trouverez ci-dessous la page sur laquelle les administrateurs et les agences peuvent modifier les propriétés.
Vous trouverez ci-dessous la page où les administrateurs peuvent gérer les clients.
Ci-dessous se trouve la page où créer des réservations si l'agence souhaite créer une réservation à partir du tableau de bord d'administration. Sinon, les réservations sont créées automatiquement lorsque le processus de paiement est terminé depuis le frontend ou l'application mobile.
Vous trouverez ci-dessous la page où modifier les réservations.
Ci-dessous se trouve la page où gérer les agences.
Ci-dessous se trouve la page où créer de nouvelles agences.
Vous trouverez ci-dessous la page où modifier les agences.
Vous trouverez ci-dessous la page où voir les propriétés des agences.
Ci-dessous se trouve la page où voir les réservations des clients.
Vous trouverez ci-dessous la page sur laquelle les administrateurs et les agences peuvent gérer leurs paramètres.
Il existe d'autres pages mais ce sont les pages principales du tableau de bord d'administration.
C'est tout. Ce sont les pages principales du tableau de bord d'administration.
L'API expose toutes les fonctions nécessaires au tableau de bord d'administration, au frontend et à l'application mobile. L'API suit le modèle de conception MVC. JWT est utilisé pour l'authentification. Certaines fonctions nécessitent une authentification, telles que les fonctions liées à la gestion des propriétés, des réservations et des clients, et d'autres ne nécessitent pas d'authentification, telles que la récupération des emplacements et des propriétés disponibles pour les utilisateurs non authentifiés :
index.ts est le point d'entrée principal de l'API :
import 'dotenv/config' import process from 'node:process' import fs from 'node:fs/promises' import http from 'node:http' import https, { ServerOptions } from 'node:https' import app from './app' import * as databaseHelper from './common/databaseHelper' import * as env from './config/env.config' import * as logger from './common/logger' if ( await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) && await databaseHelper.initialize() ) { let server: http.Server | https.Server if (env.HTTPS) { https.globalAgent.maxSockets = Number.POSITIVE_INFINITY const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8') const certificate = await fs.readFile(env.CERTIFICATE, 'utf8') const credentials: ServerOptions = { key: privateKey, cert: certificate } server = https.createServer(credentials, app) server.listen(env.PORT, () => { logger.info('HTTPS server is running on Port', env.PORT) }) } else { server = app.listen(env.PORT, () => { logger.info('HTTP server is running on Port', env.PORT) }) } const close = () => { logger.info('Gracefully stopping...') server.close(async () => { logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`) await databaseHelper.close(true) logger.info('MongoDB connection closed') process.exit(0) }) } ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close)) }
Il s'agit d'un fichier TypeScript qui démarre un serveur à l'aide de Node.js et Express. Il importe plusieurs modules, notamment dotenv, process, fs, http, https, mongoose et app. Il vérifie ensuite si la variable d'environnement HTTPS est définie sur true et, si tel est le cas, crée un serveur HTTPS à l'aide du module https ainsi que de la clé privée et du certificat fournis. Sinon, il crée un serveur HTTP à l'aide du module http. Le serveur écoute sur le port spécifié dans la variable d'environnement PORT.
La fonction close est définie pour arrêter gracieusement le serveur lorsqu'un signal de terminaison est reçu. Il ferme le serveur et la connexion MongoDB, puis quitte le processus avec un code d'état de 0. Enfin, il enregistre la fonction de fermeture à appeler lorsque le processus reçoit un signal SIGINT, SIGTERM ou SIGQUIT.
app.ts est le point d'entrée principal de l'API :
import express from 'express' import compression from 'compression' import helmet from 'helmet' import nocache from 'nocache' import cookieParser from 'cookie-parser' import i18n from './lang/i18n' import * as env from './config/env.config' import cors from './middlewares/cors' import allowedMethods from './middlewares/allowedMethods' import agencyRoutes from './routes/agencyRoutes' import bookingRoutes from './routes/bookingRoutes' import locationRoutes from './routes/locationRoutes' import notificationRoutes from './routes/notificationRoutes' import propertyRoutes from './routes/propertyRoutes' import userRoutes from './routes/userRoutes' import stripeRoutes from './routes/stripeRoutes' import countryRoutes from './routes/countryRoutes' import * as helper from './common/helper' const app = express() app.use(helmet.contentSecurityPolicy()) app.use(helmet.dnsPrefetchControl()) app.use(helmet.crossOriginEmbedderPolicy()) app.use(helmet.frameguard()) app.use(helmet.hidePoweredBy()) app.use(helmet.hsts()) app.use(helmet.ieNoOpen()) app.use(helmet.noSniff()) app.use(helmet.permittedCrossDomainPolicies()) app.use(helmet.referrerPolicy()) app.use(helmet.xssFilter()) app.use(helmet.originAgentCluster()) app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' })) app.use(helmet.crossOriginOpenerPolicy()) app.use(nocache()) app.use(compression({ threshold: 0 })) app.use(express.urlencoded({ limit: '50mb', extended: true })) app.use(express.json({ limit: '50mb' })) app.use(cors()) app.options('*', cors()) app.use(cookieParser(env.COOKIE_SECRET)) app.use(allowedMethods) app.use('/', agencyRoutes) app.use('/', bookingRoutes) app.use('/', locationRoutes) app.use('/', notificationRoutes) app.use('/', propertyRoutes) app.use('/', userRoutes) app.use('/', stripeRoutes) app.use('/', countryRoutes) i18n.locale = env.DEFAULT_LANGUAGE helper.mkdir(env.CDN_USERS) helper.mkdir(env.CDN_TEMP_USERS) helper.mkdir(env.CDN_PROPERTIES) helper.mkdir(env.CDN_TEMP_PROPERTIES) helper.mkdir(env.CDN_LOCATIONS) helper.mkdir(env.CDN_TEMP_LOCATIONS) export default app
Tout d'abord, nous récupérons la chaîne de connexion MongoDB, puis nous établissons une connexion avec la base de données MongoDB. Ensuite, nous créons une application Express et chargeons des middlewares tels que cors, compression, casque et nocache. Nous avons mis en place diverses mesures de sécurité à l'aide de la bibliothèque middleware du casque. nous importons également divers fichiers d'itinéraire pour différentes parties de l'application telles que supplierRoutes, bookingRoutes, locationRoutes, notificationRoutes, propertyRoutes et userRoutes. Enfin, nous chargeons les itinéraires Express et exportons l'application.
Il y a 8 routes dans l'API. Chaque route possède son propre contrôleur suivant le modèle de conception MVC et les principes SOLID. Ci-dessous les principaux itinéraires :
Nous n'allons pas expliquer chaque itinéraire un par un. Prenons, par exemple, propertyRoutes et voyons comment il a été réalisé. Vous pouvez parcourir le code source et voir tous les itinéraires.
Voici propertyRoutes.ts :
import express from 'express' import multer from 'multer' import routeNames from '../config/propertyRoutes.config' import authJwt from '../middlewares/authJwt' import * as propertyController from '../controllers/propertyController' const routes = express.Router() routes.route(routeNames.create).post(authJwt.verifyToken, propertyController.create) routes.route(routeNames.update).put(authJwt.verifyToken, propertyController.update) routes.route(routeNames.checkProperty).get(authJwt.verifyToken, propertyController.checkProperty) routes.route(routeNames.delete).delete(authJwt.verifyToken, propertyController.deleteProperty) routes.route(routeNames.uploadImage).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('image')], propertyController.uploadImage) routes.route(routeNames.deleteImage).post(authJwt.verifyToken, propertyController.deleteImage) routes.route(routeNames.deleteTempImage).post(authJwt.verifyToken, propertyController.deleteTempImage) routes.route(routeNames.getProperty).get(propertyController.getProperty) routes.route(routeNames.getProperties).post(authJwt.verifyToken, propertyController.getProperties) routes.route(routeNames.getBookingProperties).post(authJwt.verifyToken, propertyController.getBookingProperties) routes.route(routeNames.getFrontendProperties).post(propertyController.getFrontendProperties) export default routes
Tout d’abord, nous créons un routeur express. Ensuite, nous créons les routes en utilisant leur nom, leur méthode, leurs middlewares et leurs contrôleurs.
routeNames contient les noms de routes propertyRoutes :
const routes = { create: '/api/create-property', update: '/api/update-property', delete: '/api/delete-property/:id', uploadImage: '/api/upload-property-image', deleteTempImage: '/api/delete-temp-property-image/:fileName', deleteImage: '/api/delete-property-image/:property/:image', getProperty: '/api/property/:id/:language', getProperties: '/api/properties/:page/:size', getBookingProperties: '/api/booking-properties/:page/:size', getFrontendProperties: '/api/frontend-properties/:page/:size', checkProperty: '/api/check-property/:id', } export default routes
propertyController contient la principale logique métier concernant les emplacements. Nous n'allons pas voir tout le code source du contrôleur car il est assez volumineux mais nous prendrons par exemple la fonction de création de contrôleur.
Ci-dessous le modèle de propriété :
import { Schema, model } from 'mongoose' import * as movininTypes from ':movinin-types' import * as env from '../config/env.config' const propertySchema = new Schema<env.Property>( { name: { type: String, required: [true, "can't be blank"], }, type: { type: String, enum: [ movininTypes.PropertyType.House, movininTypes.PropertyType.Apartment, movininTypes.PropertyType.Townhouse, movininTypes.PropertyType.Plot, movininTypes.PropertyType.Farm, movininTypes.PropertyType.Commercial, movininTypes.PropertyType.Industrial, ], required: [true, "can't be blank"], }, agency: { type: Schema.Types.ObjectId, required: [true, "can't be blank"], ref: 'User', index: true, }, description: { type: String, required: [true, "can't be blank"], }, available: { type: Boolean, default: true, }, image: { type: String, }, images: { type: [String], }, bedrooms: { type: Number, required: [true, "can't be blank"], validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, bathrooms: { type: Number, required: [true, "can't be blank"], validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, kitchens: { type: Number, default: 1, validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, parkingSpaces: { type: Number, default: 0, validate: { validator: Number.isInteger, message: '{VALUE} is not an integer value', }, }, size: { type: Number, }, petsAllowed: { type: Boolean, required: [true, "can't be blank"], }, furnished: { type: Boolean, required: [true, "can't be blank"], }, minimumAge: { type: Number, required: [true, "can't be blank"], min: env.MINIMUM_AGE, max: 99, }, location: { type: Schema.Types.ObjectId, ref: 'Location', required: [true, "can't be blank"], }, address: { type: String, }, price: { type: Number, required: [true, "can't be blank"], }, hidden: { type: Boolean, default: false, }, cancellation: { type: Number, default: 0, }, aircon: { type: Boolean, default: false, }, rentalTerm: { type: String, enum: [ movininTypes.RentalTerm.Monthly, movininTypes.RentalTerm.Weekly, movininTypes.RentalTerm.Daily, movininTypes.RentalTerm.Yearly, ], required: [true, "can't be blank"], }, }, { timestamps: true, strict: true, collection: 'Property', }, ) const Property = model<env.Property>('Property', propertySchema) export default Property
Ci-dessous le type de propriété :
export interface Property extends Document { name: string type: movininTypes.PropertyType agency: Types.ObjectId description: string image: string images?: string[] bedrooms: number bathrooms: number kitchens?: number parkingSpaces?: number, size?: number petsAllowed: boolean furnished: boolean minimumAge: number location: Types.ObjectId address?: string price: number hidden?: boolean cancellation?: number aircon?: boolean available?: boolean rentalTerm: movininTypes.RentalTerm }
Un bien est composé de :
Ci-dessous se trouve la fonction de création de contrôleur :
import 'dotenv/config' import process from 'node:process' import fs from 'node:fs/promises' import http from 'node:http' import https, { ServerOptions } from 'node:https' import app from './app' import * as databaseHelper from './common/databaseHelper' import * as env from './config/env.config' import * as logger from './common/logger' if ( await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) && await databaseHelper.initialize() ) { let server: http.Server | https.Server if (env.HTTPS) { https.globalAgent.maxSockets = Number.POSITIVE_INFINITY const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8') const certificate = await fs.readFile(env.CERTIFICATE, 'utf8') const credentials: ServerOptions = { key: privateKey, cert: certificate } server = https.createServer(credentials, app) server.listen(env.PORT, () => { logger.info('HTTPS server is running on Port', env.PORT) }) } else { server = app.listen(env.PORT, () => { logger.info('HTTP server is running on Port', env.PORT) }) } const close = () => { logger.info('Gracefully stopping...') server.close(async () => { logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`) await databaseHelper.close(true) logger.info('MongoDB connection closed') process.exit(0) }) } ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close)) }
Le frontend est une application Web construite avec Node.js, React, MUI et TypeScript. Depuis le frontend, le client peut rechercher les voitures disponibles en fonction des points de prise en charge et de dépôt et de l'heure, choisir une voiture et procéder au paiement :
Les définitions de types TypeScript sont définies dans le package ./packages/movinin-types.
App.tsx est la principale application de réaction :
import express from 'express' import compression from 'compression' import helmet from 'helmet' import nocache from 'nocache' import cookieParser from 'cookie-parser' import i18n from './lang/i18n' import * as env from './config/env.config' import cors from './middlewares/cors' import allowedMethods from './middlewares/allowedMethods' import agencyRoutes from './routes/agencyRoutes' import bookingRoutes from './routes/bookingRoutes' import locationRoutes from './routes/locationRoutes' import notificationRoutes from './routes/notificationRoutes' import propertyRoutes from './routes/propertyRoutes' import userRoutes from './routes/userRoutes' import stripeRoutes from './routes/stripeRoutes' import countryRoutes from './routes/countryRoutes' import * as helper from './common/helper' const app = express() app.use(helmet.contentSecurityPolicy()) app.use(helmet.dnsPrefetchControl()) app.use(helmet.crossOriginEmbedderPolicy()) app.use(helmet.frameguard()) app.use(helmet.hidePoweredBy()) app.use(helmet.hsts()) app.use(helmet.ieNoOpen()) app.use(helmet.noSniff()) app.use(helmet.permittedCrossDomainPolicies()) app.use(helmet.referrerPolicy()) app.use(helmet.xssFilter()) app.use(helmet.originAgentCluster()) app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' })) app.use(helmet.crossOriginOpenerPolicy()) app.use(nocache()) app.use(compression({ threshold: 0 })) app.use(express.urlencoded({ limit: '50mb', extended: true })) app.use(express.json({ limit: '50mb' })) app.use(cors()) app.options('*', cors()) app.use(cookieParser(env.COOKIE_SECRET)) app.use(allowedMethods) app.use('/', agencyRoutes) app.use('/', bookingRoutes) app.use('/', locationRoutes) app.use('/', notificationRoutes) app.use('/', propertyRoutes) app.use('/', userRoutes) app.use('/', stripeRoutes) app.use('/', countryRoutes) i18n.locale = env.DEFAULT_LANGUAGE helper.mkdir(env.CDN_USERS) helper.mkdir(env.CDN_TEMP_USERS) helper.mkdir(env.CDN_PROPERTIES) helper.mkdir(env.CDN_TEMP_PROPERTIES) helper.mkdir(env.CDN_LOCATIONS) helper.mkdir(env.CDN_TEMP_LOCATIONS) export default app
Nous utilisons le chargement différé de React pour charger chaque itinéraire.
Nous n'allons pas couvrir chaque page du frontend, mais vous pouvez parcourir le code source et voir chacune d'elles.
La plateforme propose une application mobile native pour Android et iOS. L'application mobile est construite avec React Native, Expo et TypeScript. Comme pour le frontend, l'application mobile permet au client de rechercher des voitures disponibles en fonction des points de prise en charge et de dépôt et de l'heure, de choisir une voiture et de procéder au paiement.
Le client reçoit des notifications push si sa réservation est mise à jour depuis le backend. Les notifications push sont créées avec Node.js, Expo Server SDK et Firebase.
Les définitions de types TypeScript sont définies dans :
./mobile/types/ est chargé dans ./mobile/tsconfig.json comme suit :
import 'dotenv/config' import process from 'node:process' import fs from 'node:fs/promises' import http from 'node:http' import https, { ServerOptions } from 'node:https' import app from './app' import * as databaseHelper from './common/databaseHelper' import * as env from './config/env.config' import * as logger from './common/logger' if ( await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) && await databaseHelper.initialize() ) { let server: http.Server | https.Server if (env.HTTPS) { https.globalAgent.maxSockets = Number.POSITIVE_INFINITY const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8') const certificate = await fs.readFile(env.CERTIFICATE, 'utf8') const credentials: ServerOptions = { key: privateKey, cert: certificate } server = https.createServer(credentials, app) server.listen(env.PORT, () => { logger.info('HTTPS server is running on Port', env.PORT) }) } else { server = app.listen(env.PORT, () => { logger.info('HTTP server is running on Port', env.PORT) }) } const close = () => { logger.info('Gracefully stopping...') server.close(async () => { logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`) await databaseHelper.close(true) logger.info('MongoDB connection closed') process.exit(0) }) } ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close)) }
App.tsx est le point d'entrée principal de l'application React Native :
importer 'react-native-gesture-handler' importer React, { useCallback, useEffect, useRef, useState } depuis 'react' importer { RootSiblingParent } depuis 'react-native-root-siblings' importer { NavigationContainer, NavigationContainerRef } depuis '@react-navigation/native' importer { StatusBar comme ExpoStatusBar } depuis 'expo-status-bar' importer { SafeAreaProvider } depuis 'react-native-safe-area-context' importer { Fournisseur } depuis 'react-native-paper' importer * en tant que SplashScreen depuis 'expo-splash-screen' importer * en tant que notifications de 'expo-notifications' importer { StripeProvider } depuis '@stripe/stripe-react-native' importer DrawerNavigator depuis './components/DrawerNavigator' importer * comme assistant depuis './common/helper' importer * en tant que NotificationService depuis './services/NotificationService' importer * en tant que UserService depuis './services/UserService' importer { GlobalProvider } depuis './context/GlobalContext' importer * en tant qu'environnement depuis './config/env.config' Notifications.setNotificationHandler({ handleNotification : async () => ({ ShouldShowAlert : vrai, ShouldPlaySound : vrai, ShouldSetBadge : vrai, }), }) // // Empêche l'écran de démarrage natif de se masquer automatiquement avant la déclaration du composant App // SplashScreen.preventAutoHideAsync() .then((result) => console.log(`SplashScreen.preventAutoHideAsync() a réussi : ${result}`)) .catch(console.warn) // il est bon de détecter et d'inspecter explicitement toute erreur const App = () => { const [appIsReady, setAppIsReady] = useState (false) const réponseListener = useRef<Notifications.Subscription>() const navigationRef = useRef<NavigationContainerRef<StackParams>>(null) useEffect(() => { const registre = async () => { constloggedIn = attendre UserService.loggedIn() si (connecté) { const currentUser = attendre UserService.getCurrentUser() si (utilisateuractuel?._id) { attendre helper.registerPushToken (currentUser._id) } autre { aide.erreur() } } } // // Enregistre le jeton de notifications push // registre() // // Cet écouteur est déclenché chaque fois qu'un utilisateur appuie ou interagit avec une notification (fonctionne lorsque l'application est au premier plan, en arrière-plan ou supprimée) // réponseListener.current = Notifications.addNotificationResponseReceivedListener (async (réponse) => { essayer { si (navigationRef.current) { const {données} = réponse.notification.request.content si (data.booking) { si (data.user && data.notification) { attendre NotificationService.markAsRead (data.user, [data.notification]) } navigationRef.current.navigate('Réservation', { id : data.booking }) } autre { navigationRef.current.navigate('Notifications', {}) } } } attraper (erreur) { helper.error(erreur, faux) } }) return () => { Notifications.removeNotificationSubscription(responseListener.current!) } }, []) setTimeout(() => { setAppIsReady (vrai) }, 500) const onReady = useCallback(async () => { si (appIsReady) { // // Cela indique à l'écran de démarrage de se cacher immédiatement ! Si nous appelons cela après // `setAppIsReady`, alors nous pouvons voir un écran vide pendant que l'application est // chargement de son état initial et rendu de ses premiers pixels. Alors à la place, // nous masquons l'écran de démarrage une fois que nous savons que la vue racine est déjà présente // effectué la mise en page. // attendre SplashScreen.hideAsync() } }, [appIsReady]) si (!appIsReady) { renvoyer nul } retour ( <GlobalProvider> <SafeAreaProvider> <Fournisseur> <StripeProvider publiableKey={env.STRIPE_PUBLISHABLE_KEY} MerchantIdentifier={env.STRIPE_MERCHANT_IDENTIFIER}> <RootSiblingParent> <NavigationContainer ref={navigationRef} onReady={onReady}> <ExpoStatusBar> <p>Nous n'allons pas couvrir chaque écran de l'application mobile, mais vous pouvez parcourir le code source et voir chacun.</p> <h2> Tableau de bord d'administration </h2> <p>Le tableau de bord d'administration est une application Web construite avec Node.js, React, MUI et TypeScript. Depuis le backend, les administrateurs peuvent créer et gérer des fournisseurs, des voitures, des emplacements, des clients et des réservations. Lorsque de nouveaux fournisseurs sont créés depuis le backend, ils recevront un e-mail les invitant à créer un compte afin d'accéder au tableau de bord d'administration et de gérer leur flotte de voitures et leurs réservations.</p>
Les définitions de types TypeScript sont définies dans le package ./packages/movinin-types.
App.tsx du tableau de bord d'administration suit une logique similaire à celle App.tsx du frontend.
Nous n'allons pas couvrir chaque page du tableau de bord d'administration mais vous pouvez parcourir le code source et voir chacune d'elles.
Créer l'application mobile avec React Native et Expo est très simple. Expo rend le développement mobile avec React Native très simple.
Utiliser le même langage (TypeScript) pour le développement backend, frontend et mobile est très pratique.
TypeScript est un langage très intéressant et présente de nombreux avantages. En ajoutant le typage statique à JavaScript, nous pouvons éviter de nombreux bugs et produire un code de haute qualité, évolutif, plus lisible et maintenable, facile à déboguer et à tester.
C'est ça ! J'espère que vous avez apprécié la lecture de cet article.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!