feat: add basis of project
This commit is contained in:
parent
0487f5c416
commit
feab348528
16 changed files with 4740 additions and 38 deletions
31
backend/README.md
Normal file
31
backend/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# agep-cargo Backend
|
||||||
|
|
||||||
|
This is a minimal Express backend for the AGEPOLY cargobike booking platform.
|
||||||
|
|
||||||
|
Setup
|
||||||
|
|
||||||
|
1. Copy the example env file and update it:
|
||||||
|
|
||||||
|
cp .env.example .env
|
||||||
|
# then edit .env to set DATABASE_URL
|
||||||
|
|
||||||
|
2. Install dependencies
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
3. Run migration (using psql or your database tool) to create the bookings table:
|
||||||
|
|
||||||
|
psql "$DATABASE_URL" -f migrations/create_bookings.sql
|
||||||
|
|
||||||
|
4. Start the server
|
||||||
|
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
API
|
||||||
|
|
||||||
|
- GET /api/health - health check
|
||||||
|
- GET /api/bikes - list available bike types
|
||||||
|
- POST /api/bookings - create a booking (bike_type, start_date, end_date, name, email)
|
||||||
|
- GET /api/bookings - list recent bookings
|
||||||
|
|
||||||
|
|
||||||
19
backend/db.js
Normal file
19
backend/db.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import pkg from 'pg'
|
||||||
|
const { Pool } = pkg
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
// Optional SSL setting for hosted DBs (uncomment when needed)
|
||||||
|
// ssl: { rejectUnauthorized: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
pool.on('error', (err) => {
|
||||||
|
console.error('Unexpected error on idle client', err)
|
||||||
|
process.exit(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
export { pool }
|
||||||
|
|
||||||
1392
backend/package-lock.json
generated
Normal file
1392
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
backend/package.json
Normal file
20
backend/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "agep-cargo-backend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon server.js",
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"pg": "^8.11.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
82
backend/server.js
Normal file
82
backend/server.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import express from 'express'
|
||||||
|
import cors from 'cors'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { pool } from './db.js'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(cors())
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
// Simple health
|
||||||
|
app.get('/api/health', (req, res) => res.json({ ok: true }))
|
||||||
|
|
||||||
|
// List available cargobikes
|
||||||
|
app.get('/api/bikes', async (req, res) => {
|
||||||
|
// fixed bike types
|
||||||
|
const bikes = [1000, 2000, 3000, 4000, 5000]
|
||||||
|
res.json({ bikes })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a booking
|
||||||
|
app.post('/api/bookings', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { bike_type, start_date, end_date, name, email } = req.body
|
||||||
|
if (!bike_type || !start_date || !end_date || !name || !email) {
|
||||||
|
return res.status(400).json({ error: 'Missing fields' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = `INSERT INTO bookings(bike_type, start_date, end_date, name, email) VALUES($1, $2, $3, $4, $5) RETURNING *`
|
||||||
|
const values = [bike_type, start_date, end_date, name, email]
|
||||||
|
const result = await pool.query(text, values)
|
||||||
|
res.status(201).json({ booking: result.rows[0] })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
res.status(500).json({ error: 'Server error' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// List bookings (simple)
|
||||||
|
app.get('/api/bookings', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM bookings ORDER BY created_at DESC LIMIT 100')
|
||||||
|
res.json({ bookings: result.rows })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
res.status(500).json({ error: 'Server error' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- ensure DB tables exist (auto-migration for dev) ---
|
||||||
|
async function ensureTables() {
|
||||||
|
const createSql = `
|
||||||
|
CREATE TABLE IF NOT EXISTS bookings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
bike_type INTEGER NOT NULL,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pool.query(createSql)
|
||||||
|
console.log('Database: bookings table is present (created if it did not exist)')
|
||||||
|
} catch (err) {
|
||||||
|
// If DATABASE_URL is not configured or DB not reachable, warn but still allow server to start for frontend dev
|
||||||
|
console.warn('Warning: could not ensure bookings table (DB may be unavailable).', err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000
|
||||||
|
|
||||||
|
// Run migrations (if possible) then start server
|
||||||
|
;(async () => {
|
||||||
|
await ensureTables()
|
||||||
|
app.listen(port, () => console.log(`Backend listening on port ${port}`))
|
||||||
|
})()
|
||||||
2873
package-lock.json
generated
Normal file
2873
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,8 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.22"
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
|
|
||||||
92
src/App.vue
92
src/App.vue
|
|
@ -1,47 +1,71 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import TheWelcome from './components/TheWelcome.vue'
|
|
||||||
|
const theme = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// prefer saved preference, fall back to system preference
|
||||||
|
const saved = localStorage.getItem('theme')
|
||||||
|
if (saved) {
|
||||||
|
theme.value = saved
|
||||||
|
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
theme.value = 'dark'
|
||||||
|
} else {
|
||||||
|
theme.value = 'light'
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', theme.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
theme.value = theme.value === 'dark' ? 'light' : 'dark'
|
||||||
|
document.documentElement.setAttribute('data-theme', theme.value)
|
||||||
|
localStorage.setItem('theme', theme.value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<div>
|
||||||
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
|
<header class="site-header">
|
||||||
|
<nav class="container">
|
||||||
<div class="wrapper">
|
<div class="brand">
|
||||||
<HelloWorld msg="You did it!" />
|
<img alt="AGEPOLY logo" src="./assets/logo-agep.png" class="logo" />
|
||||||
|
<div class="title">AGEPOLY — Cargobike Booking</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="links">
|
||||||
|
<router-link to="/" class="nav-link">Réserver</router-link>
|
||||||
|
<router-link to="/admin" class="nav-link">Gestion</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer"></div>
|
||||||
|
|
||||||
|
<button class="theme-toggle" @click="toggleTheme" :aria-pressed="theme === 'dark'" aria-label="Basculer thème">
|
||||||
|
<span v-if="theme === 'dark'">☀️</span>
|
||||||
|
<span v-else>🌙</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main style="padding:1.5rem;max-width:980px;margin:0 auto">
|
||||||
<TheWelcome />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
header {
|
.site-header { width: 100%; }
|
||||||
line-height: 1.5;
|
.container { display:flex; align-items:center; gap:1rem; max-width:1280px; margin:0 auto; padding:0.5rem 1rem; }
|
||||||
}
|
.brand { display:flex; align-items:center; gap:0.75rem }
|
||||||
|
.logo { width:48px; height:auto }
|
||||||
.logo {
|
.title { font-weight:600; color:var(--color-heading) }
|
||||||
display: block;
|
.links { display:flex; gap:0.5rem }
|
||||||
margin: 0 auto 2rem;
|
.nav-link { color:var(--color-text); text-decoration:none; padding:6px 8px; border-radius:6px }
|
||||||
}
|
.nav-link:hover { background:var(--color-background-mute) }
|
||||||
|
.spacer { flex:1 }
|
||||||
@media (min-width: 1024px) {
|
.theme-toggle { background:transparent; border:1px solid var(--color-border); padding:6px 8px; border-radius:6px; cursor:pointer }
|
||||||
header {
|
@media (max-width:640px) {
|
||||||
display: flex;
|
.links { display:none }
|
||||||
place-items: center;
|
.title { font-size:0.95rem }
|
||||||
padding-right: calc(var(--section-gap) / 2);
|
.logo { width:40px }
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
margin: 0 2rem 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .wrapper {
|
|
||||||
display: flex;
|
|
||||||
place-items: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
55
src/api.js
Normal file
55
src/api.js
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Simple frontend API helper with graceful fallbacks when the backend is unavailable (useful during frontend-only development)
|
||||||
|
|
||||||
|
const DEFAULT_BIKES = [1000,2000,3000,4000,5000]
|
||||||
|
|
||||||
|
export async function getBikes() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/bikes')
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch bikes')
|
||||||
|
return res.json()
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback to defaults when backend is not reachable
|
||||||
|
console.warn('getBikes: using fallback bikes because backend is unavailable', err)
|
||||||
|
return { bikes: DEFAULT_BIKES }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createBooking(payload) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/bookings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data?.error || 'Failed to create booking')
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback: simulate a successful booking locally for frontend development
|
||||||
|
console.warn('createBooking: backend unavailable, creating a local mock booking', err)
|
||||||
|
const mock = {
|
||||||
|
booking: {
|
||||||
|
id: Math.floor(Math.random() * 100000) + 100,
|
||||||
|
bike_type: payload.bike_type,
|
||||||
|
start_date: payload.start_date,
|
||||||
|
end_date: payload.end_date,
|
||||||
|
name: payload.name,
|
||||||
|
email: payload.email,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listBookings() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/bookings')
|
||||||
|
if (!res.ok) throw new Error('Failed to list bookings')
|
||||||
|
return res.json()
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback: return an empty list or a small mock so Admin page can render
|
||||||
|
console.warn('listBookings: using fallback empty list because backend is unavailable', err)
|
||||||
|
return { bookings: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/assets/logo-agep.png
Normal file
BIN
src/assets/logo-agep.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
98
src/components/BookingForm.vue
Normal file
98
src/components/BookingForm.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<h2>Réserver un cargobike</h2>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
L’association emprunteuse
|
||||||
|
<input type="text" v-model="form.association" required />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
La taille de vélo souhaitée
|
||||||
|
<select v-model.number="form.bike_type">
|
||||||
|
<option v-for="b in bikes" :key="b" :value="b">{{ b }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
La période de location - début
|
||||||
|
<input type="date" v-model="form.start_date" required />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
La période de location - fin
|
||||||
|
<input type="date" v-model="form.end_date" required />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
L’adresse mail associée au compte Linka Go à autoriser
|
||||||
|
<input type="email" v-model="form.linka_email" required />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button :disabled="loading">{{ loading ? 'Envoi...' : 'Réserver' }}</button>
|
||||||
|
|
||||||
|
<p v-if="error" style="color:crimson">{{ error }}</p>
|
||||||
|
<p v-if="success" style="color:green">Réservation créée (id: {{ success.id }})</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { getBikes, createBooking } from '@/api'
|
||||||
|
|
||||||
|
const bikes = ref([1000,2000,3000,4000,5000])
|
||||||
|
// map: association -> name column in DB, linka_email -> email column in DB
|
||||||
|
const form = ref({ association: '', bike_type: 1000, start_date: '', end_date: '', linka_email: '' })
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const success = ref(null)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const data = await getBikes()
|
||||||
|
if (data?.bikes) bikes.value = data.bikes
|
||||||
|
if (bikes.value.length) form.value.bike_type = bikes.value[0]
|
||||||
|
} catch (err) {
|
||||||
|
// ignore - use defaults
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
error.value = ''
|
||||||
|
success.value = null
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// Map form to backend expected fields: name,email
|
||||||
|
const payload = {
|
||||||
|
bike_type: form.value.bike_type,
|
||||||
|
start_date: form.value.start_date,
|
||||||
|
end_date: form.value.end_date,
|
||||||
|
name: form.value.association,
|
||||||
|
email: form.value.linka_email,
|
||||||
|
}
|
||||||
|
const res = await createBooking(payload)
|
||||||
|
success.value = res.booking
|
||||||
|
// reset form
|
||||||
|
form.value = { association: '', bike_type: bikes.value[0] || 1000, start_date: '', end_date: '', linka_email: '' }
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message || String(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
label { display:block }
|
||||||
|
input, select { width:100%; padding:0.4rem; margin-top:0.2rem }
|
||||||
|
button { padding:0.6rem 1rem }
|
||||||
|
</style>
|
||||||
|
|
@ -2,5 +2,8 @@ import './assets/main.css'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
|
|
|
||||||
65
src/pages/AdminPage.vue
Normal file
65
src/pages/AdminPage.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Gestion des réservations</h1>
|
||||||
|
<p>Liste des dernières réservations</p>
|
||||||
|
|
||||||
|
<button @click="load" :disabled="loading">{{ loading ? 'Chargement...' : 'Rafraîchir' }}</button>
|
||||||
|
|
||||||
|
<p v-if="loadError" style="color:crimson;margin-top:1rem">Erreur lors du chargement des réservations: {{ loadError }}</p>
|
||||||
|
|
||||||
|
<table v-if="bookings.length" style="width:100%;margin-top:1rem;border-collapse:collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="border-bottom:1px solid #ddd;text-align:left;padding:0.5rem">ID</th>
|
||||||
|
<th style="border-bottom:1px solid #ddd;text-align:left;padding:0.5rem">Vélo</th>
|
||||||
|
<th style="border-bottom:1px solid #ddd;text-align:left;padding:0.5rem">Période</th>
|
||||||
|
<th style="border-bottom:1px solid #ddd;text-align:left;padding:0.5rem">Association</th>
|
||||||
|
<th style="border-bottom:1px solid #ddd;text-align:left;padding:0.5rem">Email Linka</th>
|
||||||
|
<th style="border-bottom:1px solid #ddd;text-align:left;padding:0.5rem">Créé</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="b in bookings" :key="b.id">
|
||||||
|
<td style="padding:0.5rem;border-top:1px solid #f1f1f1">{{ b.id }}</td>
|
||||||
|
<td style="padding:0.5rem;border-top:1px solid #f1f1f1">{{ b.bike_type }}</td>
|
||||||
|
<td style="padding:0.5rem;border-top:1px solid #f1f1f1">{{ b.start_date }} → {{ b.end_date }}</td>
|
||||||
|
<td style="padding:0.5rem;border-top:1px solid #f1f1f1">{{ b.name }}</td>
|
||||||
|
<td style="padding:0.5rem;border-top:1px solid #f1f1f1">{{ b.email }}</td>
|
||||||
|
<td style="padding:0.5rem;border-top:1px solid #f1f1f1">{{ b.created_at }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p v-else style="color:#666;margin-top:1rem">Aucune réservation trouvée.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { listBookings } from '@/api'
|
||||||
|
|
||||||
|
const bookings = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadError = ref('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
loadError.value = ''
|
||||||
|
try {
|
||||||
|
const res = await listBookings()
|
||||||
|
bookings.value = res.bookings || []
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
bookings.value = []
|
||||||
|
loadError.value = err.message || String(err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
th, td { font-size: 0.95rem }
|
||||||
|
</style>
|
||||||
17
src/pages/BookingPage.vue
Normal file
17
src/pages/BookingPage.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Réservation</h1>
|
||||||
|
<p>Utilisez ce formulaire pour réserver un cargobike AGEPOLY.</p>
|
||||||
|
<BookingForm />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BookingForm from '@/components/BookingForm.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 { margin-bottom: 0.25rem }
|
||||||
|
p { color: #666; margin-bottom: 1rem }
|
||||||
|
</style>
|
||||||
|
|
||||||
16
src/router.js
Normal file
16
src/router.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import BookingPage from './pages/BookingPage.vue'
|
||||||
|
import AdminPage from './pages/AdminPage.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/', name: 'Booking', component: BookingPage },
|
||||||
|
{ path: '/admin', name: 'Admin', component: AdminPage },
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
||||||
|
|
@ -15,4 +15,10 @@ export default defineConfig({
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Proxy API requests during development to the backend server
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:3000'
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue