Skip to content

Commit

Permalink
Add a mailing list subscription upsell on front-page and settings (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
nichochar authored Sep 7, 2024
1 parent c684eef commit b5ab034
Show file tree
Hide file tree
Showing 13 changed files with 380 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/honest-stingrays-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@srcbook/api': patch
'@srcbook/web': patch
'srcbook': patch
---

Added an upsell to subscribe to our mailing list + ability to update in settings
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ srcbook/lib/**/*

# turbo
**/.turbo

# Aide
*.code-workspace
2 changes: 2 additions & 0 deletions packages/api/db/schema.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const configs = sqliteTable('config', {
aiProvider: text('ai_provider').notNull().default('openai'),
aiModel: text('ai_model').default('gpt-4o'),
aiBaseUrl: text('ai_base_url'),
// Null: unset. Email: subscribed. "dismissed": dismissed the dialog.
subscriptionEmail: text('subscription_email'),
});

export type Config = typeof configs.$inferSelect;
Expand Down
1 change: 1 addition & 0 deletions packages/api/drizzle/0007_add_subscription_email.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `config` ADD `subscription_email` text;
139 changes: 139 additions & 0 deletions packages/api/drizzle/meta/0007_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
{
"version": "6",
"dialect": "sqlite",
"id": "a8c6d627-368f-4e6d-bdd9-42ffb1d8a8b2",
"prevId": "e99407eb-2c92-4d3c-8864-bd760846d61b",
"tables": {
"config": {
"name": "config",
"columns": {
"base_dir": {
"name": "base_dir",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_language": {
"name": "default_language",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'typescript'"
},
"openai_api_key": {
"name": "openai_api_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"anthropic_api_key": {
"name": "anthropic_api_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled_analytics": {
"name": "enabled_analytics",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"srcbook_installation_id": {
"name": "srcbook_installation_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'kkoge7ntqjmhgn1l5ljmkdsfpg'"
},
"ai_provider": {
"name": "ai_provider",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'openai'"
},
"ai_model": {
"name": "ai_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'gpt-4o'"
},
"ai_base_url": {
"name": "ai_base_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"subscription_email": {
"name": "subscription_email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"secrets": {
"name": "secrets",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"secrets_name_unique": {
"name": "secrets_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
7 changes: 7 additions & 0 deletions packages/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
"when": 1723745583289,
"tag": "0006_deprecate_ai_config",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1725736805777,
"tag": "0007_add_subscription_email",
"breakpoints": true
}
]
}
18 changes: 18 additions & 0 deletions packages/api/server/http.mts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,24 @@ router.get('/npm/search', cors(), async (req, res) => {
return res.json({ result: results });
});

router.options('/subscribe', cors());
router.post('/subscribe', cors(), async (req, res) => {
const { email } = req.body;
const hubResponse = await fetch('https://hub.srcbook.com/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});

if (hubResponse.ok) {
return res.json({ success: true });
} else {
return res.status(hubResponse.status).json({ success: false });
}
});

app.use('/api', router);

export default app;
84 changes: 84 additions & 0 deletions packages/web/src/components/mailing-list-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState } from 'react';
import { X, Mailbox } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { subscribeToMailingList } from '@/lib/server';
import { useSettings } from '@/components/use-settings';

export default function MailingListCard() {
const { subscriptionEmail, updateConfig } = useSettings();
const [isVisible, setIsVisible] = useState(!subscriptionEmail);
const [email, setEmail] = useState('');
const [subscribed, setSubscribed] = useState(false);

const handleClose = async () => {
await updateConfig({ subscriptionEmail: 'dismissed' });
setIsVisible(false);
};

const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
try {
const response = await subscribeToMailingList(email);
if (response.success) {
setSubscribed(true);
await updateConfig({ subscriptionEmail: email });
setTimeout(() => {
setIsVisible(false);
}, 3000);
} else {
toast.error('There was an error subscribing to the mailing list. Please try again later.');
}
} catch (error) {
toast.error('There was an error subscribing to the mailing list. Please try again later.');
console.error('Subscription error:', error);
}
};

if (!isVisible) return null;

return (
<Card className="fixed bottom-6 left-6 w-[460px] shadow-lg z-30">
<div className="relative">
<button
className="absolute top-2 right-2 hover:bg-muted p-1 rounded-sm"
onClick={handleClose}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
<CardContent className="p-6">
<div className="flex flex-col gap-2">
<Mailbox size={24} />
{subscribed ? (
<>
<h1 className="mt-2 text-lg font-medium">Thank you for subscribing!</h1>
<p>We'll keep you updated with the latest news and features.</p>
</>
) : (
<>
<h1 className="mt-2 text-lg font-medium">Join our mailing list!</h1>
<p className="">
Get the latest updates, early access features, and expert tips delivered to your
inbox.
</p>
<form onSubmit={handleSubmit} className="flex gap-1 py-3">
<Input
type="email"
placeholder="Email"
className=""
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Button type="submit">Subscribe</Button>
</form>
</>
)}
</div>
</CardContent>
</div>
</Card>
);
}
57 changes: 57 additions & 0 deletions packages/web/src/components/ui/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from 'react';

import { cn } from '@/lib/utils';

const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
{...props}
/>
),
);
Card.displayName = 'Card';

const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
);
CardHeader.displayName = 'CardHeader';

const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
// eslint-disable-next-line jsx-a11y/heading-has-content
<h3
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
{...props}
/>
),
);
CardTitle.displayName = 'CardTitle';

const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
));
CardDescription.displayName = 'CardDescription';

const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
),
);
CardContent.displayName = 'CardContent';

const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
);
CardFooter.displayName = 'CardFooter';

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
18 changes: 17 additions & 1 deletion packages/web/src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ interface EditConfigRequestType {
aiBaseUrl?: string;
aiModel?: string;
aiProvider?: AiProviderType;
subscriptionEmail?: string | null;
}

export async function getConfig() {
Expand All @@ -253,7 +254,7 @@ export async function getConfig() {
return response.json();
}

export async function updateConfig(request: EditConfigRequestType) {
export async function updateConfig(request: EditConfigRequestType): Promise<void> {
const response = await fetch(API_BASE_URL + '/settings', {
method: 'POST',
headers: { 'content-type': 'application/json' },
Expand Down Expand Up @@ -397,3 +398,18 @@ export async function aiHealthcheck() {
}
return response.json();
}

export async function subscribeToMailingList(email: string) {
const response = await fetch(API_BASE_URL + '/subscribe', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email }),
});

if (!response.ok) {
console.error(response);
throw new Error('Subscription request failed');
}

return response.json();
}
Loading

0 comments on commit b5ab034

Please sign in to comment.