亚洲国产日韩欧美一区二区三区,精品亚洲国产成人av在线,国产99视频精品免视看7,99国产精品久久久久久久成人热,欧美日韩亚洲国产综合乱

Home Web Front-end JS Tutorial From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

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

Nov 11, 2024 pm 04:09 PM

Contents

  1. Introduction
  2. Tech Stack
  3. Quick Overview
  4. Live demo
  5. API
  6. Frontend
  7. Mobile App
  8. Admin Dashboard
  9. Points of Interest
  10. Resources

Source code: https://github.com/aelassas/movinin

Demo: https://movinin.dynv6.net:3004

Introduction

The idea emerged from a desire to build without boundaries – a fully customizable and operational property rental website and mobile app where every aspect is within your control:

  • Own the UI/UX: Design unique customer experiences without fighting against template limitations
  • Control the Backend: Implement custom business logic and data structures that perfectly match the requirements
  • Master DevOps: Deploy, scale, and monitor the application with preferred tools and workflows
  • Extend Freely: Add new features and integrations without platform constraints or additional fees

Technical Requirements:

  • Payment Gateway:
    • Implement a secure, internationally supported payment gateway
    • Ensure compatibility across multiple countries and currencies
  • DevOps:
    • Deploy using Docker containers for consistency and scalability
    • Host on minimal infrastructure (1GB RAM server)
    • Maintain monthly hosting costs under $5 using providers like Hetzner or DigitalOcean
    • Optimize resource usage for efficient operation

Tech Stack

Here's the tech stack that made it possible:

  • TypeScript
  • Node.js
  • MongoDB
  • React
  • MUI
  • React Native
  • Expo
  • Stripe
  • Docker

A key design decision was made to use TypeScript due to its numerous advantages. TypeScript offers strong typing, tooling, and integration, resulting in high-quality, scalable, more readable and maintainable code that is easy to debug and test.

I chose React for its powerful rendering capabilities, MongoDB for flexible data modeling, and Stripe for secure payment processing.

By choosing this stack, you're not just building a website and mobile app – you're investing in a foundation that can evolve with your needs, backed by robust open-source technologies and a growing developer community.

React stands out as an excellent choice due to its:

  1. Component-Based Architecture
    • Lets you break down complex UIs into smaller, reusable pieces
    • Makes code more maintainable and easier to test
    • Enables better code organization and reusability
  2. Virtual DOM Performance
    • React's virtual DOM efficiently updates only what's necessary
    • Results in faster page loads and better user experience
    • Reduces unnecessary re-renders
  3. Rich Ecosystem
    • Vast library of pre-built components
    • Extensive tooling
    • Large community for support and resources
  4. Strong Developer Experience
    • Hot reloading for immediate feedback
    • Excellent debugging tools
    • JSX makes writing UI code more intuitive
  5. Industry Support
    • Backed by Meta (formerly Facebook)
    • Used by many major companies
    • Continuous development and improvements
  6. Flexibility
    • Works well for both small and large applications
    • Can be integrated gradually into existing projects
    • Supports multiple rendering strategies (client-side, server-side, static)

Quick overview

In this section, you'll see the main pages of the frontend, the admin dashboard and the mobile app.

Frontend

From the frontend, the customer can search for available properties, choose a property and checkout.

Below is the main page of the frontend where the customer can a location point and time, and search for available properties.

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

Below is the search result of the main page where the customer can choose a property for rental.

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

Below is the page where the customer can view the details of the property:

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

Below is a view of the images of the property:

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

Below is the checkout page where the customer can set rental options and checkout. If the customer is not registered, he can checkout and register at the same time. He will receive a confirmation and activation email to set his password if he is not registered yet.

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

Below is the sign in page. On production, authentication cookies are httpOnly, signed, secure and strict sameSite. These options prevent XSS, CSRF and MITM attacks. Authentication cookies are protected against XST attacks as well via a custom middleware.

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

Below is the sign up page.

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

Below is the page where the customer can see and manage his bookings.

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

Below is the page where the customer can see a booking in detail.

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

Below is the page where the customer can see his notifications.

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

Below is the page where the customer can manage his settings.

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

Below is the page where the customer can change his password.

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

That's it. That's the main pages of the frontend.

Admin Dashboard

Three types of users:

  • Admins: They have full access to the admin dashboard. They can do everything.
  • Agencies: They have limited access on the admin dashboard. They can only manage their properties, bookings and customers.
  • Customers: They have access to the frontend and the mobile app only. They cannot access the admin dashboard.

The platform is designed to work with multiple agencies. Each agency can manage its properties, customers and bookings from the admin dashboard. The platform can also work with only one agency as well.

