new website updates
This commit is contained in:
parent
a1d010c71e
commit
83de08ddd9
|
|
@ -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'
|
||||||
|
|
@ -3,4 +3,11 @@ dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
server/public
|
server/public
|
||||||
vite.config.ts.*
|
vite.config.ts.*
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
@ -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 <your-repo-url>
|
||||||
|
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
|
||||||
|
|
@ -2,21 +2,36 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<title>Pilates with Fadia | Feel at Home in Your Body</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Online pilates classes to help you feel stronger and more connected to your body and breath." />
|
<meta name="description" content="Pilates with Fadia - Transform your body and mind with professional Pilates instruction in a welcoming environment." />
|
||||||
<!-- Open Graph tags -->
|
<meta name="keywords" content="pilates, fitness, exercise, wellness, studio, classes, reformer, mat pilates" />
|
||||||
<meta property="og:title" content="Pilates with Fadia | Feel at Home in Your Body" />
|
<meta name="author" content="Fadia" />
|
||||||
<meta property="og:description" content="Online pilates classes to help you feel stronger and more connected to your body and breath." />
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://pilateswithfadia.com" />
|
<meta property="og:url" content="https://pilateswithfadia.com/" />
|
||||||
<!-- Icons -->
|
<meta property="og:title" content="Pilates with Fadia" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
|
<meta property="og:description" content="Transform your body and mind with professional Pilates instruction in a welcoming environment." />
|
||||||
|
<meta property="og:image" content="https://pilateswithfadia.com/og-image.jpg" />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:url" content="https://pilateswithfadia.com/" />
|
||||||
|
<meta property="twitter:title" content="Pilates with Fadia" />
|
||||||
|
<meta property="twitter:description" content="Transform your body and mind with professional Pilates instruction in a welcoming environment." />
|
||||||
|
<meta property="twitter:image" content="https://pilateswithfadia.com/og-image.jpg" />
|
||||||
|
|
||||||
|
<!-- Preconnect to external domains for performance -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="preconnect" href="https://www.google-analytics.com">
|
||||||
|
<link rel="preconnect" href="https://www.googletagmanager.com">
|
||||||
|
|
||||||
|
<title>Pilates with Fadia</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<!-- This is a replit script which adds a banner on the top of the page when opened in development mode outside the replit environment -->
|
|
||||||
<script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -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
|
||||||
20
package.json
20
package.json
|
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "rest-express",
|
"name": "pilates-with-fadia",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development tsx server/index.ts",
|
"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",
|
"start": "NODE_ENV=production node dist/index.js",
|
||||||
"check": "tsc",
|
"check": "tsc"
|
||||||
"db:push": "drizzle-kit push"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"@neondatabase/serverless": "^0.10.4",
|
|
||||||
"@radix-ui/react-accordion": "^1.2.4",
|
"@radix-ui/react-accordion": "^1.2.4",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
||||||
|
|
@ -41,18 +41,13 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.3",
|
"@radix-ui/react-toggle": "^1.1.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@sendgrid/mail": "^8.1.5",
|
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/nodemailer": "^6.4.17",
|
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"connect-pg-simple": "^10.0.0",
|
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"drizzle-orm": "^0.39.1",
|
|
||||||
"drizzle-zod": "^0.7.0",
|
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
|
|
@ -61,7 +56,6 @@
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nodemailer": "^7.0.3",
|
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|
@ -81,10 +75,7 @@
|
||||||
"zod-validation-error": "^3.4.0"
|
"zod-validation-error": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@replit/vite-plugin-cartographer": "^0.2.7",
|
|
||||||
"@replit/vite-plugin-runtime-error-modal": "^0.0.3",
|
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/connect-pg-simple": "^7.0.3",
|
|
||||||
"@types/express": "4.17.21",
|
"@types/express": "4.17.21",
|
||||||
"@types/express-session": "^1.18.0",
|
"@types/express-session": "^1.18.0",
|
||||||
"@types/node": "20.16.11",
|
"@types/node": "20.16.11",
|
||||||
|
|
@ -95,7 +86,6 @@
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.5.13",
|
||||||
"@vitejs/plugin-react": "^4.3.2",
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"drizzle-kit": "^0.30.4",
|
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,17 @@ import { Express } from "express";
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { storage } from "./storage";
|
import { storage, User } from "./storage";
|
||||||
import { User as SelectUser, insertUserSchema } from "@shared/schema";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
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}`;
|
return `${buf.toString("hex")}.${salt}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function comparePasswords(supplied: string, stored: string) {
|
async function comparePasswords(password: string, hashedPassword: string) {
|
||||||
const [hashed, salt] = stored.split(".");
|
const [hash, salt] = hashedPassword.split(".");
|
||||||
const hashedBuf = Buffer.from(hashed, "hex");
|
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
|
||||||
const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer;
|
return timingSafeEqual(buf, Buffer.from(hash, "hex"));
|
||||||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupAuth(app: Express) {
|
export function setupAuth(app: Express) {
|
||||||
|
|
@ -66,23 +70,34 @@ export function setupAuth(app: Express) {
|
||||||
|
|
||||||
app.post("/api/register", async (req, res, next) => {
|
app.post("/api/register", async (req, res, next) => {
|
||||||
try {
|
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
|
// Check if username already exists
|
||||||
const existingUserByUsername = await storage.getUserByUsername(validatedUser.username);
|
const existingUserByUsername = await storage.getUserByUsername(username);
|
||||||
if (existingUserByUsername) {
|
if (existingUserByUsername) {
|
||||||
return res.status(400).json({ message: "Username already exists" });
|
return res.status(400).json({ message: "Username already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email already exists
|
// Check if email already exists
|
||||||
const existingUserByEmail = await storage.getUserByEmail(validatedUser.email);
|
const existingUserByEmail = await storage.getUserByEmail(email);
|
||||||
if (existingUserByEmail) {
|
if (existingUserByEmail) {
|
||||||
return res.status(400).json({ message: "Email already in use" });
|
return res.status(400).json({ message: "Email already in use" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await storage.createUser({
|
const user = await storage.createUser({
|
||||||
...validatedUser,
|
username,
|
||||||
password: await hashPassword(validatedUser.password),
|
email,
|
||||||
|
password: await hashPassword(password),
|
||||||
|
fullName
|
||||||
});
|
});
|
||||||
|
|
||||||
req.login(user, (err) => {
|
req.login(user, (err) => {
|
||||||
|
|
@ -98,7 +113,7 @@ export function setupAuth(app: Express) {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/login", (req, res, next) => {
|
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 (err) return next(err);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({ message: "Invalid username or password" });
|
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) => {
|
app.get("/api/me", (req, res) => {
|
||||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
if (req.isAuthenticated()) {
|
||||||
|
const { password, ...userWithoutPassword } = req.user as User;
|
||||||
// Return user without password
|
res.json(userWithoutPassword);
|
||||||
const { password, ...userWithoutPassword } = req.user;
|
} else {
|
||||||
res.json(userWithoutPassword);
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<boolean> {
|
|
||||||
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 = `
|
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eaeaea; border-radius: 5px;">
|
|
||||||
<h2 style="color: #0c8991; border-bottom: 1px solid #eaeaea; padding-bottom: 10px;">New Message from Pilates with Fadia Website</h2>
|
|
||||||
<p><strong>From:</strong> ${data.name} (${data.email})</p>
|
|
||||||
<p><strong>Subject:</strong> ${data.subject || "No subject"}</p>
|
|
||||||
<div style="background-color: #f9f9f9; padding: 15px; border-radius: 4px; margin-top: 20px;">
|
|
||||||
<p style="white-space: pre-line;">${data.message}</p>
|
|
||||||
</div>
|
|
||||||
<p style="color: #666; font-size: 12px; margin-top: 30px; border-top: 1px solid #eaeaea; padding-top: 10px;">
|
|
||||||
This message was sent from the contact form on your Pilates with Fadia website.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -36,35 +36,36 @@ app.use((req, res, next) => {
|
||||||
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 () => {
|
(async () => {
|
||||||
const server = await registerRoutes(app);
|
server = await registerRoutes(app);
|
||||||
|
|
||||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
// Setup Vite in development, serve static in production
|
||||||
const status = err.status || err.statusCode || 500;
|
if (process.env.NODE_ENV === "development") {
|
||||||
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") {
|
|
||||||
await setupVite(app, server);
|
await setupVite(app, server);
|
||||||
} else {
|
} else {
|
||||||
serveStatic(app);
|
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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,28 @@ import { createServer, type Server } from "http";
|
||||||
import { storage } from "./storage";
|
import { storage } from "./storage";
|
||||||
import { setupAuth } from "./auth";
|
import { setupAuth } from "./auth";
|
||||||
import { subscribeToMailchimp } from "./mailchimp";
|
import { subscribeToMailchimp } from "./mailchimp";
|
||||||
import { sendEmail } from "./email";
|
|
||||||
import {
|
|
||||||
insertNewsletterSchema,
|
|
||||||
insertContactMessageSchema,
|
|
||||||
insertBookingSchema
|
|
||||||
} from "@shared/schema";
|
|
||||||
import { z } from "zod";
|
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<Server> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Set up authentication routes
|
// Set up authentication routes
|
||||||
setupAuth(app);
|
setupAuth(app);
|
||||||
|
|
@ -53,7 +67,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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);
|
res.json(bookings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ message: "Failed to fetch bookings" });
|
res.status(500).json({ message: "Failed to fetch bookings" });
|
||||||
|
|
@ -66,12 +81,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bookingData = insertBookingSchema.parse({
|
const user = req.user as any;
|
||||||
...req.body,
|
const bookingData = bookingSchema.parse(req.body);
|
||||||
userId: req.user.id
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
res.status(201).json(booking);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
|
|
@ -84,7 +101,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Newsletter signup
|
// Newsletter signup
|
||||||
app.post("/api/newsletter", async (req, res) => {
|
app.post("/api/newsletter", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const newsletterData = insertNewsletterSchema.parse(req.body);
|
const newsletterData = newsletterSchema.parse(req.body);
|
||||||
|
|
||||||
// Check if email already exists in our local database
|
// Check if email already exists in our local database
|
||||||
const existingNewsletter = await storage.getNewsletterByEmail(newsletterData.email);
|
const existingNewsletter = await storage.getNewsletterByEmail(newsletterData.email);
|
||||||
|
|
@ -120,20 +137,19 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Contact form
|
// Contact form
|
||||||
app.post("/api/contact", async (req, res) => {
|
app.post("/api/contact", async (req, res) => {
|
||||||
try {
|
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);
|
const message = await storage.createContactMessage(contactData);
|
||||||
|
|
||||||
// Add a success message with clear next steps for Fadia
|
// Log the contact request
|
||||||
console.log(`Contact form submission stored in database from ${contactData.name} (${contactData.email})`);
|
console.log(`Contact form submission from ${contactData.name} (${contactData.email})`);
|
||||||
console.log(`Subject: ${contactData.subject || "No subject"}`);
|
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({
|
res.status(201).json({
|
||||||
message: "Message sent successfully",
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
|
|
@ -143,44 +159,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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);
|
const httpServer = createServer(app);
|
||||||
return httpServer;
|
return httpServer;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 session from "express-session";
|
||||||
import createMemoryStore from "memorystore";
|
import createMemoryStore from "memorystore";
|
||||||
|
|
||||||
const MemoryStore = createMemoryStore(session);
|
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 {
|
export interface IStorage {
|
||||||
// User Management
|
// User Management
|
||||||
getUser(id: number): Promise<User | undefined>;
|
getUser(id: number): Promise<User | undefined>;
|
||||||
getUserByUsername(username: string): Promise<User | undefined>;
|
getUserByUsername(username: string): Promise<User | undefined>;
|
||||||
getUserByEmail(email: string): Promise<User | undefined>;
|
getUserByEmail(email: string): Promise<User | undefined>;
|
||||||
createUser(user: InsertUser): Promise<User>;
|
createUser(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
|
||||||
|
|
||||||
// Class Management
|
// Class Management
|
||||||
getClasses(): Promise<Class[]>;
|
getClasses(): Promise<Class[]>;
|
||||||
getClass(id: number): Promise<Class | undefined>;
|
getClass(id: number): Promise<Class | undefined>;
|
||||||
createClass(classData: InsertClass): Promise<Class>;
|
createClass(classData: Omit<Class, 'id'>): Promise<Class>;
|
||||||
|
|
||||||
// Booking Management
|
// Booking Management
|
||||||
getBookings(userId?: number): Promise<Booking[]>;
|
getBookings(userId?: number): Promise<Booking[]>;
|
||||||
getBooking(id: number): Promise<Booking | undefined>;
|
getBooking(id: number): Promise<Booking | undefined>;
|
||||||
createBooking(booking: InsertBooking): Promise<Booking>;
|
createBooking(booking: Omit<Booking, 'id' | 'createdAt'>): Promise<Booking>;
|
||||||
updateBookingStatus(id: number, status: string): Promise<Booking | undefined>;
|
updateBookingStatus(id: number, status: string): Promise<Booking | undefined>;
|
||||||
|
|
||||||
// Newsletter Management
|
// Newsletter Management
|
||||||
getNewsletterByEmail(email: string): Promise<Newsletter | undefined>;
|
getNewsletterByEmail(email: string): Promise<Newsletter | undefined>;
|
||||||
createNewsletter(newsletter: InsertNewsletter): Promise<Newsletter>;
|
createNewsletter(newsletter: Omit<Newsletter, 'id' | 'createdAt'>): Promise<Newsletter>;
|
||||||
|
|
||||||
// Contact Management
|
// Contact Management
|
||||||
createContactMessage(message: InsertContactMessage): Promise<ContactMessage>;
|
createContactMessage(message: Omit<ContactMessage, 'id' | 'createdAt'>): Promise<ContactMessage>;
|
||||||
|
|
||||||
// Session Store
|
// Session Store
|
||||||
sessionStore: session.SessionStore;
|
sessionStore: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemStorage implements IStorage {
|
export class MemStorage implements IStorage {
|
||||||
|
|
@ -52,7 +91,7 @@ export class MemStorage implements IStorage {
|
||||||
currentBookingId: number;
|
currentBookingId: number;
|
||||||
currentNewsletterId: number;
|
currentNewsletterId: number;
|
||||||
currentContactMessageId: number;
|
currentContactMessageId: number;
|
||||||
sessionStore: session.SessionStore;
|
sessionStore: any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.users = new Map();
|
this.users = new Map();
|
||||||
|
|
@ -92,7 +131,7 @@ export class MemStorage implements IStorage {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(insertUser: InsertUser): Promise<User> {
|
async createUser(insertUser: Omit<User, 'id' | 'createdAt'>): Promise<User> {
|
||||||
const id = this.currentUserId++;
|
const id = this.currentUserId++;
|
||||||
const user: User = {
|
const user: User = {
|
||||||
...insertUser,
|
...insertUser,
|
||||||
|
|
@ -112,7 +151,7 @@ export class MemStorage implements IStorage {
|
||||||
return this.classes.get(id);
|
return this.classes.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createClass(classData: InsertClass): Promise<Class> {
|
async createClass(classData: Omit<Class, 'id'>): Promise<Class> {
|
||||||
const id = this.currentClassId++;
|
const id = this.currentClassId++;
|
||||||
const newClass: Class = { ...classData, id };
|
const newClass: Class = { ...classData, id };
|
||||||
this.classes.set(id, newClass);
|
this.classes.set(id, newClass);
|
||||||
|
|
@ -132,7 +171,7 @@ export class MemStorage implements IStorage {
|
||||||
return this.bookings.get(id);
|
return this.bookings.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBooking(booking: InsertBooking): Promise<Booking> {
|
async createBooking(booking: Omit<Booking, 'id' | 'createdAt'>): Promise<Booking> {
|
||||||
const id = this.currentBookingId++;
|
const id = this.currentBookingId++;
|
||||||
const newBooking: Booking = {
|
const newBooking: Booking = {
|
||||||
...booking,
|
...booking,
|
||||||
|
|
@ -160,7 +199,7 @@ export class MemStorage implements IStorage {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNewsletter(newsletter: InsertNewsletter): Promise<Newsletter> {
|
async createNewsletter(newsletter: Omit<Newsletter, 'id' | 'createdAt'>): Promise<Newsletter> {
|
||||||
const id = this.currentNewsletterId++;
|
const id = this.currentNewsletterId++;
|
||||||
const newNewsletter: Newsletter = {
|
const newNewsletter: Newsletter = {
|
||||||
...newsletter,
|
...newsletter,
|
||||||
|
|
@ -172,7 +211,7 @@ export class MemStorage implements IStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contact Management
|
// Contact Management
|
||||||
async createContactMessage(message: InsertContactMessage): Promise<ContactMessage> {
|
async createContactMessage(message: Omit<ContactMessage, 'id' | 'createdAt'>): Promise<ContactMessage> {
|
||||||
const id = this.currentContactMessageId++;
|
const id = this.currentContactMessageId++;
|
||||||
const newMessage: ContactMessage = {
|
const newMessage: ContactMessage = {
|
||||||
...message,
|
...message,
|
||||||
|
|
@ -185,7 +224,7 @@ export class MemStorage implements IStorage {
|
||||||
|
|
||||||
// Seed default data
|
// Seed default data
|
||||||
private seedClasses() {
|
private seedClasses() {
|
||||||
const defaultClasses: InsertClass[] = [
|
const defaultClasses: Omit<Class, 'id'>[] = [
|
||||||
{
|
{
|
||||||
name: "Mat Pilates",
|
name: "Mat Pilates",
|
||||||
description: "A foundational class focusing on core strength, proper alignment, and mindful movement patterns.",
|
description: "A foundational class focusing on core strength, proper alignment, and mindful movement patterns.",
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export async function setupVite(app: Express, server: Server) {
|
||||||
const serverOptions = {
|
const serverOptions = {
|
||||||
middlewareMode: true,
|
middlewareMode: true,
|
||||||
hmr: { server },
|
hmr: { server },
|
||||||
allowedHosts: true,
|
allowedHosts: true as true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const vite = await createViteServer({
|
const vite = await createViteServer({
|
||||||
|
|
@ -68,7 +68,7 @@ export async function setupVite(app: Express, server: Server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serveStatic(app: Express) {
|
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)) {
|
if (!fs.existsSync(distPath)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
||||||
|
|
@ -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<typeof insertUserSchema>;
|
|
||||||
export type User = typeof users.$inferSelect;
|
|
||||||
|
|
||||||
export type InsertClass = z.infer<typeof insertClassSchema>;
|
|
||||||
export type Class = typeof classes.$inferSelect;
|
|
||||||
|
|
||||||
export type InsertBooking = z.infer<typeof insertBookingSchema>;
|
|
||||||
export type Booking = typeof bookings.$inferSelect;
|
|
||||||
|
|
||||||
export type InsertNewsletter = z.infer<typeof insertNewsletterSchema>;
|
|
||||||
export type Newsletter = typeof newsletters.$inferSelect;
|
|
||||||
|
|
||||||
export type InsertContactMessage = z.infer<typeof insertContactMessageSchema>;
|
|
||||||
export type ContactMessage = typeof contactMessages.$inferSelect;
|
|
||||||
|
|
||||||
export type Login = z.infer<typeof loginSchema>;
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,10 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
runtimeErrorOverlay(),
|
|
||||||
...(process.env.NODE_ENV !== "production" &&
|
|
||||||
process.env.REPL_ID !== undefined
|
|
||||||
? [
|
|
||||||
await import("@replit/vite-plugin-cartographer").then((m) =>
|
|
||||||
m.cartographer(),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|
@ -25,7 +15,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
root: path.resolve(import.meta.dirname, "client"),
|
root: path.resolve(import.meta.dirname, "client"),
|
||||||
build: {
|
build: {
|
||||||
outDir: path.resolve(import.meta.dirname, "dist/public"),
|
outDir: path.resolve(import.meta.dirname, "client", "dist"),
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue