Compare commits

..

2 Commits

Author SHA1 Message Date
Jeff Emmett ed7b210f78 Update .gitignore to exclude environment files
- Add .env files to .gitignore (contains secrets)
- Fix repository remote configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 16:53:37 -08:00
Jeff Emmett 2b30cae68f update website 2025-06-15 18:46:31 +02:00
64 changed files with 4521 additions and 88350 deletions

View File

@ -1,121 +0,0 @@
2025-05-10 00:33:24,807 - semgrep.notifications - WARNING - METRICS: Using configs from the Registry (like --config=p/ci) reports pseudonymous rule metrics to semgrep.dev.
To disable Registry rule metrics, use "--metrics=off".
Using configs only from local files (like --config=xyz.yml) does not enable metrics.
More information: https://semgrep.dev/docs/metrics
2025-05-10 00:33:24,810 - semgrep.run_scan - DEBUG - semgrep version 1.2.0
2025-05-10 00:33:24,819 - semgrep.git - DEBUG - Failed to get project url from 'git ls-remote': Command failed with exit code: 128
-----
Command failed with output:
fatal: No remote configured to list refs from.
Failed to run 'git ls-remote --get-url'. Possible reasons:
- the git binary is not available
- the current working directory is not a git repository
- the baseline commit is not a parent of the current commit
(if you are running through semgrep-app, check if you are setting `SEMGREP_BRANCH` or `SEMGREP_BASELINE_COMMIT` properly)
- the current working directory is not marked as safe
(fix with `git config --global --add safe.directory $(pwd)`)
Try running the command yourself to debug the issue.
2025-05-10 00:33:24,820 - semgrep.config_resolver - DEBUG - Loading local config from /home/runner/workspace/.config/.semgrep/semgrep_rules.json
2025-05-10 00:33:24,823 - semgrep.config_resolver - DEBUG - Done loading local config from /home/runner/workspace/.config/.semgrep/semgrep_rules.json
2025-05-10 00:33:24,831 - semgrep.config_resolver - DEBUG - Saving rules to /tmp/semgrep-mthvn42m.rules
2025-05-10 00:33:25,197 - semgrep.semgrep_core - DEBUG - Failed to open resource semgrep-core-proprietary: [Errno 2] No such file or directory: '/tmp/_MEI1ufMVi/semgrep/bin/semgrep-core-proprietary'.
2025-05-10 00:33:25,912 - semgrep.rule_lang - DEBUG - semgrep-core validation response: valid=True
2025-05-10 00:33:25,913 - semgrep.rule_lang - DEBUG - semgrep-core validation succeeded
2025-05-10 00:33:25,913 - semgrep.rule_lang - DEBUG - RPC validation succeeded
2025-05-10 00:33:25,913 - semgrep.config_resolver - DEBUG - loaded 1 configs in 1.0931837558746338
2025-05-10 00:33:26,154 - semgrep.run_scan - VERBOSE - running 1250 rules from 1 config /home/runner/workspace/.config/.semgrep/semgrep_rules.json_0
2025-05-10 00:33:26,154 - semgrep.run_scan - VERBOSE - No .semgrepignore found. Using default .semgrepignore rules. See the docs for the list of default ignores: https://semgrep.dev/docs/cli-usage/#ignore-files
2025-05-10 00:33:26,158 - semgrep.run_scan - VERBOSE - Rules:
2025-05-10 00:33:26,158 - semgrep.run_scan - VERBOSE - <SKIPPED DATA (too many entries; use --max-log-list-entries)>
2025-05-10 00:33:26,661 - semgrep.core_runner - DEBUG - Passing whole rules directly to semgrep_core
2025-05-10 00:33:26,855 - semgrep.core_runner - DEBUG - Running Semgrep engine with command:
2025-05-10 00:33:26,855 - semgrep.core_runner - DEBUG - /tmp/_MEI1ufMVi/semgrep/bin/opengrep-core -json -rules /tmp/tmp6vt4ey5_.json -j 8 -targets /tmp/tmpsta40moi -timeout 5 -timeout_threshold 3 -max_memory 0 -fast
2025-05-10 00:33:29,983 - semgrep.core_runner - DEBUG - --- semgrep-core stderr ---
[00.07][INFO]: Executed as: /tmp/_MEI1ufMVi/semgrep/bin/opengrep-core -json -rules /tmp/tmp6vt4ey5_.json -j 8 -targets /tmp/tmpsta40moi -timeout 5 -timeout_threshold 3 -max_memory 0 -fast
[00.07][INFO]: Version: 1.2.0
[00.07][INFO]: Parsing rules in /tmp/tmp6vt4ey5_.json
[00.82][INFO]: scan: processing 303 files (skipping 0), with 487 rules (skipping 0 )
[03.08][INFO]: Custom ignore pattern: None
[03.08][INFO]: Custom ignore pattern: None
--- end semgrep-core stderr ---
2025-05-10 00:33:29,991 - semgrep.rule_match - DEBUG - match_key = ('', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 0ee74fd49637bebe183eca7188dbde26e386314e62cc2e7ba1ee60b377b638243fcd84e6c6fa04886198ccacfa6a711bfbcc61a28f9ddc913d5b3c53083cbc90_0
2025-05-10 00:33:29,992 - semgrep.rule_match - DEBUG - match_key = (' rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / (?is).*integrity= (google-analytics\\.com|fonts\\.googleapis\\.com|fonts\\.gstatic\\.com|googletagmanager\\.com) .*rel\\s*=\\s*[\'"]?preconnect.* href="... :// ..." href="//..." href=\'... :// ...\' href=\'//...\' src="... :// ..." src="//..." src=\'... :// ...\' src=\'//...\' <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / > <script rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / >...</script>', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 1068497918b233fd4d3e50f287aaf6ab1a03059c638e7dd12249612bb34d624f7036ebd3f250440227c595b791530ce2bb70f5a13d3e4f169ddcde97bedfc6bc_0
2025-05-10 00:33:29,993 - semgrep.rule_match - DEBUG - match_key = (' rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / (?is).*integrity= (google-analytics\\.com|fonts\\.googleapis\\.com|fonts\\.gstatic\\.com|googletagmanager\\.com) .*rel\\s*=\\s*[\'"]?preconnect.* href="... :// ..." href="//..." href=\'... :// ...\' href=\'//...\' src="... :// ..." src="//..." src=\'... :// ...\' src=\'//...\' <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / > <script rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / >...</script>', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 1068497918b233fd4d3e50f287aaf6ab1a03059c638e7dd12249612bb34d624f7036ebd3f250440227c595b791530ce2bb70f5a13d3e4f169ddcde97bedfc6bc_0
2025-05-10 00:33:29,993 - semgrep.rule_match - DEBUG - match_key = (' rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / (?is).*integrity= (google-analytics\\.com|fonts\\.googleapis\\.com|fonts\\.gstatic\\.com|googletagmanager\\.com) .*rel\\s*=\\s*[\'"]?preconnect.* href="... :// ..." href="//..." href=\'... :// ...\' href=\'//...\' src="... :// ..." src="//..." src=\'... :// ...\' src=\'//...\' <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / > <script rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" / >...</script>', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 1068497918b233fd4d3e50f287aaf6ab1a03059c638e7dd12249612bb34d624f7036ebd3f250440227c595b791530ce2bb70f5a13d3e4f169ddcde97bedfc6bc_0
2025-05-10 00:33:29,994 - semgrep.rule_match - DEBUG - match_key = ('', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 0ee74fd49637bebe183eca7188dbde26e386314e62cc2e7ba1ee60b377b638243fcd84e6c6fa04886198ccacfa6a711bfbcc61a28f9ddc913d5b3c53083cbc90_0
2025-05-10 00:33:29,995 - semgrep.rule_match - DEBUG - match_key = (' type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" (?is).*integrity= (google-analytics\\.com|fonts\\.googleapis\\.com|fonts\\.gstatic\\.com|googletagmanager\\.com) .*rel\\s*=\\s*[\'"]?preconnect.* href="... :// ..." href="//..." href=\'... :// ...\' href=\'//...\' src="... :// ..." src="//..." src=\'... :// ...\' src=\'//...\' <link type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" > <script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" >...</script>', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 0728b64e224596592d04447ba8a642ff94e1fb9fcc07be26d49dc7e7f6898e638ad16ffcaca086932c58f4c6400fe32603323afef02cf9bfebcb0e4a53562a40_0
2025-05-10 00:33:29,995 - semgrep.rule_match - DEBUG - match_key = (' type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" (?is).*integrity= (google-analytics\\.com|fonts\\.googleapis\\.com|fonts\\.gstatic\\.com|googletagmanager\\.com) .*rel\\s*=\\s*[\'"]?preconnect.* href="... :// ..." href="//..." href=\'... :// ...\' href=\'//...\' src="... :// ..." src="//..." src=\'... :// ...\' src=\'//...\' <link type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" > <script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" >...</script>', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 0728b64e224596592d04447ba8a642ff94e1fb9fcc07be26d49dc7e7f6898e638ad16ffcaca086932c58f4c6400fe32603323afef02cf9bfebcb0e4a53562a40_0
2025-05-10 00:33:29,995 - semgrep.rule_match - DEBUG - match_key = (' type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" (?is).*integrity= (google-analytics\\.com|fonts\\.googleapis\\.com|fonts\\.gstatic\\.com|googletagmanager\\.com) .*rel\\s*=\\s*[\'"]?preconnect.* href="... :// ..." href="//..." href=\'... :// ...\' href=\'//...\' src="... :// ..." src="//..." src=\'... :// ...\' src=\'//...\' <link type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" > <script type="text/javascript" src="https://replit.com/public/js/replit-dev-banner.js" >...</script>', PosixPath('client/index.html'), 'config..semgrep.vendored-rules.html.security.audit.missing-integrity') match_id = 0728b64e224596592d04447ba8a642ff94e1fb9fcc07be26d49dc7e7f6898e638ad16ffcaca086932c58f4c6400fe32603323afef02cf9bfebcb0e4a53562a40_0
2025-05-10 00:33:29,998 - semgrep.core_runner - DEBUG - semgrep ran in 0:00:03.337440 on 103 files
2025-05-10 00:33:30,000 - semgrep.core_runner - DEBUG - findings summary: 2 warning, 0 error, 0 info
2025-05-10 00:33:30,004 - semgrep.app.auth - DEBUG - Getting API token from settings file
2025-05-10 00:33:30,004 - semgrep.app.auth - DEBUG - No API token found in settings file
2025-05-10 00:33:30,005 - semgrep.semgrep_core - DEBUG - Failed to open resource semgrep-core-proprietary: [Errno 2] No such file or directory: '/tmp/_MEI1ufMVi/semgrep/bin/semgrep-core-proprietary'.
2025-05-10 00:33:30,110 - semgrep.output - VERBOSE -
========================================
Files skipped:
========================================
Always skipped by Opengrep:
• <none>
Skipped by .gitignore:
(Disable by passing --no-git-ignore)
• <all files not listed by `git ls-files` were skipped>
Skipped by .semgrepignore:
- https://semgrep.dev/docs/ignoring-files-folders-code/#understand-semgrep-defaults
• <none>
Skipped by --include patterns:
• <none>
Skipped by --exclude patterns:
• <none>
Files skipped due to insufficient read permissions:
• <none>
Skipped by limiting to files smaller than 1000000 bytes:
(Adjust with the --max-target-bytes flag)
• attached_assets/8w-oevrI.jpeg
• attached_assets/DSC01368 2.jpeg
• attached_assets/DSC01380.jpeg
• attached_assets/DSC01394 2.jpeg
• attached_assets/DSC01466 2.jpeg
• attached_assets/Fadia-132.jpg
• attached_assets/Fadia-15.jpg
• attached_assets/Fadia-156.jpg
• attached_assets/save.jpeg
• client/src/assets/Fadia-132.jpg
• client/src/assets/Fadia-15.jpg
• client/src/assets/Fadia-156.jpg
• generated-icon.png
Partially analyzed due to parsing or internal Opengrep errors
• client/index.html (1 lines skipped)
• tailwind.config.ts (1 lines skipped)
2025-05-10 00:33:30,111 - semgrep.output - INFO - Some files were skipped or only partially analyzed.
Scan was limited to files tracked by git.
Partially scanned: 2 files only partially analyzed due to parsing or internal Opengrep errors
Scan skipped: 13 files larger than 1.0 MB
For a full list of skipped files, run opengrep with the --verbose flag.
Ran 443 rules on 103 files: 2 findings.
2025-05-10 00:33:30,112 - semgrep.app.version - DEBUG - Version cache does not exist
2025-05-10 00:33:30,133 - semgrep.metrics - VERBOSE - Not sending pseudonymous metrics since metrics are configured to OFF and registry usage is False

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
has_shown_metrics_notification: true
anonymous_user_id: e3c333de-dabb-4909-a90d-30c3616cc5f8

6
.gitignore vendored
View File

@ -5,9 +5,7 @@ server/public
vite.config.ts.*
*.tar.gz
# Environment variables
# Environment variables (contains secrets)
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*.local

165
README.md
View File

@ -1,165 +0,0 @@
# 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

View File

@ -1,203 +0,0 @@
import express from 'express';
import { storage } from '../server/storage';
import { setupAuth } from '../server/auth';
import { subscribeToMailchimp } from '../server/mailchimp';
import { z } from 'zod';
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Add logging middleware
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: any;
const originalResJson = res.json;
res.json = function(bodyJson: any, ...args: any[]) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on('finish', () => {
const duration = Date.now() - start;
if (path.startsWith('/api')) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + '…';
}
console.log(logLine);
}
});
next();
});
// Set up authentication routes
setupAuth(app);
// Simple validation schemas
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")
});
// API Routes
// ----------
// Classes
app.get("/api/classes", async (_req, res) => {
try {
const classes = await storage.getClasses();
res.json(classes);
} catch (error) {
res.status(500).json({ message: "Failed to fetch classes" });
}
});
app.get("/api/classes/:id", async (req, res) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ message: "Invalid class ID" });
}
const classData = await storage.getClass(id);
if (!classData) {
return res.status(404).json({ message: "Class not found" });
}
res.json(classData);
} catch (error) {
res.status(500).json({ message: "Failed to fetch class" });
}
});
// Bookings
app.get("/api/bookings", async (req: any, res: any) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
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" });
}
});
app.post("/api/bookings", async (req: any, res: any) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
const user = req.user as any;
const bookingData = bookingSchema.parse(req.body);
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) {
return res.status(400).json({ message: "Invalid booking data", errors: error.errors });
}
res.status(500).json({ message: "Failed to create booking" });
}
});
// Newsletter signup
app.post("/api/newsletter", async (req, res) => {
try {
const newsletterData = newsletterSchema.parse(req.body);
// Check if email already exists in our local database
const existingNewsletter = await storage.getNewsletterByEmail(newsletterData.email);
// Store in our local database
if (!existingNewsletter) {
await storage.createNewsletter(newsletterData);
}
// Subscribe to Mailchimp
try {
const mailchimpResponse = await subscribeToMailchimp(newsletterData.email);
if (mailchimpResponse && mailchimpResponse.status === 'already_subscribed') {
return res.status(200).json({ message: mailchimpResponse.message });
}
res.status(201).json({ message: "Successfully subscribed to the newsletter" });
} catch (mailchimpError: any) {
console.error('Mailchimp error:', mailchimpError.message);
// Still return success if we stored in our database but Mailchimp failed
res.status(201).json({ message: "Successfully subscribed to the newsletter" });
}
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: "Invalid newsletter data", errors: error.errors });
}
console.error('Newsletter subscription error:', error);
res.status(500).json({ message: "Failed to subscribe to newsletter" });
}
});
// Contact form
app.post("/api/contact", async (req, res) => {
try {
const contactData = contactMessageSchema.parse(req.body);
// Store in database
const message = await storage.createContactMessage(contactData);
// Log the contact request
console.log(`Contact form submission from ${contactData.name} (${contactData.email})`);
console.log(`Subject: ${contactData.subject || "No subject"}`);
console.log(`Message: ${contactData.message}`);
res.status(201).json({
message: "Message sent successfully",
info: "Your message has been received and will be reviewed shortly."
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: "Invalid contact data", errors: error.errors });
}
res.status(500).json({ message: "Failed to send message" });
}
});
// Error handling middleware
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(status).json({ message });
throw err;
});
// Export the Express app for Vercel
export default app;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

