Skip to content

Commit 0305e03

Browse files
ralyodioclaude
andcommitted
Alerts: make /alerts public, require sign-in to save
Anyone can open /alerts and build an alert (category, term, recency, frequency). Saving requires an account: for anonymous visitors the Create button routes to /login?redirect=/alerts?new=... so the built alert is created immediately after sign-in. Signed-in users get the full dashboard. Moved from the auth-gated (app) group into (marketing) (auth-adaptive header, no forced redirect). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0cd76c2 commit 0305e03

3 files changed

Lines changed: 78 additions & 16 deletions

File tree

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,19 @@ const RECENCIES: { value: Recency; label: string }[] = [
1717
{ value: "any", label: "Any time" },
1818
];
1919

20-
export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
20+
/**
21+
* The alert builder. Anyone can build an alert; `authed` decides "Save":
22+
* - authed: create it immediately (and offer an instant test run).
23+
* - anonymous: bounce to /login carrying the built alert, so it's created
24+
* right after sign-in. Saving always requires an account.
25+
*/
26+
export function CreateAlert({
27+
remainingSlots,
28+
authed,
29+
}: {
30+
remainingSlots: number;
31+
authed: boolean;
32+
}) {
2133
const router = useRouter();
2234
const [pending, startTransition] = useTransition();
2335
const [categoryKey, setCategoryKey] = useState(PICKABLE[0].key);
@@ -29,7 +41,7 @@ export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
2941
const [notice, setNotice] = useState<string | null>(null);
3042

3143
const category = useMemo(() => PICKABLE.find((c) => c.key === categoryKey)!, [categoryKey]);
32-
const atCap = remainingSlots <= 0;
44+
const atCap = authed && remainingSlots <= 0;
3345

3446
function pickCategory(key: string) {
3547
const cat = PICKABLE.find((c) => c.key === key)!;
@@ -58,6 +70,16 @@ export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
5870
setError(null);
5971
setNotice(null);
6072
if (!term.trim()) return setError(`${category.inputLabel} is required.`);
73+
74+
if (!authed) {
75+
// Require login to save: carry the built alert through the login round-trip.
76+
const target = `/alerts?new=${encodeURIComponent(categoryKey)}&term=${encodeURIComponent(
77+
term,
78+
)}&recency=${recency}&frequency=${frequency}`;
79+
router.push(`/login?redirect=${encodeURIComponent(target)}`);
80+
return;
81+
}
82+
6183
startTransition(async () => {
6284
const res = await createAlert({ category: categoryKey, term, customQuery: term, recency, frequency });
6385
if (!res.ok) return setError(res.error);
@@ -119,11 +141,13 @@ export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
119141
</div>
120142

121143
<div className="flex flex-wrap items-center gap-2">
122-
<button type="button" className="btn" onClick={runTest} disabled={pending}>
123-
{pending ? "Checking…" : "See current results"}
124-
</button>
144+
{authed && (
145+
<button type="button" className="btn" onClick={runTest} disabled={pending}>
146+
{pending ? "Checking…" : "See current results"}
147+
</button>
148+
)}
125149
<button type="submit" className="btn btn-primary" disabled={pending || atCap}>
126-
Create alert
150+
{authed ? "Create alert" : "Create alert — sign in to save"}
127151
</button>
128152
{atCap && (
129153
<span className="text-xs text-[var(--color-warn)]">
Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import {
88
SERP_CALLS_PER_MONTH,
99
planFromProfile,
1010
} from "@/lib/alerts/limits";
11-
import { getCategory } from "@/lib/alerts/categories";
11+
import { getCategory, type Recency } from "@/lib/alerts/categories";
1212
import { CreateAlert } from "./create-alert";
1313
import { AlertActions } from "./alert-actions";
1414

15-
export const metadata = { title: "Alerts" };
15+
export const metadata = {
16+
title: "Free web alerts — CrawlProof Alerts",
17+
description:
18+
"Free, near-realtime email alerts for anything Google can see: brand mentions, new backlinks, buying-intent searches, and more. Build one now — sign in to save it.",
19+
};
1620

1721
type AlertRow = {
1822
id: string;
@@ -35,25 +39,59 @@ function fmt(ts: string | null): string {
3539
});
3640
}
3741

42+
const RECENCIES = new Set<Recency>(["day", "week", "month", "any"]);
43+
3844
export default async function AlertsPage({
3945
searchParams,
4046
}: {
41-
searchParams: Promise<{ new?: string; term?: string }>;
47+
searchParams: Promise<{ new?: string; term?: string; recency?: string; frequency?: string }>;
4248
}) {
43-
const { new: newCat, term } = await searchParams;
49+
const { new: newCat, term, recency, frequency } = await searchParams;
4450

4551
const supabase = await createClient();
4652
const {
4753
data: { user },
4854
} = await supabase.auth.getUser();
49-
if (!user) redirect("/login?next=/alerts");
5055

51-
// Post-signup magic-link redirect: create the pending alert, then clean the URL.
56+
// ---------- Public (not signed in): build the alert, sign in to save ----------
57+
if (!user) {
58+
return (
59+
<main className="mx-auto max-w-2xl px-4 py-14 sm:px-6">
60+
<div className="text-center">
61+
<h1 className="text-3xl font-bold sm:text-4xl">Know the moment the web mentions you</h1>
62+
<p className="mx-auto mt-3 max-w-xl text-[var(--color-muted)]">
63+
Free email alerts for brand mentions, new backlinks, competitor moves, and buying-intent
64+
searches — powered by Google, verified by CrawlProof&apos;s crawler. Build one below; sign in
65+
to save it.
66+
</p>
67+
</div>
68+
<div className="mt-8">
69+
<CreateAlert authed={false} remainingSlots={MAX_ACTIVE_ALERTS.free} />
70+
</div>
71+
<p className="mt-4 text-center text-sm text-[var(--color-muted)]">
72+
Already have an account?{" "}
73+
<Link href="/login?redirect=/alerts" className="text-[var(--color-accent)]">
74+
Log in
75+
</Link>
76+
.
77+
</p>
78+
</main>
79+
);
80+
}
81+
82+
// Post-signin: create the alert carried through login, then clean the URL.
5283
if (newCat && getCategory(newCat)) {
53-
await createAlert({ category: newCat, term: term ?? "", customQuery: term ?? "" });
84+
await createAlert({
85+
category: newCat,
86+
term: term ?? "",
87+
customQuery: term ?? "",
88+
recency: recency && RECENCIES.has(recency as Recency) ? (recency as Recency) : undefined,
89+
frequency: frequency === "hourly" || frequency === "daily" ? frequency : undefined,
90+
});
5491
redirect("/alerts");
5592
}
5693

94+
// ---------- Signed in: full management dashboard ----------
5795
const svc = serviceClient();
5896
const { data: profile } = await svc
5997
.from("profiles")
@@ -76,7 +114,7 @@ export default async function AlertsPage({
76114
const used = (profile?.alert_serp_calls_used as number) ?? 0;
77115

78116
return (
79-
<div className="space-y-8">
117+
<main className="mx-auto max-w-4xl space-y-8 px-4 py-10 sm:px-6">
80118
<div className="flex flex-wrap items-end justify-between gap-3">
81119
<div>
82120
<h1 className="text-2xl font-bold">Alerts</h1>
@@ -90,7 +128,7 @@ export default async function AlertsPage({
90128
</div>
91129
</div>
92130

93-
<CreateAlert remainingSlots={remaining} />
131+
<CreateAlert authed remainingSlots={remaining} />
94132

95133
<div className="space-y-3">
96134
<h2 className="text-sm font-semibold uppercase tracking-wide text-[var(--color-muted)]">
@@ -117,6 +155,6 @@ export default async function AlertsPage({
117155
</div>
118156
))}
119157
</div>
120-
</div>
158+
</main>
121159
);
122160
}

0 commit comments

Comments
 (0)