From the backend, admins can create and manage agencies, properties, locations, customers and bookings.

When new agencies are created, they receive an email prompting them to create their account to access the admin dashboard so they can manage their properties, customers and bookings.

Below is the sign in page of the admin dashboard.

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

Below is the dashboard page where admins and agencies can see and manage bookings.

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

If the status of a booking changes, the related customer will receive a notification and an email.

Below is the page where properties are displayed and can be managed.

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

Below is the page where admins and agencies can create new properties by providing images and property info. For cancellation for free, set it to 0. Otherwise, set the price of the option or leave it empty if you don't want to include it.

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

Below is the page where admins and agencies can edit properties.

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

Below is the page where admins can manage customers.

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

Below is the page where to create bookings if the agency wants to create a booking from the admin dashboard. Otherwise, bookings are created automatically when the checkout process is completed from the frontend or the mobile app.

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

Below is the page where to edit bookings.

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

Below is the page where to manage agencies.

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

Below is the page where to create new agencies.

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

Below is the page where to edit agencies.

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

Below is the page where to see agencies' properties.

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

Below is the page where to see customer's bookings.

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

Below is the page where admins and agencies can manage their settings.

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

There are other pages but these are the main pages of the admin dashboard.

That's it. That's the main pages of the admin dashboard.

Live Demo

Frontend

  • URL: https://movinin.dynv6.net:3004/
  • Login: jdoe@movinin.io
  • Password: M00vinin

Admin Dashboard

  • URL: https://movinin.dynv6.net:3003/
  • Login: admin@movinin.io
  • Password: M00vinin

Mobile App

You can install the Android app on any Android device.

Scan this code with a device

Open the Camera app and point it at this code. Then tap the notification that appears.

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

How to install the Mobile App on Android

  • On devices running Android 8.0 (API level 26) and higher, you must navigate to the Install unknown apps system settings screen to enable app installations from a particular location (i.e. the web browser you are downloading the app from).

  • On devices running Android 7.1.1 (API level 25) and lower, you should enable the Unknown sources system setting, found in Settings > Security on your device.

Alternative Way

You can also install the Android App by directly downloading the APK and installing it on any Android device.

  • Download APK
  • Login: jdoe@movinin.io
  • Password: M00vinin

API

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

The API exposes all functions needed for the admin dashboard, the frontend and the mobile app. The API follows the MVC design pattern. JWT is used for authentication. There are some functions that need authentication such as functions related to managing properties, bookings and customers, and others that do not need authentication such as retrieving locations and available properties for non authenticated users:

  • ./api/src/models/ folder contains MongoDB models.
  • ./api/src/routes/ folder contains Express routes.
  • ./api/src/controllers/ folder contains controllers.
  • ./api/src/middlewares/ folder contains middlewares.
  • ./api/src/config/env.config.ts contains the configuration and TypeScript type definitions.
  • ./api/src/lang/ folder contains localization.
  • ./api/src/app.ts is the main server where routes are loaded.
  • ./api/index.ts is the main entry point of the API.

index.ts is the main entry point of the 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))
}

This is a TypeScript file that starts a server using Node.js and Express. It imports several modules including dotenv, process, fs, http, https, mongoose, and app. It then checks if the HTTPS environment variable is set to true, and if so, creates an HTTPS server using the https module and the provided private key and certificate. Otherwise, it creates an HTTP server using the http module. The server listens on the port specified in the PORT environment variable.

The close function is defined to gracefully stop the server when a termination signal is received. It closes the server and the MongoDB connection, and then exits the process with a status code of 0. Finally, it registers the close function to be called when the process receives a SIGINT, SIGTERM, or SIGQUIT signal.

app.ts is the main entry point of the 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

First of all, we retrieve MongoDB connection string, then we establish a connection with MongoDB database. Then we create an Express app and load middlewares such as cors, compression, helmet, and nocache. We set up various security measures using the helmet middleware library. we also import various route files for different parts of the application such as supplierRoutes, bookingRoutes, locationRoutes, notificationRoutes, propertyRoutes, and userRoutes. Finally, we load Express routes and export app.

There are 8 routes in the API. Each route has its own controller following the MVC design pattern and SOLID principles. Below are the main routes:

  • userRoutes: Provides REST functions related to users
  • agencyRoutes: Provides REST functions related to agencies
  • countryRoutes: Provides REST functions related to countries
  • locationRoutes: Provides REST functions related to locations
  • propertyRoutes: Provides REST functions related to properties
  • bookingRoutes: Provides REST functions related to bookings
  • notificationRoutes: Provides REST functions related to notifications
  • stripeRoutes: Provides REST functions related to Stripe payment gateway

