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
53 changes: 11 additions & 42 deletions app/(app)/alerts/create-alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createAlert, testRunAlert } from "@/app/actions/alerts";
import type { SerpResult } from "@/lib/alerts/valueserp";

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

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

export function CreateAlert({
remainingSlots,
allowHourly,
}: {
remainingSlots: number;
allowHourly: boolean;
}) {
export function CreateAlert({ remainingSlots }: { remainingSlots: number }) {
const router = useRouter();
const [pending, startTransition] = useTransition();
const [categoryKey, setCategoryKey] = useState(PICKABLE[0].key);
Expand All @@ -49,41 +43,24 @@ export function CreateAlert({
function runTest() {
setError(null);
setNotice(null);
if (!term.trim()) {
setError(`${category.inputLabel} is required.`);
return;
}
if (!term.trim()) return setError(`${category.inputLabel} is required.`);
startTransition(async () => {
const res = await testRunAlert({ category: categoryKey, term, customQuery: term });
if (!res.ok) {
setError(res.error);
return;
}
if (!res.ok) return setError(res.error);
setPreview(res.results);
if (res.results.length === 0) setNotice("No current results — you'll be emailed when something new appears.");
if (res.results.length === 0)
setNotice("No current results — you'll be emailed when something new appears.");
});
}

function submit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setNotice(null);
if (!term.trim()) {
setError(`${category.inputLabel} is required.`);
return;
}
if (!term.trim()) return setError(`${category.inputLabel} is required.`);
startTransition(async () => {
const res = await createAlert({
category: categoryKey,
term,
customQuery: term,
recency,
frequency,
});
if (!res.ok) {
setError(res.error);
return;
}
const res = await createAlert({ category: categoryKey, term, customQuery: term, recency, frequency });
if (!res.ok) return setError(res.error);
setTerm("");
setPreview(null);
setNotice("Alert created. First check runs shortly.");
Expand Down Expand Up @@ -135,11 +112,9 @@ export function CreateAlert({
value={frequency}
onChange={(e) => setFrequency(e.target.value as "daily" | "hourly")}
aria-label="Check frequency"
disabled={!allowHourly}
title={allowHourly ? "" : "Hourly checks are a paid feature"}
>
<option value="daily">Daily</option>
<option value="hourly">Hourly{allowHourly ? "" : " (paid)"}</option>
<option value="hourly">Hourly</option>
</select>
</div>

Expand All @@ -166,13 +141,7 @@ export function CreateAlert({
Current results preview (you'll only be emailed NEW ones)
</p>
{preview.map((r) => (
<a
key={r.url}
href={r.url}
target="_blank"
rel="noreferrer"
className="block text-sm"
>
<a key={r.url} href={r.url} target="_blank" rel="noreferrer" className="block text-sm">
<span className="font-medium text-[var(--color-accent)]">{r.title || r.url}</span>
<span className="block truncate text-xs text-[var(--color-muted)]">{r.url}</span>
</a>
Expand Down
20 changes: 9 additions & 11 deletions app/(app)/alerts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ type AlertRow = {

function fmt(ts: string | null): string {
if (!ts) return "not yet";
const d = new Date(ts);
return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
return new Date(ts).toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}

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

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

<CreateAlert remainingSlots={remaining} allowHourly={plan !== "free"} />
<CreateAlert remainingSlots={remaining} />

<div className="space-y-3">
<h2 className="text-sm font-semibold uppercase tracking-wide text-[var(--color-muted)]">
Expand Down
8 changes: 5 additions & 3 deletions lib/alerts/limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export const SERP_CALLS_PER_MONTH: Record<Plan, number> = {
team: 1_000_000,
};

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

// Results requested per poll and surfaced per email. Capping at 10 keeps SERP
Expand Down
Loading