Init commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vue + Laravel App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1173
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"pinia": "^2.1.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<router-link to="/" class="nav-logo">
|
||||
🚲 Fiets Service
|
||||
</router-link>
|
||||
<div class="nav-menu">
|
||||
<router-link to="/" class="nav-link">Home</router-link>
|
||||
<router-link to="/booking" class="nav-link">Service Boeken</router-link>
|
||||
<router-link v-if="isAuthenticated" to="/admin" class="nav-link">Beheer</router-link>
|
||||
<router-link v-else to="/login" class="nav-link">Beheer Login</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onMounted } from 'vue'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
onMounted(() => {
|
||||
authStore.checkAuth()
|
||||
})
|
||||
|
||||
return {
|
||||
isAuthenticated: authStore.isAuthenticated
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
padding: 1rem 0;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
/* Global styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mb-1 { margin-bottom: 0.5rem; }
|
||||
.mb-2 { margin-bottom: 1rem; }
|
||||
.mb-3 { margin-bottom: 1.5rem; }
|
||||
.mb-4 { margin-bottom: 2rem; }
|
||||
|
||||
.mt-1 { margin-top: 0.5rem; }
|
||||
.mt-2 { margin-top: 1rem; }
|
||||
.mt-3 { margin-top: 1.5rem; }
|
||||
.mt-4 { margin-top: 2rem; }
|
||||
|
||||
.p-1 { padding: 0.5rem; }
|
||||
.p-2 { padding: 1rem; }
|
||||
.p-3 { padding: 1.5rem; }
|
||||
.p-4 { padding: 2rem; }
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive utilities */
|
||||
@media (max-width: 768px) {
|
||||
.nav-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './assets/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import Home from '../views/Home.vue'
|
||||
import Booking from '../views/Booking.vue'
|
||||
import Admin from '../views/Admin.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
import Pricelist from '../views/Pricelist.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/pricelist',
|
||||
name: 'Pricelist',
|
||||
component: Pricelist
|
||||
},
|
||||
{
|
||||
path: '/booking',
|
||||
name: 'Booking',
|
||||
component: Booking
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'Admin',
|
||||
component: Admin,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// Navigation guard for authentication
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requiresAuth) {
|
||||
const authStore = useAuthStore()
|
||||
if (!authStore.isAuthenticated) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,60 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref(null)
|
||||
const token = ref(null)
|
||||
const isAuthenticated = ref(false)
|
||||
|
||||
const login = (userData, authToken) => {
|
||||
user.value = userData
|
||||
token.value = authToken
|
||||
isAuthenticated.value = true
|
||||
|
||||
// Store in localStorage
|
||||
localStorage.setItem('auth_token', authToken)
|
||||
localStorage.setItem('user', JSON.stringify(userData))
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
user.value = null
|
||||
token.value = null
|
||||
isAuthenticated.value = false
|
||||
|
||||
// Remove from localStorage
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
const checkAuth = () => {
|
||||
const storedToken = localStorage.getItem('auth_token')
|
||||
const storedUser = localStorage.getItem('user')
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
token.value = storedToken
|
||||
user.value = JSON.parse(storedUser)
|
||||
isAuthenticated.value = true
|
||||
} else {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
if (!token.value) return {}
|
||||
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token.value}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
isAuthenticated,
|
||||
login,
|
||||
logout,
|
||||
checkAuth,
|
||||
getAuthHeaders
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<div class="about-header">
|
||||
<h1>About This Application</h1>
|
||||
<p>Learn more about the technologies and architecture used in this project</p>
|
||||
</div>
|
||||
|
||||
<div class="tech-stack">
|
||||
<h2>Technology Stack</h2>
|
||||
|
||||
<div class="tech-section">
|
||||
<h3>Frontend</h3>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-item">
|
||||
<h4>Vue.js 3</h4>
|
||||
<p>Progressive JavaScript framework with Composition API</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h4>Vue Router 4</h4>
|
||||
<p>Official router for Vue.js applications</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h4>Pinia</h4>
|
||||
<p>Intuitive, type safe store for Vue</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h4>Vite</h4>
|
||||
<p>Next generation frontend tooling</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tech-section">
|
||||
<h3>Backend</h3>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-item">
|
||||
<h4>Laravel 12</h4>
|
||||
<p>Modern PHP framework for web artisans</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h4>PHP 8.3</h4>
|
||||
<p>Latest PHP version with modern features</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h4>MySQL/SQLite</h4>
|
||||
<p>Database support with Eloquent ORM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="architecture">
|
||||
<h2>Architecture</h2>
|
||||
<p>This application follows a modern full-stack architecture:</p>
|
||||
<ul>
|
||||
<li><strong>Frontend:</strong> Single Page Application (SPA) built with Vue.js</li>
|
||||
<li><strong>Backend:</strong> RESTful API built with Laravel</li>
|
||||
<li><strong>Communication:</strong> HTTP/JSON API calls between frontend and backend</li>
|
||||
<li><strong>Database:</strong> Relational database with Eloquent ORM</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'About'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.about-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.about-header h1 {
|
||||
color: #667eea;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.about-header p {
|
||||
color: #666;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tech-stack {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.tech-stack h2 {
|
||||
color: #333;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tech-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.tech-section h3 {
|
||||
color: #667eea;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tech-item {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tech-item h4 {
|
||||
color: #667eea;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tech-item p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.architecture {
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.architecture h2 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.architecture p {
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.architecture ul {
|
||||
color: #666;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.architecture li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,660 @@
|
||||
<template>
|
||||
<div class="booking">
|
||||
<div class="booking-header">
|
||||
<h1>🚲 Boek Je Fiets Service</h1>
|
||||
<p>Vul het onderstaande formulier in om je fietsreparatie of onderhoud in te plannen</p>
|
||||
</div>
|
||||
|
||||
<div class="booking-form-container">
|
||||
<form @submit.prevent="submitForm" class="booking-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name">Volledige Naam *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
required
|
||||
placeholder="Voer je volledige naam in"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="address">Adres *</label>
|
||||
<textarea
|
||||
id="address"
|
||||
v-model="form.address"
|
||||
required
|
||||
placeholder="Voer je volledige adres in"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="place">Plaats *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="place"
|
||||
v-model="form.place"
|
||||
required
|
||||
placeholder="Voer je plaats in"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="email">E-mail</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
placeholder="Voer je e-mailadres in"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">Telefoon</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
v-model="form.phone"
|
||||
placeholder="Voer je telefoonnummer in"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group full-width">
|
||||
<label for="repair_description">Wat moet er gerepareerd worden? *</label>
|
||||
<textarea
|
||||
id="repair_description"
|
||||
v-model="form.repair_description"
|
||||
required
|
||||
placeholder="Beschrijf het probleem met je fiets of welke service je nodig hebt"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="requested_date">Gewenste Datum *</label>
|
||||
<div class="custom-date-picker">
|
||||
<div class="date-picker-header">
|
||||
<button type="button" @click="previousMonth" class="nav-btn">‹</button>
|
||||
<span class="current-month">{{ currentMonthYear }}</span>
|
||||
<button type="button" @click="nextMonth" class="nav-btn">›</button>
|
||||
</div>
|
||||
<div class="date-picker-grid">
|
||||
<div class="weekday-header" v-for="day in weekdays" :key="day">{{ day }}</div>
|
||||
<div
|
||||
v-for="date in calendarDates"
|
||||
:key="date.key"
|
||||
:class="[
|
||||
'calendar-date',
|
||||
{
|
||||
'other-month': !date.isCurrentMonth,
|
||||
'disabled': date.isDisabled,
|
||||
'blocked': date.isBlocked,
|
||||
'selected': date.isSelected,
|
||||
'today': date.isToday
|
||||
}
|
||||
]"
|
||||
@click="selectDate(date)"
|
||||
>
|
||||
{{ date.day }}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
id="requested_date"
|
||||
v-model="form.requested_date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="requested_time">Gewenste Tijd *</label>
|
||||
<input
|
||||
type="time"
|
||||
id="requested_time"
|
||||
v-model="form.requested_time"
|
||||
required
|
||||
min="08:00"
|
||||
max="18:00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-error" v-if="formError">
|
||||
{{ formError }}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="submit-btn" :disabled="loading">
|
||||
{{ loading ? 'Versturen...' : 'Service Aanvraag Versturen' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Success Modal -->
|
||||
<div class="modal-overlay" v-if="showSuccessModal" @click="closeSuccessModal">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>✅ Service Aanvraag Verzonden!</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Bedankt voor je service aanvraag. We nemen binnenkort contact met je op om je afspraak te bevestigen.</p>
|
||||
<p><strong>Aanvraag ID:</strong> {{ submittedRequest.id }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="closeSuccessModal" class="btn-primary">Sluiten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'Booking',
|
||||
setup() {
|
||||
const form = ref({
|
||||
name: '',
|
||||
address: '',
|
||||
place: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
repair_description: '',
|
||||
requested_date: '',
|
||||
requested_time: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const formError = ref('')
|
||||
const showSuccessModal = ref(false)
|
||||
const submittedRequest = ref(null)
|
||||
|
||||
// Date picker state
|
||||
const currentDate = ref(new Date())
|
||||
const blockedDates = ref([])
|
||||
const weekdays = ['Zo', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za']
|
||||
|
||||
const minDate = computed(() => {
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
return tomorrow.toISOString().split('T')[0]
|
||||
})
|
||||
|
||||
const currentMonthYear = computed(() => {
|
||||
return currentDate.value.toLocaleDateString('nl-NL', {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
})
|
||||
|
||||
const calendarDates = computed(() => {
|
||||
const year = currentDate.value.getFullYear()
|
||||
const month = currentDate.value.getMonth()
|
||||
|
||||
// Get first day of month and last day of month
|
||||
const firstDay = new Date(year, month, 1)
|
||||
const lastDay = new Date(year, month + 1, 0)
|
||||
|
||||
// Get first day of week (Sunday = 0)
|
||||
const firstDayOfWeek = firstDay.getDay()
|
||||
|
||||
// Get last day of week (Saturday = 6)
|
||||
const lastDayOfWeek = lastDay.getDay()
|
||||
|
||||
const dates = []
|
||||
|
||||
// Add dates from previous month to fill first week
|
||||
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
|
||||
const date = new Date(year, month, -i)
|
||||
dates.push(createDateObject(date, false))
|
||||
}
|
||||
|
||||
// Add dates from current month
|
||||
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||||
const date = new Date(year, month, day)
|
||||
dates.push(createDateObject(date, true))
|
||||
}
|
||||
|
||||
// Add dates from next month to fill last week
|
||||
for (let i = 1; i <= 6 - lastDayOfWeek; i++) {
|
||||
const date = new Date(year, month + 1, i)
|
||||
dates.push(createDateObject(date, false))
|
||||
}
|
||||
|
||||
return dates
|
||||
})
|
||||
|
||||
const createDateObject = (date, isCurrentMonth) => {
|
||||
const dateString = date.toISOString().split('T')[0]
|
||||
const today = new Date()
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
|
||||
const isToday = dateString === today.toISOString().split('T')[0]
|
||||
const isDisabled = dateString < tomorrow.toISOString().split('T')[0]
|
||||
const isBlocked = blockedDates.value.some(blocked => {
|
||||
// Convert ISO date to YYYY-MM-DD format for comparison
|
||||
const blockedDate = new Date(blocked.date).toISOString().split('T')[0]
|
||||
return blockedDate === dateString
|
||||
})
|
||||
const isSelected = dateString === form.value.requested_date
|
||||
|
||||
return {
|
||||
date: dateString,
|
||||
day: date.getDate(),
|
||||
key: dateString,
|
||||
isCurrentMonth,
|
||||
isToday,
|
||||
isDisabled,
|
||||
isBlocked,
|
||||
isSelected
|
||||
}
|
||||
}
|
||||
|
||||
const fetchBlockedDates = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/public/blocked-dates')
|
||||
if (response.ok) {
|
||||
blockedDates.value = await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch blocked dates:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const previousMonth = () => {
|
||||
currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() - 1, 1)
|
||||
}
|
||||
|
||||
const nextMonth = () => {
|
||||
currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() + 1, 1)
|
||||
}
|
||||
|
||||
const selectDate = (date) => {
|
||||
if (date.isDisabled || date.isBlocked) return
|
||||
form.value.requested_date = date.date
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (!form.value.email && !form.value.phone) {
|
||||
formError.value = 'Geef alstublieft een e-mailadres of telefoonnummer op'
|
||||
return false
|
||||
}
|
||||
formError.value = ''
|
||||
return true
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
loading.value = true
|
||||
formError.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/public/service-requests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.message || 'Aanvraag versturen mislukt')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
submittedRequest.value = data
|
||||
showSuccessModal.value = true
|
||||
|
||||
// Reset form
|
||||
Object.keys(form.value).forEach(key => {
|
||||
form.value[key] = ''
|
||||
})
|
||||
} catch (error) {
|
||||
formError.value = error.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeSuccessModal = () => {
|
||||
showSuccessModal.value = false
|
||||
submittedRequest.value = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Set default date to tomorrow
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
form.value.requested_date = tomorrow.toISOString().split('T')[0]
|
||||
fetchBlockedDates() // Fetch blocked dates on mount
|
||||
})
|
||||
|
||||
return {
|
||||
form,
|
||||
loading,
|
||||
formError,
|
||||
showSuccessModal,
|
||||
submittedRequest,
|
||||
minDate,
|
||||
submitForm,
|
||||
closeSuccessModal,
|
||||
currentDate,
|
||||
blockedDates,
|
||||
weekdays,
|
||||
calendarDates,
|
||||
currentMonthYear,
|
||||
previousMonth,
|
||||
nextMonth,
|
||||
selectDate
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.booking {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.booking-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.booking-header h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.booking-header p {
|
||||
color: #666;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.booking-form-container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.booking-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row .full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Custom Date Picker Styles */
|
||||
.custom-date-picker {
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.date-picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: #e1e8ed;
|
||||
}
|
||||
|
||||
.current-month {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.date-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1px;
|
||||
background: #e1e8ed;
|
||||
}
|
||||
|
||||
.weekday-header {
|
||||
background: #f8f9fa;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.calendar-date {
|
||||
background: white;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.calendar-date:hover:not(.disabled):not(.blocked) {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.calendar-date.other-month {
|
||||
color: #bdc3c7;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.calendar-date.disabled {
|
||||
color: #bdc3c7;
|
||||
background: #f8f9fa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.calendar-date.blocked {
|
||||
color: #dc3545;
|
||||
background: #f8d7da;
|
||||
cursor: not-allowed;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.calendar-date.blocked::after {
|
||||
content: '🚫';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.calendar-date.selected {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.calendar-date.today {
|
||||
font-weight: bold;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.calendar-date.today.selected {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
background: #fee;
|
||||
color: #c53030;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #feb2b2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 10px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.modal-body p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.booking-form-container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="hero">
|
||||
<h1>🚲 Welkom bij Fiets Service Centrum</h1>
|
||||
<p>Professionele fietsreparatie en onderhoudsdiensten</p>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<h3>🔧 Deskundige Reparaties</h3>
|
||||
<p>Professionele fietsmonteurs met jarenlange ervaring</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h3>⚡ Snelle Service</h3>
|
||||
<p>Snelle doorlooptijden voor de meeste reparaties</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h3>💰 Eerlijke Prijzen</h3>
|
||||
<p>Concurrerende tarieven zonder verborgen kosten</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h3>📅 Eenvoudig Boeken</h3>
|
||||
<p>Eenvoudig online boekingssysteem</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta">
|
||||
<h2>Klaar om je fiets te laten repareren?</h2>
|
||||
<p>Boek je afspraak online en krijg je fiets weer in topconditie.</p>
|
||||
<div class="cta-buttons">
|
||||
<router-link to="/booking" class="cta-button">Nu Service Boeken</router-link>
|
||||
<router-link to="/pricelist" class="cta-button secondary">💰 Prijslijst Bekijken</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
color: white;
|
||||
padding: 4rem 2rem;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cta {
|
||||
background: #f8f9fa;
|
||||
padding: 3rem 2rem;
|
||||
border-radius: 15px;
|
||||
border: 2px dashed #3498db;
|
||||
}
|
||||
|
||||
.cta h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cta p {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.cta-button.secondary {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
.cta-button.secondary:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>🔐 Beheerder Login</h1>
|
||||
<p>Log in om toegang te krijgen tot het beheerpaneel</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="email">E-mail</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
required
|
||||
placeholder="admin@fietservice.nl"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Wachtwoord</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
required
|
||||
placeholder="Voer je wachtwoord in"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-error" v-if="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn" :disabled="loading">
|
||||
{{ loading ? 'Inloggen...' : 'Inloggen' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-info">
|
||||
<p><strong>Demo gegevens:</strong></p>
|
||||
<p>E-mail: admin@fietservice.nl</p>
|
||||
<p>Wachtwoord: Admin174</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const form = ref({
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form.value)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.message || 'Inloggen mislukt')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Use auth store to login
|
||||
authStore.login(data.user, data.token)
|
||||
|
||||
// Redirect to admin page
|
||||
router.push('/admin')
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
loading,
|
||||
error,
|
||||
handleLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.form-group input:disabled {
|
||||
background: #f8f9fa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
background: #fee;
|
||||
color: #c53030;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #feb2b2;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled) {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
background: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-info {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
.login-info p {
|
||||
margin: 0.5rem 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-info strong {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="pricelist">
|
||||
<div class="pricelist-header">
|
||||
<h1>💰 Prijslijst</h1>
|
||||
<p>Transparante prijzen voor al onze fiets service diensten</p>
|
||||
<router-link to="/" class="back-btn">← Terug naar Home</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Prijzen laden...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="pricelist.length === 0" class="empty-state">
|
||||
<p>Geen prijzen beschikbaar op dit moment.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="pricelist-grid">
|
||||
<div v-for="item in pricelist" :key="item.id" class="price-card">
|
||||
<div class="price-header">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<div class="price">€{{ item.price }}</div>
|
||||
</div>
|
||||
<p class="description">{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta-section">
|
||||
<h2>Klaar om je afspraak te boeken?</h2>
|
||||
<p>Boek nu je service afspraak online!</p>
|
||||
<router-link to="/booking" class="cta-button">Nu Boeken</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'Pricelist',
|
||||
setup() {
|
||||
const pricelist = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
const fetchPricelist = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/pricelist')
|
||||
if (response.ok) {
|
||||
pricelist.value = await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch pricelist:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPricelist()
|
||||
})
|
||||
|
||||
return {
|
||||
pricelist,
|
||||
loading
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pricelist {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.pricelist-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.pricelist-header h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pricelist-header p {
|
||||
color: #666;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: inline-block;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.loading-state, .empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.pricelist-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.price-card {
|
||||
background: white;
|
||||
border: 2px solid #e1e8ed;
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.price-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.price-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.price-header h3 {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.price {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 25px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
padding: 3rem 2rem;
|
||||
border-radius: 15px;
|
||||
border: 2px dashed #3498db;
|
||||
}
|
||||
|
||||
.cta-section h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cta-section p {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pricelist {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.pricelist-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.pricelist-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.price-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="users">
|
||||
<div class="users-header">
|
||||
<h1>Users Management</h1>
|
||||
<p>Manage users from the Laravel backend</p>
|
||||
</div>
|
||||
|
||||
<div class="users-content">
|
||||
<div class="loading" v-if="loading">
|
||||
<p>Loading users...</p>
|
||||
</div>
|
||||
|
||||
<div class="error" v-if="error">
|
||||
<p>{{ error }}</p>
|
||||
<button @click="fetchUsers" class="retry-btn">Retry</button>
|
||||
</div>
|
||||
|
||||
<div class="users-list" v-if="users.length > 0">
|
||||
<div class="user-card" v-for="user in users" :key="user.id">
|
||||
<div class="user-avatar">
|
||||
{{ user.name.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h3>{{ user.name }}</h3>
|
||||
<p>{{ user.email }}</p>
|
||||
<small>Created: {{ formatDate(user.created_at) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-users" v-if="!loading && !error && users.length === 0">
|
||||
<p>No users found.</p>
|
||||
</div>
|
||||
|
||||
<div class="refresh-section">
|
||||
<button @click="fetchUsers" class="refresh-btn" :disabled="loading">
|
||||
{{ loading ? 'Refreshing...' : 'Refresh Users' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
setup() {
|
||||
const users = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
users.value = data
|
||||
} catch (err) {
|
||||
error.value = `Failed to fetch users: ${err.message}`
|
||||
console.error('Error fetching users:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
})
|
||||
|
||||
return {
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
fetchUsers,
|
||||
formatDate
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.users-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.users-header h1 {
|
||||
color: #667eea;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.users-header p {
|
||||
color: #666;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.users-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.loading, .error, .no-users {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.users-list {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
transform: translateX(5px);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
.user-info h3 {
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.user-info p {
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.user-info small {
|
||||
color: #999;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.refresh-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user