diff --git a/SAVE_CHEF_IMAGE.md b/SAVE_CHEF_IMAGE.md new file mode 100644 index 0000000..9c932a5 --- /dev/null +++ b/SAVE_CHEF_IMAGE.md @@ -0,0 +1,29 @@ +# Save Chef Profile Image + +## Quick Instructions + +The chef's profile image has been uploaded to the conversation. + +**Save it as:** `/home/alex/git/warme/public/img/christian-warme.jpg` + +### Manual Steps: + +1. Right-click the image in the conversation +2. Save as: `christian-warme.jpg` +3. Move to: `/home/alex/git/warme/public/img/` + +OR use this command after downloading: + +```bash +cp ~/Downloads/christian-warme.jpg /home/alex/git/warme/public/img/ +``` + +### Then rebuild: + +```bash +cd /home/alex/git/warme +npm run build +pm2 restart warme +``` + +The profile page at `/om-christian` is already configured to use this image! diff --git a/UPLOAD_IMAGE.md b/UPLOAD_IMAGE.md new file mode 100644 index 0000000..23ec380 --- /dev/null +++ b/UPLOAD_IMAGE.md @@ -0,0 +1,50 @@ +# Upload Christian's Profile Image + +## Instructions + +To add Christian's profile image to the website: + +1. Save the chef's portrait image as: `public/img/christian-warme.jpg` + +2. Then uncomment the image code in: `src/app/om-christian/page.tsx` + + Replace this: + ```tsx + {/* Placeholder - Replace with actual image */} +
+ +
+ {/* Uncomment when image is uploaded: + Christian Wärme - Kok og Konditor + */} + ``` + + With this: + ```tsx + Christian Wärme - Kok og Konditor + ``` + +3. Rebuild and restart: + ```bash + npm run build + pm2 restart warme + ``` + +## Image Requirements + +- Format: JPG or PNG +- Recommended size: 800x800px minimum +- Aspect ratio: Square (1:1) +- File size: Keep under 500KB for optimal performance diff --git a/cache-debug-test.spec.ts b/cache-debug-test.spec.ts new file mode 100644 index 0000000..f76039d --- /dev/null +++ b/cache-debug-test.spec.ts @@ -0,0 +1,54 @@ +import { test } from '@playwright/test'; + +test('Debug browser cache vs server response', async ({ page }) => { + // Completely clear cache and storage + await page.context().clearCookies(); + + // Set basic auth header + await page.setExtraHTTPHeaders({ + 'Authorization': 'Basic ' + Buffer.from('brotha:makefood1').toString('base64'), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); + + console.log('1. Going to page with cache disabled...'); + await page.goto('http://localhost:3000/admin/availability', { + waitUntil: 'networkidle' + }); + + await page.waitForSelector('.cook-calendar', { timeout: 10000 }); + await page.waitForTimeout(3000); + + // Check what we get immediately + const headers1 = await page.locator('.cook-calendar thead th').allTextContents(); + const firstRow1 = await page.locator('.cook-calendar tbody tr:first-child td button').allTextContents(); + + console.log('2. Initial load:'); + console.log(' Headers:', headers1.join(' ')); + console.log(' First row:', firstRow1.join(' ')); + + // Force another reload with cache busting + console.log('3. Hard reload...'); + await page.reload({ waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + + const headers2 = await page.locator('.cook-calendar thead th').allTextContents(); + const firstRow2 = await page.locator('.cook-calendar tbody tr:first-child td button').allTextContents(); + + console.log('4. After reload:'); + console.log(' Headers:', headers2.join(' ')); + console.log(' First row:', firstRow2.join(' ')); + + // Check if there's a difference between SSR and client hydration + const pageSource = await page.content(); + if (pageSource.includes('27 { + // Set basic auth header + await page.setExtraHTTPHeaders({ + 'Authorization': 'Basic ' + Buffer.from('brotha:makefood1').toString('base64') + }); + + await page.goto('http://localhost:3000/admin/availability'); + await page.waitForSelector('.cook-calendar'); + + // Tjek månedsnavn + const monthHeader = await page.locator('text=november 2025'); + await expect(monthHeader).toBeVisible(); + console.log('✅ Month header shows November 2025'); + + // Find alle dage i kalenderen + const dayElements = await page.locator('.cook-calendar .rdp-day').all(); + console.log(`Found ${dayElements.length} day elements`); + + // Extract dag numre og deres position + const dayData = []; + for (let i = 0; i < dayElements.length; i++) { + const dayText = await dayElements[i].textContent(); + const classes = await dayElements[i].getAttribute('class'); + const isOutside = classes?.includes('rdp-day_outside') || false; + + dayData.push({ + index: i, + text: dayText?.trim(), + isOutside: isOutside + }); + } + + console.log('Calendar days:', dayData.slice(0, 10)); // First 10 days + + // Find første dag i november (1) + const firstNovemberDay = dayData.find(day => day.text === '1' && !day.isOutside); + if (firstNovemberDay) { + console.log(`✅ First day of November (1) found at position ${firstNovemberDay.index}`); + + // November 1, 2025 er en lørdag (weekday 6, men vi starter ugen på mandag så position 5) + // Så 1. november skulle være på position 5 (if we count from 0: Ma=0, Ti=1, On=2, To=3, Fr=4, Lø=5) + const expectedPosition = 5; // Saturday in week starting Monday + const actualPosition = firstNovemberDay.index % 7; + + console.log(`November 1st is at grid position ${actualPosition}, expected ${expectedPosition}`); + + if (actualPosition === expectedPosition) { + console.log('✅ November 1st is correctly positioned on Saturday'); + } else { + console.log(`❌ November 1st position is wrong. Expected ${expectedPosition}, got ${actualPosition}`); + } + } else { + console.log('❌ Could not find November 1st'); + } + + // Find 5. november (today) + const fifthNovemberDay = dayData.find(day => day.text === '5' && !day.isOutside); + if (fifthNovemberDay) { + console.log(`✅ November 5th (today) found at position ${fifthNovemberDay.index}`); + + // November 5, 2025 er en onsdag (position 2 if week starts Monday) + const expectedPosition = 2; // Wednesday + const actualPosition = fifthNovemberDay.index % 7; + + console.log(`November 5th is at grid position ${actualPosition}, expected ${expectedPosition}`); + + if (actualPosition === expectedPosition) { + console.log('✅ November 5th is correctly positioned on Wednesday'); + } else { + console.log(`❌ November 5th position is wrong. Expected ${expectedPosition}, got ${actualPosition}`); + } + } + + // Tjek at vi har 30 dage i november + const novemberDays = dayData.filter(day => !day.isOutside); + console.log(`November has ${novemberDays.length} days visible`); + + // Vis de første par uger for debugging + console.log('\nFirst two weeks layout:'); + for (let week = 0; week < 2; week++) { + const weekDays = dayData.slice(week * 7, (week + 1) * 7); + const weekString = weekDays.map(day => + (day.isOutside ? `(${day.text})` : day.text || 'X').padStart(4, ' ') + ).join(' '); + console.log(`Week ${week + 1}: ${weekString}`); + } +}); \ No newline at end of file diff --git a/calendar-debug.png b/calendar-debug.png new file mode 100644 index 0000000..754c0fa Binary files /dev/null and b/calendar-debug.png differ diff --git a/calendar-screenshot.spec.ts b/calendar-screenshot.spec.ts new file mode 100644 index 0000000..3a0229c --- /dev/null +++ b/calendar-screenshot.spec.ts @@ -0,0 +1,27 @@ +import { test } from '@playwright/test'; + +test('Take screenshot of calendar for visual inspection', async ({ page }) => { + // Set basic auth header + await page.setExtraHTTPHeaders({ + 'Authorization': 'Basic ' + Buffer.from('brotha:makefood1').toString('base64') + }); + + await page.goto('http://localhost:3000/admin/availability'); + await page.waitForSelector('.cook-calendar'); + + // Wait a bit more to ensure everything is loaded + await page.waitForTimeout(2000); + + // Take screenshot of just the calendar + const calendar = page.locator('.cook-calendar'); + await calendar.screenshot({ path: 'calendar-debug.png' }); + + // Also log the actual HTML structure + const calendarHTML = await calendar.innerHTML(); + console.log('=== FULL CALENDAR HTML ==='); + console.log(calendarHTML); + + // Check actual header text + const headers = await page.locator('.cook-calendar th, .cook-calendar .rdp-head_cell').allTextContents(); + console.log('Week headers:', headers); +}); \ No newline at end of file diff --git a/calendar-test.spec.ts b/calendar-test.spec.ts new file mode 100644 index 0000000..4c9e575 --- /dev/null +++ b/calendar-test.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Admin Availability Calendar', () => { + test.beforeEach(async ({ page }) => { + // Set basic auth header + await page.setExtraHTTPHeaders({ + 'Authorization': 'Basic ' + Buffer.from('brotha:makefood1').toString('base64') + }); + + // Gå til admin availability siden + await page.goto('http://localhost:3000/admin/availability'); + + // Vent på siden loader + await page.waitForSelector('.cook-calendar'); + }); + + test('should display calendar with proper layout', async ({ page }) => { + // Tjek at kalenderen vises + const calendar = await page.locator('.cook-calendar'); + await expect(calendar).toBeVisible(); + + // Tjek at månedsnavn vises + const monthHeader = await page.locator('text=november 2025'); + await expect(monthHeader).toBeVisible(); + + // Tjek at weekday headers vises + const weekdays = await page.locator('[class*="rdp"]'); + await expect(weekdays.first()).toBeVisible(); + + console.log('✅ Calendar layout looks good'); + }); + + test('should have proper day styling', async ({ page }) => { + // Find en dag i kalenderen + const dayButtons = await page.locator('.cook-calendar .rdp-day'); + const firstDay = dayButtons.first(); + + // Tjek at dagen er synlig + await expect(firstDay).toBeVisible(); + + // Tjek background farve (should be green for available or red for blocked) + const styles = await firstDay.evaluate((el) => { + const computed = window.getComputedStyle(el); + return { + backgroundColor: computed.backgroundColor, + color: computed.color, + border: computed.border + }; + }); + + console.log('Day styling:', styles); + + // Tjek at der er enten grøn eller rød background + const isGreenOrRed = styles.backgroundColor.includes('240, 253, 244') || // green-50 + styles.backgroundColor.includes('220, 38, 38'); // red-600 + + expect(isGreenOrRed).toBeTruthy(); + console.log('✅ Day colors are correct'); + }); + + test('should handle day clicks', async ({ page }) => { + // Find en klikkbar dag + const dayButtons = await page.locator('.cook-calendar .rdp-day'); + + // Vent på at dage er loaded + await expect(dayButtons.first()).toBeVisible(); + + // Klik på første dag + await dayButtons.first().click(); + + // Vent kort på API response + await page.waitForTimeout(1000); + + // Tjek at der er en toast besked eller loading state + const possibleMessages = [ + page.locator('text=✅'), + page.locator('text=🚫'), + page.locator('text=Gemmer ændring'), + page.locator('text=Der skete en fejl') + ]; + + let messageFound = false; + for (const message of possibleMessages) { + if (await message.isVisible()) { + messageFound = true; + console.log('✅ Click interaction working - message found'); + break; + } + } + + // Hvis ingen besked, så tjek om API kaldet skete via network requests + if (!messageFound) { + console.log('⚠️ No visible message, but click may have worked'); + } + }); + + test('should fit within container', async ({ page }) => { + // Tjek calendar container størrelse + const calendar = await page.locator('.cook-calendar'); + const calendarBox = await calendar.boundingBox(); + + // Tjek container + const container = await page.locator('div.max-w-md'); + const containerBox = await container.boundingBox(); + + if (calendarBox && containerBox) { + // Kalenderen skal passe inden i containeren + expect(calendarBox.width).toBeLessThanOrEqual(containerBox.width + 20); // 20px tolerance + console.log(`✅ Calendar fits: ${calendarBox.width}px ≤ ${containerBox.width}px`); + } + }); + + test('should show correct blocked dates', async ({ page }) => { + // Tjek API data + const response = await page.request.get('http://localhost:3000/api/availability/blocked'); + const blockedDates = await response.json(); + + console.log('Blocked dates from API:', blockedDates.length); + + if (blockedDates.length > 0) { + // Find røde dage i kalenderen + const redDays = await page.locator('.cook-calendar .rdp-day').evaluateAll((buttons) => { + return buttons.filter(btn => { + const styles = window.getComputedStyle(btn); + return styles.backgroundColor.includes('220, 38, 38'); // red-600 + }).length; + }); + + console.log(`Found ${redDays} red days in calendar for ${blockedDates.length} blocked dates`); + + // Der skal være mindst nogle røde dage hvis der er blokerede datoer + if (blockedDates.length > 0) { + expect(redDays).toBeGreaterThan(0); + console.log('✅ Blocked dates show as red'); + } + } + }); +}); \ No newline at end of file diff --git a/cookie-test.spec.ts b/cookie-test.spec.ts new file mode 100644 index 0000000..e535619 --- /dev/null +++ b/cookie-test.spec.ts @@ -0,0 +1,39 @@ +import { test } from '@playwright/test'; + +test('Check cookie and redirect after login', async ({ page }) => { + // Go to login page + await page.goto('http://localhost:3000/login'); + + // Fill and submit form + await page.fill('#username', 'brotha'); + await page.fill('#password', 'makefood1'); + await page.click('button[type="submit"]'); + + // Wait for response + await page.waitForTimeout(2000); + + // Check cookies + const cookies = await page.context().cookies(); + const authCookie = cookies.find(c => c.name === 'authToken'); + + console.log('Auth cookie:', authCookie ? 'Present' : 'Missing'); + if (authCookie) { + console.log('Cookie value length:', authCookie.value.length); + console.log('Cookie secure:', authCookie.secure); + console.log('Cookie httpOnly:', authCookie.httpOnly); + } + + // Try to navigate to admin manually + console.log('Trying to navigate to /admin...'); + await page.goto('http://localhost:3000/admin'); + await page.waitForTimeout(1000); + + const finalUrl = page.url(); + console.log('Final URL:', finalUrl); + + if (finalUrl.includes('/login')) { + console.log('❌ Redirected back to login - middleware issue'); + } else if (finalUrl.includes('/admin')) { + console.log('✅ Successfully accessed admin'); + } +}); \ No newline at end of file diff --git a/current-calendar.png b/current-calendar.png new file mode 100644 index 0000000..ca29dec Binary files /dev/null and b/current-calendar.png differ diff --git a/debug-calendar.spec.ts b/debug-calendar.spec.ts new file mode 100644 index 0000000..4111fd0 --- /dev/null +++ b/debug-calendar.spec.ts @@ -0,0 +1,41 @@ +import { test } from '@playwright/test'; + +test('Debug calendar DOM structure', async ({ page }) => { + // Set basic auth header + await page.setExtraHTTPHeaders({ + 'Authorization': 'Basic ' + Buffer.from('brotha:makefood1').toString('base64') + }); + + await page.goto('http://localhost:3000/admin/availability'); + await page.waitForSelector('.cook-calendar'); + + // Log all classes i kalenderen + const calendarHTML = await page.locator('.cook-calendar').innerHTML(); + console.log('Calendar HTML structure:'); + console.log(calendarHTML.slice(0, 1000) + '...'); + + // Find alle elements med rdp classes + const rdpElements = await page.locator('[class*="rdp"]').all(); + console.log(`Found ${rdpElements.length} elements with rdp classes`); + + for (let i = 0; i < Math.min(10, rdpElements.length); i++) { + const className = await rdpElements[i].getAttribute('class'); + const tagName = await rdpElements[i].evaluate(el => el.tagName); + console.log(`${i}: ${tagName}.${className}`); + } + + // Tjek computed styles på nogle elements + const dayElements = await page.locator('.cook-calendar [class*="day"]').all(); + console.log(`Found ${dayElements.length} day elements`); + + if (dayElements.length > 0) { + const firstDayStyle = await dayElements[0].evaluate((el) => { + const computed = window.getComputedStyle(el); + return { + backgroundColor: computed.backgroundColor, + className: el.className + }; + }); + console.log('First day style:', firstDayStyle); + } +}); \ No newline at end of file diff --git a/debug-login.spec.ts b/debug-login.spec.ts new file mode 100644 index 0000000..245e36f --- /dev/null +++ b/debug-login.spec.ts @@ -0,0 +1,40 @@ +import { test } from '@playwright/test'; + +test('Debug login form behavior', async ({ page }) => { + // Capture console messages + const messages: string[] = []; + page.on('console', msg => { + messages.push(`${msg.type()}: ${msg.text()}`); + }); + + // Go to login page + await page.goto('http://localhost:3000/login'); + + // Fill form + await page.fill('#username', 'brotha'); + await page.fill('#password', 'makefood1'); + + // Submit form + await page.click('button[type="submit"]'); + + // Wait a bit + await page.waitForTimeout(3000); + + // Check current URL + const currentUrl = page.url(); + console.log('Current URL after login:', currentUrl); + + // Print console messages + console.log('Console messages:'); + messages.forEach(msg => console.log(' ', msg)); + + // Check if there are any error messages on page + const errorMessage = await page.locator('[class*="red"]').textContent(); + if (errorMessage) { + console.log('Error message on page:', errorMessage); + } + + // Check localStorage + const authToken = await page.evaluate(() => localStorage.getItem('authToken')); + console.log('Auth token in localStorage:', authToken ? 'Present' : 'Missing'); +}); \ No newline at end of file diff --git a/final-calendar-debug.png b/final-calendar-debug.png new file mode 100644 index 0000000..d5dbe93 Binary files /dev/null and b/final-calendar-debug.png differ diff --git a/force-refresh-test.spec.ts b/force-refresh-test.spec.ts new file mode 100644 index 0000000..4e2d935 --- /dev/null +++ b/force-refresh-test.spec.ts @@ -0,0 +1,40 @@ +import { test } from '@playwright/test'; + +test('Force browser refresh and check cache', async ({ page }) => { + // Set basic auth header + await page.setExtraHTTPHeaders({ + 'Authorization': 'Basic ' + Buffer.from('brotha:makefood1').toString('base64') + }); + + // Force hard reload + await page.goto('http://localhost:3000/admin/availability', { + waitUntil: 'networkidle' + }); + + // Bypass cache + await page.reload({ waitUntil: 'networkidle' }); + + await page.waitForSelector('.cook-calendar'); + await page.waitForTimeout(3000); + + // Take screenshot + await page.screenshot({ path: 'current-calendar.png', fullPage: true }); + + // Check if there are any console errors + const logs = []; + page.on('console', msg => { + logs.push(`${msg.type()}: ${msg.text()}`); + }); + + // Reload one more time and check + await page.reload({ waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + + const tableHTML = await page.locator('.cook-calendar table').innerHTML(); + console.log('Table HTML (first 500 chars):'); + console.log(tableHTML.substring(0, 500) + '...'); + + // Check actual layout one more time + const firstRowCells = await page.locator('.cook-calendar tbody tr:first-child td button').allTextContents(); + console.log('After hard refresh - First row:', firstRowCells); +}); \ No newline at end of file diff --git a/logs/err-0.log b/logs/err-0.log index fe985c1..f99d502 100644 --- a/logs/err-0.log +++ b/logs/err-0.log @@ -64,3 +64,45 @@ Error: Could not find a production build in the '.next' directory. Try building at async cacheEntry.imageResponseCache.get.incrementalCache (/home/alex/git/warme/node_modules/next/dist/server/next-server.js:182:65) at async /home/alex/git/warme/node_modules/next/dist/server/response-cache/index.js:90:36 at async /home/alex/git/warme/node_modules/next/dist/lib/batcher.js:45:32 +2025-11-07T09:40:47: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:40:49: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:40:53: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:41:01: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:41:17: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:41:51: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:42:55: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:45:02: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T09:45:04: ⨯ TypeError: Cannot read properties of undefined (reading 'bind') + at NextNodeServer.handleRequestImpl (/home/alex/git/warme/node_modules/next/dist/server/base-server.js:476:50) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) +2025-11-07T10:48:04: ⨯ TypeError: Cannot read properties of undefined (reading 'clientModules') + at /home/alex/git/warme/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:16:24425 + at /home/alex/git/warme/node_modules/next/dist/server/lib/trace/tracer.js:191:62 + at /home/alex/git/warme/node_modules/next/dist/server/lib/trace/tracer.js:140:36 + at NoopContextManager.with (/home/alex/git/warme/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:7062) + at ContextAPI.with (/home/alex/git/warme/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:518) + at NoopTracer.startActiveSpan (/home/alex/git/warme/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:18093) + at ProxyTracer.startActiveSpan (/home/alex/git/warme/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:18854) + at /home/alex/git/warme/node_modules/next/dist/server/lib/trace/tracer.js:122:103 + at NoopContextManager.with (/home/alex/git/warme/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:7062) + at ContextAPI.with (/home/alex/git/warme/node_modules/next/dist/compiled/@opentelemetry/api/index.js:1:518) +2025-11-07T10:52:00: ⨯ The requested resource isn't a valid image for /img/christian-warme.jpg received null +2025-11-07T10:52:06: ⨯ The requested resource isn't a valid image for /img/christian-warme.jpg received null +2025-11-07T10:52:11: ⨯ The requested resource isn't a valid image for /img/christian-warme.jpg received null +2025-11-07T10:54:38: ⨯ The requested resource isn't a valid image for /img/christian-warme.jpg received null diff --git a/logs/out-0.log b/logs/out-0.log index ade1d34..0dce4ad 100644 --- a/logs/out-0.log +++ b/logs/out-0.log @@ -46,3 +46,412 @@ 2025-11-05T10:18:24: > Ready on http://localhost:3000 2025-11-05T10:25:45: > Ready on http://localhost:3000 2025-11-05T10:33:36: > Ready on http://localhost:3000 +2025-11-05T10:44:48: > Ready on http://localhost:3000 +2025-11-05T13:03:54: > Ready on http://localhost:3000 +2025-11-05T13:13:52: > Ready on http://localhost:3000 +2025-11-05T13:15:35: > Ready on http://localhost:3000 +2025-11-05T13:19:52: > Ready on http://localhost:3000 +2025-11-05T13:26:22: > Ready on http://localhost:3000 +2025-11-05T13:40:32: > Ready on http://localhost:3000 +2025-11-05T13:51:31: > Ready on http://localhost:3000 +2025-11-05T13:57:37: > Ready on http://localhost:3000 +2025-11-05T14:00:08: > Ready on http://localhost:3000 +2025-11-05T14:05:54: > Ready on http://localhost:3000 +2025-11-05T14:11:22: > Ready on http://localhost:3000 +2025-11-05T14:12:29: > Ready on http://localhost:3000 +2025-11-05T14:15:42: > Ready on http://localhost:3000 +2025-11-05T14:22:07: > Ready on http://localhost:3000 +2025-11-05T14:26:53: > Ready on http://localhost:3000 +2025-11-05T14:32:02: > Ready on http://localhost:3000 +2025-11-05T14:33:33: > Ready on http://localhost:3000 +2025-11-05T14:34:21: > Ready on http://localhost:3000 +2025-11-05T14:49:28: Middleware - Path: /admin +2025-11-05T14:49:28: Middleware - Token present: true +2025-11-05T14:49:28: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T14:49:28: Middleware - Invalid token: [Error: The edge runtime does not support Node.js 'crypto' module. +Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime] +2025-11-05T14:49:29: Middleware - Path: /admin +2025-11-05T14:49:29: Middleware - Token present: true +2025-11-05T14:49:29: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T14:49:29: Middleware - Invalid token: [Error: The edge runtime does not support Node.js 'crypto' module. +Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime] +2025-11-05T14:52:40: > Ready on http://localhost:3000 +2025-11-05T14:52:56: Middleware - Path: /admin +2025-11-05T14:52:56: Middleware - Token present: true +2025-11-05T14:52:56: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T14:52:56: Middleware - Token valid, user: brotha +2025-11-05T14:52:56: Middleware - Path: /admin/availability +2025-11-05T14:52:56: Middleware - Token present: true +2025-11-05T14:52:56: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T14:52:56: Middleware - Path: /admin/bookings +2025-11-05T14:52:56: Middleware - Token present: true +2025-11-05T14:52:56: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T14:52:56: Middleware - Token valid, user: brotha +2025-11-05T14:52:56: Middleware - Token valid, user: brotha +2025-11-05T14:52:57: Middleware - Path: /admin +2025-11-05T14:52:57: Middleware - Token present: true +2025-11-05T14:52:57: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T14:52:57: Middleware - Token valid, user: brotha +2025-11-05T14:53:56: Middleware - Path: /admin +2025-11-05T14:53:56: Middleware - Token present: false +2025-11-05T14:53:56: Middleware - Token value: undefined... +2025-11-05T14:53:56: Middleware - No token, redirecting to login +2025-11-05T14:54:33: Middleware - Path: /admin +2025-11-05T14:54:33: Middleware - Token present: true +2025-11-05T14:54:33: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T14:54:33: Middleware - Token valid, user: brotha +2025-11-05T15:00:18: Middleware - Path: /admin +2025-11-05T15:00:18: Middleware - Token present: true +2025-11-05T15:00:18: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T15:00:18: Middleware - Token valid, user: brotha +2025-11-05T15:00:18: Middleware - Path: /admin/availability +2025-11-05T15:00:18: Middleware - Token present: true +2025-11-05T15:00:18: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T15:00:18: Middleware - Path: /admin/bookings +2025-11-05T15:00:18: Middleware - Token present: true +2025-11-05T15:00:18: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-05T15:00:18: Middleware - Token valid, user: brotha +2025-11-05T15:00:18: Middleware - Token valid, user: brotha +2025-11-07T07:40:24: > Ready on http://localhost:3000 +2025-11-07T08:57:58: Middleware - Path: /admin +2025-11-07T08:57:58: Middleware - Token present: true +2025-11-07T08:57:58: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T08:57:58: Middleware - Token valid, user: brotha +2025-11-07T08:57:58: Middleware - Path: /admin/availability +2025-11-07T08:57:58: Middleware - Token present: true +2025-11-07T08:57:58: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T08:57:58: Middleware - Path: /admin/bookings +2025-11-07T08:57:58: Middleware - Token present: true +2025-11-07T08:57:58: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T08:57:58: Middleware - Token valid, user: brotha +2025-11-07T08:57:58: Middleware - Token valid, user: brotha +2025-11-07T09:00:04: Middleware - Path: /admin +2025-11-07T09:00:04: Middleware - Token present: true +2025-11-07T09:00:04: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T09:00:04: Middleware - Token valid, user: brotha +2025-11-07T09:01:41: > Ready on http://localhost:3000 +2025-11-07T09:02:27: === API LOGIN REQUEST START === +2025-11-07T09:02:27: Timestamp: 2025-11-07T08:02:27.169Z +2025-11-07T09:02:27: Request body received: { username: 'brotha', passwordLength: 9 } +2025-11-07T09:02:27: Connecting to database... +2025-11-07T09:02:27: Database connected +2025-11-07T09:02:27: Querying user from database: brotha +2025-11-07T09:02:27: Query result: found 1 users +2025-11-07T09:02:27: User found: { + id: 'd87cf9cf-ba4a-11f0-8c13-cffd87a9b9a6', + username: 'brotha', + role: 'admin' +} +2025-11-07T09:02:27: Verifying password with salt... +2025-11-07T09:02:27: Password valid: true +2025-11-07T09:02:27: Creating JWT token... +2025-11-07T09:02:27: JWT token created, length: 239 +2025-11-07T09:02:27: Setting authToken cookie... +2025-11-07T09:02:27: Cookie set successfully +2025-11-07T09:02:27: === API LOGIN SUCCESS === +2025-11-07T09:02:27: Database connection closed +2025-11-07T09:02:46: === API LOGIN REQUEST START === +2025-11-07T09:02:46: Timestamp: 2025-11-07T08:02:46.950Z +2025-11-07T09:02:46: Request body received: { username: 'brotha', passwordLength: 9 } +2025-11-07T09:02:46: Connecting to database... +2025-11-07T09:02:46: Database connected +2025-11-07T09:02:46: Querying user from database: brotha +2025-11-07T09:02:46: Query result: found 1 users +2025-11-07T09:02:46: User found: { + id: 'd87cf9cf-ba4a-11f0-8c13-cffd87a9b9a6', + username: 'brotha', + role: 'admin' +} +2025-11-07T09:02:46: Verifying password with salt... +2025-11-07T09:02:47: Password valid: true +2025-11-07T09:02:47: Creating JWT token... +2025-11-07T09:02:47: JWT token created, length: 239 +2025-11-07T09:02:47: Setting authToken cookie... +2025-11-07T09:02:47: Cookie set successfully +2025-11-07T09:02:47: === API LOGIN SUCCESS === +2025-11-07T09:02:47: Database connection closed +2025-11-07T09:02:47: Middleware - Path: /admin +2025-11-07T09:02:47: Middleware - Token present: true +2025-11-07T09:02:47: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T09:02:47: Middleware - Token valid, user: brotha +2025-11-07T09:03:46: > Ready on http://localhost:3000 +2025-11-07T09:04:22: === API LOGIN REQUEST START === +2025-11-07T09:04:22: Timestamp: 2025-11-07T08:04:22.360Z +2025-11-07T09:04:22: Request body received: { username: 'brotha', passwordLength: 9 } +2025-11-07T09:04:22: Connecting to database... +2025-11-07T09:04:22: Database connected +2025-11-07T09:04:22: Querying user from database: brotha +2025-11-07T09:04:22: Query result: found 1 users +2025-11-07T09:04:22: User found: { + id: 'd87cf9cf-ba4a-11f0-8c13-cffd87a9b9a6', + username: 'brotha', + role: 'admin' +} +2025-11-07T09:04:22: Verifying password with salt... +2025-11-07T09:04:22: Password valid: true +2025-11-07T09:04:22: Creating JWT token... +2025-11-07T09:04:22: JWT token created, length: 239 +2025-11-07T09:04:22: Setting authToken cookie... +2025-11-07T09:04:22: Cookie set successfully +2025-11-07T09:04:22: === API LOGIN SUCCESS === +2025-11-07T09:04:22: Database connection closed +2025-11-07T09:04:22: Middleware - Path: /admin +2025-11-07T09:04:22: Middleware - Token present: true +2025-11-07T09:04:22: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T09:04:22: Middleware - Token valid, user: brotha +2025-11-07T09:04:22: Middleware - Path: /admin/availability +2025-11-07T09:04:22: Middleware - Token present: true +2025-11-07T09:04:22: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T09:04:22: Middleware - Token valid, user: brotha +2025-11-07T09:04:33: Middleware - Path: /admin/availability +2025-11-07T09:04:33: Middleware - Token present: true +2025-11-07T09:04:33: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T09:04:33: Middleware - Token valid, user: brotha +2025-11-07T09:06:58: > Ready on http://localhost:3000 +2025-11-07T09:09:53: > Ready on http://localhost:3000 +2025-11-07T09:12:14: Middleware - Path: /admin +2025-11-07T09:12:14: Middleware - Token present: true +2025-11-07T09:12:14: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T09:12:14: Middleware - Token valid, user: brotha +2025-11-07T09:14:03: > Ready on http://localhost:3000 +2025-11-07T09:19:57: > Ready on http://localhost:3000 +2025-11-07T09:39:02: > Ready on http://localhost:3000 +2025-11-07T09:44:40: > Ready on http://localhost:3000 +2025-11-07T09:44:53: > Ready on http://localhost:3000 +2025-11-07T09:45:19: === BOOKING REQUEST === +2025-11-07T09:45:19: Request body: { + "name": "Test Booking", + "email": "test@test.dk", + "phone": "12345678", + "address": "København", + "party_size": 20, + "date": "2025-11-15", + "start_time": "18:00", + "duration_hours": 3, + "menu_id": "klassisk-3-retter", + "include_service": true, + "include_cleanup": true, + "message": "Test booking" +} +2025-11-07T09:45:19: Booking time: { date: '2025-11-15', start: '18:00:00', end: '21:00:00', duration: 3 } +2025-11-07T09:45:19: Blocked dates check: { cnt: 0 } +2025-11-07T09:45:19: Checking availability for weekday: 6 +2025-11-07T09:45:19: Available slots for weekday: [] +2025-11-07T09:45:19: Time slot covered by availability: false +2025-11-07T09:45:19: ERROR: Requested time is outside of availability +2025-11-07T09:46:02: > Ready on http://localhost:3000 +2025-11-07T09:50:55: === BOOKING REQUEST === +2025-11-07T09:50:55: Request body: { + "name": "sfds", + "email": "dsffds", + "phone": "214214124", + "address": "2400", + "party_size": 15, + "date": "2025-11-04", + "start_time": "08:00", + "duration_hours": 3, + "menu_id": "italiensk-aften", + "package_id": null, + "include_service": true, + "include_cleanup": true, + "message": "" +} +2025-11-07T09:50:55: Booking time: { date: '2025-11-04', start: '08:00:00', end: '11:00:00', duration: 3 } +2025-11-07T09:50:55: Blocked dates check: { cnt: 0 } +2025-11-07T09:50:55: Checking availability for weekday: 2 +2025-11-07T09:50:55: Available slots for weekday: [ + { + id: 'da219369-491d-4d34-a6ea-3f51808c0611', + weekday: 2, + start_time: '08:00:00', + end_time: '16:00:00', + note: 'arbejde', + created_at: 2025-10-29T10:29:01.000Z + } +] +2025-11-07T09:50:55: Time slot covered by availability: true +2025-11-07T09:50:55: Overlapping bookings check: { cnt: 1 } +2025-11-07T09:50:55: ERROR: Time slot already booked +2025-11-07T09:58:35: > Ready on http://localhost:3000 +2025-11-07T09:59:54: === API: GET /api/availability/weekly === +2025-11-07T09:59:54: === API: GET /api/availability/blocked === +2025-11-07T09:59:54: ✓ Weekly availability fetched: 2 slots +2025-11-07T09:59:54: ✓ Blocked dates fetched: 3 dates +2025-11-07T10:00:14: === BOOKING REQUEST === +2025-11-07T10:00:14: Request body: { + "name": "bobbo", + "email": "bobo", + "phone": "1255151", + "address": "2400", + "party_size": 15, + "date": "2025-11-04", + "start_time": "10:00", + "duration_hours": 3, + "menu_id": "italiensk-aften", + "package_id": null, + "include_service": true, + "include_cleanup": false, + "message": "" +} +2025-11-07T10:00:14: Booking time: { date: '2025-11-04', start: '10:00:00', end: '13:00:00', duration: 3 } +2025-11-07T10:00:14: Blocked dates check: { cnt: 0 } +2025-11-07T10:00:14: Checking availability for weekday: 2 +2025-11-07T10:00:14: Available slots for weekday: [ + { + id: 'da219369-491d-4d34-a6ea-3f51808c0611', + weekday: 2, + start_time: '08:00:00', + end_time: '16:00:00', + note: 'arbejde', + created_at: 2025-10-29T10:29:01.000Z + } +] +2025-11-07T10:00:14: Time slot covered by availability: true +2025-11-07T10:00:14: Overlapping bookings check: { cnt: 1 } +2025-11-07T10:00:14: ERROR: Time slot already booked +2025-11-07T10:00:26: === BOOKING REQUEST === +2025-11-07T10:00:26: Request body: { + "name": "bobbo", + "email": "bobo", + "phone": "1255151", + "address": "2400", + "party_size": 15, + "date": "2025-11-04", + "start_time": "11:00", + "duration_hours": 3, + "menu_id": "italiensk-aften", + "package_id": null, + "include_service": true, + "include_cleanup": false, + "message": "" +} +2025-11-07T10:00:26: Booking time: { date: '2025-11-04', start: '11:00:00', end: '14:00:00', duration: 3 } +2025-11-07T10:00:26: Blocked dates check: { cnt: 0 } +2025-11-07T10:00:26: Checking availability for weekday: 2 +2025-11-07T10:00:26: Available slots for weekday: [ + { + id: 'da219369-491d-4d34-a6ea-3f51808c0611', + weekday: 2, + start_time: '08:00:00', + end_time: '16:00:00', + note: 'arbejde', + created_at: 2025-10-29T10:29:01.000Z + } +] +2025-11-07T10:00:26: Time slot covered by availability: true +2025-11-07T10:00:26: Overlapping bookings check: { cnt: 0 } +2025-11-07T10:00:26: === BOOKING SUCCESS === +2025-11-07T10:00:26: Booking ID: 77b5b617-25d0-46c2-b0f4-42a1b92e0924 +2025-11-07T10:00:26: Total price: 5250 +2025-11-07T10:00:38: Middleware - Path: /admin/bookings +2025-11-07T10:00:38: Middleware - Token present: true +2025-11-07T10:00:38: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T10:00:38: Middleware - Token valid, user: brotha +2025-11-07T10:00:38: === API: GET /api/bookings === +2025-11-07T10:00:38: Query params: { date: null } +2025-11-07T10:00:38: ✓ All bookings fetched. Count: 2 +2025-11-07T10:04:54: > Ready on http://localhost:3000 +2025-11-07T10:27:43: === BOOKING REQUEST === +2025-11-07T10:27:43: Request body: { + "name": "gffdgdgf", + "email": "dfgdfgdgf", + "phone": "24124124", + "address": "2400", + "party_size": 15, + "date": "2025-11-04", + "start_time": "10:30", + "duration_hours": 3, + "menu_id": "italiensk-aften", + "package_id": null, + "include_service": true, + "include_cleanup": false, + "message": "fafssfsf" +} +2025-11-07T10:27:43: Booking time: { date: '2025-11-04', start: '10:30:00', end: '13:30:00', duration: 3 } +2025-11-07T10:27:43: Blocked dates check: { cnt: 0 } +2025-11-07T10:27:43: Checking availability for weekday: 2 +2025-11-07T10:27:43: Available slots for weekday: [ + { + id: 'da219369-491d-4d34-a6ea-3f51808c0611', + weekday: 2, + start_time: '08:00:00', + end_time: '16:00:00', + note: 'arbejde', + created_at: 2025-10-29T10:29:01.000Z + } +] +2025-11-07T10:27:43: Time slot covered by availability: true +2025-11-07T10:27:43: Overlapping bookings check: { cnt: 2 } +2025-11-07T10:27:43: ERROR: Time slot already booked +2025-11-07T10:28:06: === BOOKING REQUEST === +2025-11-07T10:28:06: Request body: { + "name": "gffdgdgf", + "email": "dfgdfgdgf", + "phone": "24124124", + "address": "2400", + "party_size": 15, + "date": "2025-11-18", + "start_time": "10:00", + "duration_hours": 3, + "menu_id": "brunch-luksus", + "package_id": null, + "include_service": true, + "include_cleanup": false, + "message": "fafssfsf" +} +2025-11-07T10:28:06: Booking time: { date: '2025-11-18', start: '10:00:00', end: '13:00:00', duration: 3 } +2025-11-07T10:28:06: Blocked dates check: { cnt: 0 } +2025-11-07T10:28:06: Checking availability for weekday: 2 +2025-11-07T10:28:06: Available slots for weekday: [ + { + id: 'da219369-491d-4d34-a6ea-3f51808c0611', + weekday: 2, + start_time: '08:00:00', + end_time: '16:00:00', + note: 'arbejde', + created_at: 2025-10-29T10:29:01.000Z + } +] +2025-11-07T10:28:06: Time slot covered by availability: true +2025-11-07T10:28:06: Overlapping bookings check: { cnt: 0 } +2025-11-07T10:28:06: === BOOKING SUCCESS === +2025-11-07T10:28:06: Booking ID: f09136a1-7766-42a7-8f67-f02200137c98 +2025-11-07T10:28:06: Total price: 10175 +2025-11-07T10:31:38: > Ready on http://localhost:3000 +2025-11-07T10:44:13: > Ready on http://localhost:3000 +2025-11-07T10:45:42: === API: GET /api/availability/weekly === +2025-11-07T10:45:42: ✓ Weekly availability fetched: 2 slots +2025-11-07T10:48:04: Middleware - Path: /admin/bookings +2025-11-07T10:48:04: Middleware - Token present: true +2025-11-07T10:48:04: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T10:48:04: Middleware - Token valid, user: brotha +2025-11-07T10:48:08: === API: GET /api/availability/weekly === +2025-11-07T10:48:08: === API: GET /api/availability/blocked === +2025-11-07T10:48:08: ✓ Weekly availability fetched: 2 slots +2025-11-07T10:48:08: ✓ Blocked dates fetched: 3 dates +2025-11-07T10:48:18: > Ready on http://localhost:3000 +2025-11-07T10:48:24: === API: GET /api/availability/weekly === +2025-11-07T10:48:24: === API: GET /api/availability/blocked === +2025-11-07T10:48:24: ✓ Weekly availability fetched: 2 slots +2025-11-07T10:48:24: ✓ Blocked dates fetched: 3 dates +2025-11-07T10:49:46: > Ready on http://localhost:3000 +2025-11-07T10:52:03: Middleware - Path: /admin/bookings +2025-11-07T10:52:03: Middleware - Token present: true +2025-11-07T10:52:03: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T10:52:03: Middleware - Token valid, user: brotha +2025-11-07T10:52:03: === API: GET /api/bookings === +2025-11-07T10:52:03: Query params: { date: null } +2025-11-07T10:52:03: Middleware - Path: /admin +2025-11-07T10:52:03: Middleware - Token present: true +2025-11-07T10:52:03: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T10:52:03: Middleware - Path: /admin/availability +2025-11-07T10:52:03: Middleware - Token present: true +2025-11-07T10:52:03: Middleware - Token value: eyJhbGciOiJIUzI1NiIs... +2025-11-07T10:52:03: Middleware - Token valid, user: brotha +2025-11-07T10:52:03: Middleware - Token valid, user: brotha +2025-11-07T10:52:03: ✓ All bookings fetched. Count: 3 +2025-11-07T10:52:58: > Ready on http://localhost:3000 +2025-11-07T10:54:04: === API: GET /api/availability/weekly === +2025-11-07T10:54:04: === API: GET /api/availability/blocked === +2025-11-07T10:54:04: ✓ Blocked dates fetched: 3 dates +2025-11-07T10:54:04: ✓ Weekly availability fetched: 2 slots diff --git a/package-lock.json b/package-lock.json index 4b46e4d..18c3431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,14 @@ "name": "warme", "version": "0.1.0", "dependencies": { + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^2.30.0", "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.292.0", "mysql2": "^3.15.3", "next": "^14.2.33", @@ -26,6 +30,7 @@ "devDependencies": { "@eslint/config-array": "^0.21.1", "@eslint/object-schema": "^2.1.7", + "@playwright/test": "^1.56.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -1011,6 +1016,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1052,6 +1073,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1059,11 +1089,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1876,6 +1921,20 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1947,6 +2006,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2432,6 +2497,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.241", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", @@ -4352,6 +4426,28 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4368,6 +4464,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4448,6 +4565,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4455,6 +4608,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -4578,7 +4737,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mysql2": { @@ -4753,6 +4911,26 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -5144,6 +5322,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5599,6 +5824,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -6637,7 +6882,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { diff --git a/package.json b/package.json index 56d05db..960a30a 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,14 @@ "addFeatured": "node scripts/add-featured-column.mjs" }, "dependencies": { + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "date-fns": "^2.30.0", "dotenv": "^17.2.3", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.292.0", "mysql2": "^3.15.3", "next": "^14.2.33", @@ -40,6 +44,7 @@ "devDependencies": { "@eslint/config-array": "^0.21.1", "@eslint/object-schema": "^2.1.7", + "@playwright/test": "^1.56.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/playwright-report/data/fd20cb4fab8ff344c3ca7f7ca29dd378d3dce484.md b/playwright-report/data/fd20cb4fab8ff344c3ca7f7ca29dd378d3dce484.md new file mode 100644 index 0000000..b53929f --- /dev/null +++ b/playwright-report/data/fd20cb4fab8ff344c3ca7f7ca29dd378d3dce484.md @@ -0,0 +1,114 @@ +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e4]: + - complementary [ref=e5]: + - heading "Admin" [level=2] [ref=e6] + - navigation [ref=e7]: + - link "Menuer" [ref=e8] [cursor=pointer]: + - /url: /admin + - link "Tilgængelighed" [ref=e9] [cursor=pointer]: + - /url: /admin/availability + - link "Bookinger" [ref=e10] [cursor=pointer]: + - /url: /admin/bookings + - main [ref=e11]: + - generic [ref=e13]: + - generic [ref=e14]: + - heading "Din Arbejdskalender" [level=1] [ref=e15] + - paragraph [ref=e16]: Klik på en dag for at åbne eller lukke for bookinger + - generic [ref=e17]: + - heading "Hvad betyder farverne?" [level=2] [ref=e18] + - generic [ref=e19]: + - generic [ref=e22]: + - generic [ref=e23]: ÅBEN DAG + - generic [ref=e24]: Kunder kan booke dig + - generic [ref=e27]: + - generic [ref=e28]: LUKKET DAG + - generic [ref=e29]: Ingen bookinger mulige + - generic [ref=e30]: + - generic [ref=e31]: + - heading "Klik på en dag for at ændre status" [level=2] [ref=e32] + - paragraph [ref=e33]: Grønne dage = åbne for booking | Røde dage = lukkede + - generic [ref=e39]: + - generic [ref=e40]: + - generic [ref=e41]: november 2025 + - generic: + - button "Go to previous month" [ref=e42] [cursor=pointer]: + - img [ref=e43] + - button "Go to next month" [ref=e45] [cursor=pointer]: + - img [ref=e46] + - grid "november 2025" [ref=e48]: + - rowgroup [ref=e49]: + - row "mandag tirsdag onsdag torsdag fredag lørdag søndag" [ref=e50]: + - columnheader "mandag" [ref=e51]: ma + - columnheader "tirsdag" [ref=e52]: ti + - columnheader "onsdag" [ref=e53]: "on" + - columnheader "torsdag" [ref=e54]: to + - columnheader "fredag" [ref=e55]: fr + - columnheader "lørdag" [ref=e56]: lø + - columnheader "søndag" [ref=e57]: sø + - rowgroup [ref=e58]: + - row "1 2" [ref=e59]: + - gridcell + - gridcell + - gridcell + - gridcell + - gridcell + - gridcell "1" [ref=e60] [cursor=pointer] + - gridcell "2" [ref=e61] [cursor=pointer] + - row "3 4 5 6 7 8 9" [ref=e62]: + - gridcell "3" [ref=e63] [cursor=pointer] + - gridcell "4" [ref=e64] [cursor=pointer] + - gridcell "5" [ref=e65] [cursor=pointer] + - gridcell "6" [ref=e66] [cursor=pointer] + - gridcell "7" [ref=e67] [cursor=pointer] + - gridcell "8" [ref=e68] [cursor=pointer] + - gridcell "9" [ref=e69] [cursor=pointer] + - row "10 11 12 13 14 15 16" [ref=e70]: + - gridcell "10" [ref=e71] [cursor=pointer] + - gridcell "11" [ref=e72] [cursor=pointer] + - gridcell "12" [ref=e73] [cursor=pointer] + - gridcell "13" [ref=e74] [cursor=pointer] + - gridcell "14" [ref=e75] [cursor=pointer] + - gridcell "15" [ref=e76] [cursor=pointer] + - gridcell "16" [ref=e77] [cursor=pointer] + - row "17 18 19 20 21 22 23" [ref=e78]: + - gridcell "17" [ref=e79] [cursor=pointer] + - gridcell "18" [ref=e80] [cursor=pointer] + - gridcell "19" [ref=e81] [cursor=pointer] + - gridcell "20" [ref=e82] [cursor=pointer] + - gridcell "21" [ref=e83] [cursor=pointer] + - gridcell "22" [ref=e84] [cursor=pointer] + - gridcell "23" [ref=e85] [cursor=pointer] + - row "24 25 26 27 28 29 30" [ref=e86]: + - gridcell "24" [ref=e87] [cursor=pointer] + - gridcell "25" [ref=e88] [cursor=pointer] + - gridcell "26" [ref=e89] [cursor=pointer] + - gridcell "27" [ref=e90] [cursor=pointer] + - gridcell "28" [ref=e91] [cursor=pointer] + - gridcell "29" [ref=e92] [cursor=pointer] + - gridcell "30" [ref=e93] [cursor=pointer] + - generic [ref=e94]: + - heading "Dine lukkede dage" [level=2] [ref=e95] + - generic [ref=e96]: + - generic [ref=e97]: + - generic [ref=e100]: onsdag 5. november + - button "Åbn dag" [ref=e101] [cursor=pointer] + - generic [ref=e102]: + - generic [ref=e105]: torsdag 6. november + - button "Åbn dag" [ref=e106] [cursor=pointer] + - generic [ref=e107]: + - heading "Sådan fungerer det" [level=3] [ref=e108] + - generic [ref=e109]: + - paragraph [ref=e110]: + - text: 🟢 + - strong [ref=e111]: Grøn dag + - text: = Kunder kan booke dig (klik for at lukke) + - paragraph [ref=e112]: + - text: 🔴 + - strong [ref=e113]: Rød dag + - text: = Lukket for bookinger (klik for at åbne) + - paragraph [ref=e114]: 📱 Alle ændringer gemmes automatisk + - alert [ref=e115] +``` \ No newline at end of file diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 0000000..14caf80 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..654139d --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'echo "Server already running"', + port: 3000, + reuseExistingServer: true, + }, +}); \ No newline at end of file diff --git a/public/img/christianprofile.png b/public/img/christianprofile.png new file mode 100644 index 0000000..5d68a21 Binary files /dev/null and b/public/img/christianprofile.png differ diff --git a/scripts/add-address-to-bookings.mjs b/scripts/add-address-to-bookings.mjs new file mode 100644 index 0000000..3c0b6a3 --- /dev/null +++ b/scripts/add-address-to-bookings.mjs @@ -0,0 +1,51 @@ +import mysql from 'mysql2/promise' +import dotenv from 'dotenv' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +// Load environment variables from .env file in parent directory +dotenv.config({ path: join(__dirname, '..', '.env') }) + +async function addAddressColumn() { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }) + + try { + console.log('Checking if address column exists in bookings table...') + + // Check if column exists + const [columns] = await connection.query( + `SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = ? + AND TABLE_NAME = 'bookings' + AND COLUMN_NAME = 'address'`, + [process.env.DB_NAME] + ) + + if (columns.length > 0) { + console.log('✓ Address column already exists') + } else { + console.log('Adding address column to bookings table...') + await connection.query( + `ALTER TABLE bookings ADD COLUMN address VARCHAR(255) DEFAULT NULL AFTER phone` + ) + console.log('✓ Address column added successfully') + } + + } catch (error) { + console.error('Error:', error) + process.exit(1) + } finally { + await connection.end() + } +} + +addAddressColumn() diff --git a/scripts/add-booking-extras.mjs b/scripts/add-booking-extras.mjs new file mode 100644 index 0000000..ef426e5 --- /dev/null +++ b/scripts/add-booking-extras.mjs @@ -0,0 +1,72 @@ +import mysql from 'mysql2/promise' +import dotenv from 'dotenv' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +dotenv.config({ path: join(__dirname, '..', '.env') }) + +async function addBookingExtras() { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }) + + try { + console.log('Adding booking extras columns...') + + // Check and add include_service + const [serviceCol] = await connection.query( + `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'bookings' AND COLUMN_NAME = 'include_service'`, + [process.env.DB_NAME] + ) + + if (serviceCol.length === 0) { + await connection.query(`ALTER TABLE bookings ADD COLUMN include_service BOOLEAN DEFAULT FALSE`) + console.log('✓ Added include_service column') + } else { + console.log('✓ include_service column already exists') + } + + // Check and add include_cleanup + const [cleanupCol] = await connection.query( + `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'bookings' AND COLUMN_NAME = 'include_cleanup'`, + [process.env.DB_NAME] + ) + + if (cleanupCol.length === 0) { + await connection.query(`ALTER TABLE bookings ADD COLUMN include_cleanup BOOLEAN DEFAULT FALSE`) + console.log('✓ Added include_cleanup column') + } else { + console.log('✓ include_cleanup column already exists') + } + + // Check and add total_price + const [priceCol] = await connection.query( + `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'bookings' AND COLUMN_NAME = 'total_price'`, + [process.env.DB_NAME] + ) + + if (priceCol.length === 0) { + await connection.query(`ALTER TABLE bookings ADD COLUMN total_price DECIMAL(10,2) DEFAULT NULL`) + console.log('✓ Added total_price column') + } else { + console.log('✓ total_price column already exists') + } + + } catch (error) { + console.error('Error:', error) + process.exit(1) + } finally { + await connection.end() + } +} + +addBookingExtras() diff --git a/scripts/setup-users.mjs b/scripts/setup-users.mjs new file mode 100644 index 0000000..cb6834d --- /dev/null +++ b/scripts/setup-users.mjs @@ -0,0 +1,59 @@ +import { createConnection } from 'mysql2/promise' + +async function createUsersTable() { + const connection = await createConnection({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'warme' + }) + + try { + // Create users table + await connection.execute(` + CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + salt VARCHAR(32) NOT NULL, + role ENUM('admin') DEFAULT 'admin', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `) + + console.log('✅ Users table created successfully') + + // Check if admin user exists + const [rows] = await connection.execute( + 'SELECT * FROM users WHERE username = ?', + ['brotha'] + ) + + if (rows.length === 0) { + // Create admin user with salted password + const bcrypt = await import('bcrypt') + const crypto = await import('crypto') + + const salt = crypto.default.randomBytes(16).toString('hex') + const saltedPassword = 'makefood1' + salt + const passwordHash = await bcrypt.default.hash(saltedPassword, 12) + + await connection.execute( + 'INSERT INTO users (username, password_hash, salt, role) VALUES (?, ?, ?, ?)', + ['brotha', passwordHash, salt, 'admin'] + ) + + console.log('✅ Admin user created with salted password') + } else { + console.log('ℹ️ Admin user already exists') + } + + } catch (error) { + console.error('❌ Error setting up users table:', error) + } finally { + await connection.end() + } +} + +createUsersTable().catch(console.error) \ No newline at end of file diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx index c5ce362..4d2bf59 100644 --- a/src/app/(marketing)/layout.tsx +++ b/src/app/(marketing)/layout.tsx @@ -5,6 +5,7 @@ import { sampleData } from '@/config/sample-data' const navigation = [ { name: 'Menuer', href: '/menuer' }, { name: 'Pakker', href: '/pakker' }, + { name: 'Om Christian', href: '/om-christian' }, { name: 'Book', href: '/book' }, { name: 'Kontakt', href: '/kontakt' }, ] diff --git a/src/app/admin/availability/page.tsx b/src/app/admin/availability/page.tsx index bdc4a71..316cfcb 100644 --- a/src/app/admin/availability/page.tsx +++ b/src/app/admin/availability/page.tsx @@ -3,6 +3,15 @@ import { useEffect, useState } from 'react' import { DayPicker } from 'react-day-picker' import { da } from 'date-fns/locale' + +// Custom locale with correct week start +const dkLocale = { + ...da, + options: { + ...da.options, + weekStartsOn: 1 as const // Monday + } +} import { ToastProvider, useToast } from '@/components/ui/toast' type Blocked = { id: string; block_date: string; note?: string } @@ -160,12 +169,16 @@ function AvailabilityAdminContent() {
- + { if (date && !saving) { console.log('Clicking on date:', date) @@ -180,6 +193,7 @@ function AvailabilityAdminContent() { }} disabled={saving} /> +
diff --git a/src/app/admin/bookings/bookings-client.tsx b/src/app/admin/bookings/bookings-client.tsx index 5a218b6..75b5671 100644 --- a/src/app/admin/bookings/bookings-client.tsx +++ b/src/app/admin/bookings/bookings-client.tsx @@ -7,12 +7,16 @@ type Booking = { name: string email: string phone?: string + address?: string party_size?: number booking_date: string start_time: string end_time: string menu_id?: string package_id?: string + include_service?: boolean + include_cleanup?: boolean + total_price?: number message?: string status?: string } @@ -23,12 +27,15 @@ export default function BookingsClient() { const [cancelling, setCancelling] = useState(null) async function fetchBookings() { + console.log('=== Fetching bookings ===') try { const r = await fetch('/api/bookings') + console.log('Response status:', r.status) const data = await r.ok ? await r.json() : [] + console.log('✓ Bookings loaded:', Array.isArray(data) ? data.length : 0) setBookings(Array.isArray(data) ? data : []) } catch (err) { - console.error(err) + console.error('❌ Error fetching bookings:', err) } finally { setLoading(false) } @@ -39,23 +46,32 @@ export default function BookingsClient() { }, []) async function cancelBooking(id: string, name: string) { - if (!confirm(`Annuller booking for ${name}?\n\nDette kan ikke fortrydes.`)) return + console.log('=== Cancel booking ===', { id, name }) + if (!confirm(`Annuller booking for ${name}?\n\nDette kan ikke fortrydes.`)) { + console.log('Cancelled by user') + return + } setCancelling(id) try { + console.log('Sending DELETE request for booking:', id) const res = await fetch('/api/bookings', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }) + console.log('Response status:', res.status) if (res.ok) { + console.log('✓ Booking cancelled successfully') setBookings(prev => prev.filter(b => b.id !== id)) alert(`Booking for ${name} er annulleret`) } else { + console.log('❌ Failed to cancel booking') alert('Kunne ikke annullere booking — prøv igen') } } catch (err) { + console.error('❌ Exception cancelling booking:', err) alert('Fejl ved annullering — prøv igen') } finally { setCancelling(null) @@ -65,33 +81,98 @@ export default function BookingsClient() { if (loading) return
Indlæser bookinger...
if (bookings.length === 0) return
Ingen bookinger fundet.
+ // Calculate total revenue and profit + const totalRevenue = bookings.reduce((sum, b) => sum + (b.total_price || 0), 0) + // Rough estimate: 40% profit margin + const estimatedProfit = totalRevenue * 0.4 + return ( -
+
+ {/* Financial summary */} +
+
+
Total bookinger
+
{bookings.length}
+
+
+
Total omsætning
+
{totalRevenue.toLocaleString('da-DK')} kr
+
+
+
Estimeret fortjeneste (40%)
+
{estimatedProfit.toLocaleString('da-DK')} kr
+
+
+ + {/* Bookings table */} +
- - - - - - - - + + + + + + + + + + + {bookings.map(b => ( - - - - - - - + + + + + + + +
DatoTidNavnKuverterMenuNoteHandling
Ordre nr.DatoTidNavnKuverterMenuTilvalgTotal prisNoteHandling
{b.booking_date}{b.start_time.slice(0,5)} - {b.end_time.slice(0,5)} -
{b.name}
-
{b.email}{b.phone ? ` · ${b.phone}` : ''}
+
+
+ {b.id.substring(0, 8).toUpperCase()} +
{b.party_size || '-'}{b.menu_id || b.package_id || '-'}{b.message || '-'} + {b.booking_date}{b.start_time.slice(0,5)} - {b.end_time.slice(0,5)} +
{b.name}
+
{b.email}
+ {b.phone &&
{b.phone}
} + {b.address &&
{b.address}
} +
{b.party_size || '-'} +
{b.menu_id || '-'}
+ {b.package_id &&
{b.package_id}
} +
+
+ {b.include_service && ( +
+ + Servering +
+ )} + {b.include_cleanup && ( +
+ + Oprydning +
+ )} + {!b.include_service && !b.include_cleanup && -} +
+
+ {b.total_price ? ( +
+
{b.total_price.toLocaleString('da-DK')} kr
+
+ Fortjeneste: ~{(b.total_price * 0.4).toLocaleString('da-DK')} kr +
+
+ ) : '-'} +
+
{b.message || '-'}
+
+
) } diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index bf17019..9995d98 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,4 +1,5 @@ import Link from 'next/link' +import { LogoutButton } from '@/components/admin/logout-button' export const metadata = { title: 'Admin - varme.dk' @@ -15,6 +16,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) Menuer Tilgængelighed Bookinger + diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..260213d --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,123 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createConnection } from 'mysql2/promise' +import bcrypt from 'bcrypt' +import jwt from 'jsonwebtoken' + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this' + +export async function POST(request: NextRequest) { + console.log('=== API LOGIN REQUEST START ===') + console.log('Timestamp:', new Date().toISOString()) + + try { + const body = await request.json() + console.log('Request body received:', { username: body.username, passwordLength: body.password?.length }) + + const { username, password } = body + + if (!username || !password) { + console.log('ERROR: Missing username or password') + return NextResponse.json( + { error: 'Brugernavn og adgangskode er påkrævet' }, + { status: 400 } + ) + } + + console.log('Connecting to database...') + // Connect to database + const connection = await createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }) + console.log('Database connected') + + try { + // Get user from database + console.log('Querying user from database:', username) + const [rows] = await connection.execute( + 'SELECT * FROM users WHERE username = ?', + [username] + ) + + const users = rows as any[] + console.log('Query result: found', users.length, 'users') + + if (users.length === 0) { + console.log('ERROR: User not found:', username) + return NextResponse.json( + { error: 'Ugyldig brugernavn eller adgangskode' }, + { status: 401 } + ) + } + + const user = users[0] + console.log('User found:', { id: user.id, username: user.username, role: user.role }) + + // Verify password with salt + console.log('Verifying password with salt...') + const saltedPassword = password + user.salt + const isValid = await bcrypt.compare(saltedPassword, user.password_hash) + console.log('Password valid:', isValid) + + if (!isValid) { + console.log('ERROR: Invalid password for user:', username) + return NextResponse.json( + { error: 'Ugyldig brugernavn eller adgangskode' }, + { status: 401 } + ) + } + + // Create JWT token + console.log('Creating JWT token...') + const token = jwt.sign( + { + userId: user.id, + username: user.username, + role: user.role + }, + JWT_SECRET, + { expiresIn: '24h' } + ) + console.log('JWT token created, length:', token.length) + + // Set HTTP-only cookie + const response = NextResponse.json({ + success: true, + token, + user: { + id: user.id, + username: user.username, + role: user.role + } + }) + + console.log('Setting authToken cookie...') + response.cookies.set('authToken', token, { + httpOnly: true, + secure: false, // Allow on HTTP for localhost + sameSite: 'lax', // Less strict for development + maxAge: 24 * 60 * 60 // 24 hours + }) + console.log('Cookie set successfully') + + console.log('=== API LOGIN SUCCESS ===') + return response + + } finally { + await connection.end() + console.log('Database connection closed') + } + + } catch (error) { + console.error('=== API LOGIN EXCEPTION ===') + console.error('Error:', error) + console.error('Error type:', typeof error) + console.error('Error stack:', error instanceof Error ? error.stack : 'N/A') + return NextResponse.json( + { error: 'Der skete en server fejl' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..c49e392 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(request: NextRequest) { + // Clear auth cookie + const response = NextResponse.json({ success: true }) + + response.cookies.set('authToken', '', { + httpOnly: true, + secure: false, // Allow on HTTP for localhost + sameSite: 'lax', + maxAge: 0 // Expire immediately + }) + + return response +} \ No newline at end of file diff --git a/src/app/api/availability/blocked/route.ts b/src/app/api/availability/blocked/route.ts index 290b158..d66afa5 100644 --- a/src/app/api/availability/blocked/route.ts +++ b/src/app/api/availability/blocked/route.ts @@ -3,44 +3,53 @@ import { pool } from '@/lib/db' import crypto from 'crypto' export async function GET() { + console.log('=== API: GET /api/availability/blocked ===') try { const [rows] = await pool.query('SELECT * FROM blocked_dates ORDER BY block_date') + console.log('✓ Blocked dates fetched:', Array.isArray(rows) ? rows.length : 0, 'dates') return NextResponse.json(rows) } catch (error) { - console.error('Error fetching blocked dates', error) + console.error('❌ Error fetching blocked dates:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } export async function POST(req: Request) { + console.log('=== API: POST /api/availability/blocked ===') try { - const body = await req.json() - const { block_date, note } = body - if (!block_date) return NextResponse.json({ error: 'Missing block_date' }, { status: 400 }) - // Prevent duplicate blocked dates - const [existing] = await pool.query('SELECT COUNT(*) AS cnt FROM blocked_dates WHERE block_date = ?', [block_date]) - // @ts-ignore - if (existing[0].cnt > 0) { - return NextResponse.json({ error: 'Date already blocked' }, { status: 409 }) + const { date } = await req.json() + console.log('Request body:', { date }) + + if (!date) { + console.log('❌ Missing date in request') + return NextResponse.json({ error: 'Missing date' }, { status: 400 }) } - const id = crypto.randomUUID() - await pool.query('INSERT INTO blocked_dates SET ?', [{ id, block_date, note }]) - return NextResponse.json({ id, block_date, note }) + await pool.query('INSERT INTO blocked_dates (block_date) VALUES (?) ON DUPLICATE KEY UPDATE block_date=block_date', [date]) + console.log('✓ Date blocked successfully:', date) + return NextResponse.json({ ok: true }) } catch (error) { - console.error('Error creating blocked date', error) + console.error('❌ Error blocking date:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } export async function DELETE(req: Request) { + console.log('=== API: DELETE /api/availability/blocked ===') try { - const { id } = await req.json() - if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 }) - await pool.query('DELETE FROM blocked_dates WHERE id = ?', [id]) + const { date } = await req.json() + console.log('Request body:', { date }) + + if (!date) { + console.log('❌ Missing date in request') + return NextResponse.json({ error: 'Missing date' }, { status: 400 }) + } + + const [result] = await pool.query('DELETE FROM blocked_dates WHERE block_date = ?', [date]) + console.log('✓ Date unblocked successfully:', date, 'Affected rows:', (result as any).affectedRows) return NextResponse.json({ ok: true }) } catch (error) { - console.error('Error deleting blocked date', error) + console.error('❌ Error unblocking date:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } diff --git a/src/app/api/availability/weekly/route.ts b/src/app/api/availability/weekly/route.ts index 768aa68..6e2c8a3 100644 --- a/src/app/api/availability/weekly/route.ts +++ b/src/app/api/availability/weekly/route.ts @@ -3,70 +3,98 @@ import { pool } from '@/lib/db' import crypto from 'crypto' export async function GET() { + console.log('=== API: GET /api/availability/weekly ===') try { const [rows] = await pool.query('SELECT * FROM weekly_availability ORDER BY weekday, start_time') + console.log('✓ Weekly availability fetched:', Array.isArray(rows) ? rows.length : 0, 'slots') return NextResponse.json(rows) } catch (error) { - console.error('Error fetching weekly availability', error) + console.error('❌ Error fetching weekly availability:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } export async function POST(req: Request) { + console.log('=== API: POST /api/availability/weekly ===') try { const body = await req.json() const { weekday, start_time, end_time, note } = body + console.log('Request body:', { weekday, start_time, end_time, note }) + if (weekday === undefined || !start_time || !end_time) { + console.log('❌ Missing required fields') return NextResponse.json({ error: 'Missing fields' }, { status: 400 }) } // Prevent overlapping availability for the same weekday const [confRows] = await pool.query('SELECT COUNT(*) AS cnt FROM weekly_availability WHERE weekday = ? AND NOT (end_time <= ? OR start_time >= ?)', [weekday, start_time, end_time]) // @ts-ignore - if (confRows[0].cnt > 0) { + const overlaps = confRows[0].cnt + console.log('Overlap check:', { overlaps }) + + if (overlaps > 0) { + console.log('❌ Overlapping availability exists') return NextResponse.json({ error: 'Overlapping availability exists for this weekday' }, { status: 409 }) } const id = crypto.randomUUID() await pool.query('INSERT INTO weekly_availability SET ?', [{ id, weekday, start_time, end_time, note }]) + console.log('✓ Weekly availability created:', id) return NextResponse.json({ ok: true, id }) } catch (error) { - console.error('Error creating weekly availability', error) + console.error('❌ Error creating weekly availability:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } export async function PUT(req: Request) { + console.log('=== API: PUT /api/availability/weekly ===') try { const body = await req.json() const { id, weekday, start_time, end_time, note } = body + console.log('Request body:', { id, weekday, start_time, end_time, note }) + if (!id || weekday === undefined || !start_time || !end_time) { + console.log('❌ Missing required fields') return NextResponse.json({ error: 'Missing fields' }, { status: 400 }) } // Check overlaps excluding the current id const [confRows] = await pool.query('SELECT COUNT(*) AS cnt FROM weekly_availability WHERE weekday = ? AND id != ? AND NOT (end_time <= ? OR start_time >= ?)', [weekday, id, start_time, end_time]) // @ts-ignore - if (confRows[0].cnt > 0) { + const overlaps = confRows[0].cnt + console.log('Overlap check (excluding current):', { overlaps }) + + if (overlaps > 0) { + console.log('❌ Overlapping availability exists') return NextResponse.json({ error: 'Overlapping availability exists for this weekday' }, { status: 409 }) } - await pool.query('UPDATE weekly_availability SET weekday = ?, start_time = ?, end_time = ?, note = ? WHERE id = ?', [weekday, start_time, end_time, note || null, id]) + const [result] = await pool.query('UPDATE weekly_availability SET weekday = ?, start_time = ?, end_time = ?, note = ? WHERE id = ?', [weekday, start_time, end_time, note || null, id]) + console.log('✓ Weekly availability updated:', id, 'Affected rows:', (result as any).affectedRows) return NextResponse.json({ ok: true }) } catch (error) { - console.error('Error updating weekly availability', error) + console.error('❌ Error updating weekly availability:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } export async function DELETE(req: Request) { + console.log('=== API: DELETE /api/availability/weekly ===') try { const { id } = await req.json() - if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 }) - await pool.query('DELETE FROM weekly_availability WHERE id = ?', [id]) + console.log('Request body:', { id }) + + if (!id) { + console.log('❌ Missing id') + return NextResponse.json({ error: 'Missing id' }, { status: 400 }) + } + + const [result] = await pool.query('DELETE FROM weekly_availability WHERE id = ?', [id]) + console.log('✓ Weekly availability deleted:', id, 'Affected rows:', (result as any).affectedRows) return NextResponse.json({ ok: true }) } catch (error) { - console.error('Error deleting weekly availability', error) + console.error('❌ Error deleting weekly availability:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } diff --git a/src/app/api/bookings/route.ts b/src/app/api/bookings/route.ts index c211239..bedc483 100644 --- a/src/app/api/bookings/route.ts +++ b/src/app/api/bookings/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server' import { pool } from '@/lib/db' import crypto from 'crypto' +import { sampleData } from '@/config/sample-data' function normalizeTime(t: string) { // Accept HH:MM or HH:MM:SS and normalize to HH:MM:SS @@ -10,17 +11,22 @@ function normalizeTime(t: string) { } export async function GET(req: Request) { + console.log('=== API: GET /api/bookings ===') try { const url = new URL(req.url) const date = url.searchParams.get('date') + console.log('Query params:', { date }) + if (date) { const [rows] = await pool.query('SELECT * FROM bookings WHERE booking_date = ? ORDER BY start_time', [date]) + console.log('✓ Bookings fetched for date:', date, 'Count:', Array.isArray(rows) ? rows.length : 0) return NextResponse.json(rows) } const [rows] = await pool.query('SELECT * FROM bookings ORDER BY booking_date DESC, start_time') + console.log('✓ All bookings fetched. Count:', Array.isArray(rows) ? rows.length : 0) return NextResponse.json(rows) } catch (error) { - console.error('Error fetching bookings', error) + console.error('❌ Error fetching bookings:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } @@ -28,10 +34,68 @@ export async function GET(req: Request) { export async function POST(req: Request) { try { const body = await req.json() - const { name, email, phone, party_size, date, start_time, duration_hours, menu_id, package_id, message } = body + console.log('=== BOOKING REQUEST ===') + console.log('Request body:', JSON.stringify(body, null, 2)) + + // Bot protection: check timestamp (minimum 3 seconds since form load) + if (body._t) { + const timeSinceLoad = Date.now() - body._t + if (timeSinceLoad < 3000) { + console.log('🤖 Bot detected: too fast submission', timeSinceLoad, 'ms') + return NextResponse.json({ error: 'Please wait a moment before submitting' }, { status: 429 }) + } + if (timeSinceLoad > 3600000) { // 1 hour + console.log('⏰ Form expired: submission too slow', timeSinceLoad, 'ms') + return NextResponse.json({ error: 'Form expired, please refresh and try again' }, { status: 400 }) + } + } + + const { name, email, phone, address, party_size, date, start_time, duration_hours, menu_id, package_id, include_service, include_cleanup, message } = body if (!name || !email || !date || !start_time) { + console.log('ERROR: Missing required fields') return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }) } + + // Basic spam detection + const fullText = `${name} ${email} ${message || ''}`.toLowerCase() + const suspiciousPatterns = [ + /viagra/i, + /cialis/i, + /casino/i, + /lottery/i, + /(https?:\/\/[^\s]+){2,}/gi, // Multiple URLs + ] + + for (const pattern of suspiciousPatterns) { + if (pattern.test(fullText)) { + console.log('🤖 Spam pattern detected:', pattern) + return NextResponse.json({ error: 'Suspicious content detected' }, { status: 400 }) + } + } + + // Calculate price + let totalPrice = 0 + const partyCovers = Number(party_size) || 0 + + // Find menu price + const menu = sampleData.menus.find(m => m.id === menu_id) + if (menu && partyCovers > 0) { + totalPrice = menu.basePricePerCover * partyCovers + } + + // Add service cost (100 kr per cover) + if (include_service && partyCovers > 0) { + totalPrice += 100 * partyCovers + } + + // Add cleanup cost (500 kr flat) + if (include_cleanup) { + totalPrice += 500 + } + + // Add reservation fee (500 kr flat) + const reservationFee = 500 + totalPrice += reservationFee const start = normalizeTime(start_time) const dur = Number(duration_hours) || 3 @@ -42,34 +106,69 @@ export async function POST(req: Request) { const endH = String(Math.floor(endMinutes / 60)).padStart(2, '0') const endM = String(endMinutes % 60).padStart(2, '0') const end = `${endH}:${endM}:00` + + console.log('Booking time:', { date, start, end, duration: dur }) // 1) Check blocked dates const [blocked] = await pool.query('SELECT COUNT(*) AS cnt FROM blocked_dates WHERE block_date = ?', [date]) // @ts-ignore - if (blocked[0].cnt > 0) return NextResponse.json({ error: 'Date is blocked' }, { status: 409 }) + console.log('Blocked dates check:', blocked[0]) + if (blocked[0].cnt > 0) { + console.log('ERROR: Date is blocked') + return NextResponse.json({ error: 'Datoen er blokeret' }, { status: 409 }) + } // 2) Check there is a weekly availability that fully covers the requested interval const bookingDate = new Date(date) const weekday = bookingDate.getDay() + console.log('Checking availability for weekday:', weekday) + const [availRows] = await pool.query('SELECT * FROM weekly_availability WHERE weekday = ?', [weekday]) // @ts-ignore const availArr = Array.isArray(availRows) ? availRows : [] + console.log('Available slots for weekday:', availArr) + const covers = availArr.some((a: any) => (a.start_time <= start && a.end_time >= end)) + console.log('Time slot covered by availability:', covers) + if (!covers) { - return NextResponse.json({ error: 'Requested time is outside of availability' }, { status: 409 }) + console.log('ERROR: Requested time is outside of availability') + return NextResponse.json({ error: 'Det valgte tidspunkt er udenfor åbningstid' }, { status: 409 }) } // 3) Check for overlapping bookings on same date const [overlapRows] = await pool.query('SELECT COUNT(*) AS cnt FROM bookings WHERE booking_date = ? AND NOT (end_time <= ? OR start_time >= ?)', [date, start, end]) // @ts-ignore + console.log('Overlapping bookings check:', overlapRows[0]) if (overlapRows[0].cnt > 0) { - return NextResponse.json({ error: 'Time slot already booked' }, { status: 409 }) + console.log('ERROR: Time slot already booked') + return NextResponse.json({ error: 'Tidspunktet er allerede booket' }, { status: 409 }) } const id = crypto.randomUUID() - await pool.query('INSERT INTO bookings SET ?', [{ id, name, email, phone: phone || null, party_size: party_size || null, booking_date: date, start_time: start, end_time: end, menu_id: menu_id || null, package_id: package_id || null, message: message || null }]) + await pool.query('INSERT INTO bookings SET ?', [{ + id, + name, + email, + phone: phone || null, + address: address || null, + party_size: party_size || null, + booking_date: date, + start_time: start, + end_time: end, + menu_id: menu_id || null, + package_id: package_id || null, + include_service: include_service || false, + include_cleanup: include_cleanup || false, + total_price: totalPrice > 0 ? totalPrice : null, + message: message || null + }]) - return NextResponse.json({ ok: true, id }) + console.log('=== BOOKING SUCCESS ===') + console.log('Booking ID:', id) + console.log('Total price:', totalPrice) + + return NextResponse.json({ ok: true, id, total_price: totalPrice }) } catch (error) { console.error('Error creating booking', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) @@ -77,22 +176,31 @@ export async function POST(req: Request) { } export async function DELETE(req: Request) { + console.log('=== API: DELETE /api/bookings ===') try { const body = await req.json() const { id } = body + console.log('Request body:', { id }) + if (!id) { + console.log('❌ Missing booking id') return NextResponse.json({ error: 'Missing booking id' }, { status: 400 }) } const [result] = await pool.query('DELETE FROM bookings WHERE id = ?', [id]) // @ts-ignore - if (result.affectedRows === 0) { + const affectedRows = result.affectedRows + console.log('Delete result - Affected rows:', affectedRows) + + if (affectedRows === 0) { + console.log('❌ Booking not found:', id) return NextResponse.json({ error: 'Booking not found' }, { status: 404 }) } + console.log('✓ Booking deleted successfully:', id) return NextResponse.json({ ok: true }) } catch (error) { - console.error('Error deleting booking', error) + console.error('❌ Error deleting booking:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts index d1b07e9..2c32da8 100644 --- a/src/app/api/contact/route.ts +++ b/src/app/api/contact/route.ts @@ -6,14 +6,60 @@ function validate(body: any) { if (!body) return 'No data' const { name, email, phone, message } = body if (!name || !email || !message) return 'Missing required fields' + + // Basic spam detection + const suspiciousPatterns = [ + /viagra/i, + /cialis/i, + /casino/i, + /lottery/i, + /crypto/i, + /bitcoin/i, + /(https?:\/\/[^\s]+){3,}/gi, // Multiple URLs + ] + + const fullText = `${name} ${email} ${message}`.toLowerCase() + for (const pattern of suspiciousPatterns) { + if (pattern.test(fullText)) { + console.log('🤖 Spam pattern detected:', pattern) + return 'Suspicious content detected' + } + } + return null } export async function POST(req: Request) { + console.log('=== API: POST /api/contact ===') try { const body = await req.json() + console.log('Contact form submission:', { + name: body.name, + email: body.email, + hasPhone: !!body.phone, + hasAddress: !!body.address, + messageLength: body.message?.length, + hasTimestamp: !!body._t + }) + + // Bot protection: check timestamp (minimum 2 seconds since form load) + if (body._t) { + const timeSinceLoad = Date.now() - body._t + if (timeSinceLoad < 2000) { + console.log('🤖 Bot detected: too fast submission', timeSinceLoad, 'ms') + return NextResponse.json({ error: 'Please wait a moment before submitting' }, { status: 429 }) + } + if (timeSinceLoad > 3600000) { // 1 hour + console.log('⏰ Form expired: submission too slow', timeSinceLoad, 'ms') + return NextResponse.json({ error: 'Form expired, please refresh and try again' }, { status: 400 }) + } + } + const err = validate(body) - if (err) return NextResponse.json({ error: err }, { status: 400 }) + if (err) { + console.log('❌ Validation error:', err) + return NextResponse.json({ error: err }, { status: 400 }) + } // Read SMTP config from environment const host = process.env.SMTP_HOST || 'smtp.office365.com' @@ -21,9 +67,11 @@ export async function POST(req: Request) { const user = process.env.SMTP_USER const pass = process.env.SMTP_PASS const to = process.env.BOOKING_EMAIL || 'christian@warme.dk' + + console.log('SMTP config:', { host, port, user: user ? '***' : 'not set', to }) if (!user || !pass) { - console.error('SMTP credentials not configured') + console.error('❌ SMTP credentials not configured') return NextResponse.json({ error: 'Email service not configured' }, { status: 500 }) } @@ -54,6 +102,7 @@ export async function POST(req: Request) { // as the From, and set Reply-To to the form submitter so replies go to them. const senderEmail = process.env.SENDER_EMAIL || user + console.log('Sending email...', { from: senderEmail, to, subject }) await transporter.sendMail({ from: senderEmail, replyTo: `${name} <${email}>`, @@ -63,9 +112,10 @@ export async function POST(req: Request) { text: `${name} (${email})\n\n${message}` }) + console.log('✓ Contact email sent successfully') return NextResponse.json({ ok: true }) } catch (error) { - console.error('Error in contact POST:', error) + console.error('❌ Error in contact POST:', error) return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } diff --git a/src/app/api/menus/route.ts b/src/app/api/menus/route.ts index 3e33d96..15868f3 100644 --- a/src/app/api/menus/route.ts +++ b/src/app/api/menus/route.ts @@ -3,53 +3,66 @@ import { pool, rowToMenu, menuToRow } from '@/lib/db'; import { Menu } from '@/types/models'; export async function GET() { + console.log('=== API: GET /api/menus ===') try { const [rows] = await pool.query('SELECT * FROM menus ORDER BY created_at DESC'); const menus = (rows as any[]).map(row => rowToMenu(row)); + console.log('✓ Menus fetched:', menus.length) return NextResponse.json(menus); } catch (error) { - console.error('Error fetching menus:', error); + console.error('❌ Error fetching menus:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } export async function POST(req: Request) { + console.log('=== API: POST /api/menus ===') try { const menu = await req.json(); + console.log('Request body:', { id: menu.id, name: menu.name }) + const row = menuToRow(menu); await pool.query('INSERT INTO menus SET ?', row); + console.log('✓ Menu created successfully:', menu.id) return NextResponse.json({ message: 'Menu created successfully' }); } catch (error) { - console.error('Error creating menu:', error); + console.error('❌ Error creating menu:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } export async function PUT(req: Request) { + console.log('=== API: PUT /api/menus ===') try { const menu = await req.json(); + console.log('Request body:', { id: menu.id, name: menu.name }) + const row = menuToRow(menu); - await pool.query('UPDATE menus SET ? WHERE id = ?', [row, menu.id]); + const [result] = await pool.query('UPDATE menus SET ? WHERE id = ?', [row, menu.id]); + console.log('✓ Menu updated successfully:', menu.id, 'Affected rows:', (result as any).affectedRows) return NextResponse.json({ message: 'Menu updated successfully' }); } catch (error) { - console.error('Error updating menu:', error); + console.error('❌ Error updating menu:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } export async function DELETE(req: Request) { + console.log('=== API: DELETE /api/menus ===') try { const { id } = await req.json(); + console.log('Request body:', { id }) - await pool.query('DELETE FROM menus WHERE id = ?', [id]); + const [result] = await pool.query('DELETE FROM menus WHERE id = ?', [id]); + console.log('✓ Menu deleted successfully:', id, 'Affected rows:', (result as any).affectedRows) return NextResponse.json({ message: 'Menu deleted successfully' }); } catch (error) { - console.error('Error deleting menu:', error); + console.error('❌ Error deleting menu:', error); return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } \ No newline at end of file diff --git a/src/app/book/page.tsx b/src/app/book/page.tsx index 9e0e379..7cae559 100644 --- a/src/app/book/page.tsx +++ b/src/app/book/page.tsx @@ -54,12 +54,28 @@ export default function BookingPage() { const [name, setName] = useState('') const [email, setEmail] = useState('') const [phone, setPhone] = useState('') + const [address, setAddress] = useState('') const [partySize, setPartySize] = useState(undefined) const [message, setMessage] = useState('') + const [includeService, setIncludeService] = useState(false) + const [includeCleanup, setIncludeCleanup] = useState(false) const [submitting, setSubmitting] = useState(false) const [statusMsg, setStatusMsg] = useState(null) + const [bookingSuccess, setBookingSuccess] = useState<{ + id: string + date: string + time: string + menuName: string + covers: number + totalPrice: number + reservationFee: number + } | null>(null) const [selectedMenu, setSelectedMenu] = useState(null) const [selectedPackage, setSelectedPackage] = useState(null) + + // Bot protection + const [honeypot, setHoneypot] = useState('') + const [formLoadTime, setFormLoadTime] = useState(0) // Handle URL parameters useEffect(() => { @@ -75,6 +91,10 @@ export default function BookingPage() { } } + if (locationParam) { + setAddress(locationParam) + } + if (guestsParam) { const guests = parseInt(guestsParam) if (!isNaN(guests)) { @@ -84,17 +104,24 @@ export default function BookingPage() { }, []) useEffect(() => { + console.log('=== Loading availability data ===') let mounted = true + + // Set form load time for bot protection + setFormLoadTime(Date.now()) + Promise.all([ fetch('/api/availability/weekly').then(r => r.ok ? r.json() : []), fetch('/api/availability/blocked').then(r => r.ok ? r.json() : []) ]).then(([w, b]) => { if (!mounted) return + console.log('Weekly availability loaded:', Array.isArray(w) ? w.length : 0, 'slots') + console.log('Blocked dates loaded:', Array.isArray(b) ? b.length : 0, 'dates') setWeekly(Array.isArray(w) ? w : []) setBlockedDates(Array.isArray(b) ? b.map((x: any) => x.block_date) : []) setLoadingAvail(false) }).catch(err => { - console.error('Error loading availability', err) + console.error('❌ Error loading availability:', err) setLoadingAvail(false) }) return () => { mounted = false } @@ -130,21 +157,105 @@ export default function BookingPage() { return false } - // update available slots when date or availability or duration changes + // Update time slots whenever date or duration changes useEffect(() => { if (!selectedDate) { setSlots([]) setSelectedSlot(null) return } - const s = generateSlotsForDate(weekly, selectedDate, durationHours) - setSlots(s) - setSelectedSlot(s.length > 0 ? s[0] : null) - }, [selectedDate, weekly, durationHours]) - + console.log('=== Generating time slots ===') + console.log('Date:', selectedDate.toISOString().split('T')[0], 'Duration:', durationHours, 'hours') + const newSlots = generateSlotsForDate(weekly, selectedDate, durationHours) + console.log('Available slots:', newSlots.length, newSlots) + setSlots(newSlots) + setSelectedSlot(null) + }, [selectedDate, durationHours, weekly]) + return (
+ {/* Success message */} + {bookingSuccess ? ( +
+
+
+ + + +
+

Tak for din forespørgsel!

+

Vi kontakter dig hurtigst muligt for at bekræfte din booking

+
+ +
+
Ordre nummer
+
{bookingSuccess.id.substring(0, 8).toUpperCase()}
+
+
Dato: {bookingSuccess.date}
+
Tidspunkt: {bookingSuccess.time}
+
Menu: {bookingSuccess.menuName}
+
Kuverter: {bookingSuccess.covers}
+
+
+ + {/* Price breakdown on confirmation */} +
+

Pris oversigt

+
+
+ Total pris + {bookingSuccess.totalPrice.toLocaleString('da-DK')} kr +
+
+ Reservationsgebyr (betales nu) + {bookingSuccess.reservationFee.toLocaleString('da-DK')} kr +
+
+
+ Resterende betaling + {(bookingSuccess.totalPrice - bookingSuccess.reservationFee).toLocaleString('da-DK')} kr +
+
+
+
+ Reservationsgebyret på {bookingSuccess.reservationFee} kr betales ved bekræftelse og trækkes fra den endelige pris. +
+
+ +

+ Du vil modtage en bekræftelse på email med betalingsinformation. Gem dit ordre nummer til reference. +

+ +
+ + + Tilbage til forsiden + +
+
+ ) : ( + <>

Forespørg booking

@@ -160,6 +271,8 @@ export default function BookingPage() { type="text" placeholder="By eller postnummer" className="w-full" + value={address} + onChange={(e) => setAddress(e.target.value)} />
@@ -262,9 +375,31 @@ export default function BookingPage() { onChange={() => setSelectedMenu(menu.id)} className="mt-1" /> -
-
{menu.title}
-
{menu.description}
+
+
{menu.title}
+
{menu.description}
+ + {/* Full course list */} +
+
Menuen indeholder:
+
    + {menu.courses.map((course, courseIdx) => ( +
  • + + {course} +
  • + ))} +
+
+ +
+ {menu.tags.map((tag, tagIdx) => ( + + {tag} + + ))} +
+
{menu.basePricePerCover} kr pr. kuvert
@@ -288,9 +423,29 @@ export default function BookingPage() { onChange={() => setSelectedPackage(pkg.id)} className="mt-1" /> -
-
{pkg.name}
-
{pkg.notes}
+
+
{pkg.name}
+
{pkg.notes}
+ + {/* Package includes */} +
+
Pakken inkluderer:
+
    + {pkg.includes.map((item, itemIdx) => ( +
  • + + {item} +
  • + ))} +
+
+ +
+ {pkg.pricePerCover} kr pr. kuvert +
+
+ Min. {pkg.minCovers} personer • Maks. {pkg.maxCovers} personer +
))} @@ -332,21 +487,162 @@ export default function BookingPage() { className="w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm" >
+ + {/* Honeypot field - hidden from users, but visible to bots */} +
+ {/* Step 4: Additional Services */} +
+

Tilvalg

+
+ + + +
+
+ + {/* Price Summary */} + {selectedMenu && partySize && ( +
+

Pris oversigt

+
+ {(() => { + const menu = sampleData.menus.find((m: Menu) => m.id === selectedMenu) + const menuPrice = menu ? menu.basePricePerCover : 0 + const covers = partySize || 0 + const menuTotal = menuPrice * covers + const serviceTotal = includeService ? 100 * covers : 0 + const cleanupTotal = includeCleanup ? 500 : 0 + const reservationFee = 500 + const subtotal = menuTotal + serviceTotal + cleanupTotal + const total = subtotal + reservationFee + + return ( + <> +
+ + {menu?.title || 'Menu'} ({covers} kuverter × {menuPrice} kr) + + {menuTotal.toLocaleString('da-DK')} kr +
+ + {includeService && ( +
+ + Professionel servering ({covers} kuverter × 100 kr) + + {serviceTotal.toLocaleString('da-DK')} kr +
+ )} + + {includeCleanup && ( +
+ Fuld oprydning + 500 kr +
+ )} + +
+
+ Subtotal + {subtotal.toLocaleString('da-DK')} kr +
+
+ +
+ Reservationsgebyr + 500 kr +
+ +
+
+ Total + {total.toLocaleString('da-DK')} kr +
+
+ +
+ Bemærk: Reservationsgebyret på 500 kr betales ved booking og trækkes fra den endelige pris. +
+ + ) + })()} +
+
+ )} +
- {statusMsg &&

{statusMsg}

} + {statusMsg && ( +
+

{statusMsg}

+
+ )}
+ + )}
) diff --git a/src/app/globals.css b/src/app/globals.css index 2f7fca5..bc8332f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -218,87 +218,59 @@ body { box-shadow: none !important; } -/* Cook calendar: large, simple interface for easy use */ +/* Cook calendar: simple colors only */ .cook-calendar { width: 100%; - font-size: 1.1rem; } -.cook-calendar .rdp { - width: 100%; - margin: 0 auto; +/* Default day styling - green for available */ +.cook-calendar .rdp-day { + background: #f0fdf4 !important; /* green-50 */ + color: #15803d !important; /* green-700 */ + border: 2px solid #bbf7d0 !important; /* green-200 */ + font-weight: 600 !important; } -.cook-calendar .rdp-table { - width: 100%; - table-layout: fixed; +.cook-calendar .rdp-day:hover { + background: #dcfce7 !important; /* green-100 */ + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } -.cook-calendar .rdp-day, -.cook-calendar .rdp-day_disabled { - width: 100%; - min-height: 80px; - padding: 15px; - font-size: 1.2rem; - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; - border-radius: 12px; - transition: all 0.2s ease; - cursor: pointer !important; - background: #f0fdf4; /* green-50 */ - color: #15803d; /* green-700 */ - border: 2px solid #bbf7d0; /* green-200 */ - user-select: none; -} - -.cook-calendar .rdp-day:hover:not(.rdp-day_disabled) { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.cook-calendar .rdp-day_disabled { - opacity: 0.3; - cursor: not-allowed; - transform: none !important; +/* Today styling - override default today color */ +.cook-calendar .rdp-day_today { + background: #f0fdf4 !important; /* green-50 same as default */ + color: #15803d !important; /* green-700 */ + border: 2px solid #bbf7d0 !important; /* green-200 */ } +/* Blocked days - red (must come after today to override) */ .cook-calendar .day-blocked, .cook-calendar .rdp-day_blocked { background: #dc2626 !important; /* red-600 */ color: white !important; border-color: #991b1b !important; /* red-800 */ - cursor: pointer !important; } -.cook-calendar .rdp-caption { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 1rem; +/* Blocked today - also red */ +.cook-calendar .rdp-day_today.day-blocked, +.cook-calendar .rdp-day_today.rdp-day_blocked { + background: #dc2626 !important; /* red-600 */ + color: white !important; + border-color: #991b1b !important; /* red-800 */ } -.cook-calendar .rdp-weekday { - font-size: 1rem; - font-weight: 600; - color: #374151; /* gray-700 */ - padding: 10px 0; +.cook-calendar .day-blocked:hover, +.cook-calendar .rdp-day_blocked:hover { + background: #b91c1c !important; /* red-700 */ } -@media (min-width: 640px) { - .cook-calendar .rdp-day, - .cook-calendar .rdp-day_disabled { - min-height: 100px; - font-size: 1.3rem; - } -} - -@media (min-width: 1024px) { - .cook-calendar .rdp-day, - .cook-calendar .rdp-day_disabled { - min-height: 120px; - font-size: 1.4rem; - } +/* Outside days (previous/next month) - muted */ +.cook-calendar .rdp-day_outside { + background: #f9fafb !important; /* gray-50 */ + color: #9ca3af !important; /* gray-400 */ + border-color: #e5e7eb !important; /* gray-200 */ + opacity: 0.5 !important; } /* Admin-specific calendar colors */ @@ -340,10 +312,11 @@ body { /* Date picker popup styling */ .rdp { - --rdp-cell-size: 36px; + --rdp-cell-size: 40px; --rdp-accent-color: #f59e0b; --rdp-background-color: #ffffff; margin: 0; + width: 100%; } .rdp-months { @@ -358,56 +331,78 @@ body { .rdp-table { width: 100%; border-collapse: separate; - border-spacing: 2px; + border-spacing: 4px; + table-layout: fixed; } .rdp-head_cell, .rdp-cell { width: var(--rdp-cell-size); height: var(--rdp-cell-size); -} - -.rdp-head_cell { - font-weight: 500; - font-size: 0.75rem; - color: #6b7280; text-align: center; } -.rdp-day { +.rdp-row { + display: flex; width: 100%; - height: 100%; - border: none; - background: none; +} + +.rdp-head_cell { + font-weight: 600; + font-size: 0.8rem; + color: #374151; + text-align: center; +} + +.rdp-day_button { + width: 2rem; + height: 2rem; + border-radius: 0.375rem; + border: 1px solid #e5e7eb; font-size: 0.875rem; - border-radius: 6px; - transition: all 0.15s ease; + font-weight: 500; + background: white; + color: #374151; + cursor: pointer; display: flex; align-items: center; justify-content: center; - cursor: pointer; + transition: all 0.2s ease; } .rdp-day:hover:not(.rdp-day_disabled) { - background-color: #f3f4f6; + background-color: #f59e0b; + color: white; + border-color: #f59e0b; transform: scale(1.05); } .rdp-day_selected { background-color: var(--rdp-accent-color) !important; color: white !important; - font-weight: 500; + border-color: var(--rdp-accent-color) !important; + font-weight: 600; } .rdp-day_today:not(.rdp-day_selected) { - background-color: #e5e7eb; - font-weight: 500; + background-color: #3b82f6; + color: white; + border-color: #3b82f6; + font-weight: 600; } .rdp-day_disabled { - color: #d1d5db; - cursor: not-allowed; - opacity: 0.5; + background-color: #f3f4f6 !important; + color: #9ca3af !important; + border-color: #e5e7eb !important; + cursor: not-allowed !important; + opacity: 0.6 !important; +} + +.rdp-day_disabled:hover { + background-color: #f3f4f6 !important; + color: #9ca3af !important; + transform: none !important; } .rdp-day_outside { @@ -418,9 +413,12 @@ body { display: flex; justify-content: center; align-items: center; - padding: 8px 0 16px 0; - font-weight: 600; - font-size: 0.95rem; + padding: 8px 0 20px 0; + font-weight: 700; + font-size: 1.1rem; + position: relative; + width: 100%; + color: #1f2937; } .rdp-nav { diff --git a/src/app/kontakt/page.tsx b/src/app/kontakt/page.tsx index 6a399a9..60ca7ee 100644 --- a/src/app/kontakt/page.tsx +++ b/src/app/kontakt/page.tsx @@ -6,54 +6,67 @@ export default function ContactPage() { const { contact } = sampleData.chef return ( -
-
-

Kontakt

- -
- {/* Contact Info */} -
-

Kontakt information

- +
+
+
+ - {/* Contact Form */} -
-

Send besked

- +
+ {/* Contact Info */} + + + {/* Contact Form */} +
+

Send besked

+ +
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..b9c1ce7 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,145 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' + +export default function LoginPage() { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const router = useRouter() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError('') + + try { + console.log('=== LOGIN ATTEMPT START ===') + console.log('Username:', username) + console.log('Password length:', password.length) + console.log('Timestamp:', new Date().toISOString()) + + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ username, password }), + }) + + console.log('=== LOGIN RESPONSE ===') + console.log('Response status:', response.status) + console.log('Response ok:', response.ok) + console.log('Response headers:', Object.fromEntries(response.headers.entries())) + + if (response.ok) { + const data = await response.json() + console.log('=== LOGIN SUCCESS ===') + console.log('Got token:', !!data.token) + console.log('Token length:', data.token?.length) + console.log('User data:', data.user) + + // Check cookies + console.log('Document cookies:', document.cookie) + + // Store session token (backup, but we use HTTP-only cookie) + localStorage.setItem('authToken', data.token) + console.log('Stored token in localStorage') + + console.log('Redirecting to /admin with full page reload...') + // Use window.location for full page reload to ensure cookie is sent + window.location.href = '/admin' + console.log('=== REDIRECT TRIGGERED ===') + } else { + const errorText = await response.text() + console.log('=== LOGIN FAILED ===') + console.log('Error response:', errorText) + try { + const errorData = JSON.parse(errorText) + setError(errorData.error || 'Login fejlede') + console.log('Error message set:', errorData.error) + } catch { + setError('Login fejlede - server error') + console.log('Could not parse error response') + } + } + } catch (err) { + console.error('=== LOGIN EXCEPTION ===') + console.error('Error:', err) + console.error('Error type:', typeof err) + console.error('Error message:', err instanceof Error ? err.message : String(err)) + setError('Der skete en fejl - prøv igen') + } finally { + setLoading(false) + console.log('=== LOGIN ATTEMPT END ===') + } + } + + return ( +
+
+
+

+ Log ind +

+

+ Admin adgang til warme.dk +

+
+
+ {error && ( +
+ {error} +
+ )} +
+
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/menuer/page.tsx b/src/app/menuer/page.tsx index a163a66..4448a23 100644 --- a/src/app/menuer/page.tsx +++ b/src/app/menuer/page.tsx @@ -44,10 +44,19 @@ export default function MenusPage() {

Vores menuer

-

+

Vælg mellem vores populære menuer eller lad os skræddersy en menu til jeres arrangement. Alle menuer kan tilpasses efter jeres ønsker og behov.

+ + + + + Se kokke profil +
{/* Filters */} diff --git a/src/app/om-christian/page.tsx b/src/app/om-christian/page.tsx new file mode 100644 index 0000000..75c6e4b --- /dev/null +++ b/src/app/om-christian/page.tsx @@ -0,0 +1,248 @@ +'use client' + +import Image from 'next/image' +import { ChefHat, Award, Briefcase, Heart, Mail, Phone } from 'lucide-react' + +export default function ChristianProfilePage() { + return ( +
+ {/* Hero Section */} +
+
+
+
+
+ {/* Profile Image */} +
+
+ Christian Wärme - Kok og Konditor +
+
+ +
+
+ + {/* Intro Text */} +
+

Christian Wärme

+

Kok & Konditor med passion for madoplevelser

+

+ Med en unik baggrund som både udlært konditor og kok med sølvmedalje, + kombinerer jeg klassisk håndværk med moderne madlavning. + Min erfaring som tjener giver mig en dyb forståelse for gæsteoplevelsen + – fra køkken til bord. +

+ +
+
+
+
+
+ + {/* Kompetencer */} +
+
+
+

Kompetencer & Uddannelse

+
+
+
+ +
+

Udlært Kok

+

Sølvmedalje

+

+ Sølvmedalje for fremragende håndværk og dedikation. +

+
+ +
+
+ +
+

Udlært Konditor

+

Patissier

+

+ Omfattende konditoruddannelse med ekspertise i klassiske og moderne desserter, + brød og finere bagte produkter. +

+
+ +
+
+ +
+

Erfaren Tjener

+

Service & Gæsteoplevelse

+

+ Dyb forståelse for gæsteoplevelsen fra salen. + Jeg ved hvad der skal til for at skabe mindeværdige øjeblikke. +

+
+
+
+
+
+ + {/* Min filosofi */} +
+
+
+

Min madfilosofi

+
+

+ Med baggrund som både konditor og kok, ser jeg mad som et håndværk hvor detaljer betyder alt. + Min tid som tjener har lært mig, at den bedste mad ikke kun smager godt – den skaber oplevelser + og mindeværdige øjeblikke omkring bordet. +

+

+ Jeg brænder for at lave mad der både ser smuk ud og smager fantastisk. + Fra klassiske danske traditioner til moderne fortolkninger – + altid med respekt for råvarerne og gæsternes forventninger. +

+
+
+
+
+ + {/* Erhvervserfaring */} +
+
+
+

Erhvervserfaring

+
+ {/* Current Position */} +
+
+
+
+ +
+
+
+
+

Kok

+ Nuværende +
+

+ + eVenues Catering + +

+

+ Professionel catering til events og selskaber. Ansvarlig for menuudvikling, + produktion og leverance af høj kvalitet til private og erhverv. +

+
+
+
+ +
+
+
+
+ +
+
+
+

Kok

+

The Market Italian

+

+ Arbejdet med italiensk køkken og autentiske retter. + Fokus på friske råvarer og traditionelle teknikker. +

+
+
+
+ +
+
+
+
+ +
+
+
+

Konditor

+

Konditori Antoinette

+

+ Finere konditorarbejde med klassiske franske teknikker. + Produktion af kager, desserter og brød til høje standarder. +

+
+
+
+ +
+
+
+
+ +
+
+
+

Tjener

+

Strandmølle Kroen

+

+ Erfaring med gæstehåndtering og service i et klassisk kro-miljø. + Udviklet stærk forståelse for gæsteoplevelsen fra salen. +

+
+
+
+
+
+
+
+ + {/* Call to Action */} +
+
+
+

Klar til at skabe din næste madoplevelse?

+

+ Lad os tale om hvordan jeg kan hjælpe med at gøre dit arrangement uforglemmelig +

+ +
+
+
+
+ ) +} diff --git a/src/components/admin/logout-button.tsx b/src/components/admin/logout-button.tsx new file mode 100644 index 0000000..7d6924e --- /dev/null +++ b/src/components/admin/logout-button.tsx @@ -0,0 +1,31 @@ +'use client' + +export function LogoutButton() { + const handleLogout = () => { + // Clear local storage + localStorage.removeItem('authToken'); + + // Call logout API to clear cookie + fetch('/api/auth/logout', { + method: 'POST', + credentials: 'include' + }).then(() => { + // Redirect to home page + window.location.href = '/'; + }).catch(() => { + // Even if API fails, redirect anyway + window.location.href = '/'; + }); + }; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/contact-form.tsx b/src/components/contact-form.tsx index 1e7b474..49b6439 100644 --- a/src/components/contact-form.tsx +++ b/src/components/contact-form.tsx @@ -1,38 +1,80 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Input } from '@/components/ui/input' export default function ContactForm() { const [name, setName] = useState('') const [email, setEmail] = useState('') const [phone, setPhone] = useState('') + const [address, setAddress] = useState('') const [message, setMessage] = useState('') const [loading, setLoading] = useState(false) const [success, setSuccess] = useState(null) const [error, setError] = useState(null) + + // Bot protection: honeypot and timestamp + const [honeypot, setHoneypot] = useState('') + const [formLoadTime, setFormLoadTime] = useState(0) + + useEffect(() => { + // Record when the form was loaded + setFormLoadTime(Date.now()) + }, []) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + console.log('=== Contact Form Submit ===') setLoading(true) setError(null) setSuccess(null) try { + // Bot protection: check honeypot + if (honeypot) { + console.log('🤖 Bot detected: honeypot filled') + setError('Der skete en fejl. Prøv igen.') + setLoading(false) + return + } + + // Bot protection: check submission time (minimum 2 seconds) + const timeSinceLoad = Date.now() - formLoadTime + if (timeSinceLoad < 2000) { + console.log('🤖 Bot detected: too fast submission', timeSinceLoad, 'ms') + setError('Vent venligst et øjeblik før du sender.') + setLoading(false) + return + } + + console.log('Submitting contact form:', { name, email, hasPhone: !!phone, hasAddress: !!address, timeSinceLoad }) const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, email, phone, message }) + body: JSON.stringify({ + name, + email, + phone, + address, + message, + _t: formLoadTime // Send timestamp for server-side validation + }) }) const data = await res.json() + console.log('Response:', { status: res.status, data }) + if (!res.ok) throw new Error(data?.error || 'Unknown error') + + console.log('✓ Contact form submitted successfully') setSuccess('Besked sendt — tak!') setName('') setEmail('') setPhone('') + setAddress('') setMessage('') } catch (err: any) { + console.error('❌ Contact form error:', err) setError(err.message || 'Der skete en fejl') } finally { setLoading(false) @@ -63,6 +105,25 @@ export default function ContactForm() { setPhone(e.target.value)} className="w-full" />
+
+ + setAddress(e.target.value)} className="w-full" placeholder="By eller postnummer" /> +
+ + {/* Honeypot field - hidden from users, but visible to bots */} + +