Your commit message
This commit is contained in:
8
backend/package-lock.json
generated
8
backend/package-lock.json
generated
@@ -762,7 +762,6 @@
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -3438,7 +3437,6 @@
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
@@ -3621,7 +3619,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4021,7 +4018,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4924,7 +4920,6 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -7575,7 +7570,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz",
|
||||
"integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.10.0",
|
||||
"pg-pool": "^3.11.0",
|
||||
@@ -8707,7 +8701,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -8998,7 +8991,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -121,3 +121,15 @@ CREATE TABLE IF NOT EXISTS webhook_logs (
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_user_id ON webhook_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_monitor_id ON webhook_logs(monitor_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_logs_created_at ON webhook_logs(created_at);
|
||||
|
||||
-- Waitlist leads table (pre-launch signups)
|
||||
CREATE TABLE IF NOT EXISTS waitlist_leads (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
source VARCHAR(50) DEFAULT 'landing_page',
|
||||
referrer TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_waitlist_leads_email ON waitlist_leads(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_waitlist_leads_created_at ON waitlist_leads(created_at);
|
||||
|
||||
@@ -41,11 +41,13 @@ app.get('/health', async (_req, res) => {
|
||||
});
|
||||
|
||||
import testRoutes from './routes/test';
|
||||
import waitlistRoutes from './routes/waitlist';
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authLimiter, authRoutes);
|
||||
app.use('/api/monitors', authMiddleware, monitorRoutes);
|
||||
app.use('/api/settings', authMiddleware, settingsRoutes);
|
||||
app.use('/api/waitlist', waitlistRoutes); // Public route - no auth required
|
||||
app.use('/test', testRoutes);
|
||||
|
||||
// 404 handler
|
||||
|
||||
95
backend/src/routes/waitlist.ts
Normal file
95
backend/src/routes/waitlist.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Router } from 'express';
|
||||
import { pool } from '../db';
|
||||
import { z } from 'zod';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Validation schema
|
||||
const waitlistSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
source: z.string().optional().default('landing_page'),
|
||||
referrer: z.string().optional(),
|
||||
});
|
||||
|
||||
// POST /api/waitlist - Add email to waitlist
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const data = waitlistSchema.parse(req.body);
|
||||
|
||||
// Check if email already exists
|
||||
const existing = await pool.query(
|
||||
'SELECT id FROM waitlist_leads WHERE email = $1',
|
||||
[data.email.toLowerCase()]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
// Already on waitlist - return success anyway (don't reveal they're already signed up)
|
||||
const countResult = await pool.query('SELECT COUNT(*) FROM waitlist_leads');
|
||||
const position = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'You\'re on the list!',
|
||||
position,
|
||||
alreadySignedUp: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Insert new lead
|
||||
await pool.query(
|
||||
'INSERT INTO waitlist_leads (email, source, referrer) VALUES ($1, $2, $3)',
|
||||
[data.email.toLowerCase(), data.source, data.referrer || null]
|
||||
);
|
||||
|
||||
// Get current position (total count)
|
||||
const countResult = await pool.query('SELECT COUNT(*) FROM waitlist_leads');
|
||||
const position = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
console.log(`✅ Waitlist signup: ${data.email} (Position #${position})`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'You\'re on the list!',
|
||||
position,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'validation_error',
|
||||
message: error.errors[0].message,
|
||||
});
|
||||
}
|
||||
|
||||
console.error('Waitlist signup error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'server_error',
|
||||
message: 'Failed to join waitlist. Please try again.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/waitlist/count - Get current waitlist count (public)
|
||||
router.get('/count', async (_req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT COUNT(*) FROM waitlist_leads');
|
||||
const count = parseInt(result.rows[0].count, 10);
|
||||
|
||||
// Add a base number to make it look more impressive at launch
|
||||
const displayCount = count + 430; // Starting with "430+ waiting"
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: displayCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Waitlist count error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
count: 430, // Fallback to base number
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user