We are not going to explain each route one by one. We'll take, for example, propertyRoutes and see how it was made. You can browse the source code and see all the routes.

Here is 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))
}

First of all, we create an Express Router. Then, we create the routes using their name, method, middlewares and controllers.

routeNames contains propertyRoutes route names:

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

propertyController contains the main business logic regarding locations. We are not going to see all the source code of the controller since it's quite large but we'll take create controller function for example.

Below is Property model:

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

Below is Property type:

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

A property is composed of:

  • A name
  • A type (Apartment, Commercial, Farm, House, Industrial, Plot, Townhouse)
  • A reference to the agency who created it
  • A description
  • A main image
  • Additional images
  • Number of bedrooms
  • Number of bathrooms
  • Number of kitchens
  • Number of parking spaces
  • A Size
  • Minimum age for rental
  • A location
  • An address (optional)
  • A price
  • A rental term (Monthly, Weekly, Daily, Yearly)
  • Cancellation price (set it to 0 to be included for free, leave it empty if you don't want to include it, or set the price for cancellation)
  • A flag that indicates whether pets are allowed or not
  • A flag that indicates whether the property is furnished or not
  • A flag that indicates whether the property is hidden or not
  • A flag that indicates whether aircon is available or not
  • A flag that indicates whether the property is available for rental or not

Below is create controller function:

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

Frontend

The frontend is a web application built with Node.js, React, MUI and TypeScript. From the frontend, the customer can search for available cars depending on pickup and drop-off points and time, choose a car and proceed to checkout:

  • ./frontend/src/assets/ folder contains CSS and images.
  • ./frontend/src/pages/ folder contains React pages.
  • ./frontend/src/components/ folder contains React components.
  • ./frontend/src/services/ contains api client services.
  • ./frontend/src/App.tsx is the main React App that contains routes.
  • ./frontend/src/index.tsx is the main entry point of the frontend.

TypeScript type definitions are defined in the package ./packages/movinin-types.

App.tsx is the main react App:

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))
}

We are using React lazy loading to load each route.

We are not going to cover each page of the frontend, but you can browse the source code and see each one.

Mobile App

The platform provides a native mobile app for Android and iOS. The mobile app is built with React Native, Expo and TypeScript. Like for the frontend, the mobile app allows the customer to search for available cars depending on pickup and drop-off points and time, choose a car and proceed to checkout.

The customer receives push notifications if his booking is updated from the backend. Push notifications are built with Node.js, Expo Server SDK and Firebase.

  • ./mobile/assets/ folder contains images.
  • ./mobile/screens/ folder contains main React Native screens.
  • ./mobile/components/ folder contains React Native components.
  • ./mobile/services/ contains api client services.
  • ./mobile/App.tsx is the main React Native App.

TypeScript type definitions are defined in:

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

./mobile/types/ is loaded in ./mobile/tsconfig.json as follow:

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

App.tsx is the main entry point of the React Native app:

import 'react-native-gesture-handler'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { RootSiblingParent } from 'react-native-root-siblings'
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'
import { StatusBar as ExpoStatusBar } from 'expo-status-bar'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { Provider } from 'react-native-paper'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
import { StripeProvider } from '@stripe/stripe-react-native'
import DrawerNavigator from './components/DrawerNavigator'
import * as helper from './common/helper'
import * as NotificationService from './services/NotificationService'
import * as UserService from './services/UserService'
import { GlobalProvider } from './context/GlobalContext'
import * as env from './config/env.config'

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
})

//
// Prevent native splash screen from autohiding before App component declaration
//
SplashScreen.preventAutoHideAsync()
  .then((result) => console.log(`SplashScreen.preventAutoHideAsync() succeeded: ${result}`))
  .catch(console.warn) // it's good to explicitly catch and inspect any error

