('Token', tokenSchema)
36 |
37 | export default Token
38 |
--------------------------------------------------------------------------------
/admin/src/assets/css/header.css:
--------------------------------------------------------------------------------
1 | /* Header */
2 |
3 | .header {
4 | position: sticky !important;
5 | top: 0;
6 | z-index: 1400;
7 | }
8 |
9 | .menu {
10 | z-index: 1401 !important;
11 | }
12 |
13 | .side-menu li {
14 | cursor: pointer;
15 | }
16 |
17 | .side-menu li:hover {
18 | background-color: #f1f1f1;
19 | }
20 |
21 | .header-action {
22 | margin-right: 20px;
23 | }
24 |
25 | .header-desktop {
26 | display: none;
27 | }
28 |
29 | .header-mobile {
30 | display: flex;
31 | margin-right: -13px;
32 | }
33 |
34 | /* Device width is less than or equal to 960px */
35 |
36 | @media only screen and (width <=960px) {
37 | .header {
38 | min-height: 56px;
39 | }
40 |
41 | .toolbar {
42 | min-height: 56px !important;
43 | }
44 | }
45 |
46 | /* Device width is greater than or equal to 960px */
47 |
48 | @media only screen and (width >=960px) {
49 | .header {
50 | min-height: 64px;
51 | }
52 |
53 | .header-desktop {
54 | display: flex;
55 | }
56 |
57 | .header-mobile {
58 | display: none;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/admin/src/components/scheduler/hooks/useWindowResize.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | export function useWindowResize() {
4 | const [state, setState] = useState({
5 | width: 0,
6 | height: 0,
7 | })
8 |
9 | useEffect(() => {
10 | const handler = () => {
11 | setState((_state) => {
12 | const { innerWidth, innerHeight } = window
13 | // Check state for change, return same state if no change happened to prevent rerender
14 | return _state.width !== innerWidth || _state.height !== innerHeight
15 | ? {
16 | width: innerWidth,
17 | height: innerHeight,
18 | }
19 | : _state
20 | })
21 | }
22 |
23 | if (typeof window !== 'undefined') {
24 | handler()
25 | window.addEventListener('resize', handler, {
26 | capture: false,
27 | passive: true,
28 | })
29 | }
30 |
31 | return () => {
32 | window.removeEventListener('resize', handler)
33 | }
34 | }, [])
35 |
36 | return state
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/components/Toast.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import {
3 | CheckCircle as CheckIcon,
4 | Error as ErrorIcon,
5 | Close as CloseIcon
6 | } from '@mui/icons-material'
7 |
8 | import '@/assets/css/toast.css'
9 |
10 | interface ToastProps {
11 | title?: string,
12 | text: string
13 | status: 'success' | 'error'
14 | }
15 |
16 | const Toast = ({ title, text, status }: ToastProps) => {
17 | const [close, setClose] = useState(false)
18 | return (
19 | !close && (
20 |
21 | {status === 'success' ?
:
}
22 |
23 | {title && {title}}
24 | {text}
25 |
26 |
setClose(true)} role="presentation" className="close">
27 |
28 |
29 |
30 | )
31 | )
32 | }
33 |
34 | export default Toast
35 |
--------------------------------------------------------------------------------
/admin/src/assets/css/status-filter.css:
--------------------------------------------------------------------------------
1 | div.status-filter {
2 | background: #fafafa;
3 | margin: 10px 10px 0 0;
4 | border: 1px solid #dadada;
5 | font-size: 13px;
6 | text-align: center;
7 | }
8 |
9 | div.status-filter ul.status-list {
10 | list-style-type: none;
11 | font-size: 13px;
12 | margin: 15px 0 0;
13 | padding: 0;
14 | display: inline-block;
15 | width: 250px;
16 | }
17 |
18 | div.status-filter ul.status-list li {
19 | width: 42%;
20 | float: left;
21 | margin-bottom: 12px;
22 | margin-left: 4%;
23 | margin-right: 4%;
24 | clear: none;
25 | }
26 |
27 | div.status-filter ul.status-list li input[type='checkbox'].status-checkbox {
28 | cursor: pointer;
29 | }
30 |
31 | div.status-filter ul.status-list li span.bs:hover {
32 | opacity: 0.9;
33 | cursor: pointer;
34 | }
35 |
36 | div.status-filter div.filter-actions {
37 | text-align: center;
38 | padding-bottom: 10px;
39 | }
40 |
41 | div.status-filter div.filter-actions span.uncheckall {
42 | text-decoration: underline;
43 | cursor: pointer;
44 | color: #0064c8;
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/assets/css/status-filter.css:
--------------------------------------------------------------------------------
1 | div.status-filter {
2 | background: #fff;
3 | margin: 10px 10px 0 0;
4 | border: 1px solid #dadada;
5 | font-size: 13px;
6 | text-align: center;
7 | }
8 |
9 | div.status-filter ul.status-list {
10 | list-style-type: none;
11 | font-size: 13px;
12 | margin: 15px 0 0;
13 | padding: 0;
14 | display: inline-block;
15 | width: 250px;
16 | }
17 |
18 | div.status-filter ul.status-list li {
19 | width: 42%;
20 | float: left;
21 | margin-bottom: 12px;
22 | margin-left: 4%;
23 | margin-right: 4%;
24 | clear: none;
25 | }
26 |
27 | div.status-filter ul.status-list li input[type='checkbox'].status-checkbox {
28 | cursor: pointer;
29 | }
30 |
31 | div.status-filter ul.status-list li span.bs:hover {
32 | opacity: 0.9;
33 | cursor: pointer;
34 | }
35 |
36 | div.status-filter div.filter-actions {
37 | text-align: center;
38 | padding-bottom: 10px;
39 | }
40 |
41 | div.status-filter div.filter-actions span.uncheckall {
42 | text-decoration: underline;
43 | cursor: pointer;
44 | color: #0064c8;
45 | }
46 |
--------------------------------------------------------------------------------
/admin/src/assets/css/create-booking.css:
--------------------------------------------------------------------------------
1 | /* Create Booking */
2 | div.create-booking {
3 | display: flex;
4 | flex-direction: column;
5 | flex: 1 0 auto;
6 | align-items: center;
7 | transform: translate3d(0, 0, 0);
8 | margin: 45px 0;
9 | }
10 |
11 | .booking-form-wrapper {
12 | margin: 32px 0;
13 | }
14 |
15 | .booking-form-title {
16 | text-align: center;
17 | text-transform: capitalize;
18 | color: #121212;
19 | }
20 |
21 | .checkbox-fc {
22 | margin: 13px 0 !important;
23 | }
24 |
25 | .checkbox-fc .checkbox-fcl {
26 | color: rgb(0 0 0 / 60%);
27 | font-size: 0.9em;
28 | line-height: 1em;
29 | }
30 |
31 | /* Device width is less than or equal to 960px */
32 |
33 | @media only screen and (width <=960px) {
34 | .booking-form {
35 | width: 360px;
36 | padding: 30px;
37 | display: inline-block;
38 | }
39 | }
40 |
41 | /* Device width is greater than or equal to 960px */
42 |
43 | @media only screen and (width >=960px) {
44 | .booking-form {
45 | width: 600px;
46 | padding: 30px;
47 | display: inline-block;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/mobile/components/AutocompleteDropdown-v4/HOC/withFadeAnimation.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ComponentType } from 'react'
2 | import React, { useEffect, useRef } from 'react'
3 | import type { ViewProps } from 'react-native'
4 | import { Animated, Easing } from 'react-native'
5 |
6 | interface WithFadeAnimationProps {
7 | containerStyle?: ViewProps['style']
8 | }
9 |
10 | export const withFadeAnimation = (
11 | WrappedComponent: ComponentType
,
12 | { containerStyle }: WithFadeAnimationProps = {},
13 | ): FC
=> function (props: P) {
14 | const opacityAnimationValue = useRef(new Animated.Value(0)).current
15 |
16 | useEffect(() => {
17 | Animated.timing(opacityAnimationValue, {
18 | duration: 800,
19 | toValue: 1,
20 | useNativeDriver: true,
21 | easing: Easing.bezier(0.3, 0.58, 0.25, 0.99),
22 | }).start()
23 | }, [opacityAnimationValue])
24 |
25 | return (
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/mobile/components/NavigationWrapper.tsx:
--------------------------------------------------------------------------------
1 | // App.tsx or main layout component
2 | import { useAuth } from '@/context/AuthContext'
3 | import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'
4 | import { StatusBar as ExpoStatusBar } from 'expo-status-bar'
5 | import Toast from 'react-native-toast-message'
6 | import DrawerNavigator from '@/components/DrawerNavigator'
7 |
8 | interface NavigationWrapperProps {
9 | ref?: React.RefObject | null>
10 | onReady: () => void
11 | }
12 |
13 | const NavigationWrapper: React.FC = ({ onReady, ref: navigationRef }) => {
14 | const { language, refresh } = useAuth()
15 |
16 | return (
17 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default NavigationWrapper
30 |
--------------------------------------------------------------------------------
/mobile/components/AutocompleteDropdown-v4.3.1/HOC/withFadeAnimation.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ComponentType } from 'react'
2 | import React, { useEffect, useRef } from 'react'
3 | import type { ViewProps } from 'react-native'
4 | import { Animated, Easing } from 'react-native'
5 |
6 | interface WithFadeAnimationProps {
7 | containerStyle?: ViewProps['style']
8 | }
9 |
10 | export const withFadeAnimation = (
11 | WrappedComponent: ComponentType
,
12 | { containerStyle }: WithFadeAnimationProps = {},
13 | ): FC
=> function (props: P) {
14 | const opacityAnimationValue = useRef(new Animated.Value(0)).current
15 |
16 | useEffect(() => {
17 | Animated.timing(opacityAnimationValue, {
18 | duration: 800,
19 | toValue: 1,
20 | useNativeDriver: true,
21 | easing: Easing.bezier(0.3, 0.58, 0.25, 0.99),
22 | }).start()
23 | }, [opacityAnimationValue])
24 |
25 | return (
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/admin/src/assets/css/country-list.css:
--------------------------------------------------------------------------------
1 | section.country-list {
2 | margin: 0 10px;
3 | display: flex;
4 | flex-direction: column;
5 | align-items: center;
6 | flex: 1 0 auto;
7 | }
8 |
9 | section.country-list .empty-list {
10 | margin-top: 15px;
11 | text-align: center;
12 | width: 250px;
13 | }
14 |
15 | @media only screen and (width <=960px) {
16 | section.country-list .country-list-items {
17 | width: 100%;
18 | }
19 | }
20 |
21 | section.country-list .country-list-item {
22 | position: relative;
23 | background: #fff;
24 | border: 1px solid #ddd;
25 | border-radius: 5px;
26 | color: #333;
27 | font-size: 12px;
28 | margin-bottom: 10px;
29 | height: 75px;
30 | word-break: break-word;
31 | }
32 |
33 | section.country-list .country-title {
34 | margin-right: 20px;
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | white-space: nowrap;
38 | }
39 |
40 | /* Device width is greater than or equal to 960px */
41 |
42 | @media only screen and (width >=960px) {
43 | section.country-list .country-list-item {
44 | width: 480px;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/admin/src/components/scheduler/hooks/useDragAttributes.ts:
--------------------------------------------------------------------------------
1 | import { DragEvent } from 'react'
2 | import { useTheme } from '@mui/material'
3 | import { ProcessedEvent } from '../types'
4 | import useStore from './useStore'
5 |
6 | const useDragAttributes = (event: ProcessedEvent) => {
7 | const { setCurrentDragged } = useStore()
8 | const theme = useTheme()
9 | return {
10 | draggable: true,
11 | onDragStart: (e: DragEvent) => {
12 | e.stopPropagation()
13 | setCurrentDragged(event)
14 | e.currentTarget.style.backgroundColor = theme.palette.error.main
15 | },
16 | onDragEnd: (e: DragEvent) => {
17 | setCurrentDragged()
18 | e.currentTarget.style.backgroundColor = event.color || theme.palette.primary.main
19 | },
20 | onDragOver: (e: DragEvent) => {
21 | e.stopPropagation()
22 | e.preventDefault()
23 | },
24 | onDragEnter: (e: DragEvent) => {
25 | e.stopPropagation()
26 | e.preventDefault()
27 | },
28 | }
29 | }
30 |
31 | export default useDragAttributes
32 |
--------------------------------------------------------------------------------
/.github/releases.yml:
--------------------------------------------------------------------------------
1 | # This workflow will update the `.github/RELEASES.md` file whenever a new release is published.
2 | name: Update RELEASES.md
3 |
4 | on:
5 | release:
6 | types: [published] # Trigger on new release
7 | workflow_dispatch: # Manual trigger
8 |
9 | jobs:
10 | update-releases-md:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v4
16 |
17 | - name: Set up Node.js
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 'lts/*'
21 |
22 | - name: Install dependencies
23 | run: npm install
24 |
25 | - name: Generate RELEASES.md
26 | run: npm run releases
27 |
28 | - name: Commit and push .github/RELEASES.md
29 | run: |
30 | git config user.name "github-actions[bot]"
31 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
32 | git add .github/RELEASES.md
33 | git commit -m "docs: update RELEASES.md on new release" || echo "No changes"
34 | git push
35 |
--------------------------------------------------------------------------------
/admin/.env.example:
--------------------------------------------------------------------------------
1 | VITE_NODE_ENV=development
2 | VITE_PORT=3003
3 | VITE_MI_API_HOST=http://localhost:4004
4 | VITE_MI_DEFAULT_LANGUAGE=en
5 | VITE_MI_PAGE_SIZE=30
6 | VITE_MI_PROPERTIES_PAGE_SIZE=15
7 | VITE_MI_BOOKINGS_PAGE_SIZE=20
8 | VITE_MI_BOOKINGS_MOBILE_PAGE_SIZE=10
9 | VITE_MI_CDN_USERS=http://localhost:4004/cdn/movinin/users
10 | VITE_MI_CDN_TEMP_USERS=http://localhost:4004/cdn/movinin/temp/users
11 | VITE_MI_CDN_PROPERTIES=http://localhost:4004/cdn/movinin/properties
12 | VITE_MI_CDN_TEMP_PROPERTIES=http://localhost:4004/cdn/movinin/temp/properties
13 | VITE_MI_CDN_LOCATIONS=http://localhost:4004/cdn/movinin/locations
14 | VITE_MI_CDN_TEMP_LOCATIONS=http://localhost:4004/cdn/movinin/temp/locations
15 | VITE_MI_AGENCY_IMAGE_WIDTH=60
16 | VITE_MI_AGENCY_IMAGE_HEIGHT=30
17 | VITE_MI_PROPERTY_IMAGE_WIDTH=300
18 | VITE_MI_PROPERTY_IMAGE_HEIGHT=200
19 | VITE_MI_MINIMUM_AGE=21
20 | VITE_MI_PAGINATION_MODE=classic
21 | VITE_MI_CURRENCY=\$
22 | VITE_MI_WEBSITE_NAME="Movin' In"
23 | VITE_MI_RECAPTCHA_ENABLED=false
24 | VITE_MI_RECAPTCHA_SITE_KEY=RECAPTCHA_SITE_KEY
25 | VITE_MI_CONTACT_EMAIL=info@movinin.io
26 |
--------------------------------------------------------------------------------
/frontend/src/utils/ga4.ts:
--------------------------------------------------------------------------------
1 | import ga4 from 'react-ga4'
2 | import env from '@/config/env.config'
3 |
4 | const TRACKING_ID = env.GOOGLE_ANALYTICS_ID
5 | const { isProduction } = env
6 |
7 | export const init = () => {
8 | if (typeof window === 'undefined') {
9 | return
10 | }
11 | let fired = false
12 | const loadAnalytics = () => {
13 | if (!fired) {
14 | ga4.initialize(TRACKING_ID, { testMode: !isProduction })
15 | fired = true
16 | }
17 | }
18 |
19 | const startAnalytics = () => {
20 | window.removeEventListener('mousemove', startAnalytics)
21 | window.removeEventListener('touchstart', startAnalytics)
22 | loadAnalytics()
23 | }
24 |
25 | window.addEventListener('mousemove', startAnalytics, { once: true })
26 | window.addEventListener('touchstart', startAnalytics, { once: true })
27 | }
28 |
29 | export const sendEvent = (name: string) => ga4.event('screen_view', {
30 | app_name: 'bookcars',
31 | screen_name: name,
32 | })
33 |
34 | export const sendPageview = (path: string) => ga4.send({
35 | hitType: 'pageview',
36 | page: path
37 | })
38 |
--------------------------------------------------------------------------------
/backend/src/models/Location.ts:
--------------------------------------------------------------------------------
1 | import { Schema, model } from 'mongoose'
2 | import * as env from '../config/env.config'
3 |
4 | const locationSchema = new Schema(
5 | {
6 | country: {
7 | type: Schema.Types.ObjectId,
8 | required: [true, "can't be blank"],
9 | ref: 'Country',
10 | index: true,
11 | },
12 | latitude: {
13 | type: Number,
14 | },
15 | longitude: {
16 | type: Number,
17 | },
18 | values: {
19 | type: [Schema.Types.ObjectId],
20 | ref: 'LocationValue',
21 | required: [true, "can't be blank"],
22 | validate: (value: any): boolean => Array.isArray(value),
23 | },
24 | image: {
25 | type: String,
26 | },
27 | parentLocation: {
28 | type: Schema.Types.ObjectId,
29 | ref: 'Location',
30 | },
31 | },
32 | {
33 | timestamps: true,
34 | strict: true,
35 | collection: 'Location',
36 | },
37 | )
38 |
39 | locationSchema.index({ values: 1 })
40 |
41 | const Location = model('Location', locationSchema)
42 |
43 | export default Location
44 |
--------------------------------------------------------------------------------
/admin/src/context/RecaptchaContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, createContext, useContext, useMemo } from 'react'
2 | import useReCaptcha from '@/hooks/useRecaptcha'
3 |
4 | // Create context
5 | export interface RecaptchaContextType {
6 | reCaptchaLoaded: boolean,
7 | generateReCaptchaToken: (action?: string) => Promise
8 | }
9 |
10 | const RecaptchaContext = createContext(null)
11 |
12 | // Create a provider
13 | interface RecaptchaProviderProps {
14 | children: ReactNode
15 | }
16 |
17 | export const RecaptchaProvider = ({ children }: RecaptchaProviderProps) => {
18 | const { reCaptchaLoaded, generateReCaptchaToken } = useReCaptcha()
19 | const value = useMemo(() => ({ reCaptchaLoaded, generateReCaptchaToken }), [reCaptchaLoaded, generateReCaptchaToken])
20 |
21 | return (
22 | {children}
23 | )
24 | }
25 |
26 | // Create a custom hook to access context
27 | // eslint-disable-next-line react-refresh/only-export-components
28 | export const useRecaptchaContext = () => useContext(RecaptchaContext)
29 |
--------------------------------------------------------------------------------
/frontend/src/context/RecaptchaContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, createContext, useContext, useMemo } from 'react'
2 | import useReCaptcha from '@/hooks/useRecaptcha'
3 |
4 | // Create context
5 | export interface RecaptchaContextType {
6 | reCaptchaLoaded: boolean,
7 | generateReCaptchaToken: (action?: string) => Promise
8 | }
9 |
10 | const RecaptchaContext = createContext(null)
11 |
12 | // Create a provider
13 | interface RecaptchaProviderProps {
14 | children: ReactNode
15 | }
16 |
17 | export const RecaptchaProvider = ({ children }: RecaptchaProviderProps) => {
18 | const { reCaptchaLoaded, generateReCaptchaToken } = useReCaptcha()
19 | const value = useMemo(() => ({ reCaptchaLoaded, generateReCaptchaToken }), [reCaptchaLoaded, generateReCaptchaToken])
20 |
21 | return (
22 | {children}
23 | )
24 | }
25 |
26 | // Create a custom hook to access context
27 | // eslint-disable-next-line react-refresh/only-export-components
28 | export const useRecaptchaContext = () => useContext(RecaptchaContext)
29 |
--------------------------------------------------------------------------------
/frontend/src/pages/About.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useNavigate } from 'react-router-dom'
3 | import { Button } from '@mui/material'
4 | import { strings } from '@/lang/about'
5 | import Layout from '@/components/Layout'
6 | import Footer from '@/components/Footer'
7 |
8 | import '@/assets/css/about.css'
9 |
10 | const About = () => {
11 | const navigate = useNavigate()
12 |
13 | const onLoad = () => { }
14 |
15 | return (
16 |
17 |
18 |
{strings.TITLE1}
19 |
{strings.SUBTITLE1}
20 |
{strings.CONTENT1}
21 |
22 |
{strings.TITLE2}
23 |
{strings.SUBTITLE2}
24 |
{strings.CONTENT2}
25 |
26 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default About
42 |
--------------------------------------------------------------------------------
/frontend/src/lang/change-password.ts:
--------------------------------------------------------------------------------
1 | import LocalizedStrings from 'localized-strings'
2 | import * as langHelper from '@/utils/langHelper'
3 |
4 | const strings = new LocalizedStrings({
5 | fr: {
6 | CHANGE_PASSWORD_HEADING: 'Modification du mot de passe',
7 | CURRENT_PASSWORD: 'Mot de passe actuel',
8 | CURRENT_PASSWORD_ERROR: 'Mauvais mot de passe',
9 | NEW_PASSWORD: 'Nouveau mot de passe',
10 | NEW_PASSWORD_ERROR: 'Veuillez choisir un nouveau mot de passe',
11 | PASSWORD_UPDATE_ERROR: "Une erreur s'est produite lors de la modification du mot de passe.",
12 | PASSWORD_UPDATE: 'Le mot de passe a été mofifié avec succès.',
13 | },
14 | en: {
15 | CHANGE_PASSWORD_HEADING: 'Password Modification',
16 | CURRENT_PASSWORD: 'Current Password',
17 | CURRENT_PASSWORD_ERROR: 'Wrong password',
18 | NEW_PASSWORD: 'New Password',
19 | NEW_PASSWORD_ERROR: 'Please choose a new password',
20 | PASSWORD_UPDATE_ERROR: 'An error occurred while updating password.',
21 | PASSWORD_UPDATE: 'Password changed successfully.',
22 | },
23 | })
24 |
25 | langHelper.setLanguage(strings)
26 | export { strings }
27 |
--------------------------------------------------------------------------------