Skip to content

Commit f02b3da

Browse files
Merge pull request #64 from SaplingLearn/newsletter
Newsletter
2 parents 08da3f3 + 8a5e2b7 commit f02b3da

5 files changed

Lines changed: 57 additions & 4 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Migration: newsletter_emails table
2+
-- Stores emails submitted via the newsletter/beta signup modal.
3+
4+
CREATE TABLE IF NOT EXISTS newsletter_emails (
5+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6+
email text NOT NULL UNIQUE,
7+
created_at timestamptz NOT NULL DEFAULT now()
8+
);
9+
10+
-- Index for fast lookup by email
11+
CREATE INDEX IF NOT EXISTS newsletter_emails_email_idx ON newsletter_emails (email);

backend/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from routes import graph, learn, quiz, calendar, social, extract, auth, documents, flashcards, study_guide, feedback, careers, onboarding
1010
from routes.profile import router as profile_router
1111
from routes.admin import router as admin_router
12+
from routes.newsletter import router as newsletter_router
1213

1314
try:
1415
from recost.frameworks.fastapi import RecostMiddleware
@@ -52,6 +53,7 @@
5253
app.include_router(onboarding.router, prefix="/api/onboarding")
5354
app.include_router(profile_router, prefix="/api/profile")
5455
app.include_router(admin_router, prefix="/api/admin")
56+
app.include_router(newsletter_router, prefix="/api/newsletter")
5557

5658

5759
@app.get("/api/health")

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
fastapi
2+
email-validator
23
uvicorn[standard]
34
python-multipart
45
google-genai

backend/routes/newsletter.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from fastapi import APIRouter, HTTPException
2+
from pydantic import BaseModel, EmailStr, field_validator
3+
from db.connection import table
4+
5+
router = APIRouter()
6+
7+
8+
class SubscribeRequest(BaseModel):
9+
email: EmailStr
10+
11+
@field_validator('email')
12+
@classmethod
13+
def require_tld(cls, v: str) -> str:
14+
domain = v.split('@')[1]
15+
if '.' not in domain:
16+
raise ValueError('Email must have a valid domain (e.g. you@example.com)')
17+
return v
18+
19+
20+
@router.post("/subscribe")
21+
def subscribe(body: SubscribeRequest):
22+
try:
23+
table("newsletter_emails").upsert(
24+
{"email": body.email},
25+
on_conflict="email",
26+
)
27+
except Exception as e:
28+
raise HTTPException(status_code=500, detail=str(e))
29+
return {"ok": True}

frontend/src/app/page.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,16 @@ export default function LandingPage() {
3838
const [betaEmail, setBetaEmail] = useState('');
3939
const [betaEmailError, setBetaEmailError] = useState('');
4040
const [betaSubmitted, setBetaSubmitted] = useState(false);
41+
const [betaEverSubmitted, setBetaEverSubmitted] = useState(false);
4142
const closeModal = useCallback(() => {
4243
setBetaModalClosing(true);
43-
setTimeout(() => { setBetaModalOpen(false); setBetaModalClosing(false); }, 200);
44+
setTimeout(() => {
45+
setBetaModalOpen(false);
46+
setBetaModalClosing(false);
47+
setBetaSubmitted(false);
48+
setBetaEmail('');
49+
setBetaEmailError('');
50+
}, 200);
4451
}, []);
4552
useEffect(() => {
4653
if (betaSubmitted) {
@@ -786,11 +793,13 @@ export default function LandingPage() {
786793
</button>
787794
</div>
788795
<button
789-
onClick={() => setBetaModalOpen(true)}
796+
onClick={() => { setBetaModalOpen(true); if (betaEverSubmitted) setBetaSubmitted(true); }}
790797
className="liquid-glass-subtle px-10 py-4 rounded-full font-medium text-base tracking-wide transition-all duration-500 hover:scale-[1.02] active:scale-[0.98]"
791798
style={{
792-
background: 'rgba(217,119,6,0.15)',
793-
border: '1px solid rgba(217,119,6,0.45)',
799+
background: 'rgba(217,119,6,0.35)',
800+
backdropFilter: 'blur(12px) saturate(1.45)',
801+
WebkitBackdropFilter: 'blur(12px) saturate(1.45)',
802+
border: '1px solid rgba(217,119,6,0.55)',
794803
color: '#78350F',
795804
}}
796805
>
@@ -1161,6 +1170,7 @@ export default function LandingPage() {
11611170
}
11621171
setBetaSubmitting(false);
11631172
setBetaSubmitted(true);
1173+
setBetaEverSubmitted(true);
11641174
}}
11651175
style={{ marginTop: 'auto', paddingTop: 28 }}
11661176
>

0 commit comments

Comments
 (0)