Skip to content

Commit 0cd76c2

Browse files
ralyodioclaude
andauthored
Alerts: make hourly free and fix the frequency dropdown (#73)
- Hourly checks are now available on the free plan (no paywall on frequency); the per-account monthly SERP-call budget remains the cost backstop. Free active-alert cap stays at 50. - The "Check frequency" <select> was fully disabled for free users, so it read as broken. It's now always interactive with Daily/Hourly both selectable. - /alerts stays account-gated (redirect to /login?next=/alerts). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f45c5cb commit 0cd76c2

3 files changed

Lines changed: 25 additions & 56 deletions

File tree

‎app/(app)/alerts/create-alert.tsx‎

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createAlert, testRunAlert } from "@/app/actions/alerts";
77
import type { SerpResult } from "@/lib/alerts/valueserp";
88

99
// People-tracking templates (name / reputation / impersonation / legal) are
10-
// withheld from the v1 launch picker pending a trust-and-safety policy.
10+
// withheld from the v1 picker pending a trust-and-safety policy.
1111
const PICKABLE = ALERT_CATEGORIES.filter((c) => !c.gated);
1212

1313
const RECENCIES: { value: Recency; label: string }[] = [
@@ -17,13 +17,7 @@ const RECENCIES: { value: Recency; label: string }[] = [
1717
{ value: "any", label: "Any time" },
1818
];
1919

20-
export function CreateAlert({
21-
remainingSlots,
22-
allowHourly,
23-
}: {
24-
remainingSlots: number;
25-
allowHourly: boolean;
26-
}) {
20+
export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
2721
const router = useRouter();
2822
const [pending, startTransition] = useTransition();
2923
const [categoryKey, setCategoryKey] = useState(PICKABLE[0].key);
@@ -49,41 +43,24 @@ export function CreateAlert({
4943
function runTest() {
5044
setError(null);
5145
setNotice(null);
52-
if (!term.trim()) {
53-
setError(`${category.inputLabel} is required.`);
54-
return;
55-
}
46+
if (!term.trim()) return setError(`${category.inputLabel} is required.`);
5647
startTransition(async () => {
5748
const res = await testRunAlert({ category: categoryKey, term, customQuery: term });
58-
if (!res.ok) {
59-
setError(res.error);
60-
return;
61-
}
49+
if (!res.ok) return setError(res.error);
6250
setPreview(res.results);
63-
if (res.results.length === 0) setNotice("No current results — you'll be emailed when something new appears.");
51+
if (res.results.length === 0)
52+
setNotice("No current results — you'll be emailed when something new appears.");
6453
});
6554
}
6655

6756
function submit(e: React.FormEvent) {
6857
e.preventDefault();
6958
setError(null);
7059
setNotice(null);
71-
if (!term.trim()) {
72-
setError(`${category.inputLabel} is required.`);
73-
return;
74-
}
60+
if (!term.trim()) return setError(`${category.inputLabel} is required.`);
7561
startTransition(async () => {
76-
const res = await createAlert({
77-
category: categoryKey,
78-
term,
79-
customQuery: term,
80-
recency,
81-
frequency,
82-
});
83-
if (!res.ok) {
84-
setError(res.error);
85-
return;
86-
}
62+
const res = await createAlert({ category: categoryKey, term, customQuery: term, recency, frequency });
63+
if (!res.ok) return setError(res.error);
8764
setTerm("");
8865
setPreview(null);
8966
setNotice("Alert created. First check runs shortly.");
@@ -135,11 +112,9 @@ export function CreateAlert({
135112
value={frequency}
136113
onChange={(e) => setFrequency(e.target.value as "daily" | "hourly")}
137114
aria-label="Check frequency"
138-
disabled={!allowHourly}
139-
title={allowHourly ? "" : "Hourly checks are a paid feature"}
140115
>
141116
<option value="daily">Daily</option>
142-
<option value="hourly">Hourly{allowHourly ? "" : " (paid)"}</option>
117+
<option value="hourly">Hourly</option>
143118
</select>
144119
</div>
145120

@@ -166,13 +141,7 @@ export function CreateAlert({
166141
Current results preview (you'll only be emailed NEW ones)
167142
</p>
168143
{preview.map((r) => (
169-
<a
170-
key={r.url}
171-
href={r.url}
172-
target="_blank"
173-
rel="noreferrer"
174-
className="block text-sm"
175-
>
144+
<a key={r.url} href={r.url} target="_blank" rel="noreferrer" className="block text-sm">
176145
<span className="font-medium text-[var(--color-accent)]">{r.title || r.url}</span>
177146
<span className="block truncate text-xs text-[var(--color-muted)]">{r.url}</span>
178147
</a>

‎app/(app)/alerts/page.tsx‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ type AlertRow = {
2727

2828
function fmt(ts: string | null): string {
2929
if (!ts) return "not yet";
30-
const d = new Date(ts);
31-
return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
30+
return new Date(ts).toLocaleString(undefined, {
31+
month: "short",
32+
day: "numeric",
33+
hour: "numeric",
34+
minute: "2-digit",
35+
});
3236
}
3337

3438
export default async function AlertsPage({
@@ -42,10 +46,9 @@ export default async function AlertsPage({
4246
const {
4347
data: { user },
4448
} = await supabase.auth.getUser();
45-
if (!user) redirect("/login");
49+
if (!user) redirect("/login?next=/alerts");
4650

47-
// Post-signup: create the alert carried in the magic-link redirect, then
48-
// strip the params so a refresh can't double-create.
51+
// Post-signup magic-link redirect: create the pending alert, then clean the URL.
4952
if (newCat && getCategory(newCat)) {
5053
await createAlert({ category: newCat, term: term ?? "", customQuery: term ?? "" });
5154
redirect("/alerts");
@@ -84,15 +87,10 @@ export default async function AlertsPage({
8487
<div className="flex gap-2 text-xs">
8588
<span className="badge">{activeCount}/{cap} active</span>
8689
<span className="badge">{Math.max(0, budget - used)} checks left this month</span>
87-
{plan === "free" && (
88-
<Link href="/settings/billing" className="badge badge-pass">
89-
Upgrade
90-
</Link>
91-
)}
9290
</div>
9391
</div>
9492

95-
<CreateAlert remainingSlots={remaining} allowHourly={plan !== "free"} />
93+
<CreateAlert remainingSlots={remaining} />
9694

9795
<div className="space-y-3">
9896
<h2 className="text-sm font-semibold uppercase tracking-wide text-[var(--color-muted)]">

‎lib/alerts/limits.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ export const SERP_CALLS_PER_MONTH: Record<Plan, number> = {
2525
team: 1_000_000,
2626
};
2727

28-
// Only paid plans may run hourly; free is daily.
29-
export function allowedFrequencies(plan: Plan): Array<"daily" | "hourly"> {
30-
return plan === "free" ? ["daily"] : ["daily", "hourly"];
28+
// Hourly is currently free for everyone (no paywall on frequency yet — the
29+
// per-account monthly SERP-call budget is what bounds cost). Revisit if/when
30+
// hourly becomes a paid lever.
31+
export function allowedFrequencies(_plan: Plan): Array<"daily" | "hourly"> {
32+
return ["daily", "hourly"];
3133
}
3234

3335
// Results requested per poll and surfaced per email. Capping at 10 keeps SERP

0 commit comments

Comments
 (0)