Deploy: Add Cloudflare Worker, D1 database integration, and updated README
- Add complete Cloudflare Worker implementation with waitlist API endpoints - Integrate Cloudflare D1 SQLite database for production waitlist storage - Add wrangler configuration for Worker deployment and D1 binding - Rewrite README with comprehensive deployment and architecture documentation - Update environment configuration and build setup - Add waitlist utility functions and API client - Update project dependencies and Vite configuration - Deploy live to https://react-example.white-glade-ab2c.workers.dev
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Core app runtime
|
||||
VITE_APP_URL="https://your-domain.com"
|
||||
VITE_GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
VITE_WAITLIST_API_URL="/api"
|
||||
|
||||
# Firebase web config
|
||||
VITE_FIREBASE_API_KEY="YOUR_FIREBASE_API_KEY"
|
||||
|
||||
@@ -6,3 +6,8 @@ coverage/
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# wrangler files
|
||||
.wrangler
|
||||
.dev.vars*
|
||||
!.dev.vars.example
|
||||
|
||||
@@ -1,109 +1,185 @@
|
||||
# BM Connect
|
||||
# BM Connect
|
||||
|
||||
A polished React + Vite landing page and community waitlist experience for a premium meme-coin launch platform. The app includes a hero section, ecosystem overview, funding narrative, tokenomics modal, roadmap, and a live waitlist flow backed by Firebase Firestore.
|
||||
A premium React + Vite landing page and community waitlist platform for a meme-coin launch. Features dynamic sections for problem/solution storytelling, tokenomics modals, roadmap visualization, and real-time waitlist tracking backed by Cloudflare D1 SQLite.
|
||||
|
||||
**Live:** https://react-example.white-glade-ab2c.workers.dev
|
||||
|
||||
## Overview
|
||||
|
||||
BM Connect is designed to present a high-trust launch narrative for a community-driven web3 product. The project combines brand storytelling, conversion-focused sections, and a waitlist system that tracks registrations and community momentum.
|
||||
BM Connect delivers a high-trust launch narrative for community-driven web3 projects. The platform combines premium brand storytelling, conversion-focused messaging, and a production-ready waitlist system with real-time registration tracking and community stats.
|
||||
|
||||
## Highlights
|
||||
## Key Features
|
||||
|
||||
- Premium landing-page design with dark, modern visual language
|
||||
- Conversion-focused sections for problem, solution, roadmap, and funding
|
||||
- Tokenomics and vision modal experiences
|
||||
- Waitlist registration and stats tracking powered by Firebase
|
||||
- Responsive React interface built with Vite and TypeScript
|
||||
- **Premium UI**: Dark, modern design with glassmorphic cards and smooth animations
|
||||
- **Story-Driven Sections**: Hero, problem, solution, features, ecosystem, stats, roadmap
|
||||
- **Interactive Modals**: Tokenomics breakdown and vision manifesto
|
||||
- **Live Waitlist**: Real-time registration tracking with referral system and tier support
|
||||
- **Community Stats**: Dynamic counter showing total registrations and funding progress
|
||||
- **Responsive Design**: Mobile-first approach with Tailwind CSS
|
||||
- **Type-Safe**: Full TypeScript throughout frontend and backend
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- React 19
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- Firebase Firestore
|
||||
- Google GenAI SDK
|
||||
**Frontend:**
|
||||
- React 19 with TypeScript
|
||||
- Vite (build tool & dev server)
|
||||
- Tailwind CSS (styling)
|
||||
- Framer Motion (animations)
|
||||
|
||||
**Backend:**
|
||||
- Cloudflare Workers (serverless API)
|
||||
- Cloudflare D1 (SQLite database)
|
||||
|
||||
**Infrastructure:**
|
||||
- Firebase (optional: for app data beyond waitlist)
|
||||
- Wrangler CLI (deployment & management)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```bash
|
||||
`
|
||||
.
|
||||
├── src/
|
||||
├── src/ # React frontend
|
||||
│ ├── components/
|
||||
│ ├── data/
|
||||
│ │ ├── layout/ # Navbar, Footer
|
||||
│ │ ├── sections/ # Landing page sections
|
||||
│ │ └── ui/ # Reusable components (Button, Badge, etc.)
|
||||
│ ├── data/ # Static content (features, roadmap, tokenomics, FAQ)
|
||||
│ ├── lib/
|
||||
│ │ ├── firebase.ts # Firebase configuration
|
||||
│ │ ├── waitlist.ts # Waitlist API client
|
||||
│ │ └── utils.ts
|
||||
│ ├── App.tsx
|
||||
│ ├── main.tsx
|
||||
│ └── index.css
|
||||
├── firebase-applet-config.json
|
||||
├── firebase-blueprint.json
|
||||
├── firestore.rules
|
||||
├── .env.example
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── worker/ # Cloudflare Worker (backend API)
|
||||
│ ├── src/
|
||||
│ │ └── index.ts # Waitlist registration endpoints
|
||||
│ └── wrangler.toml # Worker configuration & D1 binding
|
||||
├── dist/ # Production build
|
||||
├── vite.config.ts
|
||||
├── tsconfig.json
|
||||
├── wrangler.jsonc # Build configuration
|
||||
├── package.json
|
||||
├── Dockerfile # Container build (optional)
|
||||
└── README.md
|
||||
```
|
||||
`
|
||||
|
||||
## Local Development
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm
|
||||
- npm or yarn
|
||||
- Cloudflare account (for deployment)
|
||||
|
||||
### Install dependencies
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
1. **Install dependencies:**
|
||||
`ash
|
||||
npm install
|
||||
`
|
||||
|
||||
### Environment variables
|
||||
2. **Configure environment variables:**
|
||||
`ash
|
||||
cp .env.example .env
|
||||
`
|
||||
|
||||
Required variables:
|
||||
- VITE_FIREBASE_API_KEY - Firebase API key
|
||||
- VITE_FIREBASE_AUTH_DOMAIN - Firebase auth domain
|
||||
- VITE_FIREBASE_PROJECT_ID - Firebase project ID
|
||||
- VITE_WAITLIST_API_URL - Waitlist API endpoint (local: /api, production: Worker URL)
|
||||
- GEMINI_API_KEY - Google GenAI API key (optional)
|
||||
|
||||
This project includes an example environment file at [.env.example](.env.example). Configure the variables required by your runtime environment before starting the app.
|
||||
3. **Start development server:**
|
||||
`ash
|
||||
npm run dev
|
||||
`
|
||||
The app runs on http://localhost:5173 (Vite default)
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
4. **Build for production:**
|
||||
`ash
|
||||
npm run build
|
||||
`
|
||||
|
||||
The example file includes:
|
||||
5. **Type checking:**
|
||||
`ash
|
||||
npm run lint
|
||||
`
|
||||
|
||||
- `GEMINI_API_KEY`
|
||||
- `APP_URL`
|
||||
## Database: Cloudflare D1
|
||||
|
||||
If you are running in AI Studio, configure these values through the platform’s secrets/runtime settings as needed.
|
||||
The waitlist backend is powered by **Cloudflare D1**, a serverless SQLite database. Configuration details:
|
||||
|
||||
### Run the app
|
||||
- **Database name:** bm_connect_db
|
||||
- **Database ID:** 5cf8ba8c-fe38-4db4-bf95-aa8fb8c09892
|
||||
- **Binding:** DB (available in Worker via env.DB)
|
||||
- **Tables:**
|
||||
- waitlist_entries - User registrations (email, name, tier, referral code, etc.)
|
||||
- waitlist_stats - Global statistics (total registered, funded amount)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
### Worker API Endpoints
|
||||
|
||||
The app runs on port `3000` by default via the Vite config.
|
||||
The Cloudflare Worker in worker/src/index.ts provides these endpoints:
|
||||
|
||||
### Production build
|
||||
- **POST /api/waitlist/register** - Register a new email with optional metadata
|
||||
- **GET /api/waitlist/stats** - Get global registration and funding stats
|
||||
- **POST /api/waitlist/verify** - Verify referral codes and registration status
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
## Deployment
|
||||
|
||||
### Type checking
|
||||
### Deploy to Cloudflare Workers
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
1. **Ensure wrangler.toml is configured with D1 database:**
|
||||
` oml
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "bm_connect_db"
|
||||
database_id = "5cf8ba8c-fe38-4db4-bf95-aa8fb8c09892"
|
||||
`
|
||||
|
||||
## Firebase + Waitlist
|
||||
2. **Build and deploy:**
|
||||
`ash
|
||||
npm run build
|
||||
npx wrangler deploy
|
||||
`
|
||||
|
||||
The waitlist logic is configured in [src/lib/firebase.ts](src/lib/firebase.ts). It uses Firebase Firestore to manage registrations, live statistics, and referral-based waitlist tracking.
|
||||
3. **Verify deployment:**
|
||||
- Frontend: https://react-example.white-glade-ab2c.workers.dev
|
||||
- Worker API: https://react-example.white-glade-ab2c.workers.dev/api/waitlist/stats
|
||||
|
||||
Before using the live waitlist flow, make sure your Firebase config file is populated correctly in [firebase-applet-config.json](firebase-applet-config.json).
|
||||
### Environment Setup
|
||||
|
||||
## Notes
|
||||
For production, configure these environment variables in your Cloudflare dashboard:
|
||||
- Worker secrets (via wrangler secret put)
|
||||
- D1 database bindings (configured in wrangler.toml)
|
||||
|
||||
- The app is structured as a marketing and launch-site interface, not a general-purpose backend app.
|
||||
- If you are deploying this to a static host or custom frontend platform, ensure your environment variables and Firebase configuration are available in the deployment environment.
|
||||
## Customization
|
||||
|
||||
## Contributing
|
||||
### Content
|
||||
|
||||
Pull requests and improvements are welcome. For any significant changes, open an issue or propose the update before making a large refactor.
|
||||
Static content is managed in src/data/:
|
||||
- features.ts - Feature cards and descriptions
|
||||
- roadmap.ts - Milestone timeline
|
||||
- tokenomics.ts - Token distribution and economics
|
||||
- faq.ts - Frequently asked questions
|
||||
- ecosystem.ts - Partner/ecosystem information
|
||||
|
||||
### Styling
|
||||
|
||||
- Global styles: src/index.css
|
||||
- Tailwind config: tailwind.config.js (if present)
|
||||
- Component-level: Tailwind classes in JSX
|
||||
|
||||
### API Integration
|
||||
|
||||
Update VITE_WAITLIST_API_URL to point to your deployed Worker:
|
||||
- Local development: /api (proxied via Vite dev server)
|
||||
- Production: https://your-worker.workers.dev
|
||||
|
||||
## Community & Support
|
||||
|
||||
For questions or issues, refer to:
|
||||
- **Firebase Docs**: https://firebase.google.com/docs
|
||||
- **Cloudflare Workers Docs**: https://developers.cloudflare.com/workers/
|
||||
- **Vite Docs**: https://vitejs.dev/
|
||||
|
||||
Generated
+1568
-1
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -6,9 +6,10 @@
|
||||
"scripts": {
|
||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"preview": "npm run build && wrangler dev",
|
||||
"clean": "rm -rf dist server.js",
|
||||
"lint": "tsc --noEmit"
|
||||
"lint": "tsc --noEmit",
|
||||
"deploy": "npm run build && wrangler deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
@@ -26,6 +27,8 @@
|
||||
"vite": "^6.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.54.2",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -33,6 +36,6 @@
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
"@types/express": "^4.17.21"
|
||||
"wrangler": "^4.127.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SectionHeading } from '../ui/SectionHeading';
|
||||
import { GlassCard } from '../ui/GlassCard';
|
||||
import { Button } from '../ui/Button';
|
||||
import { WaitlistEntry } from '../../types';
|
||||
import { registerForWaitlist, subscribeToWaitlistStats, WaitlistStats } from '../../lib/firebase';
|
||||
import { registerForWaitlist, subscribeToWaitlistStats, WaitlistStats } from '../../lib/waitlist';
|
||||
|
||||
export const WaitlistSection: React.FC = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const API_URL = (import.meta.env.VITE_WAITLIST_API_URL || '/api').replace(/\/$/, '');
|
||||
|
||||
export interface WaitlistRegistrationData {
|
||||
email: string;
|
||||
fullName?: string;
|
||||
tier?: string;
|
||||
investmentAmount?: number;
|
||||
referredBy?: string;
|
||||
}
|
||||
|
||||
export interface WaitlistRegistrationResult {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
position: number;
|
||||
referralCode: string;
|
||||
investmentAmount: number;
|
||||
tier: string;
|
||||
totalRegistered: number;
|
||||
}
|
||||
|
||||
export interface WaitlistStats {
|
||||
totalCount: number;
|
||||
totalFundedAmount: number;
|
||||
recentRegistrationsCount: number;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_URL}${path}`, {
|
||||
...options,
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers }
|
||||
});
|
||||
if (!response.ok) throw new Error((await response.json()).error || 'Waitlist request failed');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function registerForWaitlist(data: WaitlistRegistrationData) {
|
||||
return request<WaitlistRegistrationResult>('/waitlist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function subscribeToWaitlistStats(callback: (stats: WaitlistStats) => void) {
|
||||
let active = true;
|
||||
const poll = async () => {
|
||||
try {
|
||||
const stats = await request<WaitlistStats>('/waitlist/stats');
|
||||
if (active) callback(stats);
|
||||
} catch (error) {
|
||||
console.warn('Waitlist stats request failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
void poll();
|
||||
const interval = window.setInterval(poll, 15000);
|
||||
return () => {
|
||||
active = false;
|
||||
window.clearInterval(interval);
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllWaitlistEntries() {
|
||||
return request<unknown[]>('/waitlist/entries');
|
||||
}
|
||||
+4
-2
@@ -3,9 +3,11 @@ import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig} from 'vite';
|
||||
|
||||
import { cloudflare } from "@cloudflare/vite-plugin";
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [react(), tailwindcss(), cloudflare()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
@@ -19,4 +21,4 @@ export default defineConfig(() => {
|
||||
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
interface Env {
|
||||
DB: D1Database;
|
||||
}
|
||||
|
||||
const BASE_WAITLIST_OFFSET = 2870;
|
||||
const CORS_HEADERS = {
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
function json(data: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: CORS_HEADERS
|
||||
});
|
||||
}
|
||||
|
||||
function makeReferralCode() {
|
||||
return `SX-${crypto.randomUUID().replace(/-/g, '').slice(0, 5).toUpperCase()}`;
|
||||
}
|
||||
|
||||
async function getStats(env: Env) {
|
||||
const row = await env.DB.prepare(
|
||||
'SELECT total_registered, total_funded_amount FROM waitlist_stats WHERE id = ?'
|
||||
)
|
||||
.bind('waitlist')
|
||||
.first<{ total_registered: number; total_funded_amount: number }>();
|
||||
|
||||
const registered = Number(row?.total_registered ?? 0);
|
||||
return {
|
||||
totalCount: BASE_WAITLIST_OFFSET + registered,
|
||||
totalFundedAmount: Number(row?.total_funded_amount ?? 142500),
|
||||
recentRegistrationsCount: registered
|
||||
};
|
||||
}
|
||||
|
||||
async function register(request: Request, env: Env) {
|
||||
const body = await request.json() as {
|
||||
email?: string;
|
||||
fullName?: string;
|
||||
tier?: string;
|
||||
investmentAmount?: number;
|
||||
referredBy?: string;
|
||||
};
|
||||
|
||||
const email = body.email?.trim().toLowerCase();
|
||||
if (!email) return json({ error: 'A valid email is required.' }, 400);
|
||||
|
||||
const fullName = body.fullName?.trim() || 'BM Connect Member';
|
||||
const tier = body.tier || 'Supporter';
|
||||
const investmentAmount = Number(body.investmentAmount) || 25;
|
||||
const referredBy = body.referredBy?.trim() || null;
|
||||
const id = crypto.randomUUID();
|
||||
const referralCode = makeReferralCode();
|
||||
|
||||
const result = await env.DB.batch([
|
||||
env.DB.prepare(
|
||||
`UPDATE waitlist_stats
|
||||
SET total_registered = total_registered + 1,
|
||||
total_funded_amount = total_funded_amount + ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`
|
||||
).bind(investmentAmount, 'waitlist'),
|
||||
env.DB.prepare(
|
||||
`INSERT INTO waitlist_entries
|
||||
(id, email, full_name, tier, investment_amount, position, referral_code, referred_by)
|
||||
SELECT ?, ?, ?, ?, ?, ? + total_registered, ?, ?
|
||||
FROM waitlist_stats
|
||||
WHERE id = ?`
|
||||
).bind(
|
||||
id,
|
||||
email,
|
||||
fullName,
|
||||
tier,
|
||||
investmentAmount,
|
||||
BASE_WAITLIST_OFFSET,
|
||||
referralCode,
|
||||
referredBy,
|
||||
'waitlist'
|
||||
)
|
||||
]);
|
||||
|
||||
if (!result[1].success) {
|
||||
throw new Error('The waitlist entry could not be created.');
|
||||
}
|
||||
|
||||
const entry = await env.DB.prepare(
|
||||
`SELECT id, email, full_name, tier, investment_amount, position, referral_code, referred_by, created_at
|
||||
FROM waitlist_entries WHERE id = ?`
|
||||
).bind(id).first();
|
||||
|
||||
return json({
|
||||
id,
|
||||
email,
|
||||
fullName,
|
||||
tier,
|
||||
investmentAmount,
|
||||
position: Number(entry?.position ?? BASE_WAITLIST_OFFSET),
|
||||
referralCode,
|
||||
totalRegistered: Number(entry?.position ?? BASE_WAITLIST_OFFSET),
|
||||
referredBy,
|
||||
createdAt: entry?.created_at
|
||||
}, 201);
|
||||
}
|
||||
|
||||
async function listEntries(env: Env) {
|
||||
const { results } = await env.DB.prepare(
|
||||
`SELECT id, email, full_name, tier, investment_amount, position, referral_code, referred_by, created_at
|
||||
FROM waitlist_entries ORDER BY created_at DESC`
|
||||
).all();
|
||||
return json(results);
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
if (request.method === 'OPTIONS') return new Response(null, { headers: CORS_HEADERS });
|
||||
|
||||
const url = new URL(request.url);
|
||||
try {
|
||||
if (request.method === 'GET' && url.pathname === '/api/waitlist/stats') {
|
||||
return json(await getStats(env));
|
||||
}
|
||||
if (request.method === 'POST' && url.pathname === '/api/waitlist') {
|
||||
return register(request, env);
|
||||
}
|
||||
if (request.method === 'GET' && url.pathname === '/api/waitlist/entries') {
|
||||
return listEntries(env);
|
||||
}
|
||||
return json({ error: 'Not found' }, 404);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return json({ error: 'Unable to complete the waitlist request.' }, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "bm-connect-api"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-08-26"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "bm_connect_db"
|
||||
database_id = "5cf8ba8c-fe38-4db4-bf95-aa8fb8c09892"
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "react-example",
|
||||
"compatibility_date": "2026-08-28",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"assets": {
|
||||
"not_found_handling": "single-page-application"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user