Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Loader2, Minus, Plus } from "lucide-react";
|
||||
import {
|
||||
useCreateNotificationDestination,
|
||||
useUnlinkNotificationDestinationCompany,
|
||||
} from "@/hooks/use-notification-destinations";
|
||||
import type { NotificationDestinationResponse, NotificationType } from "@/lib/types";
|
||||
|
||||
/**
|
||||
* A simplified single-destination view over the full notification
|
||||
* destinations list, scoped to one company - shows the first destination
|
||||
* of `type` linked to this company (if any) with an add/remove control.
|
||||
* Shares the same React Query cache as the Settings page's full
|
||||
* multi-destination UI, so changes here show up there instantly.
|
||||
*/
|
||||
export function NotificationChannelBox({
|
||||
type,
|
||||
label,
|
||||
placeholder,
|
||||
companyId,
|
||||
destination,
|
||||
disabled,
|
||||
disabledReason,
|
||||
}: {
|
||||
type: NotificationType;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
companyId: string;
|
||||
destination: NotificationDestinationResponse | undefined;
|
||||
disabled?: boolean;
|
||||
disabledReason?: string;
|
||||
}) {
|
||||
const [value, setValue] = useState("");
|
||||
const create = useCreateNotificationDestination();
|
||||
const unlink = useUnlinkNotificationDestinationCompany();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700">{label}</label>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<input
|
||||
value={destination ? destination.destination_value : value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
readOnly={Boolean(destination)}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
className="focus-ring block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm transition-colors duration-150 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400"
|
||||
/>
|
||||
{destination ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unlink.mutate({ destinationId: destination.id, companyId })}
|
||||
disabled={disabled || unlink.isPending}
|
||||
title={`Remove this ${label.toLowerCase()}`}
|
||||
className="focus-ring inline-flex shrink-0 items-center justify-center rounded-md border border-red-300 p-2 text-red-600 transition-colors duration-150 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{unlink.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
||||
) : (
|
||||
<Minus className="h-4 w-4" aria-hidden />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!value.trim()) return;
|
||||
create.mutate(
|
||||
{ type, destination_value: value.trim(), company_ids: [companyId] },
|
||||
{ onSuccess: () => setValue("") },
|
||||
);
|
||||
}}
|
||||
disabled={disabled || !value.trim() || create.isPending}
|
||||
title={`Add this ${label.toLowerCase()}`}
|
||||
className="focus-ring inline-flex shrink-0 items-center justify-center rounded-md border border-green-300 p-2 text-green-700 transition-colors duration-150 hover:bg-green-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{create.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
||||
) : (
|
||||
<Plus className="h-4 w-4" aria-hidden />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{disabled && disabledReason && <p className="mt-1 text-xs text-slate-500">{disabledReason}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user