Maison > interface Web > js tutoriel > De zéro à héros : mon parcours pour créer un site Web et une application mobile de location de propriété

De zéro à héros : mon parcours pour créer un site Web et une application mobile de location de propriété

Susan Sarandon
Libérer: 2024-11-11 16:09:03
original
1065 Les gens l'ont consulté

Contenu

  1. Présentation
  2. Pile technologique
  3. Aperçu rapide
  4. Démo en direct
  5. API
  6. Frontend
  7. Application mobile
  8. Tableau de bord d'administration
  9. Points d'intérêt
  10. Ressources

Code source : https://github.com/aelassas/movinin

Démo : https://movinin.dynv6.net:3004

Introduction

L'idée est née d'une volonté de construire sans frontières – un site Web et une application mobile de location de propriétés entièrement personnalisables et opérationnels où chaque aspect est sous votre contrôle :

  • Possédez l'UI/UX : Concevez des expériences client uniques sans lutter contre les limitations des modèles
  • Contrôlez le backend : implémentez une logique métier personnalisée et des structures de données qui correspondent parfaitement aux exigences
  • Maîtrisez DevOps : Déployez, faites évoluer et surveillez l'application avec les outils et flux de travail préférés
  • Extendez librement : ajoutez de nouvelles fonctionnalités et intégrations sans contraintes de plate-forme ni frais supplémentaires

Exigences techniques :

  • Passerelle de paiement :
    • Mettre en œuvre une passerelle de paiement sécurisée et prise en charge à l'échelle internationale
    • Assurer la compatibilité entre plusieurs pays et devises
  • DevOps :
    • Déployer à l'aide de conteneurs Docker pour plus de cohérence et d'évolutivité
    • Hébergeur sur infrastructure minimale (serveur 1 Go RAM)
    • Maintenez les coûts d'hébergement mensuels à moins de 5 $ en utilisant des fournisseurs comme Hetzner ou DigitalOcean
    • Optimiser l'utilisation des ressources pour un fonctionnement efficace

Pile technologique

Voici la pile technologique qui a rendu cela possible :

  • TypeScript
  • Node.js
  • MongoDB
  • Réagir
  • MUI
  • Réagir natif
  • Expo
  • Rayure
  • Docker

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.

React se démarque comme un excellent choix grâce à :

  1. Architecture basée sur les composants
    • Vous permet de diviser les interfaces utilisateur complexes en éléments plus petits et réutilisables
    • Rend le code plus maintenable et plus facile à tester
    • Permet une meilleure organisation et réutilisation du code
  2. Performances du DOM virtuel
    • Le DOM virtuel de React met à jour efficacement uniquement ce qui est nécessaire
    • Résultat en un chargement de page plus rapide et une meilleure expérience utilisateur
    • Réduit les rendus inutiles
  3. Riche écosystème
    • Vaste bibliothèque de composants prédéfinis
    • Outillage complet
    • Grande communauté de soutien et de ressources
  4. Forte expérience de développeur
    • Rechargement à chaud pour un retour immédiat
    • Excellents outils de débogage
    • JSX rend l'écriture du code de l'interface utilisateur plus intuitive
  5. Soutien à l'industrie
    • Soutenu par Meta (anciennement Facebook)
    • Utilisé par de nombreuses grandes entreprises
    • Développement et améliorations continus
  6. Flexibilité
    • Fonctionne bien pour les petites et grandes applications
    • Peut être intégré progressivement dans des projets existants
    • Prend en charge plusieurs stratégies de rendu (côté client, côté serveur, statique)

Aperçu rapide

Dans cette section, vous verrez les pages principales du frontend, le tableau de bord d'administration et l'application mobile.

L'extrémité avant

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.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous le résultat de recherche de la page principale où le client peut choisir un bien à louer.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où le client peut consulter les détails de la propriété :

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous une vue des images de la propriété :

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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é.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page d'inscription.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où le client peut voir et gérer ses réservations.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où le client peut voir une réservation en détail.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où le client peut voir ses notifications.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où le client peut gérer ses paramètres.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous la page où le client peut modifier son mot de passe.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

C'est tout. Ce sont les pages principales du frontend.

Tableau de bord d'administration

Trois types d'utilisateurs :

  • Administrateurs : ils ont un accès complet au tableau de bord d'administration. Ils peuvent tout faire.
  • Agences : elles ont un accès limité au tableau de bord d'administration. Ils ne peuvent gérer que leurs propriétés, leurs réservations et leurs clients.
  • Clients : ils ont accès uniquement au frontend et à l'application mobile. Ils ne peuvent pas accéder au tableau de bord d'administration.

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.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page du tableau de bord où les administrateurs et les agences peuvent voir et gérer les réservations.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page sur laquelle les administrateurs et les agences peuvent modifier les propriétés.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page où les administrateurs peuvent gérer les clients.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page où modifier les réservations.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où gérer les agences.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où créer de nouvelles agences.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page où modifier les agences.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page où voir les propriétés des agences.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Ci-dessous se trouve la page où voir les réservations des clients.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Vous trouverez ci-dessous la page sur laquelle les administrateurs et les agences peuvent gérer leurs paramètres.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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.

Démo en direct

L'extrémité avant

  • URL : https://movinin.dynv6.net:3004/
  • Connexion : jdoe@movinin.io
  • Mot de passe : M00vinin

Tableau de bord d'administration

  • URL : https://movinin.dynv6.net:3003/
  • Connexion : admin@movinin.io
  • Mot de passe : M00vinin

Application mobile

Vous pouvez installer l'application Android sur n'importe quel appareil Android.

Scannez ce code avec un appareil

