Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,19 @@ const RECENCIES: { value: Recency; label: string }[] = [
{ value: "any", label: "Any time" },
];

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

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

function pickCategory(key: string) {
const cat = PICKABLE.find((c) => c.key === key)!;
Expand Down Expand Up @@ -58,6 +70,16 @@ export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
setError(null);
setNotice(null);
if (!term.trim()) return setError(`${category.inputLabel} is required.`);

if (!authed) {
// Require login to save: carry the built alert through the login round-trip.
const target = `/alerts?new=${encodeURIComponent(categoryKey)}&term=${encodeURIComponent(
term,
)}&recency=${recency}&frequency=${frequency}`;
router.push(`/login?redirect=${encodeURIComponent(target)}`);
return;
}

startTransition(async () => {
const res = await createAlert({ category: categoryKey, term, customQuery: term, recency, frequency });
if (!res.ok) return setError(res.error);
Expand Down Expand Up @@ -119,11 +141,13 @@ export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
</div>

<div className="flex flex-wrap items-center gap-2">
<button type="button" className="btn" onClick={runTest} disabled={pending}>
{pending ? "Checking…" : "See current results"}
</button>
{authed && (
<button type="button" className="btn" onClick={runTest} disabled={pending}>
{pending ? "Checking…" : "See current results"}
</button>
)}
<button type="submit" className="btn btn-primary" disabled={pending || atCap}>
Create alert
{authed ? "Create alert" : "Create alert — sign in to save"}
</button>
{atCap && (
<span className="text-xs text-[var(--color-warn)]">
Expand Down
58 changes: 48 additions & 10 deletions app/(app)/alerts/page.tsx → app/(marketing)/alerts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import {
SERP_CALLS_PER_MONTH,
planFromProfile,
} from "@/lib/alerts/limits";
import { getCategory } from "@/lib/alerts/categories";
import { getCategory, type Recency } from "@/lib/alerts/categories";
import { CreateAlert } from "./create-alert";
import { AlertActions } from "./alert-actions";

export const metadata = { title: "Alerts" };
export const metadata = {
title: "Free web alerts — CrawlProof Alerts",
description:
"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.",
};

type AlertRow = {
id: string;
Expand All @@ -35,25 +39,59 @@ function fmt(ts: string | null): string {
});
}

const RECENCIES = new Set<Recency>(["day", "week", "month", "any"]);

export default async function AlertsPage({
searchParams,
}: {
searchParams: Promise<{ new?: string; term?: string }>;
searchParams: Promise<{ new?: string; term?: string; recency?: string; frequency?: string }>;
}) {
const { new: newCat, term } = await searchParams;
const { new: newCat, term, recency, frequency } = await searchParams;

const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) redirect("/login?next=/alerts");

// Post-signup magic-link redirect: create the pending alert, then clean the URL.
// ---------- Public (not signed in): build the alert, sign in to save ----------
if (!user) {
return (
<main className="mx-auto max-w-2xl px-4 py-14 sm:px-6">
<div className="text-center">
<h1 className="text-3xl font-bold sm:text-4xl">Know the moment the web mentions you</h1>
<p className="mx-auto mt-3 max-w-xl text-[var(--color-muted)]">
Free email alerts for brand mentions, new backlinks, competitor moves, and buying-intent
searches — powered by Google, verified by CrawlProof&apos;s crawler. Build one below; sign in
to save it.
</p>
</div>
<div className="mt-8">
<CreateAlert authed={false} remainingSlots={MAX_ACTIVE_ALERTS.free} />
</div>
<p className="mt-4 text-center text-sm text-[var(--color-muted)]">
Already have an account?{" "}
<Link href="/login?redirect=/alerts" className="text-[var(--color-accent)]">
Log in
</Link>
.
</p>
</main>
);
}

// Post-signin: create the alert carried through login, then clean the URL.
if (newCat && getCategory(newCat)) {
await createAlert({ category: newCat, term: term ?? "", customQuery: term ?? "" });
await createAlert({
category: newCat,
term: term ?? "",
customQuery: term ?? "",
recency: recency && RECENCIES.has(recency as Recency) ? (recency as Recency) : undefined,
frequency: frequency === "hourly" || frequency === "daily" ? frequency : undefined,
});
redirect("/alerts");
}

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

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

<CreateAlert remainingSlots={remaining} />
<CreateAlert authed remainingSlots={remaining} />

<div className="space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-wide text-[var(--color-muted)]">
Expand All @@ -117,6 +155,6 @@ export default async function AlertsPage({
</div>
))}
</div>
</div>
</main>
);
}
Loading