redesign the homepage
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s

This commit is contained in:
root
2026-06-26 16:27:21 -04:00
parent 256ff0814e
commit d7fb7b7a7b
1030 changed files with 107374 additions and 2657 deletions
+77
View File
@@ -0,0 +1,77 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { MarketplaceApiError, marketplaceFetch, marketplaceFetchOrDefault, marketplacePost } from './api'
afterEach(() => {
delete process.env.NEXT_PUBLIC_API_URL
delete process.env.API_INTERNAL_URL
vi.restoreAllMocks()
Reflect.deleteProperty(globalThis, 'fetch')
})
describe('marketplace API helpers', () => {
it('unwraps successful GET responses from the data envelope', async () => {
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: [{ id: 'vehicle_1' }] }) }))
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
await expect(marketplaceFetch('/marketplace/vehicles')).resolves.toEqual([{ id: 'vehicle_1' }])
expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/api\/v1\/marketplace\/vehicles$/), {
cache: 'no-store',
credentials: 'include',
})
})
it('throws rich marketplace API errors', async () => {
Object.defineProperty(globalThis, 'fetch', {
configurable: true,
value: vi.fn(async () => ({
ok: false,
status: 409,
json: async () => ({ message: 'Vehicle unavailable', error: 'VEHICLE_UNAVAILABLE', nextAvailableAt: '2026-07-01T10:00:00.000Z' }),
})),
})
await expect(marketplaceFetch('/marketplace/book')).rejects.toMatchObject({
name: 'MarketplaceApiError',
message: 'Vehicle unavailable',
status: 409,
code: 'VEHICLE_UNAVAILABLE',
nextAvailableAt: '2026-07-01T10:00:00.000Z',
})
})
it('returns fallbacks when GET requests fail', async () => {
Object.defineProperty(globalThis, 'fetch', {
configurable: true,
value: vi.fn(async () => ({ ok: false, status: 500, json: async () => ({ message: 'Down' }) })),
})
await expect(marketplaceFetchOrDefault('/marketplace/homepage', { hero: null })).resolves.toEqual({ hero: null })
})
it('posts JSON payloads and unwraps successful responses', async () => {
process.env.NEXT_PUBLIC_API_URL = 'https://api.example.com/api/v1'
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { id: 'reservation_1' } }) }))
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
await expect(marketplacePost('/site/reservations', { vehicleId: 'vehicle_1' })).resolves.toEqual({ id: 'reservation_1' })
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/api/v1/site/reservations', expect.objectContaining({
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vehicleId: 'vehicle_1' }),
}))
})
it('uses a generic error message when the response body is not JSON', async () => {
Object.defineProperty(globalThis, 'fetch', {
configurable: true,
value: vi.fn(async () => ({ ok: false, status: 502, json: async () => { throw new Error('bad json') } })),
})
await expect(marketplaceFetch('/site/homepage')).rejects.toMatchObject({
name: 'MarketplaceApiError',
message: 'Request failed',
status: 502,
})
})
})
+50
View File
@@ -0,0 +1,50 @@
const API_BASE =
(typeof window === 'undefined' ? process.env.API_INTERNAL_URL : process.env.NEXT_PUBLIC_API_URL)
?? process.env.NEXT_PUBLIC_API_URL
?? 'http://localhost:4000/api/v1'
export class MarketplaceApiError extends Error {
status: number
code?: string
nextAvailableAt?: string | null
constructor(message: string, status: number, code?: string, nextAvailableAt?: string | null) {
super(message)
this.name = 'MarketplaceApiError'
this.status = status
this.code = code
this.nextAvailableAt = nextAvailableAt
}
}
export async function marketplaceFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
cache: 'no-store',
credentials: 'include',
...init,
})
const json = await res.json().catch(() => null)
if (!res.ok) throw new MarketplaceApiError(json?.message ?? 'Request failed', res.status, json?.error, json?.nextAvailableAt)
return json.data as T
}
export async function marketplaceFetchOrDefault<T>(path: string, fallback: T): Promise<T> {
try {
return await marketplaceFetch<T>(path)
} catch {
return fallback
}
}
export async function marketplacePost<T>(path: string, body: unknown): Promise<T> {
const base = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
const res = await fetch(`${base}${path}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const json = await res.json().catch(() => null)
if (!res.ok) throw new MarketplaceApiError(json?.message ?? 'Request failed', res.status, json?.error, json?.nextAvailableAt)
return json.data as T
}
+40
View File
@@ -0,0 +1,40 @@
import { afterEach, describe, expect, it } from 'vitest'
import { resolveBrowserAppUrl, resolveServerAppUrl } from './appUrls'
function installWindow(hostname: string, protocol = 'https:') {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: { location: { hostname, protocol } },
})
}
afterEach(() => {
Reflect.deleteProperty(globalThis, 'window')
})
describe('marketplace app URL resolution', () => {
it('leaves server-side browser fallback unchanged when window is unavailable', () => {
expect(resolveBrowserAppUrl('http://localhost:3000')).toBe('http://localhost:3000')
})
it('preserves localhost fallbacks but removes trailing slash', () => {
installWindow('localhost')
expect(resolveBrowserAppUrl('http://localhost:3000/')).toBe('http://localhost:3000')
})
it('rewrites production browser fallbacks to the active host and protocol', () => {
installWindow('www.rentaldrivego.ma', 'https:')
expect(resolveBrowserAppUrl('http://localhost:3000')).toBe('https://www.rentaldrivego.ma')
})
it('keeps path prefixes while rewriting the browser origin', () => {
installWindow('rental.example.com', 'https:')
expect(resolveBrowserAppUrl('http://localhost:3000/marketplace')).toBe('https://rental.example.com/marketplace')
})
it('resolves server URLs from forwarded host/proto and handles invalid fallbacks safely', () => {
expect(resolveServerAppUrl('http://localhost:3000', 'market.example.com', 'https')).toBe('https://market.example.com:3000')
expect(resolveServerAppUrl('http://localhost:3000', null)).toBe('http://localhost:3000')
expect(resolveServerAppUrl('/relative', 'market.example.com')).toBe('/relative')
})
})
+28
View File
@@ -0,0 +1,28 @@
export function resolveBrowserAppUrl(fallback: string): string {
if (typeof window === 'undefined') return fallback
const h = window.location.hostname
if (h === 'localhost' || h === '127.0.0.1') return fallback.replace(/\/$/, '')
try {
const target = new URL(fallback)
target.protocol = window.location.protocol
target.hostname = h
target.port = ''
return target.toString().replace(/\/$/, '')
} catch {
return fallback
}
}
export function resolveServerAppUrl(fallback: string, host: string | null, proto?: string | null): string {
if (!host) return fallback
try {
const target = new URL(fallback)
target.protocol = `${proto || target.protocol.replace(':', '')}:`
target.host = host
return target.toString().replace(/\/$/, '')
} catch {
return fallback
}
}
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest'
import {
appPrivacyHref,
appTermsHref,
footerPageHref,
footerPageSlugs,
getFooterPageContent,
isFooterPageSlug,
} from './footerContent'
import type { MarketplaceLanguage } from './i18n'
const languages: MarketplaceLanguage[] = ['en', 'fr', 'ar']
describe('marketplace footer content registry', () => {
it('keeps every declared slug routable and recognizable', () => {
expect(footerPageSlugs.length).toBeGreaterThan(5)
for (const slug of footerPageSlugs) {
expect(isFooterPageSlug(slug)).toBe(true)
expect(footerPageHref[slug]).toBe(`/footer/${slug}`)
}
expect(isFooterPageSlug('privacy')).toBe(false)
expect(isFooterPageSlug('../admin')).toBe(false)
})
it('provides localized title and paragraph content for every footer page', () => {
for (const language of languages) {
for (const slug of footerPageSlugs) {
const content = getFooterPageContent(language, slug)
expect(content.title.trim()).not.toBe('')
expect(content.paragraphs.length).toBeGreaterThan(0)
expect(content.paragraphs.every((paragraph) => paragraph.trim().length > 0)).toBe(true)
for (const section of content.sections ?? []) {
expect(section.heading.trim()).not.toBe('')
expect(section.paragraphs.length).toBeGreaterThan(0)
}
}
}
})
it('keeps website privacy and application privacy links separate by locale', () => {
expect(footerPageHref['privacy-policy']).toBe('/footer/privacy-policy')
expect(appPrivacyHref).toEqual({
en: '/app-privacy-en',
fr: '/app-privacy-fr',
ar: '/app-privacy-ar',
})
expect(appTermsHref).toEqual({
en: '/app-tc-en',
fr: '/app-tc-fr',
ar: '/app-tc-ar',
})
})
it('exposes footer policy copy that points users to the locale-matched app policy pages', () => {
expect(getFooterPageContent('en', 'privacy-policy').paragraphs[0]).toContain('app-privacy-en')
expect(getFooterPageContent('fr', 'privacy-policy').paragraphs[0]).toContain('app-privacy-fr')
expect(getFooterPageContent('ar', 'privacy-policy').paragraphs[0]).toContain('app-privacy-ar')
expect(getFooterPageContent('en', 'general-conditions').paragraphs[0]).toContain('app-tc-en')
expect(getFooterPageContent('fr', 'general-conditions').paragraphs[0]).toContain('app-tc-fr')
expect(getFooterPageContent('ar', 'general-conditions').paragraphs[0]).toContain('app-tc-ar')
})
})
+790
View File
@@ -0,0 +1,790 @@
import type { MarketplaceLanguage } from './i18n'
export const footerPageSlugs = [
'about-us',
'terms-of-service',
'security',
'compliance',
'privacy-policy',
'cookie-policy',
'newsletter',
'contact-sales',
'general-conditions',
] as const
export type FooterPageSlug = (typeof footerPageSlugs)[number]
export const appPrivacyHref = {
en: '/app-privacy-en',
fr: '/app-privacy-fr',
ar: '/app-privacy-ar',
} as const
export const appTermsHref = {
en: '/app-tc-en',
fr: '/app-tc-fr',
ar: '/app-tc-ar',
} as const
export type FooterPageContent = {
title: string
paragraphs: string[]
sections?: Array<{
heading: string
paragraphs: string[]
}>
}
export const footerPageHref: Record<FooterPageSlug, string> = {
'about-us': '/footer/about-us',
'terms-of-service': '/footer/terms-of-service',
security: '/footer/security',
compliance: '/footer/compliance',
'privacy-policy': '/footer/privacy-policy',
'cookie-policy': '/footer/cookie-policy',
newsletter: '/footer/newsletter',
'contact-sales': '/footer/contact-sales',
'general-conditions': '/footer/general-conditions',
}
const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPageContent>> = {
en: {
'about-us': {
title: 'About Us',
paragraphs: [
'RentalDriveGo helps car rental businesses streamline operations, manage vehicle fleets, and grow reservations through one powerful platform. Our tools are designed to simplify fleet tracking, booking management, customer reservations, payments, and day-to-day rental operations so companies can focus on delivering exceptional service instead of drowning in spreadsheets and phone calls.',
'Whether you operate a small local rental agency or a large multi-location fleet, RentalDriveGo provides the technology to help you scale efficiently and serve customers with confidence.',
],
},
'terms-of-service': {
title: 'Terms of Service',
paragraphs: [
'By using RentalDriveGo, you agree to comply with our platform policies, usage guidelines, and applicable laws. Our services are intended for legitimate car rental businesses and their authorized users. Users are responsible for maintaining account security, providing accurate business information, and using the platform responsibly.',
'RentalDriveGo reserves the right to suspend accounts involved in fraudulent activity, misuse, or violations of these terms.',
],
},
security: {
title: 'Security',
paragraphs: [
'RentalDriveGo takes security seriously. We use modern encryption standards, secure cloud infrastructure, and continuous monitoring to help protect your business data, customer information, and reservation records.',
'Our platform is built with security best practices to minimize risks and ensure reliable system performance.',
],
},
compliance: {
title: 'Compliance',
paragraphs: [
'RentalDriveGo is committed to operating in accordance with industry standards and applicable privacy and data protection regulations. We continuously review our systems and operational practices to maintain compliance, security, and transparency for our customers and partners.',
],
},
'privacy-policy': {
title: 'Privacy Policy',
paragraphs: [
'You are currently viewing the privacy policy for the RentalDriveGo website. To consult the application privacy policy, please visit: https://www.rentaldrivego.ma/app-privacy-en',
'RentalDriveGo respects your privacy and is committed to handling your personal data in a transparent manner. This policy explains how we collect, use, store, and share your personal information when you use our website, and it describes your rights regarding the processing of your personal data.',
],
sections: [
{
heading: '1. Data Controller Contact Details',
paragraphs: [
'This privacy information applies to data processing carried out by RentalDriveGo.',
'Telephone: +212 6 05 65 17 51',
'Email: service-client@rentaldrivego.ma',
],
},
{
heading: '2. Personal Data We Collect, How We Use It, and How Long We Keep It',
paragraphs: [
'When you browse our website for information only, without registering for a service or otherwise sending us personal data, your browser automatically sends certain technical information to our hosting provider, such as your browser type, language and version, the date and time of the request, your IP address, pages visited, referring website, volume of data transferred, and operating system.',
'This information is temporarily stored to make the website available on your device, support internal analysis, improve the presentation of the website, and detect or prevent misuse. We do not combine this data with other data sources and we do not seek to identify you directly from it. This data is deleted as soon as it is no longer needed for these purposes.',
'If you contact us by web form or email, we may collect your name, email address, telephone number, and any other information you choose to provide. We use this information to respond to your questions, contact you when relevant services or registrations become available, or manage participation in campaigns or competitions where applicable.',
'We keep your personal data for as long as you maintain an account with us. If you want your personal data to be removed, you must submit a formal request, subject to any legal or contractual retention obligations that may apply.',
'You may withdraw your consent at any time where processing is based on consent. Any processing carried out before withdrawal remains lawful.',
'We may also use personal data for website review and improvement, market research, analysis, surveys, and marketing activities related to our products and services, subject to applicable law.',
],
},
{
heading: '3. Third-Party Service Providers',
paragraphs: [
'We do not transfer your personal data to third parties unless we are legally required to do so, the transfer is necessary for the performance of a contractual relationship, or you have expressly agreed to it in advance.',
'We may work with service providers in Morocco and abroad for technical and website-related services, including hosting and maintenance.',
],
},
{
heading: '4. Security',
paragraphs: [
'The security of your personal data is important to us. We apply appropriate organizational, technical, and physical measures to protect the personal information we collect, both during transmission and after receipt.',
'Our security measures and privacy practices are reviewed and improved on an ongoing basis.',
],
},
{
heading: '5. Retention of Personal Data',
paragraphs: [
'We keep personal data only for as long as we are legally entitled to do so, as long as the purpose of processing continues, and provided that no valid objection has been raised.',
'Where legal retention periods apply, the data will be stored for the required duration. Once the applicable period has expired, the data will be deleted unless it is still needed for the performance or preparation of a contract.',
],
},
{
heading: '6. Transfer to a Foreign Country',
paragraphs: [
'The arrangements described above may result in your personal data being stored or processed in foreign countries.',
],
},
{
heading: '7. Your Rights',
paragraphs: [
'Under Law No. 09-08 relating to the protection of individuals with regard to the processing of personal data, you may have the right to access your personal data, obtain additional information, request correction of inaccurate data, and object, where applicable, to the processing of your data or to its transfer to third parties.',
'To exercise your rights, you may send a request including your full name and a copy of your identity document to: service-client@rentaldrivego.ma',
],
},
],
},
'cookie-policy': {
title: 'Cookie Policy',
paragraphs: [
'We use cookies exclusively to operate the platform and remember your personal preferences. We do not use cookies for advertising, cross-site tracking, or behavioural profiling, and we do not share cookie data with third parties for marketing purposes.',
],
sections: [
{
heading: '1. Authentication Cookie',
paragraphs: [
'Name: employee_session',
'This cookie is set when you sign in to the RentalDriveGo workspace. It stores a cryptographically signed token that verifies your identity for the duration of your session. It expires automatically after 8 hours and is deleted when you sign out. This cookie is strictly necessary — without it, access to the dashboard and protected areas is not possible.',
],
},
{
heading: '2. Language Preference Cookie',
paragraphs: [
'Name: rentaldrivego-language',
'Stores your chosen display language (English, French, or Arabic) so the interface appears in your preferred language on every visit across all parts of the platform. It is retained for one year.',
],
},
{
heading: '3. Theme Preference Cookie',
paragraphs: [
'Name: rentaldrivego-theme',
'Stores your preferred colour scheme (light or dark mode). It is read immediately when a page loads to apply the correct theme before the interface is drawn, preventing a visual flash. It is retained for one year.',
],
},
{
heading: '4. Per-User Preference Cookies',
paragraphs: [
'Names: rentaldrivego-language--[id], rentaldrivego-theme--[id]',
'When you are signed in, your language and theme preferences are also saved under a cookie linked to your account identifier. This allows multiple employees sharing a device to each maintain independent preferences. These cookies are retained for one year.',
],
},
{
heading: '5. Legacy Preference Cookies',
paragraphs: [
'Names: dashboard-language, marketplace-language',
'These cookies exist for backward compatibility with earlier versions of the platform and store the same language preference as the primary cookie. They are read only as a fallback and will be phased out in a future update.',
],
},
{
heading: '6. Managing Your Cookies',
paragraphs: [
'You can manage or delete cookies at any time through your browser settings. Blocking the authentication cookie will prevent you from signing in. Blocking preference cookies will cause your language and theme settings to reset on each visit. For general information about cookies, you may visit http://www.allaboutcookies.org/.',
],
},
],
},
newsletter: {
title: 'Newsletter',
paragraphs: [
'Stay updated with the latest industry insights, product updates, feature releases, and fleet management tips from RentalDriveGo.',
'Subscribe to our newsletter and receive helpful resources designed to help car rental businesses grow smarter and operate more efficiently.',
],
},
'contact-sales': {
title: 'Contact Sales',
paragraphs: [
'Want to see how RentalDriveGo can transform your rental operations? Our sales team is ready to help you explore the right solution for your business.',
'Contact us to schedule a demo, discuss pricing, or learn more about our fleet management and reservation platform.',
],
},
'general-conditions': {
title: 'General Conditions of Use',
paragraphs: [
'You are currently viewing the General Conditions of Use for the RentalDriveGo website. To consult the General Conditions of Use of the application, please visit: https://www.rentaldrivego.ma/app-tc-en',
'These General Conditions of Use define the rules that apply when you visit, access, browse, contact us through, or otherwise interact with the website www.rentaldrivego.ma.',
],
sections: [
{
heading: '1. Purpose',
paragraphs: [
'These conditions govern the use of the website www.rentaldrivego.ma, operated by RentalDriveGo, a limited liability company registered in the Azrou Trade Register under number 00000, with its registered office in Azrou',
'They also define the standards relating to the content of the website and apply to any person who visits the website, contacts us or other users through it, links to it, or interacts with it in any way.',
],
},
{
heading: '2. Website Offer',
paragraphs: [
'We operate the website solely for information, prospecting, and market research purposes.',
'Information relating to the RentalDriveGo loyalty program may be presented to visitors, and download links may be made available through the website.',
],
},
{
heading: '3. Contact',
paragraphs: [
'You may contact us through the website or by email at service-client@rentaldrivego.ma.',
],
},
{
heading: '4. Acceptance of the General Conditions of Use',
paragraphs: [
'By using the website, you accept these General Conditions of Use and agree to comply with them. If you do not accept them, you must not use the website.',
'Our Privacy Policy and Cookie Policy also apply to your use of the website.',
],
},
{
heading: '5. Changes to the General Conditions of Use',
paragraphs: [
'We may amend these General Conditions of Use at any time. Each time you use the website, you should review the current version.',
'We also reserve the right to amend, correct, or update any information published on the website at any time.',
],
},
{
heading: '6. Changes to Website Content and Availability',
paragraphs: [
'We may update or modify the content of the website at any time to reflect changes in the needs of users and partners.',
'The website is made available free of charge. While we use reasonable means to prevent and resolve interruptions, we cannot guarantee continuous or uninterrupted availability.',
'We may suspend, withdraw, or restrict the availability of all or part of the website for commercial or operational reasons.',
'You are responsible for ensuring that all persons accessing the website through your internet connection are aware of these conditions and comply with them.',
],
},
{
heading: '7. Scope of Information on the Website',
paragraphs: [
'The content of the website is provided for general information purposes only and does not constitute a recommendation on which you should rely.',
'Although we make reasonable efforts to update the information on the website, we do not guarantee, whether expressly or implicitly, that the content is accurate, complete, or current.',
],
},
{
heading: '8. Third-Party Websites',
paragraphs: [
'We are not responsible for third-party websites to which the website may link.',
'Any hyperlinks to third-party content, websites, or resources are provided for information purposes only, and we cannot be held responsible for their content, accuracy, or quality.',
],
},
{
heading: '9. Limitation of Liability',
paragraphs: [
'You agree that access to and use of the website and any associated pages is at your own risk.',
'Nothing in these conditions excludes or limits our liability where such exclusion or limitation would be unlawful.',
'Neither we nor our partners or affiliates may be held liable for damage resulting from the use of information, files, links, or websites accessible from or received through the website.',
],
},
{
heading: '10. Your Responsibilities',
paragraphs: [
'You acknowledge that we may use the information you provide to us where you have given the required consent or where another lawful basis applies.',
'You agree to use the website only for lawful purposes.',
'If we consider that a breach of these General Conditions of Use has occurred, we may take any action we deem appropriate.',
],
},
{
heading: '11. Intellectual Property',
paragraphs: [
'The website is our property and the entire website is protected by intellectual property laws.',
'All rights of reproduction and representation are reserved.',
],
},
],
},
},
fr: {
'about-us': {
title: 'À propos de nous',
paragraphs: [
'RentalDriveGo aide les entreprises de location de voitures à simplifier leurs opérations, gérer leurs flottes et augmenter leurs réservations grâce à une plateforme complète. Nos outils permettent de gérer les véhicules, les réservations, les paiements et les opérations quotidiennes de manière efficace et centralisée.',
'Que vous soyez une petite agence locale ou une grande entreprise multi-sites, RentalDriveGo vous aide à développer votre activité avec des solutions modernes et fiables.',
],
},
'terms-of-service': {
title: 'Conditions dutilisation',
paragraphs: [
'En utilisant RentalDriveGo, vous acceptez de respecter nos politiques, nos règles dutilisation et les lois applicables. Nos services sont destinés aux entreprises de location de voitures et à leurs utilisateurs autorisés.',
'RentalDriveGo se réserve le droit de suspendre tout compte impliqué dans une activité frauduleuse ou une violation des présentes conditions.',
],
},
security: {
title: 'Sécurité',
paragraphs: [
'RentalDriveGo applique des standards de sécurité modernes afin de protéger les données de votre entreprise, les informations de vos clients et vos réservations.',
'Notre plateforme utilise le chiffrement, une infrastructure cloud sécurisée et une surveillance continue afin dassurer la confidentialité et la fiabilité des services.',
],
},
compliance: {
title: 'Conformité',
paragraphs: [
'RentalDriveGo sengage à respecter les réglementations applicables ainsi que les standards du secteur concernant la protection des données et la sécurité.',
'Nous améliorons continuellement nos systèmes afin de garantir transparence, conformité et confiance à nos clients.',
],
},
'privacy-policy': {
title: 'Politique de confidentialité',
paragraphs: [
'Vous consultez actuellement la politique de confidentialité du site RentalDriveGo. Pour consulter la politique de confidentialité de lapplication, veuillez visiter : https://www.rentaldrivego.ma/app-privacy-fr',
'RentalDriveGo respecte votre vie privée et sengage à traiter vos données personnelles de manière transparente. Cette politique explique comment nous collectons, utilisons, conservons et partageons vos informations personnelles lorsque vous utilisez notre site internet, ainsi que les droits dont vous disposez concernant ce traitement.',
],
sections: [
{
heading: '1. Coordonnées du responsable du traitement',
paragraphs: [
'Les présentes informations sur la protection des données sappliquent aux traitements effectués par RentalDriveGo.',
'Téléphone : +212 6 05 65 17 51',
'Email : service-client@rentaldrivego.ma',
],
},
{
heading: '2. Données personnelles collectées, finalités dutilisation et durée de conservation',
paragraphs: [
'Lorsque vous consultez notre site à des fins dinformation uniquement, sans vous inscrire à un service et sans nous transmettre de données personnelles par un autre moyen, votre navigateur envoie automatiquement certaines informations techniques à notre hébergeur, notamment le type, la langue et la version du navigateur, la date et lheure de la demande, ladresse IP, les pages consultées, le site dorigine, le volume de données transférées et le système dexploitation.',
'Ces informations sont temporairement conservées afin de permettre laffichage du site sur votre appareil, de réaliser des analyses internes, daméliorer la présentation du site et de détecter ou prévenir les abus. Nous ne croisons pas ces données avec dautres sources et nous ne cherchons pas à vous identifier directement à partir de celles-ci. Elles sont supprimées dès quelles ne sont plus nécessaires à ces finalités.',
'Si vous nous contactez via un formulaire ou par email, nous pouvons collecter votre nom, votre adresse email, votre numéro de téléphone et toute autre information que vous choisissez de nous communiquer. Nous utilisons ces données pour répondre à vos questions, vous contacter lorsque certains services ou inscriptions deviennent disponibles, ou gérer votre participation à des campagnes ou concours le cas échéant.',
'Nous conservons vos données personnelles aussi longtemps que vous détenez un compte chez nous. Si vous souhaitez la suppression de vos données personnelles, vous devez nous adresser une demande formelle, sous réserve de toute obligation légale ou contractuelle de conservation applicable.',
'Vous pouvez retirer votre consentement à tout moment lorsque le traitement repose sur celui-ci. Les traitements effectués avant ce retrait demeurent licites.',
'Nous pouvons également utiliser les données personnelles pour l’évaluation et lamélioration du site, les études de marché, les analyses, les enquêtes ainsi que les actions de marketing liées à nos produits et services, dans le respect de la loi applicable.',
],
},
{
heading: '3. Prestataires tiers',
paragraphs: [
'Nous ne communiquons pas vos données personnelles à des tiers sauf en cas dobligation légale, lorsque ce transfert est nécessaire à lexécution dune relation contractuelle, ou lorsque vous y avez expressément consenti au préalable.',
'Nous pouvons travailler avec des prestataires situés au Maroc et à l’étranger pour des services techniques et liés au site internet, notamment lhébergement et la maintenance.',
],
},
{
heading: '4. Sécurité',
paragraphs: [
'La sécurité de vos données personnelles est importante pour nous. Nous mettons en place des mesures organisationnelles, techniques et physiques appropriées afin de protéger les informations personnelles collectées, aussi bien pendant leur transmission quaprès leur réception.',
'Nos mesures de sécurité et nos pratiques de confidentialité sont revues et améliorées de manière continue.',
],
},
{
heading: '5. Conservation des données personnelles',
paragraphs: [
'Nous conservons les données personnelles uniquement aussi longtemps que la loi nous y autorise, tant que la finalité du traitement subsiste et en labsence dopposition valable.',
'Lorsque des délais légaux de conservation sappliquent, les données sont conservées pendant la durée requise. À lexpiration de cette période, elles sont supprimées sauf si elles restent nécessaires à lexécution ou à la préparation dun contrat.',
],
},
{
heading: '6. Transfert vers un pays étranger',
paragraphs: [
'Les traitements décrits ci-dessus peuvent entraîner le stockage ou le traitement de vos données personnelles dans des pays étrangers.',
],
},
{
heading: '7. Vos droits',
paragraphs: [
'Conformément à la loi n° 09-08 relative à la protection des personnes physiques à l’égard du traitement des données à caractère personnel, vous pouvez disposer dun droit daccès à vos données, dun droit dobtenir des informations complémentaires, dun droit de rectification des données inexactes et, le cas échéant, dun droit dopposition au traitement ou au transfert de vos données à des tiers.',
'Pour exercer vos droits, vous pouvez envoyer une demande indiquant votre nom complet et accompagnée dune copie de votre pièce didentité à : service-client@rentaldrivego.ma',
],
},
],
},
'cookie-policy': {
title: 'Politique relative aux cookies',
paragraphs: [
'Nous utilisons des cookies exclusivement pour assurer le fonctionnement de la plateforme et mémoriser vos préférences personnelles. Nous nutilisons pas de cookies à des fins publicitaires, de suivi intersites ou de profilage comportemental, et nous ne partageons pas les données des cookies avec des tiers à des fins marketing.',
],
sections: [
{
heading: '1. Cookie dauthentification',
paragraphs: [
'Nom : employee_session',
'Ce cookie est défini lorsque vous vous connectez à lespace de travail RentalDriveGo. Il stocke un jeton cryptographiquement signé qui vérifie votre identité pour la durée de votre session. Il expire automatiquement après 8 heures et est supprimé lors de la déconnexion. Ce cookie est strictement nécessaire — sans lui, laccès au tableau de bord et aux espaces protégés nest pas possible.',
],
},
{
heading: '2. Cookie de préférence de langue',
paragraphs: [
'Nom : rentaldrivego-language',
'Enregistre la langue daffichage choisie (français, anglais ou arabe) afin que linterface saffiche dans votre langue préférée à chaque visite sur lensemble de la plateforme. Il est conservé pendant un an.',
],
},
{
heading: '3. Cookie de préférence de thème',
paragraphs: [
'Nom : rentaldrivego-theme',
'Enregistre le schéma de couleurs préféré (mode clair ou sombre). Il est lu dès le chargement de la page afin dappliquer le bon thème avant laffichage de linterface, évitant ainsi un changement visuel brusque. Il est conservé pendant un an.',
],
},
{
heading: '4. Cookies de préférence par utilisateur',
paragraphs: [
'Noms : rentaldrivego-language--[id], rentaldrivego-theme--[id]',
'Lorsque vous êtes connecté, vos préférences de langue et de thème sont également enregistrées dans un cookie lié à votre identifiant de compte. Cela permet à plusieurs employés partageant un appareil de conserver des préférences indépendantes. Ces cookies sont conservés pendant un an.',
],
},
{
heading: '5. Cookies de préférence hérités',
paragraphs: [
'Noms : dashboard-language, marketplace-language',
'Ces cookies existent pour assurer la compatibilité ascendante avec les versions antérieures de la plateforme et stockent la même préférence de langue que le cookie principal. Ils ne sont lus quen secours et seront supprimés dans une prochaine mise à jour.',
],
},
{
heading: '6. Gestion de vos cookies',
paragraphs: [
'Vous pouvez gérer ou supprimer les cookies à tout moment via les paramètres de votre navigateur. Bloquer le cookie dauthentification vous empêchera de vous connecter. Bloquer les cookies de préférence entraînera la réinitialisation de vos paramètres de langue et de thème à chaque visite. Pour des informations générales sur les cookies, vous pouvez consulter http://www.allaboutcookies.org/.',
],
},
],
},
newsletter: {
title: 'Newsletter',
paragraphs: [
'Recevez les dernières actualités, mises à jour produits, conseils de gestion de flotte et nouveautés de RentalDriveGo.',
'Abonnez-vous à notre newsletter pour rester informé et développer votre activité plus efficacement.',
],
},
'contact-sales': {
title: 'Contacter les ventes',
paragraphs: [
'Vous souhaitez découvrir comment RentalDriveGo peut améliorer vos opérations de location de voitures ?',
'Notre équipe commerciale est disponible pour organiser une démonstration, répondre à vos questions et vous proposer la solution adaptée à votre entreprise.',
],
},
'general-conditions': {
title: 'Conditions générales dutilisation',
paragraphs: [
'Vous consultez actuellement les Conditions générales dutilisation du site RentalDriveGo. Pour consulter les Conditions générales dutilisation de lapplication, veuillez visiter : https://www.rentaldrivego.ma/app-tc-fr',
'Les présentes Conditions générales dutilisation définissent les règles applicables lorsque vous visitez, accédez, parcourez, contactez ou utilisez de toute autre manière le site www.rentaldrivego.ma.',
],
sections: [
{
heading: '1. Objet',
paragraphs: [
'Les présentes conditions régissent lutilisation du site www.rentaldrivego.ma exploité par RentalDriveGo, société à responsabilité limitée immatriculée au registre du commerce dAzrou sous le numéro 00000, dont le siège social est situé à Azrou.',
'Elles définissent également les règles relatives au contenu du site et sappliquent à toute personne qui visite le site, nous contacte ou contacte dautres utilisateurs par son intermédiaire, y crée un lien ou interagit avec lui de quelque manière que ce soit.',
],
},
{
heading: '2. Offre du site',
paragraphs: [
'Nous exploitons le site exclusivement à des fins dinformation, de prospection et d’étude de marché.',
'Des informations relatives au programme de fidélité RentalDriveGo peuvent être portées à la connaissance des visiteurs et des liens de téléchargement peuvent être mis à leur disposition.',
],
},
{
heading: '3. Contact',
paragraphs: [
'Vous pouvez nous contacter via le site ou par email à ladresse service-client@rentaldrivego.ma.',
],
},
{
heading: '4. Acceptation des Conditions générales dutilisation',
paragraphs: [
'En utilisant le site, vous acceptez les présentes Conditions générales dutilisation et vous vous engagez à les respecter. Si vous ne les acceptez pas, vous ne devez pas utiliser le site.',
'Notre Politique de confidentialité et notre Politique relative aux cookies sappliquent également à votre utilisation du site.',
],
},
{
heading: '5. Modification des Conditions générales dutilisation',
paragraphs: [
'Nous pouvons modifier à tout moment les présentes Conditions générales dutilisation. À chaque utilisation du site, nous vous invitons à consulter la version en vigueur.',
'Nous nous réservons également le droit de modifier, corriger ou mettre à jour à tout moment les informations publiées sur le site.',
],
},
{
heading: '6. Modification du contenu et disponibilité du site',
paragraphs: [
'Nous pouvons mettre à jour ou modifier le contenu du site à tout moment afin de refléter l’évolution des besoins de nos utilisateurs et partenaires.',
'Le site est mis à votre disposition gratuitement. Bien que nous mettions en œuvre des moyens raisonnables pour prévenir et résoudre les interruptions, nous ne pouvons garantir une disponibilité constante et ininterrompue.',
'Nous pouvons suspendre, retirer ou restreindre laccès à tout ou partie du site pour des raisons commerciales ou opérationnelles.',
'Il vous appartient de veiller à ce que toute personne accédant au site par votre connexion internet ait connaissance des présentes conditions et les respecte.',
],
},
{
heading: '7. Portée des informations figurant sur le site',
paragraphs: [
'Le contenu du site est fourni à titre dinformation générale uniquement et ne constitue ni un conseil ni une recommandation sur lesquels vous devriez vous appuyer.',
'Bien que nous fassions des efforts raisonnables pour mettre à jour les informations publiées sur le site, nous ne garantissons pas, expressément ou implicitement, quelles soient exactes, complètes ou à jour.',
],
},
{
heading: '8. Responsabilité à l’égard des sites tiers',
paragraphs: [
'Nous ne sommes pas responsables des sites internet tiers vers lesquels le site peut contenir des liens.',
'Les liens hypertextes vers des contenus, sites ou ressources de tiers sont fournis uniquement à titre informatif, et nous ne pouvons être tenus responsables de leur contenu, de leur exactitude ou de leur qualité.',
],
},
{
heading: '9. Limitation de responsabilité',
paragraphs: [
'Vous reconnaissez que laccès et lutilisation du site ainsi que des pages qui y sont rattachées se font à vos propres risques.',
'Aucune disposition des présentes conditions nexclut ou ne limite notre responsabilité lorsquune telle exclusion ou limitation serait contraire à la loi.',
'Ni nous, ni nos partenaires ou affiliés, ne pourrons être tenus responsables des dommages résultant de lutilisation des informations, fichiers, liens ou sites accessibles depuis le site ou reçus par son intermédiaire.',
],
},
{
heading: '10. Vos responsabilités',
paragraphs: [
'Vous reconnaissez que nous pouvons utiliser les informations que vous nous fournissez lorsque vous avez donné le consentement requis ou lorsquune autre base légale sapplique.',
'Vous vous engagez à utiliser le site uniquement à des fins licites.',
'Si nous estimons quune violation des présentes Conditions générales dutilisation a eu lieu, nous pouvons prendre toute mesure que nous jugeons appropriée.',
],
},
{
heading: '11. Propriété intellectuelle',
paragraphs: [
'Le site est notre propriété et lensemble du site est protégé par la législation relative à la propriété intellectuelle.',
'Tous les droits de reproduction et de représentation sont réservés.',
],
},
],
},
},
ar: {
'about-us': {
title: 'من نحن',
paragraphs: [
'تساعد منصة RentalDriveGo شركات تأجير السيارات على إدارة عملياتها بسهولة، وتنظيم أساطيلها، وزيادة الحجوزات من خلال منصة متكاملة وحديثة. توفر أدواتنا حلولاً لإدارة المركبات والحجوزات والمدفوعات والعمليات اليومية بكفاءة عالية.',
'سواء كنت تدير شركة محلية صغيرة أو أسطولاً كبيراً متعدد الفروع، فإن RentalDriveGo توفر لك التكنولوجيا التي تساعدك على النمو وإدارة أعمالك بثقة.',
],
},
'terms-of-service': {
title: 'شروط الاستخدام',
paragraphs: [
'باستخدامك لمنصة RentalDriveGo، فإنك توافق على الالتزام بسياسات المنصة والقوانين المعمول بها. خدماتنا مخصصة لشركات تأجير السيارات والمستخدمين المصرح لهم فقط.',
'تحتفظ RentalDriveGo بحق تعليق أي حساب يشارك في أنشطة احتيالية أو يخالف شروط الاستخدام.',
],
},
security: {
title: 'الأمان',
paragraphs: [
'تلتزم RentalDriveGo بتوفير أعلى معايير الأمان لحماية بيانات شركتك ومعلومات عملائك وسجلات الحجوزات.',
'تستخدم منصتنا تقنيات تشفير حديثة وبنية سحابية آمنة وأنظمة مراقبة مستمرة لضمان حماية البيانات واستقرار الخدمة.',
],
},
compliance: {
title: 'الامتثال',
paragraphs: [
'تلتزم RentalDriveGo بالامتثال للمعايير واللوائح المعمول بها المتعلقة بحماية البيانات والخصوصية والأمان.',
'نعمل باستمرار على تطوير أنظمتنا وإجراءاتنا لضمان الشفافية والثقة لعملائنا وشركائنا.',
],
},
'privacy-policy': {
title: 'سياسة الخصوصية',
paragraphs: [
'أنت الآن تطّلع على سياسة الخصوصية الخاصة بموقع RentalDriveGo. للاطلاع على سياسة الخصوصية الخاصة بالتطبيق، يرجى زيارة: https://www.rentaldrivego.ma/app-privacy-ar',
'تحترم RentalDriveGo خصوصيتك وتلتزم بالتعامل مع بياناتك الشخصية بشفافية. توضح هذه السياسة كيفية جمع معلوماتك الشخصية واستخدامها وتخزينها ومشاركتها عند استخدامك لموقعنا، كما تشرح حقوقك المتعلقة بمعالجة بياناتك الشخصية.',
],
sections: [
{
heading: '1. بيانات الاتصال الخاصة بمسؤول المعالجة',
paragraphs: [
'تنطبق هذه المعلومات المتعلقة بحماية البيانات على المعالجة التي تقوم بها RentalDriveGo.',
'الهاتف: +212 6 05 65 17 51',
'البريد الإلكتروني: service-client@rentaldrivego.ma',
],
},
{
heading: '2. ما هي البيانات الشخصية التي نجمعها، وكيف نستخدمها، ومدة الاحتفاظ بها',
paragraphs: [
'عند زيارتك لموقعنا لأغراض الاطلاع فقط، من دون التسجيل في أي خدمة أو تزويدنا ببيانات شخصية بأي وسيلة أخرى، يرسل متصفحك تلقائياً بعض المعلومات التقنية إلى مزود الاستضافة، مثل نوع المتصفح ولغته وإصداره، وتاريخ ووقت الطلب، وعنوان IP، والصفحات التي تمت زيارتها، والموقع المحيل، وحجم البيانات المنقولة، ونظام التشغيل.',
'يتم الاحتفاظ بهذه المعلومات مؤقتاً لتمكين عرض الموقع على جهازك، ودعم التحليل الداخلي، وتحسين طريقة عرض الموقع، واكتشاف أو منع أي إساءة استخدام. نحن لا ندمج هذه البيانات مع مصادر أخرى ولا نسعى إلى التعرف المباشر على هويتك من خلالها. ويتم حذفها بمجرد انتفاء الحاجة إليها لهذه الأغراض.',
'إذا تواصلت معنا عبر نموذج إلكتروني أو عبر البريد الإلكتروني، فقد نجمع اسمك وبريدك الإلكتروني ورقم هاتفك وأي معلومات أخرى تختار تقديمها لنا. نستخدم هذه البيانات للرد على استفساراتك، أو التواصل معك عندما تصبح بعض الخدمات أو التسجيلات متاحة، أو لإدارة مشاركتك في الحملات أو المسابقات عند الاقتضاء.',
'نحتفظ ببياناتك الشخصية طوال الفترة التي يكون لديك فيها حساب لدينا. وإذا كنت ترغب في حذف بياناتك الشخصية، فيجب عليك تقديم طلب رسمي بذلك، مع مراعاة أي التزامات قانونية أو تعاقدية قد تفرض الاحتفاظ ببعض البيانات.',
'يمكنك سحب موافقتك في أي وقت عندما تكون المعالجة قائمة على الموافقة. وتظل المعالجة التي تمت قبل سحب الموافقة مشروعة.',
'وقد نستخدم أيضاً البيانات الشخصية لمراجعة الموقع وتحسينه، وإجراء أبحاث السوق، والتحليلات، والاستبيانات، والأنشطة التسويقية المتعلقة بمنتجاتنا وخدماتنا، وفقاً للقانون المعمول به.',
],
},
{
heading: '3. مزودو الخدمات من الأطراف الثالثة',
paragraphs: [
'لا نقوم بنقل بياناتك الشخصية إلى أطراف ثالثة إلا إذا كان ذلك مطلوباً بموجب القانون، أو كان ضرورياً لتنفيذ علاقة تعاقدية، أو إذا كنت قد وافقت على ذلك صراحة مسبقاً.',
'قد نتعاون مع مزودي خدمات في المغرب وخارجه لتقديم الخدمات التقنية والخدمات المرتبطة بالموقع، بما في ذلك الاستضافة والصيانة.',
],
},
{
heading: '4. الأمان',
paragraphs: [
'تمثل حماية بياناتك الشخصية أولوية بالنسبة لنا. لذلك نعتمد تدابير تنظيمية وتقنية ومادية مناسبة لحماية المعلومات الشخصية التي نجمعها، سواء أثناء نقلها أو بعد استلامها.',
'نقوم بمراجعة وتحسين إجراءات الأمان وممارسات الخصوصية لدينا بشكل مستمر.',
],
},
{
heading: '5. الاحتفاظ بالبيانات الشخصية',
paragraphs: [
'نحتفظ بالبيانات الشخصية فقط للمدة التي يسمح بها القانون، وطالما أن الغرض من المعالجة لا يزال قائماً، وما لم يتم تقديم اعتراض مشروع.',
'وعندما تكون هناك مدد قانونية للاحتفاظ بالبيانات، فإننا نلتزم بها. وبعد انتهاء المدة المعمول بها، يتم حذف البيانات ما لم تعد هناك حاجة إليها لتنفيذ عقد أو لاتخاذ خطوات تمهيدية لإبرام عقد.',
],
},
{
heading: '6. النقل إلى دولة أجنبية',
paragraphs: [
'قد تؤدي الترتيبات المذكورة أعلاه إلى تخزين بياناتك الشخصية أو معالجتها في دول أجنبية.',
],
},
{
heading: '7. حقوقك',
paragraphs: [
'وفقاً للقانون رقم 09-08 المتعلق بحماية الأشخاص تجاه معالجة المعطيات ذات الطابع الشخصي، قد يكون لك الحق في الوصول إلى بياناتك، والحصول على معلومات إضافية، وطلب تصحيح البيانات غير الدقيقة، والاعتراض عند الاقتضاء على معالجة بياناتك أو على نقلها إلى أطراف ثالثة.',
'لممارسة حقوقك، يمكنك إرسال طلب يتضمن اسمك الكامل ونسخة من وثيقة هويتك إلى: service-client@rentaldrivego.ma',
],
},
],
},
'cookie-policy': {
title: 'سياسة ملفات تعريف الارتباط',
paragraphs: [
'نستخدم ملفات تعريف الارتباط حصرياً لتشغيل المنصة وتذكّر تفضيلاتك الشخصية. لا نستخدم ملفات تعريف الارتباط لأغراض إعلانية أو تتبع المستخدمين عبر المواقع أو بناء ملفات تعريف سلوكية، ولا نشارك بيانات ملفات تعريف الارتباط مع أطراف ثالثة لأغراض تسويقية.',
],
sections: [
{
heading: '1. ملف تعريف الارتباط للمصادقة',
paragraphs: [
'الاسم: employee_session',
'يُعيَّن هذا الملف عند تسجيل دخولك إلى مساحة عمل RentalDriveGo. يخزّن رمزاً موقّعاً تشفيرياً يُثبت هويتك طوال مدة الجلسة. تنتهي صلاحيته تلقائياً بعد 8 ساعات ويُحذف عند تسجيل الخروج. هذا الملف ضروري بشكل مطلق — إذ لا يمكن الوصول إلى لوحة التحكم والمناطق المحمية بدونه.',
],
},
{
heading: '2. ملف تعريف الارتباط لتفضيل اللغة',
paragraphs: [
'الاسم: rentaldrivego-language',
'يحفظ لغة العرض التي اخترتها (العربية أو الفرنسية أو الإنجليزية) حتى تظهر الواجهة بلغتك المفضّلة في كل زيارة وعبر جميع أجزاء المنصة. يُحتفظ به لمدة سنة.',
],
},
{
heading: '3. ملف تعريف الارتباط لتفضيل المظهر',
paragraphs: [
'الاسم: rentaldrivego-theme',
'يحفظ نظام الألوان المفضّل لديك (الوضع الفاتح أو الداكن). يُقرأ فور تحميل الصفحة لتطبيق المظهر الصحيح قبل عرض الواجهة، ما يمنع حدوث وميض بصري غير مرغوب. يُحتفظ به لمدة سنة.',
],
},
{
heading: '4. ملفات تعريف الارتباط للتفضيلات الخاصة بكل مستخدم',
paragraphs: [
'الأسماء: rentaldrivego-language--[id], rentaldrivego-theme--[id]',
'عند تسجيل دخولك، تُحفظ تفضيلات اللغة والمظهر الخاصة بك أيضاً في ملف مرتبط بمعرّف حسابك. يتيح ذلك لأكثر من موظف يتشاركون جهازاً واحداً الاحتفاظ بتفضيلات مستقلة. يُحتفظ بهذه الملفات لمدة سنة.',
],
},
{
heading: '5. ملفات تعريف الارتباط الموروثة',
paragraphs: [
'الأسماء: dashboard-language, marketplace-language',
'توجد هذه الملفات لضمان التوافق مع الإصدارات السابقة من المنصة، وتخزّن تفضيل اللغة ذاته الموجود في الملف الرئيسي. لا تُقرأ إلا كخيار احتياطي، وستُزال في تحديث مستقبلي.',
],
},
{
heading: '6. إدارة ملفات تعريف الارتباط',
paragraphs: [
'يمكنك إدارة ملفات تعريف الارتباط أو حذفها في أي وقت من خلال إعدادات متصفحك. سيمنعك حظر ملف المصادقة من تسجيل الدخول. أما حظر ملفات التفضيلات فسيؤدي إلى إعادة ضبط إعدادات اللغة والمظهر في كل زيارة. للاطلاع على معلومات عامة حول ملفات تعريف الارتباط، يمكنك زيارة http://www.allaboutcookies.org/.',
],
},
],
},
newsletter: {
title: 'النشرة الإخبارية',
paragraphs: [
'ابقَ على اطلاع بآخر تحديثات المنصة وأخبار القطاع ونصائح إدارة الأساطيل من RentalDriveGo.',
'اشترك في نشرتنا الإخبارية للحصول على محتوى مفيد يساعدك على تطوير أعمالك بكفاءة أكبر.',
],
},
'contact-sales': {
title: 'تواصل مع المبيعات',
paragraphs: [
'هل ترغب في معرفة كيف يمكن لـ RentalDriveGo تطوير عمليات شركتك؟',
'فريق المبيعات لدينا جاهز لتقديم عرض توضيحي والإجابة على استفساراتك ومساعدتك في اختيار الحل المناسب لاحتياجاتك.',
],
},
'general-conditions': {
title: 'الشروط العامة لاستخدام الموقع',
paragraphs: [
'أنت الآن تطّلع على الشروط العامة لاستخدام موقع RentalDriveGo. للاطلاع على الشروط العامة لاستخدام التطبيق، يرجى زيارة: https://www.rentaldrivego.ma/app-tc-ar',
'تحدد هذه الشروط العامة القواعد المطبقة عند زيارتك أو دخولك أو تصفحك أو تواصلك أو تفاعلك بأي شكل آخر مع الموقع www.rentaldrivego.ma.',
],
sections: [
{
heading: '1. الغرض',
paragraphs: [
'تنظم هذه الشروط استخدام الموقع www.rentaldrivego.ma الذي تديره شركة RentalDriveGo، وهي شركة ذات مسؤولية محدودة مسجلة في السجل التجاري بأزرو تحت رقم 00000، ويقع مقرها الاجتماعي في أزرو.',
'كما تحدد هذه الشروط القواعد المتعلقة بمحتوى الموقع، وتطبق على كل شخص يزور الموقع أو يتواصل معنا أو مع مستخدمين آخرين من خلاله أو يضع رابطاً إليه أو يتفاعل معه بأي طريقة كانت.',
],
},
{
heading: '2. ما يتيحه الموقع',
paragraphs: [
'نقوم بتشغيل الموقع حصرياً لأغراض الإعلام والاستكشاف ودراسة السوق.',
'قد يتم عرض معلومات تتعلق ببرنامج الولاء الخاص بـ RentalDriveGo للزوار، كما قد تُتاح لهم روابط للتنزيل عبر الموقع.',
],
},
{
heading: '3. الاتصال',
paragraphs: [
'يمكنك التواصل معنا عبر الموقع أو عبر البريد الإلكتروني على العنوان service-client@rentaldrivego.ma.',
],
},
{
heading: '4. قبول الشروط العامة للاستخدام',
paragraphs: [
'باستخدامك للموقع، فإنك توافق على هذه الشروط العامة للاستخدام وتتعهد بالالتزام بها. وإذا كنت لا تقبلها، فيجب عليك عدم استخدام الموقع.',
'كما تنطبق سياسة الخصوصية وسياسة ملفات تعريف الارتباط الخاصة بنا على استخدامك للموقع.',
],
},
{
heading: '5. تعديل الشروط العامة للاستخدام',
paragraphs: [
'يجوز لنا تعديل هذه الشروط العامة للاستخدام في أي وقت. ولذلك ننصحك بالاطلاع على النسخة السارية كلما رغبت في استخدام الموقع.',
'كما نحتفظ بالحق في تعديل أو تصحيح أو تحديث أي معلومات منشورة على الموقع في أي وقت.',
],
},
{
heading: '6. تعديل محتوى الموقع وتوفّره',
paragraphs: [
'يجوز لنا تحديث أو تعديل محتوى الموقع في أي وقت بما يعكس تطور احتياجات المستخدمين والشركاء.',
'يتم توفير الموقع مجاناً. وعلى الرغم من أننا نبذل وسائل معقولة لمنع الانقطاعات وحلها، فإننا لا نضمن التوفر المستمر أو غير المنقطع للموقع.',
'يجوز لنا تعليق أو سحب أو تقييد إتاحة كل الموقع أو جزء منه لأسباب تجارية أو تشغيلية.',
'أنت مسؤول عن التأكد من أن جميع الأشخاص الذين يدخلون إلى الموقع عبر اتصالك بالإنترنت على علم بهذه الشروط ويلتزمون بها.',
],
},
{
heading: '7. نطاق المعلومات الواردة في الموقع',
paragraphs: [
'يتم تقديم محتوى الموقع لأغراض إعلامية عامة فقط، ولا يشكل خدمة توصية أو مشورة يمكن الاعتماد عليها.',
'ورغم أننا نبذل جهوداً معقولة لتحديث المعلومات المنشورة على الموقع، فإننا لا نضمن، صراحةً أو ضمناً، أن يكون المحتوى دقيقاً أو كاملاً أو محدثاً.',
],
},
{
heading: '8. المسؤولية تجاه المواقع التابعة للغير',
paragraphs: [
'لا نتحمل المسؤولية عن المواقع الإلكترونية التابعة للغير التي قد يتضمن الموقع روابط إليها.',
'أي روابط نحو محتوى أو مواقع أو موارد تابعة للغير يتم توفيرها لأغراض إعلامية فقط، ولا يمكن تحميلنا المسؤولية عن محتواها أو دقتها أو جودتها.',
],
},
{
heading: '9. تحديد المسؤولية',
paragraphs: [
'أنت توافق على أن الدخول إلى الموقع واستخدامه، وكذلك الصفحات المرتبطة به، يكون على مسؤوليتك الخاصة.',
'لا تستبعد هذه الشروط ولا تحد من مسؤوليتنا في الحالات التي يكون فيها هذا الاستبعاد أو التحديد غير قانوني.',
'ولا نحن ولا شركاؤنا أو الجهات المرتبطة بنا نتحمل أي مسؤولية عن الأضرار الناتجة عن استخدام المعلومات أو الملفات أو الروابط أو المواقع التي يمكن الوصول إليها من خلال الموقع أو استلامها عبره.',
],
},
{
heading: '10. مسؤولياتك',
paragraphs: [
'أنت تقر بأننا قد نستخدم المعلومات التي تقدمها لنا عندما تكون قد منحت الموافقة المطلوبة أو عندما يوجد أساس قانوني آخر لذلك.',
'وتتعهد باستخدام الموقع فقط لأغراض مشروعة.',
'وإذا رأينا أن هناك خرقاً لهذه الشروط العامة للاستخدام، فيجوز لنا اتخاذ أي إجراء نراه مناسباً.',
],
},
{
heading: '11. الملكية الفكرية',
paragraphs: [
'الموقع ملك لنا، والموقع بكامله محمي بموجب القوانين المتعلقة بالملكية الفكرية.',
'وجميع حقوق النسخ والتمثيل محفوظة.',
],
},
],
},
},
}
export function isFooterPageSlug(value: string): value is FooterPageSlug {
return footerPageSlugs.includes(value as FooterPageSlug)
}
export function getFooterPageContent(language: MarketplaceLanguage, slug: FooterPageSlug): FooterPageContent {
return footerContent[language][slug]
}
+41
View File
@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const cookieValues = vi.hoisted(() => new Map<string, string>())
vi.mock('next/headers', () => ({
cookies: async () => ({
get: (name: string) => {
const value = cookieValues.get(name)
return value === undefined ? undefined : { value }
},
}),
}))
import { getMarketplaceLanguage } from './i18n.server'
import { MARKETPLACE_LANGUAGE_COOKIE, SHARED_LANGUAGE_COOKIE } from './i18n'
describe('getMarketplaceLanguage', () => {
beforeEach(() => {
cookieValues.clear()
})
it('prefers the shared language cookie over the legacy marketplace cookie', async () => {
cookieValues.set(SHARED_LANGUAGE_COOKIE, 'ar')
cookieValues.set(MARKETPLACE_LANGUAGE_COOKIE, 'fr')
await expect(getMarketplaceLanguage()).resolves.toBe('ar')
})
it('falls back to the legacy marketplace cookie during migration', async () => {
cookieValues.set(MARKETPLACE_LANGUAGE_COOKIE, 'fr')
await expect(getMarketplaceLanguage()).resolves.toBe('fr')
})
it('defaults to English when cookies are absent or invalid', async () => {
await expect(getMarketplaceLanguage()).resolves.toBe('en')
cookieValues.set(SHARED_LANGUAGE_COOKIE, 'es')
await expect(getMarketplaceLanguage()).resolves.toBe('en')
})
})
+10
View File
@@ -0,0 +1,10 @@
import { cookies } from 'next/headers'
import { MARKETPLACE_LANGUAGE_COOKIE, SHARED_LANGUAGE_COOKIE, isMarketplaceLanguage, type MarketplaceLanguage } from './i18n'
export async function getMarketplaceLanguage(): Promise<MarketplaceLanguage> {
const cookieStore = await cookies()
const cookieValue =
cookieStore.get(SHARED_LANGUAGE_COOKIE)?.value ??
cookieStore.get(MARKETPLACE_LANGUAGE_COOKIE)?.value
return isMarketplaceLanguage(cookieValue) ? cookieValue : 'en'
}
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { isMarketplaceLanguage } from './i18n'
describe('marketplace language guard', () => {
it('accepts the supported marketplace locales', () => {
expect(isMarketplaceLanguage('en')).toBe(true)
expect(isMarketplaceLanguage('fr')).toBe(true)
expect(isMarketplaceLanguage('ar')).toBe(true)
})
it('rejects unsupported, missing, and case-mismatched locales', () => {
expect(isMarketplaceLanguage('de')).toBe(false)
expect(isMarketplaceLanguage('EN')).toBe(false)
expect(isMarketplaceLanguage('')).toBe(false)
expect(isMarketplaceLanguage(null)).toBe(false)
expect(isMarketplaceLanguage(undefined)).toBe(false)
})
})
+8
View File
@@ -0,0 +1,8 @@
export type MarketplaceLanguage = 'en' | 'fr' | 'ar'
export const MARKETPLACE_LANGUAGE_COOKIE = 'marketplace-language'
export const SHARED_LANGUAGE_COOKIE = 'rentaldrivego-language'
export function isMarketplaceLanguage(value: string | null | undefined): value is MarketplaceLanguage {
return value === 'en' || value === 'fr' || value === 'ar'
}
+69
View File
@@ -0,0 +1,69 @@
import { afterEach, describe, expect, it } from 'vitest'
import { getScopedPreferenceCookieName, getScopedPreferenceKey, readScopedPreference, writeScopedPreference } from './preferences'
function installBrowser(cookie = '') {
const store = new Map<string, string>()
let cookieValue = cookie
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
localStorage: {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => store.set(key, value),
},
},
})
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
get cookie() { return cookieValue },
set cookie(value: string) {
const [pair] = value.split(';')
const [name, val] = pair.split('=')
cookieValue = cookieValue
.split(';')
.map((chunk) => chunk.trim())
.filter(Boolean)
.filter((chunk) => !chunk.startsWith(`${name}=`))
.concat(`${name}=${val}`)
.join('; ')
},
},
})
Object.defineProperty(globalThis, 'atob', {
configurable: true,
value: (value: string) => Buffer.from(value, 'base64').toString('binary'),
})
return { store, get cookie() { return cookieValue } }
}
afterEach(() => {
Reflect.deleteProperty(globalThis, 'window')
Reflect.deleteProperty(globalThis, 'document')
Reflect.deleteProperty(globalThis, 'atob')
})
describe('marketplace scoped preferences', () => {
it('returns unscoped keys when no usable token exists', () => {
installBrowser()
expect(getScopedPreferenceKey('rentaldrivego-language')).toBe('rentaldrivego-language')
expect(getScopedPreferenceCookieName('rentaldrivego-language')).toBe('rentaldrivego-language')
})
it('reads shared cookies before legacy local-storage fallbacks', () => {
const browser = installBrowser('rentaldrivego-language=fr')
browser.store.set('marketplace-language', 'ar')
expect(readScopedPreference('rentaldrivego-language', ['marketplace-language'])).toBe('fr')
})
it('writes shared and legacy values without auth-token scoping', () => {
const browser = installBrowser()
writeScopedPreference('rentaldrivego-theme', 'dark', ['dashboard-theme'])
expect(browser.store.get('rentaldrivego-theme')).toBe('dark')
expect(browser.store.get('dashboard-theme')).toBe('dark')
expect(browser.cookie).toContain('rentaldrivego-theme=dark')
})
})
+94
View File
@@ -0,0 +1,94 @@
export const SHARED_LANGUAGE_COOKIE = 'rentaldrivego-language'
export const SHARED_LANGUAGE_KEY = 'rentaldrivego-language'
export const SHARED_THEME_KEY = 'rentaldrivego-theme'
function readEmployeeToken() {
return null
}
function decodeEmployeeId(token: string | null) {
if (!token) return null
try {
const encoded = token.split('.')[1] ?? ''
const normalized = encoded.replace(/-/g, '+').replace(/_/g, '/')
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
const payload = JSON.parse(atob(padded)) as { sub?: string }
return typeof payload.sub === 'string' && payload.sub ? payload.sub : null
} catch {
return null
}
}
export function getScopedPreferenceKey(baseKey: string) {
const employeeId = decodeEmployeeId(readEmployeeToken())
return employeeId ? `${baseKey}:${employeeId}` : baseKey
}
export function getScopedPreferenceCookieName(baseKey: string) {
const employeeId = decodeEmployeeId(readEmployeeToken())
return employeeId ? `${baseKey}--${employeeId}` : baseKey
}
function readCookie(name: string) {
if (typeof document === 'undefined') return null
return document.cookie
.split(';')
.map((chunk) => chunk.trim())
.find((chunk) => chunk.startsWith(`${name}=`))
?.slice(name.length + 1) ?? null
}
function writeCookie(name: string, value: string) {
if (typeof document === 'undefined') return
document.cookie = `${name}=${value}; path=/; max-age=31536000; samesite=lax`
}
export function readCurrentUserScopedPreference(baseKey: string) {
if (typeof window === 'undefined') return null
const scopedCookie = readCookie(getScopedPreferenceCookieName(baseKey))
if (scopedCookie) return scopedCookie
return window.localStorage.getItem(getScopedPreferenceKey(baseKey))
}
export function readScopedPreference(baseKey: string, legacyKeys: string[] = []) {
if (typeof window === 'undefined') return null
const scopedCookie = readCookie(getScopedPreferenceCookieName(baseKey))
if (scopedCookie) return scopedCookie
const sharedCookie = readCookie(baseKey)
if (sharedCookie) return sharedCookie
const scopedKey = getScopedPreferenceKey(baseKey)
const candidates = [scopedKey, baseKey, ...legacyKeys]
for (const key of candidates) {
const value = window.localStorage.getItem(key)
if (value) return value
}
return null
}
export function writeScopedPreference(baseKey: string, value: string, legacyKeys: string[] = []) {
if (typeof window === 'undefined') return
const scopedKey = getScopedPreferenceKey(baseKey)
const scopedCookie = getScopedPreferenceCookieName(baseKey)
writeCookie(baseKey, value)
if (scopedCookie !== baseKey) {
writeCookie(scopedCookie, value)
}
window.localStorage.setItem(scopedKey, value)
window.localStorage.setItem(baseKey, value)
for (const key of legacyKeys) {
window.localStorage.setItem(key, value)
}
}
+78
View File
@@ -0,0 +1,78 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
function installBrowser(_token?: string) {
const store = new Map<string, string>()
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
localStorage: {
getItem: vi.fn((key: string) => store.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => store.set(key, value)),
removeItem: vi.fn((key: string) => store.delete(key)),
},
},
})
return store
}
afterEach(() => {
vi.restoreAllMocks()
Reflect.deleteProperty(globalThis, 'window')
Reflect.deleteProperty(globalThis, 'fetch')
vi.resetModules()
})
describe('renter client helpers', () => {
it('clears renter sessions through the API logout endpoint', async () => {
installBrowser('renter-token')
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { success: true } }) }))
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
const { clearRenterSession } = await import('./renter')
clearRenterSession()
expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/api\/v1\/auth\/renter\/logout$/), expect.objectContaining({
method: 'POST',
credentials: 'include',
}))
})
it('sends cookies and unwraps renter profile data', async () => {
installBrowser('renter-token')
const fetchMock = vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ data: { id: 'renter_1' } }) }))
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
const { loadRenterProfile } = await import('./renter')
await expect(loadRenterProfile()).resolves.toEqual({ id: 'renter_1' })
expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/api\/v1\/auth\/renter\/me$/), expect.objectContaining({
credentials: 'include',
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
}))
})
it('removes stale tokens and throws renter auth errors on 401', async () => {
installBrowser('expired-token')
Object.defineProperty(globalThis, 'fetch', {
configurable: true,
value: vi.fn(async () => ({ ok: false, status: 401, json: async () => ({ message: 'Unauthorized' }) })),
})
const { loadRenterNotifications, RenterAuthError } = await import('./renter')
await expect(loadRenterNotifications()).rejects.toBeInstanceOf(RenterAuthError)
})
it('serializes preference updates to the renter notification endpoint', async () => {
installBrowser('renter-token')
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { success: true } }) }))
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
const { updateRenterPreferences } = await import('./renter')
const body = [{ notificationType: 'reservation', channel: 'email', enabled: true }]
await expect(updateRenterPreferences(body)).resolves.toEqual({ success: true })
expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/api\/v1\/notifications\/renter\/preferences$/), expect.objectContaining({
method: 'PATCH',
body: JSON.stringify(body),
}))
})
})
+107
View File
@@ -0,0 +1,107 @@
'use client'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000/api/v1'
export interface SavedCompany {
id: string
brand?: {
displayName: string
subdomain: string
logoUrl?: string | null
} | null
}
export interface RenterProfile {
id: string
firstName: string
lastName: string
email: string
phone?: string | null
preferredLocale?: string
preferredCurrency?: string
emailVerified?: boolean
savedCompanies?: SavedCompany[]
}
export interface RenterPreference {
notificationType: string
channel: string
enabled: boolean
}
export interface RenterNotification {
id: string
title: string
body: string
type: string
status: string
readAt: string | null
createdAt: string
}
export class RenterAuthError extends Error {
constructor(message = 'Authentication required') {
super(message)
this.name = 'RenterAuthError'
}
}
async function renterFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
...init,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(init?.headers ?? {}),
},
})
const json = await res.json().catch(() => null)
if (res.status === 401) {
throw new RenterAuthError()
}
if (!res.ok) throw new Error(json?.message ?? 'Request failed')
return (json?.data ?? json) as T
}
export function clearRenterSession() {
void fetch(`${API_BASE}/auth/renter/logout`, { method: 'POST', credentials: 'include' })
}
export function loadRenterProfile() {
return renterFetch<RenterProfile>('/auth/renter/me')
}
export function updateRenterProfile(body: Partial<RenterProfile>) {
return renterFetch<RenterProfile>('/auth/renter/me', {
method: 'PATCH',
body: JSON.stringify(body),
})
}
export function loadRenterNotifications() {
return renterFetch<RenterNotification[]>('/notifications/renter')
}
export function markRenterNotificationRead(id: string) {
return renterFetch<{ success: boolean }>(`/notifications/renter/${id}/read`, {
method: 'POST',
})
}
export function markAllRenterNotificationsRead() {
return renterFetch<{ success: boolean }>('/notifications/renter/read-all', {
method: 'POST',
})
}
export function loadRenterPreferences() {
return renterFetch<RenterPreference[]>('/notifications/renter/preferences')
}
export function updateRenterPreferences(body: RenterPreference[]) {
return renterFetch<{ success: boolean }>('/notifications/renter/preferences', {
method: 'PATCH',
body: JSON.stringify(body),
})
}