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
2 changes: 1 addition & 1 deletion src/components/dashboard/settings-billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@
}`}
>
{isPopular && !isCurrent && (
<Badge className="absolute -top-2.5 left-1/2 -translate-x-1/2 text-[10px]">
<Badge className="absolute -top-2.5 left-1/2 -translate-x-1/2 text-center text-[10px] whitespace-nowrap">
Most popular
</Badge>
)}
Expand Down Expand Up @@ -657,7 +657,7 @@
</Button>
) : (
<Button variant="outline" size="sm" className="w-full" asChild>
<a href="/contact">Contact Sales</a>

Check warning on line 660 in src/components/dashboard/settings-billing.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not use an `<a>` element to navigate to `/contact/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages

Check warning on line 660 in src/components/dashboard/settings-billing.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not use an `<a>` element to navigate to `/contact/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages

Check warning on line 660 in src/components/dashboard/settings-billing.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not use an `<a>` element to navigate to `/contact/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages

Check warning on line 660 in src/components/dashboard/settings-billing.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not use an `<a>` element to navigate to `/contact/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages

Check warning on line 660 in src/components/dashboard/settings-billing.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not use an `<a>` element to navigate to `/contact/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages

Check warning on line 660 in src/components/dashboard/settings-billing.tsx

View workflow job for this annotation

GitHub Actions / lint

Do not use an `<a>` element to navigate to `/contact/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages
</Button>
)}
</div>
Expand Down
153 changes: 132 additions & 21 deletions src/components/dashboard/settings-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import {
Crown,
TriangleAlert,
Info,
Loader2,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
Expand Down Expand Up @@ -101,6 +103,8 @@ export default function SettingsGeneral({
const [addChannelOpen, setAddChannelOpen] = useState(false);
const [selectedSlackChannel, setSelectedSlackChannel] = useState("");
const [addingChannel, setAddingChannel] = useState(false);
const [manualMode, setManualMode] = useState(false);
const [manualChannelName, setManualChannelName] = useState("");
const [deleteTarget, setDeleteTarget] = useState<Channel | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -154,6 +158,60 @@ export default function SettingsGeneral({
}
}

async function addChannelManually() {
const name = manualChannelName.trim().replace(/^#/, "");
if (!name) return;

setAddingChannel(true);
try {
const lookupRes = await fetch("/api/settings/slack-channels", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Workspace-Id": workspaceId,
},
body: JSON.stringify({ channelName: name }),
});

if (!lookupRes.ok) {
const data = await lookupRes.json();
toast.error(data.error || "Channel not found");
return;
}

const slackChannel = await lookupRes.json();

const res = await fetch("/api/settings/channels", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Workspace-Id": workspaceId,
},
body: JSON.stringify({
slackChannelId: slackChannel.id,
channelName: slackChannel.name,
}),
});

if (!res.ok) {
const data = await res.json();
toast.error(data.error || "Failed to add channel");
return;
}

const channel = await res.json();
setChannels((prev) => [...prev, { ...channel, disabledStyleIds: [] }]);
setAddChannelOpen(false);
setManualChannelName("");
setManualMode(false);
toast.success(`#${slackChannel.name} connected`);
} catch {
toast.error("Failed to add channel");
} finally {
setAddingChannel(false);
}
}

async function removeChannel(channel: Channel) {
setChannels((prev) => prev.filter((ch) => ch.id !== channel.id));
setDeleteTarget(null);
Expand Down Expand Up @@ -427,20 +485,39 @@ export default function SettingsGeneral({
quotes.
</DialogDescription>
</DialogHeader>
{availableSlackChannels.length === 0 ? (
<div className="flex items-start gap-3 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3 text-sm">
<TriangleAlert className="mt-0.5 h-5 w-5 shrink-0 text-yellow-500" />
<div>
<p className="font-medium text-yellow-500">
No channels available
</p>
<p className="text-muted-foreground mt-1">
The No Context bot needs to be added to a Slack
channel first. In Slack, open the channel you want to
monitor, click the channel name at the top, go to the{" "}
<strong>Integrations</strong> tab, and add the{" "}
<strong>No Context</strong> app.
</p>
{manualMode || availableSlackChannels.length === 0 ? (
<div className="space-y-3">
{availableSlackChannels.length === 0 && !manualMode && (
<div className="flex items-start gap-3 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3 text-sm">
<TriangleAlert className="mt-0.5 h-5 w-5 shrink-0 text-yellow-500" />
<div>
<p className="font-medium text-yellow-500">
No channels found automatically
</p>
<p className="text-muted-foreground mt-1">
You can manually enter a channel name below, or
add the No Context bot to a channel in Slack
first.
</p>
</div>
</div>
)}
<div className="relative">
<span className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm font-medium">
#
</span>
<Input
type="text"
value={manualChannelName}
onChange={(e) => setManualChannelName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && manualChannelName.trim().replace(/^#/, "")) {
addChannelManually();
}
}}
placeholder="channel-name"
className="pl-7"
/>
</div>
</div>
) : (
Expand All @@ -460,19 +537,53 @@ export default function SettingsGeneral({
</SelectContent>
</Select>
)}
{availableSlackChannels.length > 0 && (
<button
type="button"
onClick={() => {
setManualMode((m) => !m);
setSelectedSlackChannel("");
setManualChannelName("");
}}
className="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
>
{manualMode
? "Select from channel list"
: "Manually enter a channel name"}
</button>
)}
<DialogFooter>
<Button
variant="secondary"
onClick={() => setAddChannelOpen(false)}
onClick={() => {
setAddChannelOpen(false);
setManualMode(false);
setManualChannelName("");
}}
>
Cancel
</Button>
<Button
onClick={addChannel}
disabled={!selectedSlackChannel || addingChannel}
>
{addingChannel ? "Adding..." : "Add Channel"}
</Button>
{manualMode || availableSlackChannels.length === 0 ? (
<Button
onClick={addChannelManually}
disabled={
!manualChannelName.trim().replace(/^#/, "") ||
addingChannel
}
>
{addingChannel && (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
)}
{addingChannel ? "Adding..." : "Add Channel"}
</Button>
) : (
<Button
onClick={addChannel}
disabled={!selectedSlackChannel || addingChannel}
>
{addingChannel ? "Adding..." : "Add Channel"}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const badgeVariants = cva(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
"inline-flex items-center justify-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors",
{
variants: {
variant: {
Expand Down
Loading