View File

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - Pilates with Fadia</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}
.container {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c5282;
border-bottom: 3px solid #4299e1;
padding-bottom: 10px;
margin-bottom: 30px;
}
h2 {
color: #2d3748;
margin-top: 30px;
margin-bottom: 15px;
}
.last-updated {
background: #e2e8f0;
padding: 10px;
border-radius: 5px;
margin-bottom: 30px;
font-style: italic;
}
.contact-info {
background: #f7fafc;
padding: 15px;
border-left: 4px solid #4299e1;
margin: 20px 0;
}
ul {
padding-left: 20px;
}
li {
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class="container">
<h1>Privacy Policy</h1>
<div class="last-updated">
<strong>Last Updated:</strong> [INSERT DATE]
</div>
<p>At Pilates with Fadia ("we," "our," or "us"), we are committed to protecting your privacy and personal information. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website pilateswithfadia.com and use our services.</p>
<h2>1. Information We Collect</h2>
<h3>Personal Information</h3>
<p>We may collect personal information that you voluntarily provide to us, including:</p>
<ul>
<li>Name and contact information (email address, phone number, mailing address)</li>
<li>Account credentials (username and password)</li>
<li>Payment information (processed securely through third-party payment processors)</li>
<li>Health and fitness information relevant to your Pilates practice</li>
<li>Class preferences and booking history</li>
<li>Communication preferences</li>
</ul>
<h3>Automatically Collected Information</h3>
<p>When you visit our website, we may automatically collect:</p>
<ul>
<li>IP address and location data</li>
<li>Browser type and version</li>
<li>Device information</li>
<li>Pages visited and time spent on our site</li>
<li>Referring website information</li>
</ul>
<h2>2. How We Use Your Information</h2>
<p>We use your information to:</p>
<ul>
<li>Provide and improve our Pilates services and classes</li>
<li>Process bookings and payments</li>
<li>Send class schedules, updates, and important notifications</li>
<li>Respond to your inquiries and provide customer support</li>
<li>Personalize your experience and class recommendations</li>
<li>Send marketing communications (with your consent)</li>
<li>Ensure the safety and security of our services</li>
<li>Comply with legal obligations</li>
</ul>
<h2>3. Information Sharing and Disclosure</h2>
<p>We do not sell, trade, or rent your personal information to third parties. We may share your information in the following circumstances:</p>
<ul>
<li><strong>Service Providers:</strong> With trusted third-party service providers who assist us in operating our website and providing services</li>
<li><strong>Legal Requirements:</strong> When required by law or to protect our rights and safety</li>
<li><strong>Business Transfers:</strong> In connection with a merger, acquisition, or sale of assets</li>
<li><strong>Consent:</strong> With your explicit consent for specific purposes</li>
</ul>
<h2>4. Data Security</h2>
<p>We implement appropriate technical and organizational security measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. However, no method of transmission over the internet is 100% secure.</p>
<h2>5. Your Rights and Choices</h2>
<p>You have the right to:</p>
<ul>
<li>Access, update, or delete your personal information</li>
<li>Opt-out of marketing communications</li>
<li>Request data portability</li>
<li>Object to certain data processing activities</li>
<li>Withdraw consent at any time</li>
</ul>
<h2>6. Cookies and Tracking Technologies</h2>
<p>We use cookies and similar tracking technologies to enhance your browsing experience. Please refer to our Cookie Policy for detailed information about our use of cookies.</p>
<h2>7. Third-Party Links</h2>
<p>Our website may contain links to third-party websites. We are not responsible for the privacy practices of these external sites. We encourage you to review their privacy policies.</p>
<h2>8. Children's Privacy</h2>
<p>Our services are not intended for children under 13. We do not knowingly collect personal information from children under 13. If you are a parent or guardian and believe your child has provided us with personal information, please contact us.</p>
<h2>9. International Data Transfers</h2>
<p>Your information may be transferred to and processed in countries other than your own. We ensure appropriate safeguards are in place for such transfers.</p>
<h2>10. Data Retention</h2>
<p>We retain your personal information only as long as necessary to fulfill the purposes outlined in this Privacy Policy or as required by law.</p>
<h2>11. Changes to This Privacy Policy</h2>
<p>We may update this Privacy Policy from time to time. We will notify you of any material changes by posting the new policy on our website and updating the "Last Updated" date.</p>
<h2>12. Contact Us</h2>
<div class="contact-info">
<p>If you have any questions about this Privacy Policy or our privacy practices, please contact us:</p>
<p><strong>Pilates with Fadia</strong><br>
Email: [INSERT EMAIL]<br>
Phone: [INSERT PHONE]<br>
Address: [INSERT ADDRESS]<br>
Website: pilateswithfadia.com</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terms of Service - Pilates with Fadia</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}
.container {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c5282;
border-bottom: 3px solid #4299e1;
padding-bottom: 10px;
margin-bottom: 30px;
}
h2 {
color: #2d3748;
margin-top: 30px;
margin-bottom: 15px;
}
.last-updated {
background: #e2e8f0;
padding: 10px;
border-radius: 5px;
margin-bottom: 30px;
font-style: italic;
}
.contact-info {
background: #f7fafc;
padding: 15px;
border-left: 4px solid #4299e1;
margin: 20px 0;
}
.warning {
background: #fed7d7;
border: 1px solid #fc8181;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
ul {
padding-left: 20px;
}
li {
margin-bottom: 8px;
}
strong {
color: #2d3748;
}
</style>
</head>
<body>
<div class="container">
<h1>Terms of Service</h1>
<div class="last-updated">
<strong>Last Updated:</strong> [INSERT DATE]
</div>
<p>Welcome to Pilates with Fadia. These Terms of Service ("Terms") govern your use of our website pilateswithfadia.com and our services. By accessing or using our services, you agree to be bound by these Terms.</p>
<h2>1. Acceptance of Terms</h2>
<p>By accessing and using our website and services, you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to abide by the above, please do not use this service.</p>
<h2>2. Description of Services</h2>
<p>Pilates with Fadia provides:</p>
<ul>
<li>In-person and virtual Pilates classes</li>
<li>Private and group instruction</li>
<li>Wellness coaching and fitness guidance</li>
<li>Online class booking and scheduling</li>
<li>Educational content and resources</li>
</ul>
<h2>3. User Registration and Accounts</h2>
<p>To access certain services, you may be required to create an account. You agree to:</p>
<ul>
<li>Provide accurate, current, and complete information</li>
<li>Maintain and update your account information</li>
<li>Keep your login credentials secure and confidential</li>
<li>Notify us immediately of any unauthorized use of your account</li>
<li>Accept responsibility for all activities under your account</li>
</ul>
<h2>4. Health and Safety Considerations</h2>
<div class="warning">
<strong>Important Health Notice:</strong> Pilates and physical exercise involve inherent risks. By participating in our classes and services, you acknowledge and assume these risks.
</div>
<p>Before participating in any classes, you agree to:</p>
<ul>
<li>Consult with a healthcare provider if you have any medical conditions</li>
<li>Inform us of any injuries, limitations, or health concerns</li>
<li>Listen to your body and exercise within your limits</li>
<li>Follow all safety instructions provided by instructors</li>
<li>Understand that you participate at your own risk</li>
</ul>
<h2>5. Class Policies</h2>
<h3>Booking and Cancellation</h3>
<ul>
<li>Classes must be booked in advance through our booking system</li>
<li>Cancellations must be made at least [INSERT TIME] hours before class</li>
<li>Late cancellations may result in forfeiture of class credit</li>
<li>No-shows will be charged the full class fee</li>
<li>We reserve the right to cancel classes due to low enrollment or unforeseen circumstances</li>
</ul>
<h3>Class Conduct</h3>
<ul>
<li>Arrive on time for classes</li>
<li>Turn off or silence mobile devices</li>
<li>Respect other participants and instructors</li>
<li>Follow studio rules and guidelines</li>
<li>Maintain appropriate attire and hygiene</li>
</ul>
<h2>6. Payment Terms</h2>
<ul>
<li>Payment is due at the time of booking unless otherwise arranged</li>
<li>We accept major credit cards and other approved payment methods</li>
<li>Package deals and memberships are non-refundable unless required by law</li>
<li>Prices are subject to change with reasonable notice</li>
<li>Outstanding balances may result in suspension of services</li>
</ul>
<h2>7. Refund and Cancellation Policy</h2>
<ul>
<li>Refunds are considered on a case-by-case basis</li>
<li>Medical exemptions may qualify for partial refunds with documentation</li>
<li>Credits may be offered in lieu of refunds when appropriate</li>
<li>Membership cancellations require [INSERT NOTICE PERIOD] written notice</li>
</ul>
<h2>8. Intellectual Property Rights</h2>
<p>All content on our website and in our classes, including but not limited to text, graphics, logos, images, video content, and software, is the property of Pilates with Fadia and is protected by copyright and other intellectual property laws.</p>
<p>You may not:</p>
<ul>
<li>Copy, distribute, or reproduce our content without permission</li>
<li>Record classes without explicit written consent</li>
<li>Use our materials for commercial purposes</li>
<li>Modify or create derivative works from our content</li>
</ul>
<h2>9. Privacy and Data Protection</h2>
<p>Your privacy is important to us. Please review our Privacy Policy, which governs how we collect, use, and protect your personal information.</p>
<h2>10. Limitation of Liability</h2>
<p>To the fullest extent permitted by law, Pilates with Fadia shall not be liable for any direct, indirect, incidental, special, consequential, or punitive damages arising out of your use of our services, even if we have been advised of the possibility of such damages.</p>
<h2>11. Indemnification</h2>
<p>You agree to indemnify and hold harmless Pilates with Fadia, its instructors, and affiliates from any claims, damages, or expenses arising from your use of our services or violation of these Terms.</p>
<h2>12. Prohibited Uses</h2>
<p>You may not use our services to:</p>
<ul>
<li>Violate any applicable laws or regulations</li>
<li>Harass, abuse, or harm other users or staff</li>
<li>Transmit malicious code or attempt to gain unauthorized access</li>
<li>Impersonate others or provide false information</li>
<li>Engage in any activity that interferes with our services</li>
</ul>
<h2>13. Termination</h2>
<p>We reserve the right to terminate or suspend your account and access to our services at our sole discretion, without prior notice, for conduct that we believe violates these Terms or is harmful to other users or our business.</p>
<h2>14. Modifications to Terms</h2>
<p>We reserve the right to modify these Terms at any time. We will notify users of any material changes by posting the updated Terms on our website. Your continued use of our services after such modifications constitutes acceptance of the updated Terms.</p>
<h2>15. Governing Law</h2>
<p>These Terms shall be governed by and construed in accordance with the laws of [INSERT JURISDICTION], without regard to its conflict of law principles.</p>
<h2>16. Dispute Resolution</h2>
<p>Any disputes arising under these Terms shall be resolved through binding arbitration in accordance with the rules of [INSERT ARBITRATION RULES], except where prohibited by law.</p>
<h2>17. Severability</h2>
<p>If any provision of these Terms is found to be unenforceable or invalid, the remaining provisions will continue to be valid and enforceable.</p>
<h2>18. Contact Information</h2>
<div class="contact-info">
<p>If you have any questions about these Terms of Service, please contact us:</p>
<p><strong>Pilates with Fadia</strong><br>
Email: [INSERT EMAIL]<br>
Phone: [INSERT PHONE]<br>
Address: [INSERT ADDRESS]<br>
Website: pilateswithfadia.com</p>
</div>
<p><em>By using our services, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service.</em></p>
</div>
</body>
</html>

View File

@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cookie Policy - Pilates with Fadia</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}
.container {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c5282;
border-bottom: 3px solid #4299e1;
padding-bottom: 10px;
margin-bottom: 30px;
}
h2 {
color: #2d3748;
margin-top: 30px;
margin-bottom: 15px;
}
h3 {
color: #4a5568;
margin-top: 25px;
margin-bottom: 10px;
}
.last-updated {
background: #e2e8f0;
padding: 10px;
border-radius: 5px;
margin-bottom: 30px;
font-style: italic;
}
.contact-info {
background: #f7fafc;
padding: 15px;
border-left: 4px solid #4299e1;
margin: 20px 0;
}
.cookie-type {
background: #f7fafc;
border: 1px solid #cbd5e0;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.essential {
border-left: 4px solid #48bb78;
}
.functional {
border-left: 4px solid #4299e1;
}
.analytics {
border-left: 4px solid #ed8936;
}
.marketing {
border-left: 4px solid #9f7aea;
}
ul {
padding-left: 20px;
}
li {
margin-bottom: 8px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
border: 1px solid #e2e8f0;
padding: 12px;
text-align: left;
}
th {
background-color: #f7fafc;
font-weight: bold;
}
.toggle-info {
background: #e6fffa;
border: 1px solid #81e6d9;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Cookie Policy</h1>
<div class="last-updated">
<strong>Last Updated:</strong> [INSERT DATE]
</div>
<p>This Cookie Policy explains how Pilates with Fadia ("we," "our," or "us") uses cookies and similar tracking technologies on our website pilateswithfadia.com ("Site"). This policy should be read alongside our Privacy Policy.</p>
<h2>1. What Are Cookies?</h2>
<p>Cookies are small text files that are placed on your device (computer, smartphone, tablet) when you visit a website. They are widely used to make websites work more efficiently and to provide information to website owners about how users interact with their sites.</p>
<h2>2. How We Use Cookies</h2>
<p>We use cookies to:</p>
<ul>
<li>Remember your preferences and settings</li>
<li>Keep you logged into your account</li>
<li>Analyze how our website is used and improve its performance</li>
<li>Provide personalized content and recommendations</li>
<li>Enable social media features</li>
<li>Measure the effectiveness of our marketing campaigns</li>
<li>Ensure the security of our website</li>
</ul>
<h2>3. Types of Cookies We Use</h2>
<div class="cookie-type essential">
<h3>Essential Cookies</h3>
<p><strong>Purpose:</strong> These cookies are necessary for the website to function properly. They enable core functionality such as security, network management, and accessibility.</p>
<p><strong>Examples:</strong> Login authentication, shopping cart functionality, security features</p>
<p><strong>Can be disabled:</strong> No - the website cannot function without these cookies</p>
</div>
<div class="cookie-type functional">
<h3>Functional Cookies</h3>
<p><strong>Purpose:</strong> These cookies enable the website to provide enhanced functionality and personalization, such as remembering your preferences.</p>
<p><strong>Examples:</strong> Language preferences, region selection, accessibility settings</p>
<p><strong>Can be disabled:</strong> Yes - but may affect website functionality</p>
</div>
<div class="cookie-type analytics">
<h3>Analytics Cookies</h3>
<p><strong>Purpose:</strong> These cookies help us understand how visitors interact with our website by collecting and reporting information anonymously.</p>
<p><strong>Examples:</strong> Google Analytics, page visit tracking, user behavior analysis</p>
<p><strong>Can be disabled:</strong> Yes</p>
</div>
<div class="cookie-type marketing">
<h3>Marketing Cookies</h3>
<p><strong>Purpose:</strong> These cookies are used to track visitors across websites to display relevant and engaging advertisements.</p>
<p><strong>Examples:</strong> Social media pixels, advertising network cookies, retargeting cookies</p>
<p><strong>Can be disabled:</strong> Yes</p>
</div>
<h2>4. Specific Cookies We Use</h2>
<table>
<thead>
<tr>
<th>Cookie Name</th>
<th>Type</th>
<th>Purpose</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td>session_id</td>
<td>Essential</td>
<td>Maintains user session and login state</td>
<td>Session</td>
</tr>
<tr>
<td>user_preferences</td>
<td>Functional</td>
<td>Stores user preferences and settings</td>
<td>1 year</td>
</tr>
<tr>
<td>_ga</td>
<td>Analytics</td>
<td>Google Analytics - distinguishes unique users</td>
<td>2 years</td>
</tr>
<tr>
<td>_gat</td>
<td>Analytics</td>
<td>Google Analytics - throttles request rate</td>
<td>1 minute</td>
</tr>
<tr>
<td>_gid</td>
<td>Analytics</td>
<td>Google Analytics - distinguishes unique users</td>
<td>24 hours</td>
</tr>
<tr>
<td>fb_pixel</td>
<td>Marketing</td>
<td>Facebook advertising and retargeting</td>
<td>3 months</td>
</tr>
</tbody>
</table>
<h2>5. Third-Party Cookies</h2>
<p>We may also use third-party services that set their own cookies on our website, including:</p>
<ul>
<li><strong>Google Analytics:</strong> For website analytics and performance monitoring</li>
<li><strong>Social Media Platforms:</strong> For social sharing and engagement features</li>
<li><strong>Payment Processors:</strong> For secure payment processing</li>
<li><strong>Booking Systems:</strong> For class scheduling and appointment booking</li>
<li><strong>Live Chat Services:</strong> For customer support features</li>
</ul>
<h2>6. Cookie Duration</h2>
<p>Cookies may be either:</p>
<ul>
<li><strong>Session Cookies:</strong> Temporary cookies that are deleted when you close your browser</li>
<li><strong>Persistent Cookies:</strong> Cookies that remain on your device for a set period or until you delete them</li>
</ul>
<h2>7. Managing Your Cookie Preferences</h2>
<div class="toggle-info">
<h3>Browser Settings</h3>
<p>You can control and manage cookies through your browser settings. Most browsers allow you to:</p>
<ul>
<li>View what cookies have been set and delete them individually</li>
<li>Block third-party cookies</li>
<li>Block cookies from particular sites</li>
<li>Block all cookies from being set</li>
<li>Delete all cookies when you close your browser</li>
</ul>
</div>
<h3>Browser-Specific Instructions:</h3>
<ul>
<li><strong>Chrome:</strong> Settings > Privacy and Security > Cookies and other site data</li>
<li><strong>Firefox:</strong> Settings > Privacy & Security > Cookies and Site Data</li>
<li><strong>Safari:</strong> Preferences > Privacy > Manage Website Data</li>
<li><strong>Edge:</strong> Settings > Cookies and site permissions</li>
</ul>
<h3>Opt-Out Tools</h3>
<ul>
<li><strong>Google Analytics:</strong> <a href="https://tools.google.com/dlpage/gaoptout" target="_blank">Google Analytics Opt-out Browser Add-on</a></li>
<li><strong>Network Advertising Initiative:</strong> <a href="http://www.networkadvertising.org/choices/" target="_blank">NAI Opt-out Tool</a></li>
<li><strong>Digital Advertising Alliance:</strong> <a href="http://www.aboutads.info/choices/" target="_blank">DAA WebChoices Tool</a></li>
</ul>
<h2>8. Mobile Device Settings</h2>
<p>For mobile devices, you can manage cookies and tracking through your device settings:</p>
<ul>
<li><strong>iOS:</strong> Settings > Privacy > Tracking / Settings > Safari > Privacy & Security</li>
<li><strong>Android:</strong> Settings > Privacy > Advanced > Site Settings > Cookies</li>
</ul>
<h2>9. Impact of Disabling Cookies</h2>
<p>Please note that disabling certain cookies may impact your experience on our website:</p>
<ul>
<li>You may need to re-enter information more frequently</li>
<li>Some website features may not work properly</li>
<li>Personalized content and recommendations may not be available</li>
<li>You may still see advertisements, but they will be less relevant</li>
</ul>
<h2>10. Do Not Track Signals</h2>
<p>Some browsers include a "Do Not Track" feature that lets you tell websites you do not want to have your online activities tracked. Our website does not currently respond to Do Not Track browser signals or mechanisms.</p>
<h2>11. Updates to This Cookie Policy</h2>
<p>We may update this Cookie Policy from time to time to reflect changes in technology, legislation, or our business practices. We will notify you of any significant changes by posting the updated policy on our website.</p>
<h2>12. Contact Us</h2>
<div class="contact-info">
<p>If you have any questions about our use of cookies or this Cookie Policy, please contact us:</p>
<p><strong>Pilates with Fadia</strong><br>
Email: [INSERT EMAIL]<br>
Phone: [INSERT PHONE]<br>
Address: [INSERT ADDRESS]<br>
Website: pilateswithfadia.com</p>
</div>
<p><em>By continuing to use our website, you consent to our use of cookies in accordance with this Cookie Policy.</em></p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
attached_assets/save.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -2,36 +2,21 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Pilates with Fadia - Transform your body and mind with professional Pilates instruction in a welcoming environment." />
<meta name="keywords" content="pilates, fitness, exercise, wellness, studio, classes, reformer, mat pilates" />
<meta name="author" content="Fadia" />
<!-- Open Graph / Facebook -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>Pilates with Fadia | Feel at Home in Your Body</title>
<meta name="description" content="Online pilates classes to help you feel stronger and more connected to your body and breath." />
<!-- Open Graph tags -->
<meta property="og:title" content="Pilates with Fadia | Feel at Home in Your Body" />
<meta property="og:description" content="Online pilates classes to help you feel stronger and more connected to your body and breath." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://pilateswithfadia.com/" />
<meta property="og:title" content="Pilates with Fadia" />
<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>
<meta property="og:url" content="https://pilateswithfadia.com" />
<!-- Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
</head>
<body>
<div id="root"></div>
<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>
</html>

View File

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,4 +1,3 @@
import React from "react";
import { Switch, Route } from "wouter";
import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";

View File

@ -1,3 +1,4 @@
import { Link } from "wouter";
import FadiaStretchImage from "@assets/fadia-stretch_1749866078708.jpg";
export function AboutSection() {
@ -6,7 +7,6 @@ export function AboutSection() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row items-center">
<div className="md:w-1/2 md:pr-12 mb-8 md:mb-0">
{/* Bottom image */}
<div>
<img
src={FadiaStretchImage}
@ -18,7 +18,7 @@ export function AboutSection() {
<div className="md:w-1/2">
<div className="prose max-w-none">
<h3 className="text-3xl font-playfair font-semibold text-purple mb-4 text-center">My Pilates Story</h3>
<h3 className="text-2xl font-playfair font-semibold text-purple mb-4">My Pilates Story</h3>
<p className="text-gray-600 mb-4">
I came to movement as a way to feel good in my body and begin healing emotions I hadn't yet processed. It started simplywith home yoga classesand slowly turned into something much deeper.
</p>

View File

@ -1,4 +1,3 @@
import React from "react";
import { Link } from "wouter";
import FadiaImage from "@assets/Fadia-167-crop.jpg";
@ -17,7 +16,7 @@ export function HomeAboutSection() {
<div className="md:w-1/2">
<div className="prose max-w-none">
<h3 className="text-3xl font-playfair font-semibold text-purple mb-6 text-center">Meet Fadia</h3>
<h3 className="text-2xl font-playfair font-semibold text-purple mb-4">Meet Fadia</h3>
<p className="text-gray-600 mb-4">
Fadia is a certified Pilates instructor, former lawyer, and community builder with a passion for helping people connect with their bodies through mindful movement.
</p>

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import { Calendar } from "@/components/ui/calendar";
import { Class, insertBookingSchema } from "@/lib/schema";
import { Class, insertBookingSchema } from "@shared/schema";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { useMutation } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";

View File

@ -1,13 +1,11 @@
import React from "react";
import { StaticClass } from "@/lib/static-data";
import { Link } from "wouter";
import { Class } from "@shared/schema";
import FadiaGardenImage from "@assets/fadia-garden_1749836720986.jpg";
import PilatesClassImage from "@assets/pilates_class_1749837680834.jpeg";
import FadiaPrivateImage from "@assets/Fadia-7_1749842141071.jpg";
import FadiaBallImage from "@assets/fadia-ball_1749842241591.jpg";
interface ClassCardProps {
classData: StaticClass;
classData: Class;
}
export function ClassCard({ classData }: ClassCardProps) {
@ -48,46 +46,24 @@ export function ClassCard({ classData }: ClassCardProps) {
}
};
// Get link based on class type
const getClassLink = () => {
switch (classData.classType) {
case "group": return "https://nuncenter.com/";
case "small-group": return "https://nuncenter.com/";
case "private": return "/contact";
case "online": return "https://www.momoyoga.com/pilates-with-fadia/schedule";
default: return "#";
}
};
// Determine if link should open in new tab
const shouldOpenNewTab = (classType: string) => {
return classType === "group" || classType === "small-group" || classType === "online";
};
const linkProps = shouldOpenNewTab(classData.classType)
? { href: getClassLink(), target: "_blank", rel: "noopener noreferrer" }
: { href: getClassLink() };
return (
<a {...linkProps} className="block">
<div className="bg-white rounded-lg overflow-hidden shadow-lg transition-transform duration-300 hover:transform hover:scale-105">
<img
src={getClassImage()}
alt={classData.name}
className="w-full h-48 object-cover"
/>
<div className="p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="font-playfair font-bold text-lg leading-tight">{classData.name}</h3>
<span className={`${badgeColor()} text-xs px-2 py-1 rounded-full font-semibold ml-2 flex-shrink-0`}>
{formatClassType(classData.classType)}
</span>
</div>
<p className="text-gray-600 text-sm leading-snug">
{classData.description}
</p>
<div className="bg-white rounded-lg overflow-hidden shadow-lg transition-transform duration-300 hover:transform hover:scale-105">
<img
src={getClassImage()}
alt={classData.name}
className="w-full h-50 object-cover"
/>
<div className="p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="font-playfair font-bold text-lg leading-tight">{classData.name}</h3>
<span className={`${badgeColor()} text-xs px-2 py-1 rounded-full font-semibold ml-2 flex-shrink-0`}>
{formatClassType(classData.classType)}
</span>
</div>
<p className="text-gray-600 text-sm leading-snug">
{classData.description}
</p>
</div>
</a>
</div>
);
}

View File

@ -1,8 +1,15 @@
import React from "react";
import { ClassCard } from "./class-card";
import { STATIC_CLASSES } from "@/lib/static-data";
import { useQuery } from "@tanstack/react-query";
import { Class } from "@shared/schema";
import { Skeleton } from "@/components/ui/skeleton";
import FadiaClassImage from "../../assets/Fadia-156.jpg";
export function ClassesSection() {
const { data: classes, isLoading, error } = useQuery<Class[]>({
queryKey: ["/api/classes"],
});
return (
<section className="py-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@ -13,14 +20,39 @@ export function ClassesSection() {
<p className="max-w-3xl mx-auto text-gray-600">Join personalized pilates classes where you'll discover strength, flexibility, and mindfulness.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{STATIC_CLASSES.map((classData) => (
<ClassCard
key={classData.id}
classData={classData}
/>
))}
</div>
{/* Class description boxes removed */}
{isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[1, 2, 3, 4].map((_, i) => (
<div key={i} className="bg-white rounded-lg overflow-hidden shadow-lg">
<Skeleton className="w-full h-36" />
<div className="p-4">
<div className="flex justify-between items-start mb-2">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-5 w-16" />
</div>
<Skeleton className="h-4 w-full mb-1" />
<Skeleton className="h-4 w-full mb-1" />
<Skeleton className="h-4 w-3/4" />
</div>
</div>
))}
</div>
) : error ? (
<div className="text-center text-red-500">
<p>Error loading classes. Please try again later.</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{classes?.map((classData) => (
<ClassCard
key={classData.id}
classData={classData}
/>
))}
</div>
)}
<div className="text-center mt-12">
<a href="https://www.momoyoga.com/pilates-with-fadia/schedule" target="_blank" rel="noopener noreferrer" className="inline-block">