Ouvrez l'application Appareil photo et pointez-la sur ce code. Appuyez ensuite sur la notification qui apparaît.

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Comment installer l'application mobile sur Android

  • Sur les appareils fonctionnant sous Android 8.0 (API niveau 26) et supérieur, vous devez accéder à l'écran des paramètres système d'installation d'applications inconnues pour activer l'installation d'applications à partir d'un emplacement particulier (c'est-à-dire le navigateur Web à partir duquel vous téléchargez l'application). .

  • Sur les appareils exécutant Android 7.1.1 (API niveau 25) et versions antérieures, vous devez activer le paramètre système Sources inconnues, trouvé dans Paramètres > Sécurité sur votre appareil.

Méthode alternative

Vous pouvez également installer l'application Android en téléchargeant directement l'APK et en l'installant sur n'importe quel appareil Android.

  • Télécharger l'APK
  • Connexion : jdoe@movinin.io
  • Mot de passe : M00vinin

API

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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 :

  • Le dossier ./api/src/models/ contient des modèles MongoDB.
  • Le dossier ./api/src/routes/ contient les routes Express.
  • Le dossier ./api/src/controllers/ contient des contrôleurs.
  • Le dossier ./api/src/middlewares/ contient des middlewares.
  • ./api/src/config/env.config.ts contient la configuration et les définitions de type TypeScript.
  • Le dossier ./api/src/lang/ contient la localisation.
  • ./api/src/app.ts est le serveur principal sur lequel les routes sont chargées.
  • ./api/index.ts est le point d'entrée principal de l'API.

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))
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

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
Copier après la connexion
Copier après la connexion
Copier après la connexion

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 :

  • userRoutes : fournit des fonctions REST liées aux utilisateurs
  • agencyRoutes : Fournit des fonctions REST liées aux agences
  • countryRoutes : fournit des fonctions REST liées aux pays
  • locationRoutes : fournit des fonctions REST liées aux emplacements
  • propertyRoutes : fournit des fonctions REST liées aux propriétés
  • bookingRoutes : fournit des fonctions REST liées aux réservations
  • notificationRoutes : Fournit des fonctions REST liées aux notifications
  • stripeRoutes : fournit des fonctions REST liées à la passerelle de paiement Stripe

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 '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))
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

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 :

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
Copier après la connexion
Copier après la connexion
Copier après la connexion

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 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
Copier après la connexion

Ci-dessous le type de propriété :

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
Copier après la connexion

Un bien est composé de :

  • Un nom
  • Un type (Appartement, Commercial, Ferme, Maison, Industriel, Terrain, Maison de ville)
  • Une référence à l'agence qui l'a créé
  • Une description
  • Une image principale
  • Images supplémentaires
  • Nombre de chambres
  • Nombre de salles de bain
  • Nombre de cuisines
  • Nombre de places de stationnement
  • Une taille
  • Âge minimum pour la location
  • Un emplacement
  • Une adresse (facultatif)
  • Un prix
  • Une durée de location (mensuelle, hebdomadaire, journalière, annuelle)
  • Prix d'annulation (réglez-le à 0 pour être inclus gratuitement, laissez-le vide si vous ne souhaitez pas l'inclure, ou fixez le prix d'annulation)
  • Un drapeau qui indique si les animaux sont autorisés ou non
  • Un drapeau qui indique si le bien est meublé ou non
  • Un drapeau qui indique si la propriété est masquée ou non
  • Un drapeau qui indique si la climatisation est disponible ou non
  • Un drapeau qui indique si le bien est disponible à la location ou non

Ci-dessous se trouve la fonction de création de contrôleur :

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
Copier après la connexion

L'extrémité avant

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 :

  • Le dossier ./frontend/src/assets/ contient du CSS et des images.
  • Le dossier ./frontend/src/pages/ contient des pages React.
  • Le dossier ./frontend/src/components/ contient des composants React.
  • ./frontend/src/services/ contient les services client API.
  • ./frontend/src/App.tsx est la principale application React qui contient des routes.
  • ./frontend/src/index.tsx est le point d'entrée principal du frontend.

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 '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))
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

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.

Application mobile

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.

  • Le dossier ./mobile/assets/ contient des images.
  • Le dossier ./mobile/screens/ contient les principaux écrans React Native.
  • Le dossier ./mobile/components/ contient des composants React Native.
  • ./mobile/services/ contient des services client API.
  • ./mobile/App.tsx est la principale application native de React.

Les définitions de types TypeScript sont définies dans :

  • ./mobile/types/index.d.ts
  • ./mobile/types/env.d.ts
  • ./mobile/miscellaneous/movininTypes.ts

./mobile/types/ est chargé dans ./mobile/tsconfig.json comme suit :

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
Copier après la connexion
Copier après la connexion
Copier après la connexion

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>

Copier après la connexion
  • Le dossier ./backend/assets/ contient du CSS et des images.
  • ./backend/pages/ le dossier contient des pages React.
  • Le dossier ./backend/components/ contient des composants React.
  • ./backend/services/ contient les services client API.
  • ./backend/App.tsx est la principale application React qui contient des routes.
  • ./backend/index.tsx est le point d'entrée principal du tableau de bord d'administration.

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.

Points d'intérêt

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.

Ressources

  1. Aperçu
  2. Architecture
  3. Installation (auto-hébergé)
  4. Installation (VPS)
  5. Installation (Docker)
    1. Image Docker
    2. SSL
  6. Configuration de Stripe
  7. Créer une application mobile
  8. Base de données de démonstration
    1. Windows, Linux et macOS
    2. Docker
  9. Exécuter à partir de la source
  10. Exécuter l'application mobile
    1. Prérequis
    2. Instructions
    3. Notifications push
  11. Changer de devise
  12. Ajouter une nouvelle langue
  13. Tests unitaires et couverture
  14. Journaux

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!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal