Shop integration
This commit is contained in:
138
Pottery-website/server/db_setup.js
Normal file
138
Pottery-website/server/db_setup.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const { Client } = require('pg');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
async function setup() {
|
||||
// Connection config for the default 'postgres' database
|
||||
const config = {
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: 'postgres'
|
||||
};
|
||||
|
||||
const client = new Client(config);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('Connected to PostgreSQL default database.');
|
||||
|
||||
// Create database
|
||||
try {
|
||||
await client.query(`CREATE DATABASE ${process.env.DB_NAME || 'pottery_db'}`);
|
||||
console.log(`Database ${process.env.DB_NAME || 'pottery_db'} created.`);
|
||||
} catch (err) {
|
||||
if (err.code === '42P04') {
|
||||
console.log(`Database ${process.env.DB_NAME || 'pottery_db'} already exists.`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
await client.end();
|
||||
|
||||
// Connect to the new database to create tables
|
||||
const dbClient = new Client({
|
||||
...config,
|
||||
database: process.env.DB_NAME || 'pottery_db'
|
||||
});
|
||||
|
||||
await dbClient.connect();
|
||||
console.log(`Connected to ${process.env.DB_NAME || 'pottery_db'}.`);
|
||||
|
||||
const schemaPath = path.join(__dirname, 'schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf8');
|
||||
|
||||
await dbClient.query(schema);
|
||||
console.log('Tables created successfully.');
|
||||
|
||||
// --- SEED DATA ---
|
||||
console.log('Seeding initial data...');
|
||||
|
||||
const products = [
|
||||
{
|
||||
title: 'Tableware',
|
||||
price: 185,
|
||||
image: '/collection-tableware.png',
|
||||
description: 'A complete hand-thrown tableware set for four. Finished in our signature matte white glaze with raw clay rims.',
|
||||
gallery: ['/collection-tableware.png', '/pottery-plates.png', '/ceramic-cups.png'],
|
||||
slug: 'tableware-set',
|
||||
number: '01',
|
||||
aspect_ratio: 'aspect-[3/4]'
|
||||
},
|
||||
{
|
||||
title: 'Lighting',
|
||||
price: 240,
|
||||
image: '/collection-lighting.png',
|
||||
description: 'Sculptural ceramic pendant lights that bring warmth and texture to any space. Each piece is unique.',
|
||||
gallery: ['/collection-lighting.png', '/pottery-studio.png', '/collection-vases.png'],
|
||||
slug: 'ceramic-lighting',
|
||||
number: '04',
|
||||
aspect_ratio: 'aspect-[4/3]'
|
||||
},
|
||||
{
|
||||
title: 'Vases',
|
||||
price: 95,
|
||||
image: '/collection-vases.png',
|
||||
description: 'Organic forms inspired by the dunes of Padre Island. Perfect for dried stems or fresh bouquets.',
|
||||
gallery: ['/collection-vases.png', '/pottery-vase.png', '/collection-lighting.png'],
|
||||
slug: 'organic-vases',
|
||||
number: '02',
|
||||
aspect_ratio: 'aspect-square'
|
||||
}
|
||||
];
|
||||
|
||||
for (const p of products) {
|
||||
await dbClient.query(
|
||||
'INSERT INTO products (title, price, image, description, gallery, slug, number, aspect_ratio) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT DO NOTHING',
|
||||
[p.title, p.price, p.image, p.description, JSON.stringify(p.gallery), p.slug, p.number, p.aspect_ratio]
|
||||
);
|
||||
}
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title: 'Product Photography for Small Businesses',
|
||||
date: 'Oct 03',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAipMlYLTcRT_hdc3VePfFIlrA56VzZ5G2y3gcRfmIZMERwGFKq2N19Gqo6mw7uZowXmjl2eJ89TI3Mcud2OyOfadO3mPVF_v0sI0OHupqM49WEFcWzH-Wbu3DL6bQ46F2Y8SIAk-NUQy8psjcIdBKRrM8fqdn4eOPANYTXpVxkLMAm4R0Axy4aEKNdmj917ZKKTxvXB-J8nGlITJkJ-ua7XcZOwGnfK5ttzyWW35A0oOSffCf972gmpV27wrVQgYJNLS7UyDdyQIQ',
|
||||
slug: 'product-photography-for-small-businesses',
|
||||
category: 'Studio',
|
||||
description: "Learning that beautiful products aren't enough on their own — you also need beautiful photos to tell the story.",
|
||||
sections: [
|
||||
{ id: '1', type: 'text', content: 'Mastering Product Photography for Pottery is essential because in the world of handmade business, your work is only as good as the photo that represents it.' },
|
||||
{ id: '2', type: 'image', content: 'https://images.unsplash.com/photo-1516483638261-f4dbaf036963?q=80&w=2574&auto=format&fit=crop' },
|
||||
{ id: '3', type: 'text', content: 'Lighting is the single most critical element of successful Product Photography for Pottery. Avoid the harsh, yellow glow of indoor lamps.' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'The Art of Packaging',
|
||||
date: 'Jul 15',
|
||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAaWGnX_NYT3S_lOflL2NJZGbWge4AAkvra4ymvF8ag-c1UKsOAIB-rsLVQXW5xIlPZipDiK8-rsLVQXW5xIlPZipDiK8-ysPyv22xdgsvzs4EOXSSCcrT4Lb2YCe0u5orxRaZEA5TgxeoKq15zaWKSlmnHyPGjPd_7yglpfO13eZmbU5KaxFJ1KGO0UAxoO9BpsyCYgbgINMoSz3epGe5ZdwBWRH-5KCzjoLuXimFTLcd5bqg9T1YofTxgy2hWBMJzKkafyEniq8dP6hMmfNCLVcCHHHx0hRU',
|
||||
slug: 'the-art-of-packaging',
|
||||
category: 'Guide',
|
||||
description: "A practical guide for potters who want to package and send their handmade ceramics with care and confidence.",
|
||||
sections: [
|
||||
{ id: '1', type: 'text', content: 'When considering safe delivery—especially for large items—the double-box method is the industry standard.' },
|
||||
{ id: '2', type: 'text', content: 'The Outer Box: Place the small box inside a larger shipping box, with at least 2 inches of padding on all sides.' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
for (const a of articles) {
|
||||
await dbClient.query(
|
||||
'INSERT INTO articles (title, date, image, sections, slug, category, description) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING',
|
||||
[a.title, a.date, a.image, JSON.stringify(a.sections), a.slug, a.category, a.description]
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Seeding complete.');
|
||||
await dbClient.end();
|
||||
console.log('Setup complete!');
|
||||
} catch (err) {
|
||||
console.error('Setup failed:', err);
|
||||
console.log('\nTIP: Make sure PostgreSQL is running and your password in .env is correct.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
setup();
|
||||
180
Pottery-website/server/index.js
Normal file
180
Pottery-website/server/index.js
Normal file
@@ -0,0 +1,180 @@
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const cors = require('cors');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 5000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
// Database Connection
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('Unexpected error on idle client', err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
// Routes
|
||||
|
||||
// --- PRODUCTS ---
|
||||
app.get('/api/products', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT id, title, price, image, description, gallery as images, slug, number, aspect_ratio as "aspectRatio", details FROM products ORDER BY id ASC');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/products', async (req, res) => {
|
||||
const { title, price, image, description, images, slug, number, aspectRatio, details } = req.body;
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'INSERT INTO products (title, price, image, description, gallery, slug, number, aspect_ratio, details) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, title, price, image, description, gallery as images, slug, number, aspect_ratio as "aspectRatio", details',
|
||||
[title, price, image, description, JSON.stringify(images), slug, number, aspectRatio, JSON.stringify(details || [])]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/products/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { title, price, image, description, images, slug, number, aspectRatio, details } = req.body;
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'UPDATE products SET title = $1, price = $2, image = $3, description = $4, gallery = $5, slug = $6, number = $7, aspect_ratio = $8, details = $9 WHERE id = $10 RETURNING id, title, price, image, description, gallery as images, slug, number, aspect_ratio as "aspectRatio", details',
|
||||
[title, price, image, description, JSON.stringify(images), slug, number, aspectRatio, JSON.stringify(details || []), id]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/products/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
await pool.query('DELETE FROM products WHERE id = $1', [id]);
|
||||
res.json({ message: 'Product deleted' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// --- ARTICLES ---
|
||||
app.get('/api/articles', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT id, title, date, image, sections, slug, category, description, is_featured as "isFeatured" FROM articles ORDER BY id ASC');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/articles', async (req, res) => {
|
||||
const { title, date, image, sections, slug, category, description, isFeatured } = req.body;
|
||||
try {
|
||||
if (isFeatured) {
|
||||
await pool.query('UPDATE articles SET is_featured = FALSE');
|
||||
}
|
||||
const result = await pool.query(
|
||||
'INSERT INTO articles (title, date, image, sections, slug, category, description, is_featured) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, title, date, image, sections, slug, category, description, is_featured as "isFeatured"',
|
||||
[title, date, image, JSON.stringify(sections), slug, category, description, !!isFeatured]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/articles/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { title, date, image, sections, slug, category, description, isFeatured } = req.body;
|
||||
try {
|
||||
if (isFeatured) {
|
||||
await pool.query('UPDATE articles SET is_featured = FALSE');
|
||||
}
|
||||
const result = await pool.query(
|
||||
'UPDATE articles SET title = $1, date = $2, image = $3, sections = $4, slug = $5, category = $6, description = $7, is_featured = $8 WHERE id = $9 RETURNING id, title, date, image, sections, slug, category, description, is_featured as "isFeatured"',
|
||||
[title, date, image, JSON.stringify(sections), slug, category, description, !!isFeatured, id]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/articles/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
await pool.query('DELETE FROM articles WHERE id = $1', [id]);
|
||||
res.json({ message: 'Article deleted' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Orders API
|
||||
app.get('/api/orders', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM orders ORDER BY created_at DESC');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/orders', async (req, res) => {
|
||||
const { customer_email, customer_name, shipping_address, items, total_amount } = req.body;
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'INSERT INTO orders (customer_email, customer_name, shipping_address, items, total_amount, payment_status) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
|
||||
[customer_email, customer_name, JSON.stringify(shipping_address), JSON.stringify(items), total_amount, 'paid']
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/orders/:id/status', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { shipping_status } = req.body;
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'UPDATE orders SET shipping_status = $1 WHERE id = $2 RETURNING *',
|
||||
[shipping_status, id]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
41
Pottery-website/server/migrate.js
Normal file
41
Pottery-website/server/migrate.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
const sql = `
|
||||
-- Orders Table
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
customer_email TEXT NOT NULL,
|
||||
customer_name TEXT NOT NULL,
|
||||
shipping_address JSONB NOT NULL,
|
||||
items JSONB NOT NULL,
|
||||
total_amount DECIMAL(10, 2) NOT NULL,
|
||||
payment_status TEXT DEFAULT 'pending',
|
||||
shipping_status TEXT DEFAULT 'pending',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('Starting migration...');
|
||||
const client = await pool.connect();
|
||||
await client.query(sql);
|
||||
console.log('Migration successful: Orders table created or already exists.');
|
||||
client.release();
|
||||
} catch (err) {
|
||||
console.error('Migration failed:', err);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
1361
Pottery-website/server/package-lock.json
generated
Normal file
1361
Pottery-website/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Pottery-website/server/package.json
Normal file
23
Pottery-website/server/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"pg": "^8.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.11"
|
||||
}
|
||||
}
|
||||
40
Pottery-website/server/schema.sql
Normal file
40
Pottery-website/server/schema.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- Create Database
|
||||
-- CREATE DATABASE pottery_db;
|
||||
|
||||
-- Products Table
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
image TEXT NOT NULL,
|
||||
description TEXT,
|
||||
gallery JSONB DEFAULT '[]',
|
||||
slug TEXT,
|
||||
number TEXT,
|
||||
aspect_ratio TEXT
|
||||
);
|
||||
|
||||
-- Articles Table
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
date VARCHAR(50) NOT NULL,
|
||||
image TEXT NOT NULL,
|
||||
sections JSONB DEFAULT '[]',
|
||||
slug TEXT,
|
||||
category TEXT,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
-- Orders Table
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
customer_email TEXT NOT NULL,
|
||||
customer_name TEXT NOT NULL,
|
||||
shipping_address JSONB NOT NULL,
|
||||
items JSONB NOT NULL,
|
||||
total_amount DECIMAL(10, 2) NOT NULL,
|
||||
payment_status TEXT DEFAULT 'pending',
|
||||
shipping_status TEXT DEFAULT 'pending',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
BIN
Pottery-website/server/setup_log.txt
Normal file
BIN
Pottery-website/server/setup_log.txt
Normal file
Binary file not shown.
23
Pottery-website/server/test_db.js
Normal file
23
Pottery-website/server/test_db.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
const res = await pool.query('SELECT NOW()');
|
||||
console.log('Connection successful:', res.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Connection failed:', err);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
||||
Reference in New Issue
Block a user