View File

@ -1,5 +1,3 @@
import React from "react";
interface TestimonialProps {
quote: string;
author: string;
@ -10,14 +8,12 @@ interface TestimonialProps {
export function Testimonial({ quote, author, memberSince, initials, color }: TestimonialProps) {
return (
<div className="bg-transparent text-left flex flex-col h-full">
<div className="flex-grow">
<p className="text-gray-700 mb-6 italic">
"{quote}"
</p>
</div>
<div className="bg-transparent text-left">
<p className="text-gray-700 mb-6 italic">
"{quote}"
</p>
<div className="flex items-center mt-auto">
<div className="flex items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center mr-3 bg-${color} bg-opacity-20 text-${color}`}>
<span>{initials}</span>
</div>

View File

@ -1,5 +1,3 @@
import React from "react";
export function ContactSection() {
return (
<section className="pt-6 pb-12 bg-white">

View File

@ -0,0 +1,68 @@
import React from "react";
interface FeatureCardProps {
title: string;
description: string;
icon: string;
color: "teal" | "purple" | "rose";
}
export function FeatureCard({ title, description, icon, color }: FeatureCardProps) {
const colorMap = {
teal: "#0c8991",
purple: "#9D5E9B",
rose: "#B55076"
};
return (
<div className="text-center p-8 transition duration-300 hover:shadow-md">
<i className={`fas ${icon} text-2xl`} style={{ color: colorMap[color] }}></i>
<h3 className="text-xl font-playfair font-semibold mt-4 mb-3">{title}</h3>
<p className="text-gray-600">{description}</p>
</div>
);
}
export function FeaturesSection() {
const features = [
{
title: "Balance",
description: "Find harmony between body and mind through mindful movement.",
icon: "fa-align-center",
color: "teal" as const,
},
{
title: "Strength",
description: "Build core power and muscular endurance through controlled exercises.",
icon: "fa-dumbbell",
color: "purple" as const,
},
{
title: "Flexibility",
description: "Enhance your range of motion and release tension throughout your body.",
icon: "fa-wind",
color: "rose" as const,
},
];
return (
<section className="py-16 bg-purple">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-playfair font-semibold text-white mb-4">
Core Benefits
</h2>
<p className="text-white text-opacity-90 max-w-2xl mx-auto">
Experience the transformative power of Pilates through these foundational principles
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 bg-white p-6">
{features.map((feature, index) => (
<FeatureCard key={index} {...feature} />
))}
</div>
</div>
</section>
);
}

View File

@ -1,4 +1,3 @@
import React from "react";
import { Link } from "wouter";
import FadiaHeroImage from "../../assets/Fadia-15.jpg";

View File

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Logo } from "@/components/ui/logo";
import { Link } from "wouter";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
@ -126,9 +126,7 @@ export default function Footer() {
<ul className="space-y-3">
<li className="flex items-start">
<i className="fas fa-map-marker-alt mt-1 mr-2 text-white"></i>
<a href="https://nuncenter.com/" target="_blank" rel="noopener noreferrer" className="text-white text-opacity-70 hover:text-white hover:text-opacity-100 transition duration-300">
Nun Center<br/>Zamalek, Cairo, Egypt
</a>
<span className="text-white text-opacity-70">Nun Center<br/>Zamalek, Cairo, Egypt</span>
</li>
<li className="flex items-center">
<i className="fas fa-envelope mr-2 text-white"></i>

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { Link, useLocation } from "wouter";
import { Logo } from "@/components/ui/logo";

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { insertNewsletterSchema } from "@/lib/schema";
import { insertNewsletterSchema } from "@shared/schema";
import { useToast } from "@/hooks/use-toast";
import { Loader2 } from "lucide-react";
@ -168,7 +168,7 @@ export function NewsletterSection() {
</div>
<div className="max-w-2xl mx-auto">
<form className="bg-white rounded-lg shadow-lg p-8" style={{ backgroundColor: '#fff' }} onSubmit={handleSubmit}>
<form className="bg-white p-8 shadow-sm" onSubmit={handleSubmit}>
<div className="flex flex-col mb-6">
<label htmlFor="email" className="mb-2 text-gray-700 font-medium">Email Address</label>
<input

View File

@ -1,24 +1,24 @@
import { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
interface InstagramPost {
id: string;
media_type: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM';
media_url: string;
permalink: string;
caption?: string;
timestamp: string;
}
export function PhotoGallery() {
useEffect(() => {
// Load Curator.io script
const script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.charset = "UTF-8";
script.src = "https://cdn.curator.io/published/1964cded-8962-41c1-b7c1-d36f02707c7a.js";
// Insert script before the closing body tag
document.body.appendChild(script);
// Cleanup function to remove script when component unmounts
return () => {
if (document.body.contains(script)) {
document.body.removeChild(script);
}
};
}, []);
const [showAllPosts, setShowAllPosts] = useState(false);
const { data: posts, isLoading, error } = useQuery<InstagramPost[]>({
queryKey: ['/api/instagram-feed'],
staleTime: 1000 * 60 * 30, // Cache for 30 minutes
});
const displayPosts = showAllPosts ? posts : posts?.slice(0, 6);
return (
<section className="py-12 bg-white">
@ -31,10 +31,94 @@ export function PhotoGallery() {
<div className="flex justify-center">
<div className="w-full max-w-4xl">
{/* Curator.io Instagram Feed */}
<div id="curator-feed-default-feed-layout">
<a href="https://curator.io" target="_blank" className="crt-logo crt-tag">Powered by Curator.io</a>
</div>
{isLoading && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((index) => (
<div key={index} className="aspect-square bg-gray-200 rounded-lg animate-pulse"></div>
))}
</div>
)}
{error && (
<div className="bg-gray-50 rounded-lg p-8 text-center">
<div className="mb-6">
<i className="fab fa-instagram text-6xl text-pink-500 mb-4"></i>
<p className="text-gray-600 mb-6">Follow me on Instagram for daily inspiration, movement tips, and behind-the-scenes content from my classes.</p>
<a
href="https://www.instagram.com/pilateswithfadia/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold rounded-full hover:from-purple-600 hover:to-pink-600 transition duration-300"
>
<i className="fab fa-instagram mr-2"></i>
Visit Instagram
</a>
</div>
</div>
)}
{posts && posts.length > 0 && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{displayPosts?.map((post: InstagramPost) => (
<a
key={post.id}
href={post.permalink}
target="_blank"
rel="noopener noreferrer"
className="group block aspect-square bg-gray-100 rounded-lg overflow-hidden hover:shadow-lg transition-shadow duration-300"
>
{post.media_type === 'VIDEO' ? (
<div className="relative w-full h-full">
<video
src={post.media_url}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
muted
loop
playsInline
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
/>
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-300 flex items-center justify-center">
<i className="fas fa-play text-white text-2xl opacity-0 group-hover:opacity-80 transition-opacity duration-300"></i>
</div>
</div>
) : (
<img
src={post.media_url}
alt={post.caption ? post.caption.substring(0, 100) + '...' : 'Instagram post'}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy"
/>
)}
</a>
))}
</div>
{posts.length > 6 && (
<div className="text-center">
<button
onClick={() => setShowAllPosts(!showAllPosts)}
className="px-6 py-3 bg-rose text-white font-semibold rounded-full hover:bg-opacity-90 transition duration-300"
>
{showAllPosts ? 'Show Less' : `View All ${posts.length} Posts`}
</button>
</div>
)}
<div className="text-center mt-8">
<a
href="https://www.instagram.com/pilateswithfadia/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white font-semibold rounded-full hover:from-purple-600 hover:to-pink-600 transition duration-300"
>
<i className="fab fa-instagram mr-2"></i>
Follow on Instagram
</a>
</div>
</div>
)}
</div>
</div>
</div>

View File

@ -1,4 +1,3 @@
import React from "react";
import { Testimonial } from "@/components/community/testimonial";
export function TestimonialsSection() {
@ -11,10 +10,10 @@ export function TestimonialsSection() {
color: "teal",
},
{
quote: "Fadia's calm and soothing cueing had me go through the flow steadily, challenging my muscles without even realizing and that felt great!",
author: "Sara from Cairo",
quote: "I've seen incredible improvements in my posture and core strength since joining Fadia's classes. She truly understands how to help each individual.",
author: "Ahmed M.",
memberSince: "",
initials: "SC",
initials: "AM",
color: "purple",
},
{
@ -32,7 +31,7 @@ export function TestimonialsSection() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{testimonials.map((testimonial, index) => (
<div className="rounded-lg p-6 shadow-sm transition-transform duration-300 hover:scale-105 bg-[#92bbc45c] flex flex-col h-full" key={index}>
<div className="rounded-lg p-6 shadow-sm transition-transform duration-300 hover:scale-105 bg-[#92bbc45c]" key={index}>
<Testimonial
quote={testimonial.quote}
author={testimonial.author}

View File

@ -1,4 +1,3 @@
import React from "react";
import { Link } from "wouter";
import LogoImage from "../../assets/rectangular-logo.png";

View File

@ -1,4 +1,3 @@
import React from "react";
import { useToast } from "@/hooks/use-toast"
import {
Toast,

View File

@ -1,14 +1,15 @@
import React, { createContext, ReactNode, useContext } from "react";
import { createContext, ReactNode, useContext } from "react";
import {
useQuery,
useMutation,
UseMutationResult,
} from "@tanstack/react-query";
import { apiRequest, getQueryFn, queryClient } from "@/lib/queryClient";
import { insertUserSchema, User as SelectUser, InsertUser, Login } from "@shared/schema";
import { getQueryFn, apiRequest, queryClient } from "../lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import { z } from "zod";
import { insertUserSchema, User, InsertUser, Login } from "@/lib/schema";
import { Loader2 } from "lucide-react";
type User = Omit<SelectUser, "password">;
type AuthContextType = {
user: User | null;
@ -51,7 +52,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
queryClient.setQueryData(["/api/user"], user);
toast({
title: "Login successful",
description: `Welcome back, ${user.name}!`,
description: `Welcome back, ${user.fullName || user.username}!`,
});
},
onError: (error: Error) => {

View File

@ -15,11 +15,11 @@
--card-foreground: 20 14.3% 4.1%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--primary: 175 84% 30%;
--primary: 175 84% 30%; /* teal: #0c8991 */
--primary-foreground: 211 100% 99%;
--secondary: 302 24% 49%;
--secondary: 302 24% 49%; /* purple: #9D5E9B */
--secondary-foreground: 24 9.8% 10%;
--accent: 335 38% 51%;
--accent: 335 38% 51%; /* rose: #B55076 */
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
@ -27,25 +27,104 @@
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 175 84% 30%;
--primary-foreground: 211 100% 99%;
--secondary: 302 24% 49%;
--secondary-foreground: 0 0% 98%;
--accent: 335 38% 51%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 240 4.9% 83.9%;
}
@layer base {
* {
@apply border-gray-200;
@apply border-border;
}
body {
@apply font-sans antialiased bg-white text-gray-900;
@apply font-sans antialiased bg-background text-foreground;
font-family: 'Khula', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Lato', sans-serif;
}
.subtitle, .card-subtitle, .section-subtitle {
font-family: 'Catamaran', sans-serif;
}
}
.geometric-pattern {
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%230c8991' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.islamic-pattern {
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%239d5e9b' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E");
}
.islamic-pattern-teal {
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%230c8991' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E");
}
.islamic-pattern-rose {
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23b55076' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E");
}
.arabesque-pattern {
background-image: url("data:image/svg+xml,%3Csvg width='52' height='26' viewBox='0 0 52 26' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%230c8991' fill-opacity='0.05'%3E%3Cpath d='M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6h-2c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.arabesque-pattern-purple {
background-image: url("data:image/svg+xml,%3Csvg width='52' height='26' viewBox='0 0 52 26' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%239d5e9b' fill-opacity='0.05'%3E%3Cpath d='M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6h-2c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.arabesque-pattern-rose {
background-image: url("data:image/svg+xml,%3Csvg width='52' height='26' viewBox='0 0 52 26' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23b55076' fill-opacity='0.05'%3E%3Cpath d='M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6h-2c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.islamic-arch-divider {
position: relative;
height: 60px;
margin: -30px 0;
overflow: hidden;
z-index: 10;
}
.islamic-arch-top {
position: relative;
overflow: hidden;
}
.islamic-arch-top svg {
display: block;
width: 100%;
}
.islamic-arch-bottom {
position: relative;
overflow: hidden;
}
.islamic-arch-bottom svg {
display: block;
width: 100%;
transform: rotate(180deg);
}
.teal-bg {
background-color: #0c8991;
}
@ -82,46 +161,6 @@
color: #B55076;
}
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
line-height: 1.6;
color: #333;
background-color: white;
}
/* Custom colors */
:root {
--teal: #0c8991;
--purple: #9D5E9B;
--rose: #B55076;
--teal-light: rgba(12, 137, 145, 0.1);
--purple-light: #E8D5E6;
--rose-light: rgba(181, 80, 118, 0.1);
--white: #ffffff;
--black: #000000;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
line-height: 1.2;
}
.font-playfair, .title {
font-family: 'Lato', sans-serif;
}
@ -137,400 +176,3 @@ h1, h2, h3, h4, h5, h6 {
.font-cairo, .subtitle {
font-family: 'Catamaran', sans-serif;
}
/* Layout utilities */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
/* Spacing */
.p-0 { padding: 0; }
.p-1 { padding: 0.25rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-5 { padding: 1.25rem; }
.p-6 { padding: 1.5rem; }
.p-8 { padding: 2rem; }
.p-10 { padding: 2.5rem; }
.p-12 { padding: 3rem; }
.p-16 { padding: 4rem; }
.p-20 { padding: 5rem; }
.px-0 { padding-left: 0; padding-right: 0; }
.px-1 { padding-left: 0.25rem; padding-right: 0.25rem; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.px-8 { padding-left: 2rem; padding-right: 2rem; }
.px-10 { padding-left: 2.5rem; padding-right: 2.5rem; }
.px-12 { padding-left: 3rem; padding-right: 3rem; }
.px-16 { padding-left: 4rem; padding-right: 4rem; }
.px-20 { padding-left: 5rem; padding-right: 5rem; }
.py-0 { padding-top: 0; padding-bottom: 0; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.py-5 { padding-top: 1.25rem; padding-bottom: 1.25rem; }
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; }
.py-12 { padding-top: 3rem; padding-bottom: 3rem; }
.py-16 { padding-top: 4rem; padding-bottom: 4rem; }
.py-20 { padding-top: 5rem; padding-bottom: 5rem; }
.m-0 { margin: 0; }
.m-1 { margin: 0.25rem; }
.m-2 { margin: 0.5rem; }
.m-3 { margin: 0.75rem; }
.m-4 { margin: 1rem; }
.m-5 { margin: 1.25rem; }
.m-6 { margin: 1.5rem; }
.m-8 { margin: 2rem; }
.m-10 { margin: 2.5rem; }
.m-12 { margin: 3rem; }
.m-16 { margin: 4rem; }
.m-20 { margin: 5rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.my-auto { margin-top: auto; margin-bottom: auto; }
/* Colors */
.bg-white { background-color: var(--white); }
.bg-black { background-color: var(--black); }
.bg-gray-50 { background-color: var(--gray-50); }
.bg-gray-100 { background-color: var(--gray-100); }
.bg-gray-200 { background-color: var(--gray-200); }
.bg-gray-300 { background-color: var(--gray-300); }
.bg-teal { background-color: var(--teal); }
.bg-purple { background-color: var(--purple); }
.bg-rose { background-color: var(--rose); }
.bg-teal-light { background-color: var(--teal-light); }
.bg-purple-light { background-color: var(--purple-light); }
.bg-rose-light { background-color: var(--rose-light); }
.text-white { color: var(--white); }
.text-black { color: var(--black); }
.text-gray-500 { color: var(--gray-500); }
.text-gray-600 { color: var(--gray-600); }
.text-gray-700 { color: var(--gray-700); }
.text-gray-800 { color: var(--gray-800); }
.text-gray-900 { color: var(--gray-900); }
.text-teal { color: var(--teal); }
.text-purple { color: var(--purple); }
.text-rose { color: var(--rose); }
/* Typography sizes */
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-base { font-size: 1rem; }
.text-lg { font-size: 1.125rem; }
.text-xl { font-size: 1.25rem; }
.text-2xl { font-size: 1.5rem; }
.text-3xl { font-size: 1.875rem; }
.text-4xl { font-size: 2.25rem; }
.text-5xl { font-size: 3rem; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
/* Width and height */
.w-full { width: 100%; }
.w-auto { width: auto; }
.w-1\/2 { width: 50%; }
.w-1\/3 { width: 33.333333%; }
.w-2\/3 { width: 66.666667%; }
.w-1\/4 { width: 25%; }
.w-3\/4 { width: 75%; }
.h-full { height: 100%; }
.h-auto { height: auto; }
.h-screen { height: 100vh; }
.max-w-sm { max-width: 24rem; }
.max-w-md { max-width: 28rem; }
.max-w-lg { max-width: 32rem; }
.max-w-xl { max-width: 36rem; }
.max-w-2xl { max-width: 42rem; }
.max-w-3xl { max-width: 48rem; }
.max-w-4xl { max-width: 56rem; }
.max-w-5xl { max-width: 64rem; }
.max-w-7xl { max-width: 80rem; }
/* Grid */
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-5 { gap: 1.25rem; }
.gap-6 { gap: 1.5rem; }
.gap-8 { gap: 2rem; }
.gap-10 { gap: 2.5rem; }
.gap-12 { gap: 3rem; }
/* Borders */
.border { border-width: 1px; }
.border-0 { border-width: 0; }
.border-2 { border-width: 2px; }
.border-4 { border-width: 4px; }
.rounded { border-radius: 0.25rem; }
.rounded-sm { border-radius: 0.125rem; }
.rounded-md { border-radius: 0.375rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-full { border-radius: 9999px; }
.border-gray-200 { border-color: var(--gray-200); }
.border-gray-300 { border-color: var(--gray-300); }
.border-teal { border-color: var(--teal); }
.border-purple { border-color: var(--purple); }
.border-rose { border-color: var(--rose); }
/* Shadows */
.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
.shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }
/* Position */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.sticky { position: sticky; }
.top-0 { top: 0; }
.top-1\/2 { top: 50%; }
.top-full { top: 100%; }
.bottom-0 { bottom: 0; }
.bottom-1\/2 { bottom: 50%; }
.left-0 { left: 0; }
.left-1\/2 { left: 50%; }
.right-0 { right: 0; }
.right-1\/2 { right: 50%; }
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
.inset-x-0 { left: 0; right: 0; }
.inset-y-0 { top: 0; bottom: 0; }
/* Z-index */
.z-10 { z-index: 10; }
.z-20 { z-index: 20; }
.z-50 { z-index: 50; }
/* Display */
.block { display: block; }
.inline-block { display: inline-block; }
.inline { display: inline; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.grid { display: grid; }
.hidden { display: none; }
/* Transitions */
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transition-transform { transition-property: transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.duration-200 { transition-duration: 200ms; }
.duration-300 { transition-duration: 300ms; }
/* Hover states */
.hover\:bg-opacity-90:hover { background-color: rgba(0, 0, 0, 0.9); }
.hover\:text-white:hover { color: var(--white); }
.hover\:text-opacity-100:hover { opacity: 1; }
.hover\:scale-105:hover { transform: scale(1.05); }
/* Responsive */
@media (min-width: 640px) {
.sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.sm\:px-8 { padding-left: 2rem; padding-right: 2rem; }
.sm\:text-xl { font-size: 1.25rem; }
.sm\:text-2xl { font-size: 1.5rem; }
.sm\:text-3xl { font-size: 1.875rem; }
.sm\:text-4xl { font-size: 2.25rem; }
.sm\:text-5xl { font-size: 3rem; }
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sm\:flex { display: flex; }
.sm\:hidden { display: none; }
}
@media (min-width: 768px) {
.md\:flex { display: flex; }
.md\:hidden { display: none; }
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.md\:flex-row { flex-direction: row; }
.md\:flex-col { flex-direction: column; }
.md\:w-1\/2 { width: 50%; }
.md\:w-1\/3 { width: 33.333333%; }
.md\:w-2\/3 { width: 66.666667%; }
.md\:text-lg { font-size: 1.125rem; }
.md\:text-xl { font-size: 1.25rem; }
.md\:text-2xl { font-size: 1.5rem; }
.md\:text-3xl { font-size: 1.875rem; }
.md\:text-4xl { font-size: 2.25rem; }
.md\:text-5xl { font-size: 3rem; }
.md\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.md\:px-8 { padding-left: 2rem; padding-right: 2rem; }
.md\:py-20 { padding-top: 5rem; padding-bottom: 5rem; }
.md\:mb-0 { margin-bottom: 0; }
.md\:mb-4 { margin-bottom: 1rem; }
.md\:mb-6 { margin-bottom: 1.5rem; }
.md\:mb-8 { margin-bottom: 2rem; }
.md\:mb-16 { margin-bottom: 4rem; }
.md\:gap-8 { gap: 2rem; }
.md\:gap-12 { gap: 3rem; }
.md\:space-x-8 > * + * { margin-left: 2rem; }
.md\:space-y-1 > * + * { margin-top: 0.25rem; }
.md\:space-y-3 > * + * { margin-top: 0.75rem; }
.md\:space-y-8 > * + * { margin-top: 2rem; }
}
@media (min-width: 1024px) {
.lg\:px-4 { padding-left: 1rem; padding-right: 1rem; }
.lg\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
.lg\:text-4xl { font-size: 2.25rem; }
.lg\:text-5xl { font-size: 3rem; }
.lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.lg\:flex { display: flex; }
.lg\:hidden { display: none; }
}
/* Custom utility classes */
.bg-transparent { background-color: transparent; }
.bg-cover { background-size: cover; }
.bg-center { background-position: center; }
.leading-tight { line-height: 1.25; }
.leading-snug { line-height: 1.375; }
.antialiased { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
.object-cover { object-fit: cover; }
.object-contain { object-fit: contain; }
.object-center { object-position: center; }
.overflow-hidden { overflow: hidden; }
.overflow-auto { overflow: auto; }
.overflow-x-auto { overflow-x: auto; }
.overflow-y-auto { overflow-y: auto; }
/* Specific color values used in components */
.bg-916398 { background-color: #916398; }
.bg-7ebdc5 { background-color: #7ebdc5; }
.text-ffffff { color: #ffffff; }
.text-49878f { color: #49878f; }
.bg-92bbc45c { background-color: rgba(146, 187, 196, 0.36); }
/* Opacity utilities */
.bg-opacity-20 { background-color: rgba(0, 0, 0, 0.2); }
.bg-opacity-30 { background-color: rgba(0, 0, 0, 0.3); }
.bg-opacity-90 { background-color: rgba(0, 0, 0, 0.9); }
.text-opacity-70 { opacity: 0.7; }
.text-opacity-80 { opacity: 0.8; }
.text-opacity-100 { opacity: 1; }
.border-opacity-20 { border-color: rgba(255, 255, 255, 0.2); }
.border-opacity-30 { border-color: rgba(255, 255, 255, 0.3); }
/* Transform utilities */
.transform { transform: translateZ(0); }
.scale-105 { transform: scale(1.05); }
.translateY-2 { transform: translateY(-2px); }
.translateY-1 { transform: translateY(-1px); }
/* Additional spacing utilities */
.space-x-8 > * + * { margin-left: 2rem; }
.space-y-1 > * + * { margin-top: 0.25rem; }
.space-y-3 > * + * { margin-top: 0.75rem; }
.space-y-8 > * + * { margin-top: 2rem; }
/* Button styles */
button, .btn {
cursor: pointer;
border: none;
outline: none;
transition: all 0.3s ease;
}
button:hover, .btn:hover {
transform: translateY(-1px);
}
/* Link styles */
a {
text-decoration: none;
color: inherit;
}
a:hover {
text-decoration: underline;
}
/* Image styles */
img {
max-width: 100%;
height: auto;
}
/* List styles */
ul, ol {
list-style: none;
}
/* Form styles */
input, textarea, select {
font-family: inherit;
font-size: inherit;
}
/* Focus styles */
button:focus, input:focus, textarea:focus, select:focus {
outline: 2px solid var(--teal);
outline-offset: 2px;
}

View File

@ -44,11 +44,11 @@ export const getQueryFn: <T>(options: {
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
queryFn: getQueryFn({ on401: "throw" }),
refetchInterval: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
gcTime: 5 * 60 * 1000, // 5 minutes
retry: false,
},
mutations: {
retry: false,

View File

@ -1,75 +0,0 @@
import { z } from "zod";
// User schemas
export const insertUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2).max(100),
role: z.enum(["user", "admin"]).default("user"),
});
export type InsertUser = z.infer<typeof insertUserSchema>;
export const userSchema = insertUserSchema.extend({
id: z.number(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type User = z.infer<typeof userSchema>;
// Login schema
export const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
});
export type Login = z.infer<typeof loginSchema>;
// Class schemas
export const classSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
duration: z.number(),
price: z.number(),
maxCapacity: z.number(),
instructor: z.string(),
category: z.string(),
level: z.enum(["beginner", "intermediate", "advanced"]),
classType: z.enum(["group", "small-group", "private", "online"]),
imageUrl: z.string().optional(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type Class = z.infer<typeof classSchema>;
// Booking schemas
export const insertBookingSchema = z.object({
classId: z.number(),
userId: z.number(),
date: z.string(),
paid: z.boolean().default(false),
status: z.enum(["pending", "confirmed", "cancelled"]).default("pending"),
});
export type InsertBooking = z.infer<typeof insertBookingSchema>;
// Newsletter schemas
export const insertNewsletterSchema = z.object({
email: z.string().email(),
agreedToTerms: z.boolean(),
});
export type InsertNewsletter = z.infer<typeof insertNewsletterSchema>;
// Contact message schemas
export const insertContactMessageSchema = 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),
});
export type InsertContactMessage = z.infer<typeof insertContactMessageSchema>;

View File

@ -1,53 +0,0 @@
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"
}
];

View File

@ -1,5 +1,5 @@
import { z } from "zod";
import { insertUserSchema, insertContactMessageSchema, insertNewsletterSchema } from "@/lib/schema";
import { insertUserSchema, insertContactMessageSchema, insertNewsletterSchema } from "@shared/schema";
/**
* Extended registration schema with password confirmation

View File

@ -1,4 +1,3 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";

View File

@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAuth, registrationSchema, RegistrationData } from "@/hooks/use-auth";
import { useLocation } from "wouter";
import { loginSchema, Login } from "@/lib/schema";
import { loginSchema, Login } from "@shared/schema";
import {
Form,
FormControl,
@ -44,7 +44,7 @@ export default function AuthPage() {
const loginForm = useForm<Login>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: "",
username: "",
password: "",
},
});
@ -53,8 +53,9 @@ export default function AuthPage() {
const registerForm = useForm<RegistrationData>({
resolver: zodResolver(registrationSchema),
defaultValues: {
username: "",
email: "",
name: "",
fullName: "",
password: "",
confirmPassword: "",
},
@ -103,12 +104,12 @@ export default function AuthPage() {
<form onSubmit={loginForm.handleSubmit(onLoginSubmit)} className="space-y-6">
<FormField
control={loginForm.control}
name="email"
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter your email" {...field} />
<Input placeholder="Enter your username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -163,6 +164,19 @@ export default function AuthPage() {
<CardContent>
<Form {...registerForm}>
<form onSubmit={registerForm.handleSubmit(onRegisterSubmit)} className="space-y-6">
<FormField
control={registerForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Choose a username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={registerForm.control}
name="email"
@ -178,12 +192,12 @@ export default function AuthPage() {
/>
<FormField
control={registerForm.control}
name="name"
name="fullName"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="Enter your name" {...field} value={typeof field.value === 'string' ? field.value : ''} />
<Input placeholder="Enter your full name" {...field} value={typeof field.value === 'string' ? field.value : ''} />
</FormControl>
<FormMessage />
</FormItem>

View File

@ -1,4 +1,3 @@
import React, { useEffect } from "react";
import { HeroSection } from "@/components/home/hero-section";
import { HomeAboutSection } from "@/components/about/home-about-section";
@ -6,6 +5,8 @@ import { ClassesSection } from "@/components/classes/classes-section";
import { TestimonialsSection } from "@/components/testimonials/testimonials-section";
import { ContactSection } from "@/components/contact/contact-section";
import { CTASection } from "@/components/home/cta-section";
import { useEffect } from "react";
export default function HomePage() {
// Set meta data for SEO
@ -46,6 +47,7 @@ export default function HomePage() {
<HomeAboutSection />
<TestimonialsSection />
<ContactSection />
<CTASection />
</main>
);
}

View File

@ -1,155 +0,0 @@
import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
content: [
"./index.html",
"./src/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{html,js,jsx,ts,tsx}",
"./src/components/**/*.{js,jsx,ts,tsx}",
"./src/pages/**/*.{js,jsx,ts,tsx}",
"./src/hooks/**/*.{js,jsx,ts,tsx}",
"./src/lib/**/*.{js,jsx,ts,tsx}",
],
safelist: [
// Layout
'container', 'flex', 'grid', 'block', 'inline-block', 'hidden',
// Spacing
'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-8', 'p-10', 'p-12', 'p-16', 'p-20',
'px-0', 'px-1', 'px-2', 'px-3', 'px-4', 'px-5', 'px-6', 'px-8', 'px-10', 'px-12', 'px-16', 'px-20',
'py-0', 'py-1', 'py-2', 'py-3', 'py-4', 'py-5', 'py-6', 'py-8', 'py-10', 'py-12', 'py-16', 'py-20',
'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-5', 'm-6', 'm-8', 'm-10', 'm-12', 'm-16', 'm-20',
'mx-auto', 'my-auto',
// Colors
'bg-white', 'bg-black', 'bg-gray-50', 'bg-gray-100', 'bg-gray-200', 'bg-gray-300',
'text-white', 'text-black', 'text-gray-500', 'text-gray-600', 'text-gray-700', 'text-gray-800', 'text-gray-900',
'bg-teal', 'bg-purple', 'bg-rose', 'text-teal', 'text-purple', 'text-rose',
'bg-teal-light', 'bg-purple-light', 'bg-rose-light',
'bg-opacity-20', 'bg-opacity-30', 'bg-opacity-90',
'text-opacity-70', 'text-opacity-80', 'text-opacity-100',
'border-opacity-20', 'border-opacity-30',
// Typography
'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'text-5xl',
'font-normal', 'font-medium', 'font-semibold', 'font-bold',
'text-left', 'text-center', 'text-right',
'font-playfair', 'font-raleway', 'font-aref', 'font-cairo',
// Flexbox
'flex-row', 'flex-col', 'items-start', 'items-center', 'items-end', 'items-stretch',
'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around',
'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-5', 'gap-6', 'gap-8', 'gap-10', 'gap-12',
// Grid
'grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4',
// Width/Height
'w-full', 'w-auto', 'w-1/2', 'w-1/3', 'w-2/3', 'w-1/4', 'w-3/4',
'h-full', 'h-auto', 'h-screen', 'h-svh',
'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-3xl', 'max-w-4xl', 'max-w-5xl', 'max-w-7xl',
// Borders
'border', 'border-0', 'border-2', 'border-4',
'rounded', 'rounded-sm', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-full',
'border-gray-200', 'border-gray-300', 'border-teal', 'border-purple', 'border-rose',
// Shadows
'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl',
// Position
'relative', 'absolute', 'fixed', 'sticky',
'top-0', 'top-1/2', 'top-full',
'bottom-0', 'bottom-1/2',
'left-0', 'left-1/2',
'right-0', 'right-1/2',
'inset-0', 'inset-x-0', 'inset-y-0',
// Z-index
'z-10', 'z-20', 'z-50',
// Overflow
'overflow-hidden', 'overflow-auto', 'overflow-x-auto', 'overflow-y-auto',
// Display
'block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'hidden',
// Transitions
'transition-all', 'transition-colors', 'transition-transform', 'transition-opacity',
'duration-200', 'duration-300',
// Hover states
'hover:bg-opacity-90', 'hover:text-white', 'hover:text-opacity-100', 'hover:scale-105',
// Responsive prefixes
'sm:', 'md:', 'lg:', 'xl:', '2xl:',
],
theme: {
extend: {
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
colors: {
teal: "#0c8991",
purple: "#9D5E9B",
rose: "#B55076",
"teal-light": "rgba(12, 137, 145, 0.1)",
"purple-light": "#E8D5E6",
"rose-light": "rgba(181, 80, 118, 0.1)",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
"1": "hsl(var(--chart-1))",
"2": "hsl(var(--chart-2))",
"3": "hsl(var(--chart-3))",
"4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;

View File

@ -1,32 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [
react(),
],
root: path.resolve(__dirname),
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@shared": path.resolve(__dirname, "../shared"),
"@assets": path.resolve(__dirname, "../attached_assets"),
},
},
build: {
outDir: "dist",
emptyOutDir: true,
assetsDir: "assets",
rollupOptions: {
output: {
assetFileNames: "assets/[name]-[hash][extname]",
chunkFileNames: "assets/[name]-[hash].js",
entryFileNames: "assets/[name]-[hash].js",
},
},
},
optimizeDeps: {
force: true,
},
});

14
drizzle.config.ts Normal file
View File

@ -0,0 +1,14 @@
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,
},
});

View File

@ -1,14 +0,0 @@
# 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

4221
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,19 @@
{
"name": "pilates-with-fadia",
"name": "rest-express",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "NODE_ENV=development tsx server/index.ts",
"build": "cd client && vite build",
"build:client": "cd client && vite build",
"build:server": "esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
"build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
"start": "NODE_ENV=production node dist/index.js",
"check": "tsc"
"check": "tsc",
"db:push": "drizzle-kit push"
},
"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,14 +41,18 @@
"@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",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"@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",
@ -57,9 +61,9 @@
"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",
"postcss": "^8.4.47",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@ -68,18 +72,19 @@
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.2.5",
"vaul": "^1.1.2",
"vite": "^5.4.14",
"wouter": "^3.3.5",
"ws": "^8.18.0",
"zod": "^3.24.2",
"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",
@ -88,9 +93,15 @@
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@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",
"tsx": "^4.19.1",
"typescript": "5.6.3"
"typescript": "5.6.3",
"vite": "^5.4.14"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
}

View File

@ -4,17 +4,12 @@ import { Express } from "express";
import session from "express-session";
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";
import { storage, User } from "./storage";
import { storage } from "./storage";
import { User as SelectUser, insertUserSchema } from "@shared/schema";
declare global {
namespace Express {
interface User {
id: number;
username: string;
email: string;
fullName?: string;
createdAt: Date;
}
interface User extends SelectUser {}
}
}
@ -26,10 +21,11 @@ async function hashPassword(password: string) {
return `${buf.toString("hex")}.${salt}`;
}
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"));
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);
}
export function setupAuth(app: Express) {
@ -70,34 +66,23 @@ export function setupAuth(app: Express) {
app.post("/api/register", async (req, res, next) => {
try {
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" });
}
const validatedUser = insertUserSchema.parse(req.body);
// Check if username already exists
const existingUserByUsername = await storage.getUserByUsername(username);
const existingUserByUsername = await storage.getUserByUsername(validatedUser.username);
if (existingUserByUsername) {
return res.status(400).json({ message: "Username already exists" });
}
// Check if email already exists
const existingUserByEmail = await storage.getUserByEmail(email);
const existingUserByEmail = await storage.getUserByEmail(validatedUser.email);
if (existingUserByEmail) {
return res.status(400).json({ message: "Email already in use" });
}
const user = await storage.createUser({
username,
email,
password: await hashPassword(password),
fullName
...validatedUser,
password: await hashPassword(validatedUser.password),
});
req.login(user, (err) => {
@ -113,7 +98,7 @@ export function setupAuth(app: Express) {
});
app.post("/api/login", (req, res, next) => {
passport.authenticate("local", (err: Error, user: User) => {
passport.authenticate("local", (err: Error, user: SelectUser) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({ message: "Invalid username or password" });
@ -136,12 +121,11 @@ export function setupAuth(app: Express) {
});
});
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" });
}
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);
});
}

46
server/contact-email.ts Normal file
View File

@ -0,0 +1,46 @@
/**
* 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;
}
}

43
server/email.ts Normal file
View File

@ -0,0 +1,43 @@
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;
}
}

View File

@ -36,36 +36,35 @@ 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 () => {
server = await registerRoutes(app);
const server = await registerRoutes(app);
// Setup Vite in development, serve static in production
if (process.env.NODE_ENV === "development") {
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") {
await setupVite(app, server);
} else {
serveStatic(app);
}
})();
// Vercel serverless function export
export default app;
// Development server (only runs in development)
if (process.env.NODE_ENV === "development") {
// 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;
app.listen(port, "0.0.0.0", () => {
log(`Development server running on port ${port}`);
server.listen({
port,
host: "0.0.0.0",
reusePort: true,
}, () => {
log(`serving on port ${port}`);
});
}
})();

View File

@ -3,28 +3,14 @@ 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<Server> {
// Set up authentication routes
setupAuth(app);
@ -67,8 +53,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
try {
const user = req.user as any;
const bookings = await storage.getBookings(user.id);
const bookings = await storage.getBookings(req.user.id);
res.json(bookings);
} catch (error) {
res.status(500).json({ message: "Failed to fetch bookings" });
@ -81,14 +66,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
try {
const user = req.user as any;
const bookingData = bookingSchema.parse(req.body);
const booking = await storage.createBooking({
...bookingData,
userId: user.id,
date: new Date(bookingData.date)
const bookingData = insertBookingSchema.parse({
...req.body,
userId: req.user.id
});
const booking = await storage.createBooking(bookingData);
res.status(201).json(booking);
} catch (error) {
if (error instanceof z.ZodError) {
@ -101,7 +84,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Newsletter signup
app.post("/api/newsletter", async (req, res) => {
try {
const newsletterData = newsletterSchema.parse(req.body);
const newsletterData = insertNewsletterSchema.parse(req.body);
// Check if email already exists in our local database
const existingNewsletter = await storage.getNewsletterByEmail(newsletterData.email);
@ -137,19 +120,20 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Contact form
app.post("/api/contact", async (req, res) => {
try {
const contactData = contactMessageSchema.parse(req.body);
const contactData = insertContactMessageSchema.parse(req.body);
// Store in database
// Always store in database first
const message = await storage.createContactMessage(contactData);
// Log the contact request
console.log(`Contact form submission from ${contactData.name} (${contactData.email})`);
// Add a success message with clear next steps for Fadia
console.log(`Contact form submission stored in database from ${contactData.name} (${contactData.email})`);
console.log(`Subject: ${contactData.subject || "No subject"}`);
console.log(`Message: ${contactData.message}`);
console.log(`This message will be forwarded to hello@pilateswithfadia.com`);
// 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 reviewed shortly."
info: "Your message has been received and will be sent to hello@pilateswithfadia.com"
});
} catch (error) {
if (error instanceof z.ZodError) {
@ -159,6 +143,44 @@ 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);
return httpServer;
}

View File

@ -1,82 +1,43 @@
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<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
getUserByEmail(email: string): Promise<User | undefined>;
createUser(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
createUser(user: InsertUser): Promise<User>;
// Class Management
getClasses(): Promise<Class[]>;
getClass(id: number): Promise<Class | undefined>;
createClass(classData: Omit<Class, 'id'>): Promise<Class>;
createClass(classData: InsertClass): Promise<Class>;
// Booking Management
getBookings(userId?: number): Promise<Booking[]>;
getBooking(id: number): Promise<Booking | undefined>;
createBooking(booking: Omit<Booking, 'id' | 'createdAt'>): Promise<Booking>;
createBooking(booking: InsertBooking): Promise<Booking>;
updateBookingStatus(id: number, status: string): Promise<Booking | undefined>;
// Newsletter Management
getNewsletterByEmail(email: string): Promise<Newsletter | undefined>;
createNewsletter(newsletter: Omit<Newsletter, 'id' | 'createdAt'>): Promise<Newsletter>;
createNewsletter(newsletter: InsertNewsletter): Promise<Newsletter>;
// Contact Management
createContactMessage(message: Omit<ContactMessage, 'id' | 'createdAt'>): Promise<ContactMessage>;
createContactMessage(message: InsertContactMessage): Promise<ContactMessage>;
// Session Store
sessionStore: any;
sessionStore: session.SessionStore;
}
export class MemStorage implements IStorage {
@ -91,7 +52,7 @@ export class MemStorage implements IStorage {
currentBookingId: number;
currentNewsletterId: number;
currentContactMessageId: number;
sessionStore: any;
sessionStore: session.SessionStore;
constructor() {
this.users = new Map();
@ -131,7 +92,7 @@ export class MemStorage implements IStorage {
);
}
async createUser(insertUser: Omit<User, 'id' | 'createdAt'>): Promise<User> {
async createUser(insertUser: InsertUser): Promise<User> {
const id = this.currentUserId++;
const user: User = {
...insertUser,
@ -151,7 +112,7 @@ export class MemStorage implements IStorage {
return this.classes.get(id);
}
async createClass(classData: Omit<Class, 'id'>): Promise<Class> {
async createClass(classData: InsertClass): Promise<Class> {
const id = this.currentClassId++;
const newClass: Class = { ...classData, id };
this.classes.set(id, newClass);
@ -171,7 +132,7 @@ export class MemStorage implements IStorage {
return this.bookings.get(id);
}
async createBooking(booking: Omit<Booking, 'id' | 'createdAt'>): Promise<Booking> {
async createBooking(booking: InsertBooking): Promise<Booking> {
const id = this.currentBookingId++;
const newBooking: Booking = {
...booking,
@ -199,7 +160,7 @@ export class MemStorage implements IStorage {
);
}
async createNewsletter(newsletter: Omit<Newsletter, 'id' | 'createdAt'>): Promise<Newsletter> {
async createNewsletter(newsletter: InsertNewsletter): Promise<Newsletter> {
const id = this.currentNewsletterId++;
const newNewsletter: Newsletter = {
...newsletter,
@ -211,7 +172,7 @@ export class MemStorage implements IStorage {
}
// Contact Management
async createContactMessage(message: Omit<ContactMessage, 'id' | 'createdAt'>): Promise<ContactMessage> {
async createContactMessage(message: InsertContactMessage): Promise<ContactMessage> {
const id = this.currentContactMessageId++;
const newMessage: ContactMessage = {
...message,
@ -224,7 +185,7 @@ export class MemStorage implements IStorage {
// Seed default data
private seedClasses() {
const defaultClasses: Omit<Class, 'id'>[] = [
const defaultClasses: InsertClass[] = [
{
name: "Mat Pilates",
description: "A foundational class focusing on core strength, proper alignment, and mindful movement patterns.",

View File

@ -3,6 +3,7 @@ import fs from "fs";
import path from "path";
import { createServer as createViteServer, createLogger } from "vite";
import { type Server } from "http";
import viteConfig from "../vite.config";
import { nanoid } from "nanoid";
const viteLogger = createLogger();
@ -19,23 +20,15 @@ export function log(message: string, source = "express") {
}
export async function setupVite(app: Express, server: Server) {
const serverOptions = {
middlewareMode: true,
hmr: { server },
allowedHosts: true,
};
const vite = await createViteServer({
...viteConfig,
configFile: false,
root: path.resolve(import.meta.dirname, "..", "client"),
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "..", "client", "src"),
"@shared": path.resolve(import.meta.dirname, "..", "shared"),
"@assets": path.resolve(import.meta.dirname, "..", "attached_assets"),
},
},
server: {
hmr: false,
middlewareMode: true
},
optimizeDeps: {
exclude: ['@vite/client']
},
customLogger: {
...viteLogger,
error: (msg, options) => {
@ -43,6 +36,7 @@ export async function setupVite(app: Express, server: Server) {
process.exit(1);
},
},
server: serverOptions,
appType: "custom",
});
@ -74,7 +68,7 @@ export async function setupVite(app: Express, server: Server) {
}
export function serveStatic(app: Express) {
const distPath = path.resolve(import.meta.dirname, "..", "client", "dist");
const distPath = path.resolve(import.meta.dirname, "public");
if (!fs.existsSync(distPath)) {
throw new Error(

89
shared/schema.ts Normal file
View File

@ -0,0 +1,89 @@
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>;

View File

@ -2,74 +2,7 @@ import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
content: [
"./client/index.html",
"./client/src/**/*.{js,jsx,ts,tsx}",
"./client/src/**/*.{html,js,jsx,ts,tsx}",
"./client/src/components/**/*.{js,jsx,ts,tsx}",
"./client/src/pages/**/*.{js,jsx,ts,tsx}",
"./client/src/hooks/**/*.{js,jsx,ts,tsx}",
"./client/src/lib/**/*.{js,jsx,ts,tsx}",
],
safelist: [
// Layout
'container', 'flex', 'grid', 'block', 'inline-block', 'hidden',
// Spacing
'p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-8', 'p-10', 'p-12', 'p-16', 'p-20',
'px-0', 'px-1', 'px-2', 'px-3', 'px-4', 'px-5', 'px-6', 'px-8', 'px-10', 'px-12', 'px-16', 'px-20',
'py-0', 'py-1', 'py-2', 'py-3', 'py-4', 'py-5', 'py-6', 'py-8', 'py-10', 'py-12', 'py-16', 'py-20',
'm-0', 'm-1', 'm-2', 'm-3', 'm-4', 'm-5', 'm-6', 'm-8', 'm-10', 'm-12', 'm-16', 'm-20',
'mx-auto', 'my-auto',
// Colors
'bg-white', 'bg-black', 'bg-gray-50', 'bg-gray-100', 'bg-gray-200', 'bg-gray-300',
'text-white', 'text-black', 'text-gray-500', 'text-gray-600', 'text-gray-700', 'text-gray-800', 'text-gray-900',
'bg-teal', 'bg-purple', 'bg-rose', 'text-teal', 'text-purple', 'text-rose',
'bg-teal-light', 'bg-purple-light', 'bg-rose-light',
'bg-opacity-20', 'bg-opacity-30', 'bg-opacity-90',
'text-opacity-70', 'text-opacity-80', 'text-opacity-100',
'border-opacity-20', 'border-opacity-30',
// Typography
'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'text-5xl',
'font-normal', 'font-medium', 'font-semibold', 'font-bold',
'text-left', 'text-center', 'text-right',
'font-playfair', 'font-raleway', 'font-aref', 'font-cairo',
// Flexbox
'flex-row', 'flex-col', 'items-start', 'items-center', 'items-end', 'items-stretch',
'justify-start', 'justify-center', 'justify-end', 'justify-between', 'justify-around',
'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-5', 'gap-6', 'gap-8', 'gap-10', 'gap-12',
// Grid
'grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4',
// Width/Height
'w-full', 'w-auto', 'w-1/2', 'w-1/3', 'w-2/3', 'w-1/4', 'w-3/4',
'h-full', 'h-auto', 'h-screen', 'h-svh',
'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl', 'max-w-2xl', 'max-w-3xl', 'max-w-4xl', 'max-w-5xl', 'max-w-7xl',
// Borders
'border', 'border-0', 'border-2', 'border-4',
'rounded', 'rounded-sm', 'rounded-md', 'rounded-lg', 'rounded-xl', 'rounded-full',
'border-gray-200', 'border-gray-300', 'border-teal', 'border-purple', 'border-rose',
// Shadows
'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl',
// Position
'relative', 'absolute', 'fixed', 'sticky',
'top-0', 'top-1/2', 'top-full',
'bottom-0', 'bottom-1/2',
'left-0', 'left-1/2',
'right-0', 'right-1/2',
'inset-0', 'inset-x-0', 'inset-y-0',
// Z-index
'z-10', 'z-20', 'z-50',
// Overflow
'overflow-hidden', 'overflow-auto', 'overflow-x-auto', 'overflow-y-auto',
// Display
'block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid', 'hidden',
// Transitions
'transition-all', 'transition-colors', 'transition-transform', 'transition-opacity',
'duration-200', 'duration-300',
// Hover states
'hover:bg-opacity-90', 'hover:text-white', 'hover:text-opacity-100', 'hover:scale-105',
// Responsive prefixes
'sm:', 'md:', 'lg:', 'xl:', '2xl:',
],
content: ["./client/index.html", "./client/src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
borderRadius: {
@ -137,12 +70,20 @@ export default {
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
from: {
height: "0",
},
to: {
height: "var(--radix-accordion-content-height)",
},
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
from: {
height: "var(--radix-accordion-content-height)",
},
to: {
height: "0",
},
},
},
animation: {
@ -151,5 +92,5 @@ export default {
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
} satisfies Config;

View File

@ -1,29 +0,0 @@
{
"version": 2,
"buildCommand": "npm ci && npm run build",
"outputDirectory": "client/dist",
"installCommand": "npm ci",
"functions": {
"api/index.ts": {
"maxDuration": 30
}
},
"routes": [
{
"src": "/api/(.*)",
"dest": "/api/index.ts"
},
{
"src": "/assets/(.*)",
"dest": "/assets/$1"
},
{
"src": "/(.*\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot))",
"dest": "/$1"
},
{
"src": "/(.*)",
"dest": "/index.html"
}
]
}

31
vite.config.ts Normal file
View File

@ -0,0 +1,31 @@
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: {
"@": path.resolve(import.meta.dirname, "client", "src"),
"@shared": path.resolve(import.meta.dirname, "shared"),
"@assets": path.resolve(import.meta.dirname, "attached_assets"),
},
},
root: path.resolve(import.meta.dirname, "client"),
build: {
outDir: path.resolve(import.meta.dirname, "dist/public"),
emptyOutDir: true,
},
});