diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..69493eb --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy to Vercel + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Deploy to Vercel + uses: amondnet/vercel-action@v25 + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.ORG_ID }} + vercel-project-id: ${{ secrets.PROJECT_ID }} + vercel-args: '--prod' \ No newline at end of file diff --git a/.gitignore b/.gitignore index f9ba7f8..a2da912 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,11 @@ dist .DS_Store server/public vite.config.ts.* -*.tar.gz \ No newline at end of file +*.tar.gz + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7919df4 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# Pilates with Fadia + +A modern Pilates studio website built with React, TypeScript, and Express. + +## Features + +- 🧘‍♀️ Class information and details +- 📧 Contact form and newsletter signup +- 📱 Responsive design +- 🎨 Modern UI with Tailwind CSS +- 📸 Instagram feed integration (via Curator.io) +- 🔐 User authentication (in-memory storage) + +## Tech Stack + +- **Frontend**: React, TypeScript, Vite, Tailwind CSS +- **Backend**: Express.js, Node.js +- **Storage**: In-memory storage (no database required) +- **Deployment**: Vercel + +## Development + +### Prerequisites + +- Node.js 20+ +- npm or yarn + +### Setup + +1. **Clone the repository** + ```bash + git clone + cd pwf-website-new + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **Set up environment variables** + + **For Local Development:** + + Create a `.env` file in the root directory: + ```bash + cp env.example .env + ``` + + Then edit `.env` with your actual values: + ```env + NODE_ENV=development + SESSION_SECRET=your_secure_session_secret + MAILCHIMP_API_KEY=your_mailchimp_key + MAILCHIMP_SERVER_PREFIX=your_mailchimp_server_prefix + MAILCHIMP_LIST_ID=your_mailchimp_list_id + ``` + + **Getting the values:** + + - **SESSION_SECRET**: Generate a random string (e.g., `openssl rand -base64 32`) + - **MAILCHIMP_API_KEY**: Get from Mailchimp Account → Extras → API Keys + - **MAILCHIMP_SERVER_PREFIX**: The part after the dash in your API key (e.g., if key is `abc123-us1`, server prefix is `us1`) + - **MAILCHIMP_LIST_ID**: Get from Mailchimp Audience → Settings → Audience name and defaults + +4. **Run the development server** + ```bash + npm run dev + ``` + + The app will be available at `http://localhost:5000` + +## Deployment to Vercel + +### Option 1: Automatic Deployment (Recommended) + +1. **Connect your GitHub repository to Vercel** + - Go to [vercel.com](https://vercel.com) + - Sign up/login with GitHub + - Click "New Project" + - Import your repository + +2. **Configure environment variables in Vercel** + - Go to your project settings in Vercel + - Navigate to **Settings** → **Environment Variables** + - Add each variable: + ``` + NODE_ENV = production + SESSION_SECRET = your_secure_session_secret + MAILCHIMP_API_KEY = your_mailchimp_key + MAILCHIMP_SERVER_PREFIX = your_mailchimp_server_prefix + MAILCHIMP_LIST_ID = your_mailchimp_list_id + ``` + - Select **Production**, **Preview**, and **Development** environments + - Click **Save** + +3. **Deploy** + - Vercel will automatically deploy on every push to main branch + +### Option 2: Manual Deployment with GitHub Actions + +1. **Get Vercel tokens** + - Install Vercel CLI: `npm i -g vercel` + - Run `vercel login` + - Get your tokens from Vercel dashboard + +2. **Add GitHub secrets** + - Go to your GitHub repository settings + - Add these secrets: + - `VERCEL_TOKEN`: Your Vercel token + - `ORG_ID`: Your Vercel organization ID + - `PROJECT_ID`: Your Vercel project ID + +3. **Push to main branch** + - The GitHub Action will automatically deploy to Vercel + +### Environment Variables for Production + +Make sure to set these in your Vercel project settings: + +```env +NODE_ENV=production +SESSION_SECRET=your_secure_session_secret +MAILCHIMP_API_KEY=your_mailchimp_key +MAILCHIMP_SERVER_PREFIX=your_mailchimp_server_prefix +MAILCHIMP_LIST_ID=your_mailchimp_list_id +``` + +## Project Structure + +``` +├── client/ # React frontend +│ ├── src/ +│ │ ├── components/ # React components +│ │ ├── pages/ # Page components +│ │ ├── hooks/ # Custom hooks +│ │ └── lib/ # Utilities and config +│ └── dist/ # Built frontend (generated) +├── server/ # Express backend +│ ├── routes.ts # API routes +│ ├── auth.ts # Authentication +│ ├── storage.ts # In-memory storage +│ └── index.ts # Server entry point +├── vercel.json # Vercel configuration +├── env.example # Environment variables template +└── package.json # Dependencies and scripts +``` + +## API Endpoints + +- `GET /api/classes` - Get all classes +- `POST /api/newsletter` - Subscribe to newsletter +- `POST /api/contact` - Send contact form + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## License + +MIT License \ No newline at end of file diff --git a/client/index.html b/client/index.html index 8894480..2082aff 100644 --- a/client/index.html +++ b/client/index.html @@ -2,21 +2,36 @@ - - Pilates with Fadia | Feel at Home in Your Body - - - - + + + + + + + - - - + + + + + + + + + + + + + + + + + + + Pilates with Fadia
- - diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..2c339d5 --- /dev/null +++ b/client/package.json @@ -0,0 +1,70 @@ +{ + "name": "pilates-with-fadia-client", + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "wouter": "^3.3.5", + "@tanstack/react-query": "^5.60.5", + "axios": "^1.9.0", + "react-hook-form": "^7.55.0", + "@hookform/resolvers": "^3.10.0", + "zod": "^3.24.2", + "lucide-react": "^0.453.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "date-fns": "^3.6.0", + "react-day-picker": "^8.10.1", + "framer-motion": "^11.13.1", + "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-alert-dialog": "^1.1.7", + "@radix-ui/react-aspect-ratio": "^1.1.3", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-collapsible": "^1.1.4", + "@radix-ui/react-context-menu": "^2.2.7", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-hover-card": "^1.1.7", + "@radix-ui/react-label": "^2.1.3", + "@radix-ui/react-menubar": "^1.1.7", + "@radix-ui/react-navigation-menu": "^1.2.6", + "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-progress": "^1.1.3", + "@radix-ui/react-radio-group": "^1.2.4", + "@radix-ui/react-scroll-area": "^1.2.4", + "@radix-ui/react-select": "^2.1.7", + "@radix-ui/react-separator": "^1.1.3", + "@radix-ui/react-slider": "^1.2.4", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.4", + "@radix-ui/react-toast": "^1.2.7", + "@radix-ui/react-toggle": "^1.1.3", + "@radix-ui/react-toggle-group": "^1.1.3", + "@radix-ui/react-tooltip": "^1.2.0", + "cmdk": "^1.1.1", + "embla-carousel-react": "^8.6.0", + "vaul": "^1.1.2", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.15.2" + }, + "devDependencies": { + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.2", + "vite": "^5.4.14", + "typescript": "5.6.3", + "tailwindcss": "^3.4.17", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47" + } +} \ No newline at end of file diff --git a/client/src/lib/static-data.ts b/client/src/lib/static-data.ts new file mode 100644 index 0000000..9e9e791 --- /dev/null +++ b/client/src/lib/static-data.ts @@ -0,0 +1,53 @@ +export interface StaticClass { + id: number; + name: string; + description: string; + duration: number; + price: number; + capacity: number; + classType: "group" | "small-group" | "private" | "online"; + imageUrl: string; +} + +export const STATIC_CLASSES: StaticClass[] = [ + { + id: 1, + name: "Mat Pilates", + description: "A foundational class focusing on core strength, proper alignment, and mindful movement patterns.", + duration: 60, + price: 2500, // $25.00 + capacity: 15, + classType: "group", + imageUrl: "https://images.unsplash.com/photo-1571902943202-507ec2618e8f" + }, + { + id: 2, + name: "Reformer Classes", + description: "Equipment-based sessions that enhance resistance training for deeper muscle engagement.", + duration: 55, + price: 4000, // $40.00 + capacity: 8, + classType: "small-group", + imageUrl: "https://images.unsplash.com/photo-1562088287-bde35a1ea917" + }, + { + id: 3, + name: "Private Sessions", + description: "Personalized attention and customized programming to meet your specific goals and needs.", + duration: 60, + price: 7500, // $75.00 + capacity: 1, + classType: "private", + imageUrl: "https://images.unsplash.com/photo-1616279969856-759f316a5ac1" + }, + { + id: 4, + name: "Online Classes", + description: "Practice pilates from the comfort of your own home or wherever you happen to be with our convenient online sessions.", + duration: 50, + price: 2000, // $20.00 + capacity: 20, + classType: "online", + imageUrl: "https://images.unsplash.com/photo-1518611012118-696072aa579a" + } +]; \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index 4cd7b4c..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -if (!process.env.DATABASE_URL) { - throw new Error("DATABASE_URL, ensure the database is provisioned"); -} - -export default defineConfig({ - out: "./migrations", - schema: "./shared/schema.ts", - dialect: "postgresql", - dbCredentials: { - url: process.env.DATABASE_URL, - }, -}); diff --git a/env.example b/env.example new file mode 100644 index 0000000..8b9d104 --- /dev/null +++ b/env.example @@ -0,0 +1,14 @@ +# Environment Variables for Pilates with Fadia +# Copy this file to .env and fill in your actual values + +# Node Environment +NODE_ENV=development + +# Session Secret (generate a random string for security) +SESSION_SECRET=your_secure_session_secret_here + +# Mailchimp Configuration +# Get these from your Mailchimp account settings +MAILCHIMP_API_KEY=your_mailchimp_api_key_here +MAILCHIMP_SERVER_PREFIX=your_mailchimp_server_prefix_here +MAILCHIMP_LIST_ID=your_mailchimp_list_id_here \ No newline at end of file diff --git a/package.json b/package.json index 28c107c..d85a009 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "name": "rest-express", + "name": "pilates-with-fadia", "version": "1.0.0", "type": "module", "license": "MIT", "scripts": { "dev": "NODE_ENV=development tsx server/index.ts", - "build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist", + "build": "npm run build:client", + "build:client": "cd client && vite build", + "build:server": "esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist", "start": "NODE_ENV=production node dist/index.js", - "check": "tsc", - "db:push": "drizzle-kit push" + "check": "tsc" }, "dependencies": { "@hookform/resolvers": "^3.10.0", "@jridgewell/trace-mapping": "^0.3.25", - "@neondatabase/serverless": "^0.10.4", "@radix-ui/react-accordion": "^1.2.4", "@radix-ui/react-alert-dialog": "^1.1.7", "@radix-ui/react-aspect-ratio": "^1.1.3", @@ -41,18 +41,13 @@ "@radix-ui/react-toggle": "^1.1.3", "@radix-ui/react-toggle-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.2.0", - "@sendgrid/mail": "^8.1.5", "@tailwindcss/vite": "^4.1.3", "@tanstack/react-query": "^5.60.5", - "@types/nodemailer": "^6.4.17", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "connect-pg-simple": "^10.0.0", "date-fns": "^3.6.0", - "drizzle-orm": "^0.39.1", - "drizzle-zod": "^0.7.0", "embla-carousel-react": "^8.6.0", "express": "^4.21.2", "express-session": "^1.18.1", @@ -61,7 +56,6 @@ "lucide-react": "^0.453.0", "memorystore": "^1.6.7", "next-themes": "^0.4.6", - "nodemailer": "^7.0.3", "passport": "^0.7.0", "passport-local": "^1.0.0", "react": "^18.3.1", @@ -81,10 +75,7 @@ "zod-validation-error": "^3.4.0" }, "devDependencies": { - "@replit/vite-plugin-cartographer": "^0.2.7", - "@replit/vite-plugin-runtime-error-modal": "^0.0.3", "@tailwindcss/typography": "^0.5.15", - "@types/connect-pg-simple": "^7.0.3", "@types/express": "4.17.21", "@types/express-session": "^1.18.0", "@types/node": "20.16.11", @@ -95,7 +86,6 @@ "@types/ws": "^8.5.13", "@vitejs/plugin-react": "^4.3.2", "autoprefixer": "^10.4.20", - "drizzle-kit": "^0.30.4", "esbuild": "^0.25.0", "postcss": "^8.4.47", "tailwindcss": "^3.4.17", diff --git a/server/auth.ts b/server/auth.ts index 255db73..e9ffd90 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -4,12 +4,17 @@ import { Express } from "express"; import session from "express-session"; import { scrypt, randomBytes, timingSafeEqual } from "crypto"; import { promisify } from "util"; -import { storage } from "./storage"; -import { User as SelectUser, insertUserSchema } from "@shared/schema"; +import { storage, User } from "./storage"; declare global { namespace Express { - interface User extends SelectUser {} + interface User { + id: number; + username: string; + email: string; + fullName?: string; + createdAt: Date; + } } } @@ -21,11 +26,10 @@ async function hashPassword(password: string) { return `${buf.toString("hex")}.${salt}`; } -async function comparePasswords(supplied: string, stored: string) { - const [hashed, salt] = stored.split("."); - const hashedBuf = Buffer.from(hashed, "hex"); - const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer; - return timingSafeEqual(hashedBuf, suppliedBuf); +async function comparePasswords(password: string, hashedPassword: string) { + const [hash, salt] = hashedPassword.split("."); + const buf = (await scryptAsync(password, salt, 64)) as Buffer; + return timingSafeEqual(buf, Buffer.from(hash, "hex")); } export function setupAuth(app: Express) { @@ -66,23 +70,34 @@ export function setupAuth(app: Express) { app.post("/api/register", async (req, res, next) => { try { - const validatedUser = insertUserSchema.parse(req.body); + const { username, email, password, fullName } = req.body; + + // Basic validation + if (!username || !email || !password) { + return res.status(400).json({ message: "Username, email, and password are required" }); + } + + if (password.length < 8) { + return res.status(400).json({ message: "Password must be at least 8 characters" }); + } // Check if username already exists - const existingUserByUsername = await storage.getUserByUsername(validatedUser.username); + const existingUserByUsername = await storage.getUserByUsername(username); if (existingUserByUsername) { return res.status(400).json({ message: "Username already exists" }); } // Check if email already exists - const existingUserByEmail = await storage.getUserByEmail(validatedUser.email); + const existingUserByEmail = await storage.getUserByEmail(email); if (existingUserByEmail) { return res.status(400).json({ message: "Email already in use" }); } const user = await storage.createUser({ - ...validatedUser, - password: await hashPassword(validatedUser.password), + username, + email, + password: await hashPassword(password), + fullName }); req.login(user, (err) => { @@ -98,7 +113,7 @@ export function setupAuth(app: Express) { }); app.post("/api/login", (req, res, next) => { - passport.authenticate("local", (err: Error, user: SelectUser) => { + passport.authenticate("local", (err: Error, user: User) => { if (err) return next(err); if (!user) { return res.status(401).json({ message: "Invalid username or password" }); @@ -121,11 +136,12 @@ export function setupAuth(app: Express) { }); }); - app.get("/api/user", (req, res) => { - if (!req.isAuthenticated()) return res.sendStatus(401); - - // Return user without password - const { password, ...userWithoutPassword } = req.user; - res.json(userWithoutPassword); + app.get("/api/me", (req, res) => { + if (req.isAuthenticated()) { + const { password, ...userWithoutPassword } = req.user as User; + res.json(userWithoutPassword); + } else { + res.status(401).json({ message: "Not authenticated" }); + } }); } diff --git a/server/contact-email.ts b/server/contact-email.ts deleted file mode 100644 index 2049f95..0000000 --- a/server/contact-email.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * This file handles sending contact form submissions to hello@pilateswithfadia.com - * We're using the Mailchimp API which is already configured - */ - -import axios from 'axios'; - -interface ContactFormData { - name: string; - email: string; - subject?: string; - message: string; -} - -export async function sendContactEmail(data: ContactFormData): Promise { - try { - // Log the contact request (without sensitive data) - console.log(`Processing contact form submission from ${data.name}`); - - // Create HTML content for the email - const htmlContent = ` -
-

New Message from Pilates with Fadia Website

-

From: ${data.name} (${data.email})

-

Subject: ${data.subject || "No subject"}

-
-

${data.message}

-
-

- This message was sent from the contact form on your Pilates with Fadia website. -

-
- `; - - // For now, we'll just return true to simulate success - // In the future, we can integrate with a transactional email API - console.log('Contact form processed successfully, would send to hello@pilateswithfadia.com'); - - // Store the message in the database, so nothing is lost - return true; - - } catch (error) { - console.error('Error processing contact form submission:', error); - return false; - } -} \ No newline at end of file diff --git a/server/email.ts b/server/email.ts deleted file mode 100644 index 4fdbadd..0000000 --- a/server/email.ts +++ /dev/null @@ -1,43 +0,0 @@ -import nodemailer from 'nodemailer'; - -interface EmailOptions { - to: string; - from: string; - subject: string; - text: string; - html?: string; -} - -// Log email configuration (without password) -console.log(`Email configuration: Using ${process.env.EMAIL_USER} to send emails`); - -// Create a transporter -const transporter = nodemailer.createTransport({ - service: 'gmail', // Use predefined settings for Gmail - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASSWORD, // This needs to be an app password for Gmail - } -}); - -export async function sendEmail(options: EmailOptions): Promise { - try { - // Set sender if not specified - const mailOptions = { - from: options.from || `"Pilates with Fadia" <${process.env.EMAIL_USER}>`, - to: options.to, - subject: options.subject, - text: options.text, - html: options.html, - replyTo: options.from // Set reply-to as the sender's email - }; - - // Send the email - const info = await transporter.sendMail(mailOptions); - console.log('Email sent:', info.messageId); - return true; - } catch (error) { - console.error('Error sending email:', error); - return false; - } -} \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index 8a9b9ff..b66b384 100644 --- a/server/index.ts +++ b/server/index.ts @@ -36,35 +36,36 @@ app.use((req, res, next) => { next(); }); +// Error handling middleware +app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { + const status = err.status || err.statusCode || 500; + const message = err.message || "Internal Server Error"; + + res.status(status).json({ message }); + throw err; +}); + +// Initialize routes +let server: any; + (async () => { - const server = await registerRoutes(app); + server = await registerRoutes(app); - app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { - const status = err.status || err.statusCode || 500; - const message = err.message || "Internal Server Error"; - - res.status(status).json({ message }); - throw err; - }); - - // importantly only setup vite in development and after - // setting up all the other routes so the catch-all route - // doesn't interfere with the other routes - if (app.get("env") === "development") { + // Setup Vite in development, serve static in production + if (process.env.NODE_ENV === "development") { await setupVite(app, server); } else { serveStatic(app); } - - // ALWAYS serve the app on port 5000 - // this serves both the API and the client. - // It is the only port that is not firewalled. - const port = 5000; - server.listen({ - port, - host: "0.0.0.0", - reusePort: true, - }, () => { - log(`serving on port ${port}`); - }); })(); + +// Vercel serverless function export +export default app; + +// Development server (only runs in development) +if (process.env.NODE_ENV === "development") { + const port = 5000; + app.listen(port, "0.0.0.0", () => { + log(`Development server running on port ${port}`); + }); +} diff --git a/server/routes.ts b/server/routes.ts index 9acc19f..14fb20b 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -3,14 +3,28 @@ import { createServer, type Server } from "http"; import { storage } from "./storage"; import { setupAuth } from "./auth"; import { subscribeToMailchimp } from "./mailchimp"; -import { sendEmail } from "./email"; -import { - insertNewsletterSchema, - insertContactMessageSchema, - insertBookingSchema -} from "@shared/schema"; import { z } from "zod"; +// Simple validation schemas (since we removed the shared schema) +const newsletterSchema = z.object({ + email: z.string().email(), + agreedToTerms: z.boolean() +}); + +const contactMessageSchema = z.object({ + name: z.string().min(2).max(100), + email: z.string().email(), + subject: z.string().min(2).max(200).optional(), + message: z.string().min(10).max(2000) +}); + +const bookingSchema = z.object({ + classId: z.number(), + date: z.string(), + paid: z.boolean().default(false), + status: z.string().default("pending") +}); + export async function registerRoutes(app: Express): Promise { // Set up authentication routes setupAuth(app); @@ -53,7 +67,8 @@ export async function registerRoutes(app: Express): Promise { } try { - const bookings = await storage.getBookings(req.user.id); + const user = req.user as any; + const bookings = await storage.getBookings(user.id); res.json(bookings); } catch (error) { res.status(500).json({ message: "Failed to fetch bookings" }); @@ -66,12 +81,14 @@ export async function registerRoutes(app: Express): Promise { } try { - const bookingData = insertBookingSchema.parse({ - ...req.body, - userId: req.user.id - }); + const user = req.user as any; + const bookingData = bookingSchema.parse(req.body); - const booking = await storage.createBooking(bookingData); + const booking = await storage.createBooking({ + ...bookingData, + userId: user.id, + date: new Date(bookingData.date) + }); res.status(201).json(booking); } catch (error) { if (error instanceof z.ZodError) { @@ -84,7 +101,7 @@ export async function registerRoutes(app: Express): Promise { // Newsletter signup app.post("/api/newsletter", async (req, res) => { try { - const newsletterData = insertNewsletterSchema.parse(req.body); + const newsletterData = newsletterSchema.parse(req.body); // Check if email already exists in our local database const existingNewsletter = await storage.getNewsletterByEmail(newsletterData.email); @@ -120,20 +137,19 @@ export async function registerRoutes(app: Express): Promise { // Contact form app.post("/api/contact", async (req, res) => { try { - const contactData = insertContactMessageSchema.parse(req.body); + const contactData = contactMessageSchema.parse(req.body); - // Always store in database first + // Store in database const message = await storage.createContactMessage(contactData); - // Add a success message with clear next steps for Fadia - console.log(`Contact form submission stored in database from ${contactData.name} (${contactData.email})`); + // Log the contact request + console.log(`Contact form submission from ${contactData.name} (${contactData.email})`); console.log(`Subject: ${contactData.subject || "No subject"}`); - console.log(`This message will be forwarded to hello@pilateswithfadia.com`); + console.log(`Message: ${contactData.message}`); - // Important information for the user in the response res.status(201).json({ message: "Message sent successfully", - info: "Your message has been received and will be sent to hello@pilateswithfadia.com" + info: "Your message has been received and will be reviewed shortly." }); } catch (error) { if (error instanceof z.ZodError) { @@ -143,44 +159,6 @@ export async function registerRoutes(app: Express): Promise { } }); - // Instagram Feed - app.get("/api/instagram-feed", async (_req, res) => { - try { - const instagramAccessToken = process.env.INSTAGRAM_ACCESS_TOKEN; - - if (!instagramAccessToken) { - return res.status(500).json({ - message: "Instagram access token not configured", - error: "INSTAGRAM_ACCESS_TOKEN environment variable is required" - }); - } - - // Fetch recent posts from Instagram Basic Display API - const response = await fetch( - `https://graph.instagram.com/me/media?fields=id,media_type,media_url,permalink,caption,timestamp&access_token=${instagramAccessToken}` - ); - - if (!response.ok) { - throw new Error(`Instagram API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - - // Filter out only images and videos, exclude carousels for simplicity - const posts = data.data?.filter((post: any) => - post.media_type === 'IMAGE' || post.media_type === 'VIDEO' - ).slice(0, 12) || []; // Limit to 12 most recent posts - - res.json(posts); - } catch (error) { - console.error('Instagram API error:', error); - res.status(500).json({ - message: "Failed to fetch Instagram posts", - error: error instanceof Error ? error.message : "Unknown error" - }); - } - }); - const httpServer = createServer(app); return httpServer; } diff --git a/server/storage.ts b/server/storage.ts index 0b79c3a..745204c 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -1,43 +1,82 @@ -import { users, classes, bookings, newsletters, contactMessages } from "@shared/schema"; -import type { - User, InsertUser, - Class, InsertClass, - Booking, InsertBooking, - Newsletter, InsertNewsletter, - ContactMessage, InsertContactMessage -} from "@shared/schema"; import session from "express-session"; import createMemoryStore from "memorystore"; const MemoryStore = createMemoryStore(session); +// Simple TypeScript interfaces for in-memory data +export interface User { + id: number; + username: string; + email: string; + password: string; + fullName?: string; + createdAt: Date; +} + +export interface Class { + id: number; + name: string; + description: string; + duration: number; + price: number; + capacity: number; + classType: string; + imageUrl?: string; +} + +export interface Booking { + id: number; + userId: number; + classId: number; + date: Date; + paid: boolean; + status: string; + createdAt: Date; +} + +export interface Newsletter { + id: number; + email: string; + agreedToTerms: boolean; + createdAt: Date; +} + +export interface ContactMessage { + id: number; + name: string; + email: string; + subject?: string; + message: string; + createdAt: Date; +} + export interface IStorage { // User Management getUser(id: number): Promise; getUserByUsername(username: string): Promise; getUserByEmail(email: string): Promise; - createUser(user: InsertUser): Promise; + createUser(user: Omit): Promise; // Class Management getClasses(): Promise; getClass(id: number): Promise; - createClass(classData: InsertClass): Promise; + createClass(classData: Omit): Promise; // Booking Management getBookings(userId?: number): Promise; getBooking(id: number): Promise; - createBooking(booking: InsertBooking): Promise; + createBooking(booking: Omit): Promise; updateBookingStatus(id: number, status: string): Promise; // Newsletter Management getNewsletterByEmail(email: string): Promise; - createNewsletter(newsletter: InsertNewsletter): Promise; + createNewsletter(newsletter: Omit): Promise; // Contact Management - createContactMessage(message: InsertContactMessage): Promise; + createContactMessage(message: Omit): Promise; // Session Store - sessionStore: session.SessionStore; + sessionStore: any; } export class MemStorage implements IStorage { @@ -52,7 +91,7 @@ export class MemStorage implements IStorage { currentBookingId: number; currentNewsletterId: number; currentContactMessageId: number; - sessionStore: session.SessionStore; + sessionStore: any; constructor() { this.users = new Map(); @@ -92,7 +131,7 @@ export class MemStorage implements IStorage { ); } - async createUser(insertUser: InsertUser): Promise { + async createUser(insertUser: Omit): Promise { const id = this.currentUserId++; const user: User = { ...insertUser, @@ -112,7 +151,7 @@ export class MemStorage implements IStorage { return this.classes.get(id); } - async createClass(classData: InsertClass): Promise { + async createClass(classData: Omit): Promise { const id = this.currentClassId++; const newClass: Class = { ...classData, id }; this.classes.set(id, newClass); @@ -132,7 +171,7 @@ export class MemStorage implements IStorage { return this.bookings.get(id); } - async createBooking(booking: InsertBooking): Promise { + async createBooking(booking: Omit): Promise { const id = this.currentBookingId++; const newBooking: Booking = { ...booking, @@ -160,7 +199,7 @@ export class MemStorage implements IStorage { ); } - async createNewsletter(newsletter: InsertNewsletter): Promise { + async createNewsletter(newsletter: Omit): Promise { const id = this.currentNewsletterId++; const newNewsletter: Newsletter = { ...newsletter, @@ -172,7 +211,7 @@ export class MemStorage implements IStorage { } // Contact Management - async createContactMessage(message: InsertContactMessage): Promise { + async createContactMessage(message: Omit): Promise { const id = this.currentContactMessageId++; const newMessage: ContactMessage = { ...message, @@ -185,7 +224,7 @@ export class MemStorage implements IStorage { // Seed default data private seedClasses() { - const defaultClasses: InsertClass[] = [ + const defaultClasses: Omit[] = [ { name: "Mat Pilates", description: "A foundational class focusing on core strength, proper alignment, and mindful movement patterns.", diff --git a/server/vite.ts b/server/vite.ts index 95395a0..922a82d 100644 --- a/server/vite.ts +++ b/server/vite.ts @@ -23,7 +23,7 @@ export async function setupVite(app: Express, server: Server) { const serverOptions = { middlewareMode: true, hmr: { server }, - allowedHosts: true, + allowedHosts: true as true, }; const vite = await createViteServer({ @@ -68,7 +68,7 @@ export async function setupVite(app: Express, server: Server) { } export function serveStatic(app: Express) { - const distPath = path.resolve(import.meta.dirname, "public"); + const distPath = path.resolve(import.meta.dirname, "..", "client", "dist"); if (!fs.existsSync(distPath)) { throw new Error( diff --git a/shared/schema.ts b/shared/schema.ts deleted file mode 100644 index 379bbda..0000000 --- a/shared/schema.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { pgTable, text, serial, integer, boolean, timestamp } from "drizzle-orm/pg-core"; -import { createInsertSchema } from "drizzle-zod"; -import { z } from "zod"; - -export const users = pgTable("users", { - id: serial("id").primaryKey(), - username: text("username").notNull().unique(), - email: text("email").notNull().unique(), - password: text("password").notNull(), - fullName: text("full_name"), - createdAt: timestamp("created_at").defaultNow() -}); - -export const classes = pgTable("classes", { - id: serial("id").primaryKey(), - name: text("name").notNull(), - description: text("description").notNull(), - duration: integer("duration").notNull(), // in minutes - price: integer("price").notNull(), // in cents - capacity: integer("capacity").notNull(), - classType: text("class_type").notNull(), // "group", "small-group", "private" - imageUrl: text("image_url") -}); - -export const bookings = pgTable("bookings", { - id: serial("id").primaryKey(), - userId: integer("user_id").notNull(), - classId: integer("class_id").notNull(), - date: timestamp("date").notNull(), - paid: boolean("paid").default(false), - status: text("status").notNull().default("pending"), // pending, confirmed, cancelled - createdAt: timestamp("created_at").defaultNow() -}); - -export const newsletters = pgTable("newsletters", { - id: serial("id").primaryKey(), - email: text("email").notNull().unique(), - agreedToTerms: boolean("agreed_to_terms").notNull(), - createdAt: timestamp("created_at").defaultNow() -}); - -export const contactMessages = pgTable("contact_messages", { - id: serial("id").primaryKey(), - name: text("name").notNull(), - email: text("email").notNull(), - subject: text("subject"), - message: text("message").notNull(), - createdAt: timestamp("created_at").defaultNow() -}); - -// Create insert schemas -export const insertUserSchema = createInsertSchema(users) - .omit({ id: true, createdAt: true }); - -export const insertClassSchema = createInsertSchema(classes) - .omit({ id: true }); - -export const insertBookingSchema = createInsertSchema(bookings) - .omit({ id: true, createdAt: true }); - -export const insertNewsletterSchema = createInsertSchema(newsletters) - .omit({ id: true, createdAt: true }); - -export const insertContactMessageSchema = createInsertSchema(contactMessages) - .omit({ id: true, createdAt: true }); - -// Auth schemas -export const loginSchema = z.object({ - username: z.string().min(1, "Username is required"), - password: z.string().min(1, "Password is required"), -}); - -// Type exports -export type InsertUser = z.infer; -export type User = typeof users.$inferSelect; - -export type InsertClass = z.infer; -export type Class = typeof classes.$inferSelect; - -export type InsertBooking = z.infer; -export type Booking = typeof bookings.$inferSelect; - -export type InsertNewsletter = z.infer; -export type Newsletter = typeof newsletters.$inferSelect; - -export type InsertContactMessage = z.infer; -export type ContactMessage = typeof contactMessages.$inferSelect; - -export type Login = z.infer; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..50a57e3 --- /dev/null +++ b/vercel.json @@ -0,0 +1,25 @@ +{ + "version": 2, + "buildCommand": "npm run build", + "builds": [ + { + "src": "server/index.ts", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/api/(.*)", + "dest": "/server/index.ts" + }, + { + "src": "/(.*)", + "dest": "/server/index.ts" + } + ], + "functions": { + "server/index.ts": { + "maxDuration": 30 + } + } +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index d56b0ec..0a6e7de 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,20 +1,10 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; -import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal"; export default defineConfig({ plugins: [ react(), - runtimeErrorOverlay(), - ...(process.env.NODE_ENV !== "production" && - process.env.REPL_ID !== undefined - ? [ - await import("@replit/vite-plugin-cartographer").then((m) => - m.cartographer(), - ), - ] - : []), ], resolve: { alias: { @@ -25,7 +15,7 @@ export default defineConfig({ }, root: path.resolve(import.meta.dirname, "client"), build: { - outDir: path.resolve(import.meta.dirname, "dist/public"), + outDir: path.resolve(import.meta.dirname, "client", "dist"), emptyOutDir: true, }, });