-
Featured Story
-
{JOURNAL_ENTRIES[0].title}
-
{JOURNAL_ENTRIES[0].description}
-
-
Read Article
-
arrow_forward
+ if (isLoading) return
Loading Journal...
;
+ if (!articles || articles.length === 0) {
+ return (
+
+
Editorial
+
No stories yet. Stay tuned!
+
+ );
+ }
+
+ // Sort: Featured first, then rest
+ const featuredArticle = articles.find(a => a.isFeatured) || articles[0];
+ const otherArticles = articles.filter(a => a.id !== featuredArticle.id);
+
+ return (
+
+ {/* Featured Post */}
+
+
+
+
+
+
+
+
+
+ {featuredArticle.category}
+
+ {featuredArticle.date}
+
+
+ {featuredArticle.title}
+
+
+ {featuredArticle.description}
+
+
+
+ Read Story
+
+
+
+
+
+
+
+ {/* Other Articles Grid */}
+
+
+
+ {otherArticles.map((entry, idx) => (
+
+
+
+
+
+
+
+ {entry.category}
+
+ {entry.date}
+
+
+ {entry.title}
+
+
+ {entry.description}
+
+
+
+
+ ))}
-
- {/* Article Grid */}
-
- {JOURNAL_ENTRIES.slice(1).map((entry, idx) => (
-
-
-
-
-
- {entry.category}
-
- {entry.date}
-
- {entry.title}
- {entry.description}
-
- ))}
- {/* Dummy extra entry to fill grid */}
-
-
-
-
-
- Archive
-
- 2023
-
- Explore Past Issues
- Dive into our archive of stories, guides, and studio updates.
-
-
-
+
);
};
diff --git a/Pottery-website/pages/Home.tsx b/Pottery-website/pages/Home.tsx
index b100087..21a5644 100644
--- a/Pottery-website/pages/Home.tsx
+++ b/Pottery-website/pages/Home.tsx
@@ -7,6 +7,8 @@ import QuoteSection from '../components/QuoteSection';
import JournalSection from '../components/JournalSection';
import GallerySection from '../components/GallerySection';
+import FAQ from '../components/FAQ';
+
const Home: React.FC = () => {
return (
@@ -14,9 +16,10 @@ const Home: React.FC = () => {
-
-
+
+
+
);
};
diff --git a/Pottery-website/pages/Journal/MotivationInClay.tsx b/Pottery-website/pages/Journal/MotivationInClay.tsx
new file mode 100644
index 0000000..328b090
--- /dev/null
+++ b/Pottery-website/pages/Journal/MotivationInClay.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import BlogPostLayout from '../../components/BlogPostLayout';
+
+const MotivationInClay: React.FC = () => {
+ React.useEffect(() => {
+ document.title = "Creative Block for Potters: 10 Tips for Motivation | Hotchpotsh";
+ let meta = document.querySelector('meta[name="description"]');
+ if (!meta) {
+ meta = document.createElement('meta');
+ meta.setAttribute('name', 'description');
+ document.head.appendChild(meta);
+ }
+ meta.setAttribute('content', 'Overcoming Creative Block for Potters is possible. Use these 10 gentle, practical tips to rediscover your motivation and love for clay. Read more now.');
+ }, []);
+
+ return (
+
+
+ Dealing with Creative Block for Potters (and finding new Pottery Inspiration ) is a common struggle in the studio. Where the physical labor is intense and the failure rate is high, burnout is real. Whether you are facing general exhaustion or a specific artistic wall, know that this season is part of the cycle.
+
+
+
+ Here is how to overcome Creative Block for Potters and find your flow again.
+
+
+
+
+ 1. Play without Purpose
+
+ Stop making Collections. Stop thinking about what will sell. Grab a lump of clay and just pinch . When you remove the pressure, you often solve the Creative Block for Potters naturally.
+
+
+ 2. Switch Your Technique
+
+ If you are a wheel thrower, try hand building . Changing your physical movements can unlock new neural pathways.
+
+
+ 3. The "100 Pattern" Challenge
+
+ Commit to making 100 small test tiles. Constraints actually breed creativity.
+
+
+ 4. Clean Your Studio (Reset)
+
+ A cluttered space leads to a cluttered mind. Spend a day organizing your Atelier. A fresh, clean bat on the wheel is an invitation.
+
+
+ 5. Look Outside of Pottery
+
+ Don't look at other potters on Instagram. That leads to comparison. instead, look at:
+
+
+ Architecture : for structural shapes.
+ Nature : for textures (tree bark, river stones).
+
+
+ 6. Take a Class
+
+ Even masters are students. Taking a workshop puts you back in the "beginner's mind," which is a fertile place for ideas.
+
+
+ 7. Revisit Your "Why"
+
+ Look at the very first pot you ever kept. Reconnecting with your origin story can fuel your current practice.
+
+
+ 8. Limit Your Time
+
+ Tell yourself, "I will only work for 20 minutes." Often, the hardest part is just starting.
+
+
+ 9. Embrace functionality
+
+ Make something you need . A spoon rest. A soap dish. Solving a simple, functional problem is a great way to handle Creative Block for Potters .
+
+
+ 10. Rest
+
+ Sometimes, the block isn't mental; it's physical. Take a week off. The clay will be there when you get back.
+
+
+ );
+};
+
+export default MotivationInClay;
diff --git a/Pottery-website/pages/Journal/PackagingGuide.tsx b/Pottery-website/pages/Journal/PackagingGuide.tsx
new file mode 100644
index 0000000..21cc3ca
--- /dev/null
+++ b/Pottery-website/pages/Journal/PackagingGuide.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import BlogPostLayout from '../../components/BlogPostLayout';
+
+const PackagingGuide: React.FC = () => {
+ React.useEffect(() => {
+ document.title = "How to Package Pottery for Shipping: A Safe Guide | Hotchpotsh";
+ let meta = document.querySelector('meta[name="description"]');
+ if (!meta) {
+ meta = document.createElement('meta');
+ meta.setAttribute('name', 'description');
+ document.head.appendChild(meta);
+ }
+ meta.setAttribute('content', 'Learn how to package pottery for shipping safely. Use our double-box method and sustainable tips to ensure your handmade ceramics arrive intact. Read now.');
+ }, []);
+
+ return (
+
+
+ How to Package Pottery for Shipping safely is the most important skill for a small business owner. There is nothing more heartbreaking than a shattered creation, so mastering this art form ensures your hard work survives the journey.
+
+
+
+ Here is your comprehensive guide on shipping handmade art so it arrives safely every single time.
+
+
+
+
+ 1. The Double-Box Method (The Golden Rule)
+
+ When considering safe delivery—especially for large items—the double-box method is the industry standard.
+
+
+ Inner Box : Wrap your Collections piece and place it in a small box. It should fit snugly.
+ Outer Box : Place the small box inside a larger shipping box, with at least 2 inches of padding on all sides.
+ Why? The outer box absorbs the shock, keeping your art safe.
+
+
+ 2. Wrapping Materials: Layers Matter
+
+ Don't rely on just one material when you plan your packing strategy.
+
+
+ Layer 1: Tissue Paper : Protects the glaze.
+ Layer 2: Bubble Wrap : The workhorse. Wrap the piece tightly in small-bubble wrap.
+ The Shake Test : Shake the box hard. If you hear movement, add tougher filler.
+
+
+
+
+
Eco-friendly packaging materials ready for use.
+
+
+ 3. Sustainable Packaging Alternatives
+
+ Many customers value sustainability in our Atelier.
+
+
+ Honeycomb Paper : A biodegradable alternative.
+ Corn Starch Peanuts : Dissolve in water.
+ Cardboard Scraps : Excellent dense filler.
+
+
+ 4. Branding Your Unboxing Experience
+
+ The "Thank You" Note : Builds a connection.
+ Care Instructions : Explain microwave/dishwasher safety.
+ Stickers : Build anticipation.
+
+
+ 5. Insurance and labeling
+
+ Fragile Stickers : Helpful, but not a guarantee.
+ Shipping Insurance : Always pay the extra few dollars for peace of mind.
+
+
+ );
+};
+
+export default PackagingGuide;
diff --git a/Pottery-website/pages/Journal/ProductPhotography.tsx b/Pottery-website/pages/Journal/ProductPhotography.tsx
new file mode 100644
index 0000000..7707c35
--- /dev/null
+++ b/Pottery-website/pages/Journal/ProductPhotography.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import BlogPostLayout from '../../components/BlogPostLayout';
+// Wait, I don't know if react-helmet is installed. Checking package.json... it was not.
+// I will adhere to the "no new dependencies" rule unless necessary. I'll just render the meta tags usually, but without Helmet they won't lift to head.
+// The user asked for "Meta Title" and "Meta Description" implementation. I will add a helper to update document.title.
+
+const ProductPhotography: React.FC = () => {
+ React.useEffect(() => {
+ document.title = "Product Photography for Pottery: Tips for Sales | Hotchpotsh";
+ // Simple meta description update for basic SPA
+ let meta = document.querySelector('meta[name="description"]');
+ if (!meta) {
+ meta = document.createElement('meta');
+ meta.setAttribute('name', 'description');
+ document.head.appendChild(meta);
+ }
+ meta.setAttribute('content', 'Master Product Photography for Pottery with our DIY guide. Learn lighting and styling tips to boost your handmade ceramic sales online. Read more now.');
+ }, []);
+
+ return (
+
+
+ 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. Since customers can't touch your mugs online, your photos must bridge the gap between browsing and buying.
+
+
+
+ Here is how to elevate your Product Photography for Pottery without expensive gear.
+
+
+
+
+ 1. Treasure the Natural Light
+
+ Lighting is the single most critical element of successful Product Photography for Pottery . Avoid the harsh, yellow glow of indoor lamps. Instead, set up your "studio" next to a large North or South-facing window, similar to the natural light in our Atelier.
+
+
+ Diffused Light is Best : If the sun is beaming directly in, tape a sheet of white parchment paper over the window. This creates soft shadows that highlight the curves of your ceramic vessels without blinding glare.
+ The Golden Hour : For lifestyle shots, try shooting during the hour after sunrise or before sunset for a warm, magical glow.
+
+
+ 2. Master the "Hero Shot"
+
+ Every listing needs a clear shot. When mastering Product Photography for Pottery , the "Hero Shot" usually requires a clean background for your Collections.
+
+
+ The Infinite Curve : Use a large sheet of white poster board. Tape one end to the wall and let it curve gently down onto the table. This seamless background eliminates distracting horizon lines.
+ Tripod Stability : Blurry photos are a dealbreaker. If you don't have a tripod, prop your phone up against a stack of books.
+
+
+ 3. Tell a Story with Props
+
+ While a clean background shows the details, lifestyle **Product Photography for Pottery** sells the dream .
+
+
+ Context matters : Don't just show a mug; show it steaming with coffee next to a half-read book.
+ Keep it subtle : Your props should never compete with your work. Neutral linens complement the vibrant glaze colors of your Collections.
+
+
+ 4. Angles & Details
+
+ Don't stop at one angle. Online buyers need to see everything.
+
+
+ The Eye-Level Shot : Perfect for showing the profile of a vase.
+ The Top-Down Shot : Ideal for plates and bowls.
+ The Detail Macro : Get close. Show the texture of the raw clay body.
+
+
+ 5. Editing: Less is More
+
+ You don't need Photoshop. Free apps like Snapseed or Lightroom Mobile are powerful tools for editing Product Photography for Pottery .
+
+
+ Correction, not Alteration : Adjust brightness, contrast, and white balance.
+ True-to-Life Color : Be very careful not to over-saturate.
+
+
+ );
+};
+
+export default ProductPhotography;
diff --git a/Pottery-website/pages/MockPayment.tsx b/Pottery-website/pages/MockPayment.tsx
new file mode 100644
index 0000000..b4f65da
--- /dev/null
+++ b/Pottery-website/pages/MockPayment.tsx
@@ -0,0 +1,77 @@
+import React, { useState } from 'react';
+import { motion } from 'framer-motion';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useStore } from '../src/context/StoreContext';
+
+const MockPayment: React.FC = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { placeOrder, clearCart } = useStore();
+ const [isSimulating, setIsSimulating] = useState(false);
+
+ const orderData = location.state?.orderData;
+
+ const handleSimulatePayment = async () => {
+ if (!orderData) return;
+
+ setIsSimulating(true);
+ // Simulate network delay
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ try {
+ await placeOrder(orderData);
+ clearCart();
+ navigate('/success');
+ } catch (err) {
+ alert('Payment simulation failed');
+ setIsSimulating(false);
+ }
+ };
+
+ if (!orderData) {
+ return (
+
+
No order data found. Please go back to checkout.
+
navigate('/checkout')} className="mt-4 underline">Back to Checkout
+
+ );
+ }
+
+ return (
+
+
+
account_balance_wallet
+
Payment Simulation
+
+ This is a secure test environment. Click the button below to simulate a successful transaction of
+ ${orderData.total_amount.toFixed(2)} .
+
+
+
+ {isSimulating ? (
+ <>
+
+ Processing...
+ >
+ ) : (
+ 'Simulate Success'
+ )}
+
+
+
navigate('/checkout')}
+ disabled={isSimulating}
+ className="mt-6 text-[10px] text-stone-400 uppercase tracking-widest hover:text-stone-600 transition-colors"
+ >
+ Cancel and go back
+
+
+
+ );
+};
+
+export default MockPayment;
diff --git a/Pottery-website/pages/ProductDetail.tsx b/Pottery-website/pages/ProductDetail.tsx
new file mode 100644
index 0000000..0ec0ab2
--- /dev/null
+++ b/Pottery-website/pages/ProductDetail.tsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { useParams, Link } from 'react-router-dom';
+import { motion } from 'framer-motion';
+import { useStore } from '../src/context/StoreContext';
+
+const ProductDetail: React.FC = () => {
+ const { slug } = useParams<{ slug: string }>();
+ const { products, addToCart } = useStore();
+ const product = products.find(item => item.slug === slug);
+
+ if (!product) {
+ return (
+
+
Product Not Found
+ Return to Shop
+
+ );
+ }
+
+ return (
+
+
+ {/* Breadcrumb */}
+
+ Shop
+ /
+ {product.title}
+
+
+
+ {/* Images Column */}
+
+
+
+
+ {/* Additional images grid */}
+
+ {product.images?.slice(1).map((img, idx) => (
+
+
+
+ ))}
+
+
+
+ {/* Info Column */}
+
+
+ {product.title}
+
+
+
+ ${product.price}
+
+
+
+ {product.description}
+
+ {product.details && product.details.map((detail, i) => (
+ {detail}
+ ))}
+ Made in Corpus Christi, TX
+
+
+
+
addToCart(product)}
+ type="button"
+ className="w-full bg-text-main dark:bg-white text-white dark:text-text-main py-4 uppercase tracking-[0.2em] hover:bg-stone-800 dark:hover:bg-stone-200 transition-colors"
+ >
+ Add to Cart
+
+
+
+
Free shipping on orders over $150
+
Ships within 3-5 business days
+
+
+
+
+
+ );
+};
+
+export default ProductDetail;
diff --git a/Pottery-website/pages/Success.tsx b/Pottery-website/pages/Success.tsx
new file mode 100644
index 0000000..4ca1db9
--- /dev/null
+++ b/Pottery-website/pages/Success.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import { Link } from 'react-router-dom';
+
+const Success: React.FC = () => {
+ return (
+
+
+ check_circle
+
+
+
+ Thank You
+
+
+
+ Your order has been placed successfully. We've sent a confirmation email with all the details of your purchase.
+
+
+
+
+ Back to Home
+
+
+
+ );
+};
+
+export default Success;
diff --git a/Pottery-website/public/assets/images/packaging_guide.png b/Pottery-website/public/assets/images/packaging_guide.png
new file mode 100644
index 0000000..5a5869f
Binary files /dev/null and b/Pottery-website/public/assets/images/packaging_guide.png differ
diff --git a/Pottery-website/server/db_setup.js b/Pottery-website/server/db_setup.js
new file mode 100644
index 0000000..3d2adee
--- /dev/null
+++ b/Pottery-website/server/db_setup.js
@@ -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();
diff --git a/Pottery-website/server/index.js b/Pottery-website/server/index.js
new file mode 100644
index 0000000..42309bd
--- /dev/null
+++ b/Pottery-website/server/index.js
@@ -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}`);
+});
diff --git a/Pottery-website/server/migrate.js b/Pottery-website/server/migrate.js
new file mode 100644
index 0000000..a84f23c
--- /dev/null
+++ b/Pottery-website/server/migrate.js
@@ -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();
diff --git a/Pottery-website/server/package-lock.json b/Pottery-website/server/package-lock.json
new file mode 100644
index 0000000..47e034d
--- /dev/null
+++ b/Pottery-website/server/package-lock.json
@@ -0,0 +1,1361 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "server",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^17.2.3",
+ "express": "^5.2.1",
+ "pg": "^8.16.3"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.11"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.11",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
+ "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pg": {
+ "version": "8.16.3",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
+ "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.9.1",
+ "pg-pool": "^3.10.1",
+ "pg-protocol": "^1.10.3",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.2.7"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
+ "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
+ "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
+ "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
+ "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/Pottery-website/server/package.json b/Pottery-website/server/package.json
new file mode 100644
index 0000000..32540ab
--- /dev/null
+++ b/Pottery-website/server/package.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/Pottery-website/server/schema.sql b/Pottery-website/server/schema.sql
new file mode 100644
index 0000000..d65ed85
--- /dev/null
+++ b/Pottery-website/server/schema.sql
@@ -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
+);
diff --git a/Pottery-website/server/setup_log.txt b/Pottery-website/server/setup_log.txt
new file mode 100644
index 0000000..eb3fa27
Binary files /dev/null and b/Pottery-website/server/setup_log.txt differ
diff --git a/Pottery-website/server/test_db.js b/Pottery-website/server/test_db.js
new file mode 100644
index 0000000..f0bcdfa
--- /dev/null
+++ b/Pottery-website/server/test_db.js
@@ -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();
diff --git a/Pottery-website/src/context/StoreContext.tsx b/Pottery-website/src/context/StoreContext.tsx
new file mode 100644
index 0000000..5c4a9b9
--- /dev/null
+++ b/Pottery-website/src/context/StoreContext.tsx
@@ -0,0 +1,299 @@
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+import { CollectionItem, JournalEntry } from '../../types';
+import { COLLECTIONS, JOURNAL_ENTRIES } from '../../constants';
+
+export interface CartItem extends CollectionItem {
+ quantity: number;
+}
+
+export interface Order {
+ id: number;
+ customer_email: string;
+ customer_name: string;
+ shipping_address: any;
+ items: any[];
+ total_amount: number;
+ payment_status: string;
+ shipping_status: string;
+ created_at: string;
+}
+
+interface StoreContextType {
+ products: CollectionItem[];
+ articles: JournalEntry[];
+ cart: CartItem[];
+ orders: Order[];
+ isCartOpen: boolean;
+ setCartOpen: (open: boolean) => void;
+ addToCart: (product: CollectionItem) => void;
+ removeFromCart: (productId: number) => void;
+ updateQuantity: (productId: number, quantity: number) => void;
+ clearCart: () => void;
+ placeOrder: (orderData: Partial
) => Promise;
+ fetchOrders: () => Promise;
+ updateOrderStatus: (id: number, status: string) => Promise;
+ addProduct: (product: CollectionItem) => void;
+ updateProduct: (product: CollectionItem) => void;
+ deleteProduct: (id: number) => void;
+ addArticle: (article: JournalEntry) => void;
+ updateArticle: (article: JournalEntry) => void;
+ deleteArticle: (id: number) => void;
+}
+
+const StoreContext = createContext(undefined);
+
+const API_URL = 'http://localhost:5000/api';
+
+export const StoreProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+ const [products, setProducts] = useState([]);
+ const [articles, setArticles] = useState([]);
+ const [orders, setOrders] = useState([]);
+ const [cart, setCart] = useState([]);
+ const [isCartOpen, setCartOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Persist cart (minimal data only)
+ React.useEffect(() => {
+ try {
+ const minimalCart = cart.map(item => ({ id: item.id, quantity: item.quantity }));
+ localStorage.setItem('cart', JSON.stringify(minimalCart));
+ } catch (err) {
+ console.error('Failed to save cart to localStorage:', err);
+ }
+ }, [cart]);
+
+ // Hydrate cart when products are loaded
+ React.useEffect(() => {
+ if (!isLoading && products.length > 0) {
+ const saved = localStorage.getItem('cart');
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ // Handle both old format (full objects) and new format (minimal)
+ const hydrated = parsed.map((m: any) => {
+ const productId = typeof m === 'object' && m !== null ? (m.id || m.productId) : m;
+ const product = products.find(p => p.id === productId);
+ if (product) {
+ return { ...product, quantity: m.quantity || 1 };
+ }
+ return null;
+ }).filter(Boolean) as CartItem[];
+ setCart(hydrated);
+ } catch (err) {
+ console.error('Failed to hydrate cart:', err);
+ }
+ }
+ }
+ }, [isLoading, products]);
+
+ // Initial Fetch
+ React.useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [prodRes, artRes] = await Promise.all([
+ fetch(`${API_URL}/products`),
+ fetch(`${API_URL}/articles`)
+ ]);
+ const prods = await prodRes.json();
+ const arts = await artRes.json();
+ setProducts(prods);
+ setArticles(arts);
+ } catch (err) {
+ console.error('Failed to fetch data from backend, falling back to static data', err);
+ setProducts(COLLECTIONS);
+ setArticles(JOURNAL_ENTRIES);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchData();
+ }, []);
+
+ // Cart Actions
+ const addToCart = (product: CollectionItem) => {
+ setCart(prev => {
+ const existing = prev.find(item => item.id === product.id);
+ if (existing) {
+ return prev.map(item =>
+ item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
+ );
+ }
+ return [...prev, { ...product, quantity: 1 }];
+ });
+ setCartOpen(true);
+ };
+
+ const removeFromCart = (productId: number) => {
+ setCart(prev => prev.filter(item => item.id !== productId));
+ };
+
+ const updateQuantity = (productId: number, quantity: number) => {
+ if (quantity < 1) {
+ removeFromCart(productId);
+ return;
+ }
+ setCart(prev => prev.map(item =>
+ item.id === productId ? { ...item, quantity } : item
+ ));
+ };
+
+ const clearCart = () => setCart([]);
+
+ // Order Actions
+ const fetchOrders = async () => {
+ try {
+ const res = await fetch(`${API_URL}/orders`);
+ const data = await res.json();
+ setOrders(data);
+ } catch (err) {
+ console.error('Failed to fetch orders:', err);
+ }
+ };
+
+ const placeOrder = async (orderData: Partial) => {
+ try {
+ const res = await fetch(`${API_URL}/orders`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(orderData)
+ });
+ const newOrder = await res.json();
+ setOrders(prev => [newOrder, ...prev]);
+ return newOrder;
+ } catch (err) {
+ console.error('Failed to place order:', err);
+ throw err;
+ }
+ };
+
+ const updateOrderStatus = async (id: number, status: string) => {
+ try {
+ const res = await fetch(`${API_URL}/orders/${id}/status`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ shipping_status: status })
+ });
+ const updatedOrder = await res.json();
+ setOrders(prev => prev.map(o => o.id === id ? updatedOrder : o));
+ } catch (err) {
+ console.error('Failed to update order status:', err);
+ }
+ };
+
+ const addProduct = async (product: CollectionItem) => {
+ try {
+ const res = await fetch(`${API_URL}/products`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(product)
+ });
+ const newProduct = await res.json();
+ setProducts(prev => [...prev, newProduct]);
+ } catch (err) {
+ console.error('Failed to add product:', err);
+ }
+ };
+
+ const updateProduct = async (updatedProduct: CollectionItem) => {
+ try {
+ const res = await fetch(`${API_URL}/products/${updatedProduct.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updatedProduct)
+ });
+ const data = await res.json();
+ setProducts(prev => prev.map(p => p.id === data.id ? data : p));
+ } catch (err) {
+ console.error('Failed to update product:', err);
+ }
+ };
+
+ const deleteProduct = async (id: number) => {
+ try {
+ await fetch(`${API_URL}/products/${id}`, { method: 'DELETE' });
+ setProducts(prev => prev.filter(p => p.id !== id));
+ } catch (err) {
+ console.error('Failed to delete product:', err);
+ }
+ };
+
+ const addArticle = async (article: JournalEntry) => {
+ try {
+ const res = await fetch(`${API_URL}/articles`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(article)
+ });
+ const newArticle = await res.json();
+ setArticles(prev => [...prev, newArticle]);
+ } catch (err) {
+ console.error('Failed to add article:', err);
+ }
+ };
+
+ const updateArticle = async (updatedArticle: JournalEntry) => {
+ try {
+ const res = await fetch(`${API_URL}/articles/${updatedArticle.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updatedArticle)
+ });
+ const data = await res.json();
+
+ // If the updated article is featured, we must ensure only this one is featured in our local state
+ if (data.isFeatured) {
+ setArticles(prev => prev.map(a => ({
+ ...a,
+ isFeatured: a.id === data.id
+ })));
+ } else {
+ setArticles(prev => prev.map(a => a.id === data.id ? data : a));
+ }
+ } catch (err) {
+ console.error('Failed to update article:', err);
+ }
+ };
+
+ const deleteArticle = async (id: number) => {
+ try {
+ await fetch(`${API_URL}/articles/${id}`, { method: 'DELETE' });
+ setArticles(prev => prev.filter(a => a.id !== id));
+ } catch (err) {
+ console.error('Failed to delete article:', err);
+ }
+ };
+
+ return (
+
+ {isLoading ? Loading Store...
: children}
+
+ );
+};
+
+export const useStore = () => {
+ const context = useContext(StoreContext);
+ if (context === undefined) {
+ throw new Error('useStore must be used within a StoreProvider');
+ }
+ return context;
+};
diff --git a/Pottery-website/types.ts b/Pottery-website/types.ts
index fa88e11..55ab50f 100644
--- a/Pottery-website/types.ts
+++ b/Pottery-website/types.ts
@@ -6,20 +6,30 @@ export interface NavItem {
export interface CollectionItem {
id: number;
title: string;
- number: string;
+ price: number;
image: string;
- aspectRatio: string; // Tailwind class like aspect-[3/4]
- gridClasses?: string; // Optional layout adjustments
+ images: string[];
+ description?: string;
+ slug: string;
+ number: string;
+ aspectRatio: string;
+ details?: string[];
}
export interface JournalEntry {
id: number;
- category: string;
- date: string;
title: string;
- description: string;
+ date: string;
image: string;
- marginTop?: boolean;
+ sections: {
+ id: string;
+ type: 'text' | 'image';
+ content: string;
+ }[];
+ slug: string;
+ category?: string;
+ description?: string;
+ isFeatured?: boolean;
}
export interface FooterSection {
diff --git a/parallax-demo/generate_embedded.py b/parallax-demo/generate_embedded.py
new file mode 100644
index 0000000..ab357ca
--- /dev/null
+++ b/parallax-demo/generate_embedded.py
@@ -0,0 +1,43 @@
+
+import base64
+import os
+
+def get_base64_src(path, mime_type):
+ with open(path, "rb") as f:
+ data = f.read()
+ return f"data:{mime_type};base64,{base64.b64encode(data).decode('utf-8')}"
+
+try:
+ # 1. Read Assets
+ workshop_b64 = get_base64_src("workshop.jpg", "image/jpeg")
+ depth_b64 = get_base64_src("workshop_depth.png", "image/png")
+ vase_b64 = get_base64_src("pottery-vase.png", "image/png")
+ try:
+ vase_depth_b64 = get_base64_src("pottery-vase_depth.png", "image/png")
+ except FileNotFoundError:
+ print("Warning: pottery-vase_depth.png not found. Using placeholder or skipping.")
+ vase_depth_b64 = depth_b64 # Fallback to something to avoid crash
+
+ # 2. Read HTML Template
+ with open("index.html", "r", encoding="utf-8") as f:
+ html = f.read()
+
+ # Remove the protocol check block for the embedded version
+ # because embedded base64 images work fine on file:// protocol
+ import re
+ html = re.sub(r'// CHECK PROTOCOL[\s\S]*?return;\s+?}', '', html)
+
+ # 3. Replace with Base64
+ html = html.replace("workshop.jpg", workshop_b64)
+ html = html.replace("workshop_depth.png", depth_b64)
+ html = html.replace("pottery-vase.png", vase_b64)
+ html = html.replace("pottery-vase_depth.png", vase_depth_b64)
+
+ # 4. Write new file
+ with open("index_embedded.html", "w", encoding="utf-8") as f:
+ f.write(html)
+
+ print("Successfully created index_embedded.html")
+
+except Exception as e:
+ print(f"Error: {e}")
diff --git a/parallax-demo/generate_vase_depth.py b/parallax-demo/generate_vase_depth.py
new file mode 100644
index 0000000..a5a8983
--- /dev/null
+++ b/parallax-demo/generate_vase_depth.py
@@ -0,0 +1,42 @@
+import numpy as np
+from PIL import Image
+
+def create_cylindrical_depth_map(image_path, output_path):
+ # Load image
+ img = Image.open(image_path).convert("RGBA")
+ width, height = img.size
+
+ # Create a numpy array for the depth map
+ # We want a gradient that is white in the center horizontal axis and black at the edges (cylindrical)
+ # 0 = far (black), 255 = near (white)
+
+ # Generate X coordinates (0 to width)
+ x = np.linspace(-1, 1, width)
+ # Compute cylindrical depth: sqrt(1 - x^2) for a perfect cylinder, or just a cosine/parabolic falloff
+ # Let's use cosine for smooth roundness: cos(x * pi / 2)
+ depth_profile = np.cos(x * np.pi / 2) # Center (0) is 1, Edges (-1, 1) are 0
+
+ # Normalize to 0-255
+ depth_profile = (depth_profile * 255).astype(np.uint8)
+
+ # Tile vertically to create the full map
+ depth_map = np.tile(depth_profile, (height, 1))
+
+ # Create Image from array
+ depth_img = Image.fromarray(depth_map, mode='L')
+
+ # MASKING: We only want the vase to have depth, the background (transparent) should be flat/far.
+ # Use the alpha channel of the original image as a mask
+ alpha = np.array(img.split()[-1])
+
+ # Where alpha is 0 (background), set depth to 0 (flat/far)
+ depth_array = np.array(depth_img)
+ depth_array[alpha < 10] = 0 # Threshold for transparency
+
+ # Save
+ final_depth = Image.fromarray(depth_array)
+ final_depth.save(output_path)
+ print(f"Generated depth map: {output_path}")
+
+if __name__ == "__main__":
+ create_cylindrical_depth_map("pottery-vase.png", "pottery-vase_depth.png")
diff --git a/parallax-demo/index.html b/parallax-demo/index.html
new file mode 100644
index 0000000..73d7ee7
--- /dev/null
+++ b/parallax-demo/index.html
@@ -0,0 +1,303 @@
+
+
+
+
+
+ 3D Parallax Workshop Demo
+
+
+
+
+
+
+
+
+ Scroll Down
+ Experience the journey from the workshop to the finished form.
+
+
+
+
+
+
+
+
+
+
+
+
+ Collection 014
+ Every piece tells a story.
+
+
+ Loading Assets...
+
+
+
+
diff --git a/parallax-demo/index_embedded.html b/parallax-demo/index_embedded.html
new file mode 100644
index 0000000..d25493d
--- /dev/null
+++ b/parallax-demo/index_embedded.html
@@ -0,0 +1,291 @@
+
+
+
+
+
+ 3D Parallax Workshop Demo
+
+
+
+
+
+
+
+
+ Scroll Down
+ Experience the journey from the workshop to the finished form.
+
+
+
+
+
+
+
+
+
+
+
+
+ Collection 014
+ Every piece tells a story.
+
+
+ Loading Assets...
+
+
+
+
diff --git a/parallax-demo/pottery-vase.png b/parallax-demo/pottery-vase.png
new file mode 100644
index 0000000..17e19f2
Binary files /dev/null and b/parallax-demo/pottery-vase.png differ
diff --git a/parallax-demo/pottery-vase_depth.png b/parallax-demo/pottery-vase_depth.png
new file mode 100644
index 0000000..8c91f52
Binary files /dev/null and b/parallax-demo/pottery-vase_depth.png differ
diff --git a/parallax-demo/workshop.jpg b/parallax-demo/workshop.jpg
new file mode 100644
index 0000000..a594344
Binary files /dev/null and b/parallax-demo/workshop.jpg differ
diff --git a/parallax-demo/workshop_depth.png b/parallax-demo/workshop_depth.png
new file mode 100644
index 0000000..cf0e941
Binary files /dev/null and b/parallax-demo/workshop_depth.png differ
diff --git a/product-scroll-poc/fragmentShader.glsl b/product-scroll-poc/fragmentShader.glsl
new file mode 100644
index 0000000..bf73b92
--- /dev/null
+++ b/product-scroll-poc/fragmentShader.glsl
@@ -0,0 +1,10 @@
+
+varying vec2 vUv;
+varying float vDisplacement;
+
+uniform sampler2D tImage;
+
+void main() {
+ vec4 color = texture2D(tImage, vUv);
+ gl_FragColor = color;
+}
diff --git a/product-scroll-poc/index.html b/product-scroll-poc/index.html
index 9fb2629..9cef929 100644
--- a/product-scroll-poc/index.html
+++ b/product-scroll-poc/index.html
@@ -89,8 +89,21 @@