Init commit

This commit is contained in:
2025-08-27 09:17:56 +02:00
commit aee2d81e26
109 changed files with 18826 additions and 0 deletions
+13
View File
@@ -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>
+1173
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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"
}
}
+99
View File
@@ -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>
+104
View File
@@ -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;
}
}
+13
View File
@@ -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')
+55
View File
@@ -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
+60
View File
@@ -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
}
})
+163
View File
@@ -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
+660
View File
@@ -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>
+147
View File
@@ -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>
+240
View File
@@ -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>
+237
View File
@@ -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>
+216
View File
@@ -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>
+25
View File
@@ -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',
},
})