81c12ace7b
- Generate thumbnail (150px), medium (800px), full size on upload - OptimizedImage component with Intersection Observer lazy loading - Blur placeholder while loading - Fetches appropriate size based on props (thumbnail/medium/full) - Async image loading reduces initial page load time - Added medium and thumbnail columns to images table
502 lines
20 KiB
TypeScript
502 lines
20 KiB
TypeScript
import { db } from './db';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
export async function initSchema() {
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')),
|
|
comments_disabled BOOLEAN DEFAULT FALSE,
|
|
first_name VARCHAR(100),
|
|
last_name VARCHAR(100),
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS images (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(255) NOT NULL,
|
|
data TEXT NOT NULL,
|
|
thumbnail TEXT,
|
|
medium TEXT,
|
|
category VARCHAR(100) DEFAULT 'other',
|
|
room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL,
|
|
location_target VARCHAR(100) DEFAULT 'gallery',
|
|
uploaded_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// Add columns if they don't exist (for existing databases)
|
|
await db.query(`ALTER TABLE images ADD COLUMN IF NOT EXISTS medium TEXT`);
|
|
await db.query(`ALTER TABLE images ADD COLUMN IF NOT EXISTS thumbnail TEXT`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS reservations (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
date DATE NOT NULL,
|
|
time TIME NOT NULL,
|
|
guests INTEGER NOT NULL,
|
|
table_name VARCHAR(100) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS coupons (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
code VARCHAR(50) NOT NULL,
|
|
discount VARCHAR(255) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS offers (
|
|
id SERIAL PRIMARY KEY,
|
|
title VARCHAR(255) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS benefits (
|
|
id SERIAL PRIMARY KEY,
|
|
text VARCHAR(500) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
key VARCHAR(100) PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS rooms (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
price NUMERIC(10,2) NOT NULL,
|
|
photos TEXT[] DEFAULT '{}',
|
|
featured_photo VARCHAR(500),
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS menu_items (
|
|
id SERIAL PRIMARY KEY,
|
|
category VARCHAR(100) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
price NUMERIC(10,2) NOT NULL,
|
|
is_daily_special BOOLEAN DEFAULT FALSE,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
photos TEXT[] DEFAULT '{}',
|
|
featured_photo VARCHAR(500),
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS places (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT NOT NULL,
|
|
photos TEXT[] DEFAULT '{}',
|
|
featured_photo VARCHAR(500),
|
|
distance_km NUMERIC(8,2),
|
|
visit_time VARCHAR(100),
|
|
suggestion TEXT,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS reviews (
|
|
id SERIAL PRIMARY KEY,
|
|
author_name VARCHAR(255) NOT NULL,
|
|
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
|
text TEXT NOT NULL,
|
|
approved BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS slides (
|
|
id SERIAL PRIMARY KEY,
|
|
title VARCHAR(255),
|
|
subtitle TEXT,
|
|
image_id INTEGER REFERENCES images(id) ON DELETE SET NULL,
|
|
image_url VARCHAR(500),
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS room_comments (
|
|
id SERIAL PRIMARY KEY,
|
|
room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE,
|
|
photo_url VARCHAR(500),
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
first_name VARCHAR(100) NOT NULL,
|
|
last_initial VARCHAR(10),
|
|
text TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
deleted_by_admin BOOLEAN DEFAULT FALSE
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS social_events (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
price VARCHAR(50),
|
|
date DATE,
|
|
photos TEXT[] DEFAULT '{}',
|
|
featured_photo VARCHAR(500),
|
|
active BOOLEAN DEFAULT TRUE,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS social_event_reservations (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
social_event_id INTEGER REFERENCES social_events(id) ON DELETE CASCADE,
|
|
guests INTEGER NOT NULL,
|
|
notes TEXT,
|
|
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled')),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(user_id, social_event_id)
|
|
);
|
|
`);
|
|
|
|
await db.query(`CREATE TABLE IF NOT EXISTS calendar_events (
|
|
id SERIAL PRIMARY KEY,
|
|
date DATE NOT NULL UNIQUE,
|
|
title VARCHAR(255) NOT NULL,
|
|
type VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
color VARCHAR(20) NOT NULL,
|
|
active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
token TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE;
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS preferred_language VARCHAR(10) DEFAULT 'es';
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS terms_accepted_at TIMESTAMP;
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(255);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(50);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS country VARCHAR(100);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS terms_content (
|
|
id SERIAL PRIMARY KEY,
|
|
language VARCHAR(10) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
content TEXT NOT NULL,
|
|
version VARCHAR(20) NOT NULL DEFAULT '1.0',
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(language, version)
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS availability (
|
|
id SERIAL PRIMARY KEY,
|
|
room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE,
|
|
date DATE NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'available' CHECK (status IN ('available', 'booked', 'maintenance')),
|
|
price NUMERIC(10,2),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(room_id, date)
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS trips (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL,
|
|
check_in DATE NOT NULL,
|
|
check_out DATE NOT NULL,
|
|
notes TEXT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'confirmed' CHECK (status IN ('pending', 'confirmed', 'checked_in', 'checked_out', 'cancelled')),
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
sender_role VARCHAR(20) NOT NULL CHECK (sender_role IN ('user', 'admin')),
|
|
message TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS room_amenities (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL UNIQUE,
|
|
icon_svg TEXT NOT NULL,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS room_amenity_links (
|
|
room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE,
|
|
amenity_id INTEGER REFERENCES room_amenities(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (room_id, amenity_id)
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS reservations (
|
|
id SERIAL PRIMARY KEY,
|
|
room_id INTEGER NOT NULL REFERENCES rooms(id),
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
check_in DATE NOT NULL,
|
|
check_out DATE NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
total_price DECIMAL(10,2),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS room_availability (
|
|
id SERIAL PRIMARY KEY,
|
|
room_id INTEGER NOT NULL REFERENCES rooms(id),
|
|
date DATE NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'available',
|
|
reason TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(room_id, date)
|
|
);
|
|
`);
|
|
|
|
await db.query(`
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id SERIAL PRIMARY KEY,
|
|
room_id INTEGER REFERENCES rooms(id),
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
message TEXT NOT NULL,
|
|
read_by_admins BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
await db.query(`ALTER TABLE room_comments ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'approved'`);
|
|
|
|
await db.query(`CREATE INDEX IF NOT EXISTS idx_reservations_room_dates ON reservations(room_id, check_in, check_out)`);
|
|
await db.query(`CREATE INDEX IF NOT EXISTS idx_room_availability_room_date ON room_availability(room_id, date)`);
|
|
await db.query(`CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC)`);
|
|
}
|
|
|
|
export async function migrateFromLegacyPlaces() {
|
|
await db.query(`
|
|
UPDATE places SET photos = ARRAY[photo] WHERE photo IS NOT NULL AND (photos IS NULL OR photos = '{}');
|
|
`);
|
|
await db.query(`
|
|
ALTER TABLE places DROP COLUMN IF EXISTS photo;
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE places ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500);
|
|
`);
|
|
|
|
await db.query(`
|
|
ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500);
|
|
`);
|
|
}
|
|
|
|
export async function seed() {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
throw new Error('Seeding is disabled in production');
|
|
}
|
|
|
|
const adminPassword = process.env.BOOTSTRAP_ADMIN_PASSWORD;
|
|
const userPassword = process.env.BOOTSTRAP_USER_PASSWORD;
|
|
const wifiPassword = process.env.BOOTSTRAP_WIFI_PASSWORD;
|
|
const tagline = process.env.BOOTSTRAP_TAGLINE || 'Hotel · Restaurant · Andean Highlands';
|
|
|
|
const { rows: adminRows } = await db.query(`SELECT id FROM users WHERE username = 'admin'`);
|
|
if (adminRows.length === 0 && adminPassword) {
|
|
const adminHash = bcrypt.hashSync(adminPassword, 10);
|
|
const seedRows: Array<[string, string, string]> = [['admin', adminHash, 'admin']];
|
|
|
|
if (userPassword) {
|
|
seedRows.push(['user', bcrypt.hashSync(userPassword, 10), 'user']);
|
|
}
|
|
|
|
await db.query(
|
|
`INSERT INTO users (username, password_hash, role) VALUES ${seedRows
|
|
.map((_, index) => `($${index * 3 + 1}, $${index * 3 + 2}, $${index * 3 + 3})`)
|
|
.join(', ')}`,
|
|
seedRows.flat()
|
|
);
|
|
}
|
|
|
|
const { rows: offerRows } = await db.query(`SELECT id FROM offers`);
|
|
if (offerRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO offers (title, description) VALUES ($1, $2), ($3, $4)`,
|
|
[
|
|
'Summer Wine Weekend',
|
|
'Complimentary wine tasting with every two-night stay.',
|
|
'Sunset Dinner',
|
|
'Three-course menu on the terrace every Friday.',
|
|
]
|
|
);
|
|
}
|
|
|
|
const { rows: benefitRows } = await db.query(`SELECT id FROM benefits`);
|
|
if (benefitRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO benefits (text) VALUES ($1), ($2), ($3), ($4)`,
|
|
[
|
|
'Priority booking for peak weekends',
|
|
'Late checkout on availability',
|
|
'Free welcome cocktail',
|
|
'Member-only events and tastings',
|
|
]
|
|
);
|
|
}
|
|
|
|
const { rows: configWifiRows } = await db.query(`SELECT key FROM config WHERE key = 'wifi_password'`);
|
|
if (configWifiRows.length === 0 && wifiPassword) {
|
|
await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['wifi_password', wifiPassword]);
|
|
}
|
|
|
|
const { rows: configTaglineRows } = await db.query(`SELECT key FROM config WHERE key = 'tagline'`);
|
|
if (configTaglineRows.length === 0) {
|
|
await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['tagline', tagline]);
|
|
}
|
|
|
|
const { rows: calendarRows } = await db.query(`SELECT id FROM calendar_events`);
|
|
if (calendarRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO calendar_events (date, title, type, description, color, active) VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
['2026-06-30', 'Welcome Dinner', 'event', 'Andean tasting menu', '#b45309', true]
|
|
);
|
|
}
|
|
|
|
const { rows: socialRows } = await db.query(`SELECT id FROM social_events`);
|
|
if (socialRows.length === 0) {
|
|
await db.query(
|
|
`INSERT INTO social_events (name, description, price, date, photos, featured_photo, active, sort_order) VALUES ($1, $2, $3, $4, '{}', null, $5, $6)`,
|
|
['Sunset Tasting', 'Local wines on the terrace.', 'USD 35', '2026-07-04', true, 1]
|
|
);
|
|
}
|
|
|
|
const { rows: socialReservationRows } = await db.query(`SELECT id FROM social_event_reservations`);
|
|
if (socialReservationRows.length === 0) {
|
|
const { rows: adminIdRows } = await db.query(`SELECT id FROM users WHERE username = 'admin'`);
|
|
if (adminIdRows.length > 0) {
|
|
await db.query(
|
|
`INSERT INTO social_event_reservations (user_id, social_event_id, guests, notes, status) SELECT $1, id, $2, $3, $4 FROM social_events LIMIT 1`,
|
|
[adminIdRows[0].id, 2, 'Anniversary', 'confirmed']
|
|
);
|
|
}
|
|
}
|
|
|
|
const { rows: amenityRows } = await db.query(`SELECT id FROM room_amenities`);
|
|
if (amenityRows.length === 0) {
|
|
const defaultAmenities = [
|
|
['Wi-Fi', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>', 1],
|
|
['TV', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"/><polyline points="17 2 12 7 7 2"/></svg>', 2],
|
|
['Shower', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4v5a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5V4"/><path d="M4 9h4"/><path d="M6 9v12"/><path d="M10 4h10a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H10"/><circle cx="6" cy="14" r="1"/><circle cx="6" cy="18" r="1"/><circle cx="10" cy="14" r="1"/><circle cx="10" cy="18" r="1"/></svg>', 3],
|
|
['Bathtub', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h16a1 1 0 0 1 1 1v3a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4v-3a1 1 0 0 1 1-1z"/><path d="M6 12V5a2 2 0 0 1 2-2h3v2.25"/><circle cx="12" cy="5" r=".5"/></svg>', 4],
|
|
['Fireplace', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.14-4.052 1.5-5 .5 2 2 4 2 6a2.5 2.5 0 0 0 5 0c0-1.38-.5-2-1-3-.5-1-1-2-1-3.5 1.5 1 3 2.5 3 5a4 4 0 1 1-8 0"/><path d="M4 19h16a2 2 0 0 0 2-2v-2H2v2a2 2 0 0 0 2 2z"/></svg>', 5],
|
|
['Minibar', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h18v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3z"/><path d="M5 9v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9"/><line x1="10" y1="13" x2="14" y2="13"/></svg>', 6],
|
|
['Coffee Maker', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg>', 7],
|
|
['Safe', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/><circle cx="12" cy="16" r="1"/></svg>', 8],
|
|
['Heating', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"/></svg>', 9],
|
|
['Balcony', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><line x1="3" y1="17" x2="21" y2="17"/><line x1="7" y1="11" x2="7" y2="21"/><line x1="12" y1="11" x2="12" y2="21"/><line x1="17" y1="11" x2="17" y2="21"/></svg>', 10],
|
|
['King Bed', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 18v-4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4"/><path d="M2 14v-2a2 2 0 0 1 2-2h1"/><path d="M22 14v-2a2 2 0 0 0-2-2h-1"/><path d="M5 4h14a2 2 0 0 1 2 2v2H3V6a2 2 0 0 1 2-2z"/><rect x="3" y="10" width="18" height="4" rx="1"/></svg>', 11],
|
|
['Queen Bed', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 18v-3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3"/><path d="M2 13v-2a2 2 0 0 1 2-2h1"/><path d="M18 13v-2a2 2 0 0 0-2-2h-1"/><path d="M4 5h10a2 2 0 0 1 2 2v1H2V7a2 2 0 0 1 2-2z"/><rect x="2" y="9" width="14" height="3" rx="1"/></svg>', 13],
|
|
['2 Queen Beds', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 15v-2a1.5 1.5 0 0 1 1.5-1.5H10a1.5 1.5 0 0 1 1.5 1.5v2"/><path d="M12.5 15v-2a1.5 1.5 0 0 1 1.5-1.5H21.5a1.5 1.5 0 0 1 1.5 1.5v2"/><path d="M1 11V9.5A1.5 1.5 0 0 1 2.5 8h8A1.5 1.5 0 0 1 12 9.5V11"/><path d="M12 11V9.5A1.5 1.5 0 0 1 13.5 8h8A1.5 1.5 0 0 1 23 9.5V11"/><rect x="1" y="8" width="11" height="2.5" rx="0.75"/><rect x="12" y="8" width="11" height="2.5" rx="0.75"/></svg>', 14],
|
|
['Mountain View', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></svg>', 12],
|
|
];
|
|
|
|
for (const [name, icon_svg, sort_order] of defaultAmenities) {
|
|
await db.query(
|
|
'INSERT INTO room_amenities (name, icon_svg, sort_order) VALUES ($1, $2, $3)',
|
|
[name, icon_svg, sort_order]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function resetDatabase() {
|
|
await db.query('DROP TABLE IF EXISTS room_comments, reviews, places, menu_items, rooms, config, benefits, offers, coupons, reservations, images, users, slides, calendar_events, social_events, social_event_reservations CASCADE');
|
|
await initSchema();
|
|
await migrateFromLegacyPlaces();
|
|
await seed();
|
|
} |