const App = () => {
  const [appIsReady, setAppIsReady] = useState(false)

  const responseListener = useRef<Notifications.Subscription>()
  const navigationRef = useRef<NavigationContainerRef<StackParams>>(null)

  useEffect(() => {
    const register = async () => {
      const loggedIn = await UserService.loggedIn()
      if (loggedIn) {
        const currentUser = await UserService.getCurrentUser()
        if (currentUser?._id) {
          await helper.registerPushToken(currentUser._id)
        } else {
          helper.error()
        }
      }
    }

    //
    // Register push notifiations token
    //
    register()

    //
    // This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed)
    //
    responseListener.current = Notifications.addNotificationResponseReceivedListener(async (response) => {
      try {
        if (navigationRef.current) {
          const { data } = response.notification.request.content

          if (data.booking) {
            if (data.user && data.notification) {
              await NotificationService.markAsRead(data.user, [data.notification])
            }
            navigationRef.current.navigate('Booking', { id: data.booking })
          } else {
            navigationRef.current.navigate('Notifications', {})
          }
        }
      } catch (err) {
        helper.error(err, false)
      }
    })

    return () => {
      Notifications.removeNotificationSubscription(responseListener.current!)
    }
  }, [])

  setTimeout(() => {
    setAppIsReady(true)
  }, 500)

  const onReady = useCallback(async () => {
    if (appIsReady) {
      //
      // This tells the splash screen to hide immediately! If we call this after
      // `setAppIsReady`, then we may see a blank screen while the app is
      // loading its initial state and rendering its first pixels. So instead,
      // we hide the splash screen once we know the root view has already
      // performed layout.
      //
      await SplashScreen.hideAsync()
    }
  }, [appIsReady])

  if (!appIsReady) {
    return null
  }

  return (
    <GlobalProvider>
      <SafeAreaProvider>
        <Provider>
          <StripeProvider publishableKey={env.STRIPE_PUBLISHABLE_KEY} merchantIdentifier={env.STRIPE_MERCHANT_IDENTIFIER}>
            <RootSiblingParent>
              <NavigationContainer ref={navigationRef} onReady={onReady}>
                <ExpoStatusBar>



<p>We are not going to cover each screen of the mobile app, but you can browse the source code and see each one.</p>

<h2>
  
  
  Admin Dashboard
</h2>

<p>The admin dashboard is a web application built with Node.js, React, MUI and TypeScript. From the backend, admins can create and manage suppliers, cars, locations, customers and bookings. When new suppliers are created from the backend, they will receive an email prompting them to create an account in order to access the admin dashboard and manage their car fleet and bookings.</p>

  • ./backend/assets/ folder contains CSS and images.
  • ./backend/pages/ folder contains React pages.
  • ./backend/components/ folder contains React components.
  • ./backend/services/ contains api client services.
  • ./backend/App.tsx is the main React App that contains routes.
  • ./backend/index.tsx is the main entry point of the admin dashboard.

TypeScript type definitions are defined in the package ./packages/movinin-types.

App.tsx of the admin dashboard follow similar logic like App.tsx of the frontend.

We are not going to cover each page of the admin dashboard but you can browse the source code and see each one.

Points of Interest

Building the mobile app with React Native and Expo is very easy. Expo makes mobile development with React Native very simple.

Using the same language (TypeScript) for backend, frontend and mobile development is very convenient.

TypeScript is a very interesting language and has many advantages. By adding static typing to JavaScript, we can avoid many bugs and produce high quality, scalable, more readable and maintainable code that is easy to debug and test.

That's it! I hope you enjoyed reading this article.

Resources

  1. Overview
  2. Architecture
  3. Installing (Self-hosted)
  4. Installing (VPS)
  5. Installing (Docker)
    1. Docker Image
    2. SSL
  6. Setup Stripe
  7. Build Mobile App
  8. Demo Database
    1. Windows, Linux and macOS
    2. Docker
  9. Run from Source
  10. Run Mobile App
    1. Prerequisites
    2. Instructions
    3. Push Notifications
  11. Change Currency
  12. Add New Language
  13. Unit Tests and Coverage
  14. Logs

The above is the detailed content of From Zero to Hero: My Journey Building a Property Rental Website and Mobile App. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undress AI Tool

Undress AI Tool

Undress images for free

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

How does garbage collection work in JavaScript? How does garbage collection work in JavaScript? Jul 04, 2025 am 12:42 AM

JavaScript's garbage collection mechanism automatically manages memory through a tag-clearing algorithm to reduce the risk of memory leakage. The engine traverses and marks the active object from the root object, and unmarked is treated as garbage and cleared. For example, when the object is no longer referenced (such as setting the variable to null), it will be released in the next round of recycling. Common causes of memory leaks include: ① Uncleared timers or event listeners; ② References to external variables in closures; ③ Global variables continue to hold a large amount of data. The V8 engine optimizes recycling efficiency through strategies such as generational recycling, incremental marking, parallel/concurrent recycling, and reduces the main thread blocking time. During development, unnecessary global references should be avoided and object associations should be promptly decorated to improve performance and stability.

How to make an HTTP request in Node.js? How to make an HTTP request in Node.js? Jul 13, 2025 am 02:18 AM

There are three common ways to initiate HTTP requests in Node.js: use built-in modules, axios, and node-fetch. 1. Use the built-in http/https module without dependencies, which is suitable for basic scenarios, but requires manual processing of data stitching and error monitoring, such as using https.get() to obtain data or send POST requests through .write(); 2.axios is a third-party library based on Promise. It has concise syntax and powerful functions, supports async/await, automatic JSON conversion, interceptor, etc. It is recommended to simplify asynchronous request operations; 3.node-fetch provides a style similar to browser fetch, based on Promise and simple syntax

JavaScript Data Types: Primitive vs Reference JavaScript Data Types: Primitive vs Reference Jul 13, 2025 am 02:43 AM

JavaScript data types are divided into primitive types and reference types. Primitive types include string, number, boolean, null, undefined, and symbol. The values are immutable and copies are copied when assigning values, so they do not affect each other; reference types such as objects, arrays and functions store memory addresses, and variables pointing to the same object will affect each other. Typeof and instanceof can be used to determine types, but pay attention to the historical issues of typeofnull. Understanding these two types of differences can help write more stable and reliable code.

JavaScript time object, someone builds an eactexe, faster website on Google Chrome, etc. JavaScript time object, someone builds an eactexe, faster website on Google Chrome, etc. Jul 08, 2025 pm 02:27 PM

Hello, JavaScript developers! Welcome to this week's JavaScript news! This week we will focus on: Oracle's trademark dispute with Deno, new JavaScript time objects are supported by browsers, Google Chrome updates, and some powerful developer tools. Let's get started! Oracle's trademark dispute with Deno Oracle's attempt to register a "JavaScript" trademark has caused controversy. Ryan Dahl, the creator of Node.js and Deno, has filed a petition to cancel the trademark, and he believes that JavaScript is an open standard and should not be used by Oracle

React vs Angular vs Vue: which js framework is best? React vs Angular vs Vue: which js framework is best? Jul 05, 2025 am 02:24 AM

Which JavaScript framework is the best choice? The answer is to choose the most suitable one according to your needs. 1.React is flexible and free, suitable for medium and large projects that require high customization and team architecture capabilities; 2. Angular provides complete solutions, suitable for enterprise-level applications and long-term maintenance; 3. Vue is easy to use, suitable for small and medium-sized projects or rapid development. In addition, whether there is an existing technology stack, team size, project life cycle and whether SSR is needed are also important factors in choosing a framework. In short, there is no absolutely the best framework, the best choice is the one that suits your needs.

Understanding Immediately Invoked Function Expressions (IIFE) in JavaScript Understanding Immediately Invoked Function Expressions (IIFE) in JavaScript Jul 04, 2025 am 02:42 AM

IIFE (ImmediatelyInvokedFunctionExpression) is a function expression executed immediately after definition, used to isolate variables and avoid contaminating global scope. It is called by wrapping the function in parentheses to make it an expression and a pair of brackets immediately followed by it, such as (function(){/code/})();. Its core uses include: 1. Avoid variable conflicts and prevent duplication of naming between multiple scripts; 2. Create a private scope to make the internal variables invisible; 3. Modular code to facilitate initialization without exposing too many variables. Common writing methods include versions passed with parameters and versions of ES6 arrow function, but note that expressions and ties must be used.

What is the cache API and how is it used with Service Workers? What is the cache API and how is it used with Service Workers? Jul 08, 2025 am 02:43 AM

CacheAPI is a tool provided by the browser to cache network requests, which is often used in conjunction with ServiceWorker to improve website performance and offline experience. 1. It allows developers to manually store resources such as scripts, style sheets, pictures, etc.; 2. It can match cache responses according to requests; 3. It supports deleting specific caches or clearing the entire cache; 4. It can implement cache priority or network priority strategies through ServiceWorker listening to fetch events; 5. It is often used for offline support, speed up repeated access speed, preloading key resources and background update content; 6. When using it, you need to pay attention to cache version control, storage restrictions and the difference from HTTP caching mechanism.

Handling Promises: Chaining, Error Handling, and Promise Combinators in JavaScript Handling Promises: Chaining, Error Handling, and Promise Combinators in JavaScript Jul 08, 2025 am 02:40 AM

Promise is the core mechanism for handling asynchronous operations in JavaScript. Understanding chain calls, error handling and combiners is the key to mastering their applications. 1. The chain call returns a new Promise through .then() to realize asynchronous process concatenation. Each .then() receives the previous result and can return a value or a Promise; 2. Error handling should use .catch() to catch exceptions to avoid silent failures, and can return the default value in catch to continue the process; 3. Combinators such as Promise.all() (successfully successful only after all success), Promise.race() (the first completion is returned) and Promise.allSettled() (waiting for all completions)

See all articles