import { useState, useMemo, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAuth } from '../../contexts/AuthContext';
import { AppLayout } from '../../components/layout/AppLayout';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import {
slackApi,
type SlackChannel,
type SlackMessage,
} from '../../api/slack';
import {
Hash,
MessageSquare,
Users,
RefreshCw,
Search,
Filter,
X,
Lock,
Globe,
Archive,
Send,
Calendar,
Clock,
ArrowUpRight,
ArrowDownRight,
Activity,
MessageCircle,
Eye,
Hash as HashIcon,
Zap,
AlertCircle,
CheckCircle2,
} from 'lucide-react';
// ─── Slack Brand ────────────────────────────────────────────────────────
// Primary: #4A154B (dark purple)
// Alternatives: #611F69, #E01E5A (red), #EA4B71 (pink), #FDB033 (yellow)
const SLACK_PURPLE = '#4A154B';
const SLACK_PINK = '#E01E5A';
// ─── Helpers ────────────────────────────────────────────────────────────
function formatNumber(value: number | null): string {
if (value == null) return '—';
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
return value.toLocaleString();
}
function timeAgo(dateStr: string | null | undefined): string {
if (!dateStr) return '—';
const now = Date.now();
const then = new Date(dateStr).getTime();
const diff = Math.abs(now - then);
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days === 1) return 'yesterday';
if (days < 30) return `${days}d ago`;
return new Date(dateStr).toLocaleDateString();
}
function slackTsToTime(ts: string): string {
if (!ts) return '';
const date = new Date(parseFloat(ts) * 1000);
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
}
function slackTsToDate(ts: string): string {
if (!ts) return '';
const date = new Date(parseFloat(ts) * 1000);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
// ─── Tab Types ──────────────────────────────────────────────────────────
type TabKey = 'overview' | 'channels' | 'messages' | 'activity';
// ─── Overview Tab ──────────────────────────────────────────────────────
function OverviewTab({ companyId, channels }: { companyId: string; channels: SlackChannel[] }) {
const { data, isLoading } = useQuery({
queryKey: ['slack-workspace', companyId],
queryFn: () => slackApi.getWorkspace(companyId),
staleTime: 120_000,
});
const workspace = data?.data?.workspace;
const channelCount = data?.data?.channel_count || channels.length;
const publicCount = data?.data?.public_count || channels.filter(c => !c.is_private).length;
const privateCount = data?.data?.private_count || channels.filter(c => c.is_private).length;
const lastSync = data?.data?.last_sync_at;
const stats = [
{ label: 'Workspace', value: workspace?.team_name || '—', icon: Users, color: 'text-purple-700 bg-purple-50' },
{ label: 'Total Channels', value: channelCount, icon: HashIcon, color: 'text-blue-600 bg-blue-50' },
{ label: 'Public Channels', value: publicCount, icon: Globe, color: 'text-emerald-600 bg-emerald-50' },
{ label: 'Private Channels', value: privateCount, icon: Lock, color: 'text-violet-600 bg-violet-50' },
{ label: 'Last Sync', value: timeAgo(lastSync), icon: RefreshCw, color: 'text-amber-600 bg-amber-50' },
{ label: 'URL', value: workspace?.url?.replace('https://', '').split('.')[0] || '—', icon: Globe, color: 'text-cyan-600 bg-cyan-50' },
];
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{stats.map((stat) => (
<div key={stat.label} className="rounded-xl border border-surface-200 bg-white p-4">
<div className={`inline-flex rounded-lg p-2 mb-2 ${stat.color}`}>
<stat.icon className="h-4 w-4" />
</div>
<p className="text-xs text-surface-500">{stat.label}</p>
<p className="text-sm font-bold text-surface-900 mt-0.5 truncate">{stat.value}</p>
</div>
))}
</div>
{/* Workspace Info */}
{workspace && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Workspace Details</h3>
<div className="space-y-3">
{workspace.team_name && (
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Team Name</span>
<span className="font-semibold text-surface-900">{workspace.team_name}</span>
</div>
)}
{workspace.url && (
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Workspace URL</span>
<a
href={workspace.url}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-purple-700 hover:underline flex items-center gap-1"
>
{workspace.url.replace('https://', '')}
<ArrowUpRight className="h-3 w-3" />
</a>
</div>
)}
{workspace.team_id && (
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Team ID</span>
<span className="font-mono text-sm text-surface-500">{workspace.team_id}</span>
</div>
)}
{workspace.enterprise_name && (
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Organization</span>
<span className="font-semibold text-surface-900">{workspace.enterprise_name}</span>
</div>
)}
</div>
</div>
)}
{/* Recent Channels */}
{channels.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Recent Channels</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{channels.slice(0, 8).map((ch) => (
<div key={ch.id} className="flex items-center gap-3 rounded-lg bg-surface-50 p-3">
<div className="rounded-lg bg-purple-100 p-2">
{ch.is_private ? <Lock className="h-4 w-4 text-purple-700" /> : <Globe className="h-4 w-4 text-purple-700" />}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-surface-900 truncate">#{ch.name}</p>
<p className="text-xs text-surface-400">{ch.purpose || 'No purpose'}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
// ─── Channels Tab ──────────────────────────────────────────────────────
function ChannelsTab({ companyId }: { companyId: string }) {
const [search, setSearch] = useState('');
const [privateFilter, setPrivateFilter] = useState('');
const { data, isLoading } = useQuery({
queryKey: ['slack-channels', companyId, search, privateFilter],
queryFn: () =>
slackApi.getChannels(companyId, {
search: search || undefined,
is_private: privateFilter || undefined,
}),
staleTime: 60_000,
});
const channels = data?.data?.items || [];
return (
<div>
{/* Filters */}
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-400" />
<input
type="text"
placeholder="Search channels..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-surface-300 bg-white pl-10 pr-3 py-2 text-sm outline-none focus:border-purple-500"
/>
</div>
<select
value={privateFilter}
onChange={(e) => setPrivateFilter(e.target.value)}
className="rounded-lg border border-surface-300 bg-white px-3 py-2 text-sm outline-none focus:border-purple-500"
>
<option value="">All Channels</option>
<option value="false">Public Only</option>
<option value="true">Private Only</option>
</select>
</div>
{/* Channel List */}
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="rounded-xl border border-surface-200 bg-white">
<div className="divide-y divide-surface-100">
{channels.length === 0 ? (
<div className="text-center py-12 text-surface-400">No channels found</div>
) : (
channels.map((ch) => (
<div key={ch.id} className="flex items-center gap-4 px-6 py-4 hover:bg-surface-50">
<div className="rounded-lg bg-purple-100 p-2 flex-shrink-0">
{ch.is_private ? (
<Lock className="h-4 w-4 text-purple-700" />
) : (
<Globe className="h-4 w-4 text-purple-700" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-surface-900">#{ch.name}</span>
{ch.is_private && (
<span className="inline-flex items-center rounded-full bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
Private
</span>
)}
</div>
{ch.purpose && (
<p className="text-xs text-surface-400 mt-0.5 truncate">{ch.purpose}</p>
)}
</div>
<div className="text-right flex-shrink-0">
<p className="text-xs text-surface-400">
{ch.external_id.slice(0, 9)}
</p>
<p className="text-xs text-surface-400">
Synced {timeAgo(ch.synced_at)}
</p>
</div>
</div>
))
)}
</div>
</div>
)}
</div>
);
}
// ─── Messages Tab ──────────────────────────────────────────────────────
function MessagesTab({ companyId, channels }: { companyId: string; channels: SlackChannel[] }) {
const [channelFilter, setChannelFilter] = useState('');
const [search, setSearch] = useState('');
const [selectedMessage, setSelectedMessage] = useState<SlackMessage | null>(null);
const { data, isLoading } = useQuery({
queryKey: ['slack-messages', companyId, channelFilter, search],
queryFn: () =>
slackApi.getMessages(companyId, {
channel_id: channelFilter || undefined,
search: search || undefined,
limit: 100,
}),
staleTime: 30_000,
});
const messages = data?.data?.items || [];
const channelMap = useMemo(() => {
const map = new Map<string, string>();
channels.forEach((c) => map.set(c.external_id, c.name));
return map;
}, [channels]);
return (
<div>
{/* Filters */}
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-400" />
<input
type="text"
placeholder="Search messages..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-surface-300 bg-white pl-10 pr-3 py-2 text-sm outline-none focus:border-purple-500"
/>
</div>
<select
value={channelFilter}
onChange={(e) => setChannelFilter(e.target.value)}
className="rounded-lg border border-surface-300 bg-white px-3 py-2 text-sm outline-none focus:border-purple-500"
>
<option value="">All Channels</option>
{channels.map((ch) => (
<option key={ch.id} value={ch.external_id}>#{ch.name}</option>
))}
</select>
</div>
{/* Message Feed */}
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="rounded-xl border border-surface-200 bg-white divide-y divide-surface-100">
{messages.length === 0 ? (
<div className="text-center py-12 text-surface-400">No messages found</div>
) : (
messages.map((msg) => {
const channelName = msg.channel_name || channelMap.get(msg.channel_id) || msg.channel_id.slice(0, 9);
const isThread = !!msg.thread_ts;
const reactionCount = Array.isArray(msg.reactions) ? msg.reactions.length : 0;
return (
<div
key={msg.id}
onClick={() => setSelectedMessage(msg)}
className="px-6 py-4 hover:bg-surface-50 cursor-pointer transition-colors"
>
<div className="flex items-start gap-3">
{/* Avatar placeholder */}
<div className="rounded-full bg-purple-100 h-8 w-8 flex items-center justify-center flex-shrink-0 text-xs font-semibold text-purple-700">
{(msg.user_id || msg.bot_name || '?').charAt(0).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-surface-900">
{msg.bot_name || msg.user_id?.slice(0, 8) || 'Unknown'}
</span>
{msg.bot_name && (
<span className="inline-flex items-center rounded-full bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-700">
Bot
</span>
)}
<span className="text-xs text-surface-400">in</span>
<span className="text-xs font-medium text-purple-600">#{channelName}</span>
{isThread && (
<span className="inline-flex items-center gap-0.5 rounded-full bg-surface-100 px-1.5 py-0.5 text-xs text-surface-500">
<MessageCircle className="h-3 w-3" />
thread
</span>
)}
</div>
{/* Message text */}
<p className="text-sm text-surface-700 mt-1 line-clamp-3 whitespace-pre-wrap break-words">
{msg.text || <span className="text-surface-300 italic">No text content</span>}
</p>
{/* Footer */}
<div className="flex items-center gap-4 mt-2">
<span className="text-xs text-surface-400 flex items-center gap-1">
<Clock className="h-3 w-3" />
{slackTsToDate(msg.ts)} {slackTsToTime(msg.ts)}
</span>
{reactionCount > 0 && (
<span className="text-xs text-surface-400 flex items-center gap-1">
<Heart className="h-3 w-3" />
{reactionCount}
</span>
)}
</div>
</div>
</div>
</div>
);
})
)}
</div>
)}
{/* Message Detail Panel */}
{selectedMessage && (
<div className="fixed inset-0 z-50 flex justify-end" role="dialog" aria-modal="true">
<div className="absolute inset-0 bg-black/30" onClick={() => setSelectedMessage(null)} />
<div className="relative z-10 flex h-full w-full max-w-lg flex-col overflow-y-auto bg-white shadow-xl border-l border-surface-200">
<div className="flex items-center justify-between border-b border-surface-200 px-6 py-4">
<h2 className="text-lg font-semibold text-surface-900">Message Details</h2>
<button
onClick={() => setSelectedMessage(null)}
className="rounded-lg p-2 text-surface-400 hover:bg-surface-100 min-h-[44px] min-w-[44px] flex items-center justify-center"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="border-b border-surface-200 px-6 py-4">
<div className="flex items-center gap-3 mb-3">
<div className="rounded-full bg-purple-100 h-10 w-10 flex items-center justify-center text-sm font-semibold text-purple-700">
{(selectedMessage.user_id || selectedMessage.bot_name || '?').charAt(0).toUpperCase()}
</div>
<div>
<p className="font-medium text-surface-900">
{selectedMessage.bot_name || selectedMessage.user_id?.slice(0, 8) || 'Unknown'}
</p>
<p className="text-xs text-surface-400">
#{selectedMessage.channel_name || selectedMessage.channel_id.slice(0, 9)}
</p>
</div>
</div>
<div className="rounded-lg bg-surface-50 p-4 text-sm text-surface-700 whitespace-pre-wrap break-words">
{selectedMessage.text || <span className="text-surface-300 italic">No text content</span>}
</div>
</div>
<div className="grid grid-cols-2 gap-3 border-b border-surface-200 px-6 py-4">
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Timestamp</p>
<p className="text-sm font-medium text-surface-900">
{new Date(parseFloat(selectedMessage.ts) * 1000).toLocaleString()}
</p>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Type</p>
<p className="text-sm font-medium text-surface-900 capitalize">{selectedMessage.type || 'message'}</p>
</div>
{selectedMessage.thread_ts && (
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Thread</p>
<p className="text-sm font-medium text-surface-900">Replied in thread</p>
</div>
)}
{selectedMessage.reactions && selectedMessage.reactions.length > 0 && (
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Reactions</p>
<p className="text-sm font-medium text-surface-900">{selectedMessage.reactions.length} reactions</p>
</div>
)}
</div>
<div className="px-6 py-4">
<p className="text-xs font-medium text-surface-500 uppercase tracking-wide mb-2">External ID</p>
<p className="text-xs font-mono text-surface-400 break-all">{selectedMessage.external_id}</p>
</div>
</div>
</div>
)}
</div>
);
}
// ─── Activity Tab ──────────────────────────────────────────────────────
function ActivityTab({ companyId, messages }: { companyId: string; messages: SlackMessage[] }) {
const queryClient = useQueryClient();
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 4000);
return () => clearTimeout(timer);
}
}, [toast]);
const syncMutation = useMutation({
mutationFn: () => slackApi.sync(companyId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['slack-'] });
setToast({ message: 'Slack synced successfully', type: 'success' });
},
onError: (err: unknown) => {
const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error || 'Slack sync failed';
setToast({ message, type: 'error' });
},
});
// Group messages by date
const dailyCounts = useMemo(() => {
const counts = new Map<string, number>();
messages.forEach((msg) => {
if (msg.ts) {
const date = slackTsToDate(msg.ts);
counts.set(date, (counts.get(date) || 0) + 1);
}
});
return Array.from(counts.entries()).sort((a, b) => {
return new Date(a[0]).getTime() - new Date(b[0]).getTime();
}).slice(-30);
}, [messages]);
// Messages by channel
const channelActivity = useMemo(() => {
const counts = new Map<string, number>();
messages.forEach((msg) => {
const ch = msg.channel_name || msg.channel_id.slice(0, 9);
counts.set(ch, (counts.get(ch) || 0) + 1);
});
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);
}, [messages]);
// Bot vs user ratio
const botCount = messages.filter((m) => !!m.bot_name).length;
const userCount = messages.length - botCount;
// Thread messages
const threadCount = messages.filter((m) => !!m.thread_ts).length;
const maxCount = Math.max(...dailyCounts.map(([, c]) => c), 1);
return (
<div className="space-y-6">
{/* Toast */}
{toast && (
<div className="fixed top-4 right-4 z-[70] flex items-center gap-2 rounded-lg bg-surface-900 px-4 py-3 text-sm text-white shadow-lg">
{toast.type === 'success' ? (
<CheckCircle2 className="h-4 w-4 text-green-400" />
) : (
<AlertCircle className="h-4 w-4 text-red-400" />
)}
{toast.message}
</div>
)}
{/* Sync Button */}
<div className="flex justify-between items-center">
<div>
<h3 className="text-sm font-medium text-surface-500">Activity Overview</h3>
<p className="text-xs text-surface-400 mt-0.5">Based on {messages.length} synced messages</p>
</div>
<button
onClick={() => syncMutation.mutate()}
disabled={syncMutation.isPending}
className="flex items-center gap-2 rounded-lg bg-purple-700 hover:bg-purple-800 text-white px-4 py-2 text-sm font-medium disabled:opacity-50 transition-colors"
>
<RefreshCw className={`h-4 w-4 ${syncMutation.isPending ? 'animate-spin' : ''}`} />
{syncMutation.isPending ? 'Syncing...' : 'Sync Now'}
</button>
</div>
{/* Activity Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="inline-flex rounded-lg p-2 mb-2 text-purple-700 bg-purple-50">
<MessageSquare className="h-4 w-4" />
</div>
<p className="text-xs text-surface-500">Total Messages</p>
<p className="text-lg font-bold text-surface-900 mt-0.5">{formatNumber(messages.length)}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="inline-flex rounded-lg p-2 mb-2 text-blue-600 bg-blue-50">
<Users className="h-4 w-4" />
</div>
<p className="text-xs text-surface-500">User Messages</p>
<p className="text-lg font-bold text-surface-900 mt-0.5">{formatNumber(userCount)}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="inline-flex rounded-lg p-2 mb-2 text-violet-600 bg-violet-50">
<Zap className="h-4 w-4" />
</div>
<p className="text-xs text-surface-500">Bot Messages</p>
<p className="text-lg font-bold text-surface-900 mt-0.5">{formatNumber(botCount)}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="inline-flex rounded-lg p-2 mb-2 text-amber-600 bg-amber-50">
<MessageCircle className="h-4 w-4" />
</div>
<p className="text-xs text-surface-500">Threads</p>
<p className="text-lg font-bold text-surface-900 mt-0.5">{formatNumber(threadCount)}</p>
</div>
</div>
{/* Daily Activity Bars */}
{dailyCounts.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Messages by Day</h3>
<div className="space-y-2">
{dailyCounts.map(([date, count]) => {
const pct = (count / maxCount) * 100;
return (
<div key={date} className="flex items-center gap-3">
<span className="text-xs text-surface-400 w-16 flex-shrink-0">{date}</span>
<div className="flex-1 bg-surface-100 rounded-full h-6 overflow-hidden">
<div
className="h-full rounded-full flex items-center justify-end pr-2 transition-all"
style={{ width: `${Math.max(pct, 2)}%`, backgroundColor: SLACK_PURPLE }}
>
{pct > 15 && (
<span className="text-xs text-white font-medium">{count}</span>
)}
</div>
</div>
{pct <= 15 && <span className="text-xs text-surface-500">{count}</span>}
</div>
);
})}
</div>
</div>
)}
{/* Top Channels */}
{channelActivity.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Messages by Channel</h3>
<div className="space-y-2">
{channelActivity.slice(0, 10).map(([name, count], i) => (
<div key={name} className="flex items-center gap-3 py-2 border-b border-surface-100 last:border-0">
<span className="text-xs font-bold text-surface-300 w-6 text-right">#{i + 1}</span>
<div className="rounded-lg bg-purple-100 p-1.5 flex-shrink-0">
<HashIcon className="h-3 w-3 text-purple-700" />
</div>
<span className="text-sm font-medium text-surface-900 flex-1 truncate">#{name}</span>
<span className="text-sm font-semibold text-purple-700">{count}</span>
</div>
))}
</div>
</div>
)}
</div>
);
}
// ─── Main Page ──────────────────────────────────────────────────────────
export function SlackPage() {
const { user } = useAuth();
const companyId = user?.current_company_id || user?.companies?.[0]?.id || '';
const [activeTab, setActiveTab] = useState<TabKey>('overview');
const tabs: { key: TabKey; label: string; icon: React.ComponentType<any> }[] = [
{ key: 'overview', label: 'Overview', icon: Activity },
{ key: 'channels', label: 'Channels', icon: HashIcon },
{ key: 'messages', label: 'Messages', icon: MessageSquare },
{ key: 'activity', label: 'Activity', icon: TrendingIcon },
];
// Preload channels for overview + messages tab
const { data: channelsData } = useQuery({
queryKey: ['slack-channels', companyId],
queryFn: () => slackApi.getChannels(companyId),
staleTime: 120_000,
});
const channels = channelsData?.data?.items || [];
// Preload messages for activity tab
const { data: messagesData } = useQuery({
queryKey: ['slack-messages', companyId],
queryFn: () => slackApi.getMessages(companyId, { limit: 200 }),
staleTime: 60_000,
});
const messages = messagesData?.data?.items || [];
return (
<AppLayout>
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div>
<div className="flex items-center gap-3">
<div className="rounded-xl p-2.5" style={{ backgroundColor: SLACK_PURPLE }}>
<MessageSquare className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-surface-900">Slack</h1>
<p className="text-sm text-surface-500">Workspace communication & channel monitoring</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 mb-6 bg-surface-100 rounded-xl p-1 w-fit">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors min-h-[44px] ${
activeTab === tab.key
? 'bg-white text-surface-900 shadow-sm'
: 'text-surface-500 hover:text-surface-700'
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="space-y-6">
{activeTab === 'overview' && <OverviewTab companyId={companyId} channels={channels} />}
{activeTab === 'channels' && <ChannelsTab companyId={companyId} />}
{activeTab === 'messages' && <MessagesTab companyId={companyId} channels={channels} />}
{activeTab === 'activity' && <ActivityTab companyId={companyId} messages={messages} />}
</div>
</AppLayout>
);
}
// ─── Misc Icons ─────────────────────────────────────────────────────────
function TrendingIcon(props: any) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<polyline points="22 7 13.5 15.5 8.5 10.5 2 17" />
<polyline points="16 7 22 7 22 13" />
</svg>
);
}
function Heart(props: any) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
</svg>
);
}