310 lines
No EOL
14 KiB
TypeScript
310 lines
No EOL
14 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Hash, Globe, ToggleLeft, ToggleRight, Users, Settings, ExternalLink, Link2, X } from 'lucide-react';
|
|
import { apiClient } from '../lib/api';
|
|
|
|
const Channels: React.FC = () => {
|
|
const queryClient = useQueryClient();
|
|
const [showAccountModal, setShowAccountModal] = useState<{ channelId: number; channelName: string } | null>(null);
|
|
|
|
const { data: channels, isLoading, error } = useQuery({
|
|
queryKey: ['platformChannels'],
|
|
queryFn: () => apiClient.getPlatformChannels(),
|
|
});
|
|
|
|
const { data: accounts } = useQuery({
|
|
queryKey: ['platformAccounts'],
|
|
queryFn: () => apiClient.getPlatformAccounts(),
|
|
enabled: !!showAccountModal,
|
|
});
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: (channelId: number) => apiClient.togglePlatformChannel(channelId),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['platformChannels'] });
|
|
},
|
|
});
|
|
|
|
const attachAccountMutation = useMutation({
|
|
mutationFn: ({ channelId, accountId }: { channelId: number; accountId: number }) =>
|
|
apiClient.attachAccountToChannel(channelId, { platform_account_id: accountId }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['platformChannels'] });
|
|
setShowAccountModal(null);
|
|
},
|
|
});
|
|
|
|
const detachAccountMutation = useMutation({
|
|
mutationFn: ({ channelId, accountId }: { channelId: number; accountId: number }) =>
|
|
apiClient.detachAccountFromChannel(channelId, accountId),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['platformChannels'] });
|
|
},
|
|
});
|
|
|
|
const handleToggle = (channelId: number) => {
|
|
toggleMutation.mutate(channelId);
|
|
};
|
|
|
|
const handleAttachAccount = (channelId: number, accountId: number) => {
|
|
attachAccountMutation.mutate({ channelId, accountId });
|
|
};
|
|
|
|
const handleDetachAccount = (channelId: number, accountId: number) => {
|
|
detachAccountMutation.mutate({ channelId, accountId });
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="animate-pulse">
|
|
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div key={i} className="bg-white p-6 rounded-lg shadow">
|
|
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
|
<div className="h-3 bg-gray-200 rounded w-1/2 mb-4"></div>
|
|
<div className="h-8 bg-gray-200 rounded w-20"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
<div className="flex">
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-red-800">
|
|
Error loading channels
|
|
</h3>
|
|
<div className="mt-2 text-sm text-red-700">
|
|
<p>There was an error loading the platform channels. Please try again.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="sm:flex sm:items-center sm:justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Platform Channels</h1>
|
|
<p className="mt-2 text-sm text-gray-700">
|
|
Manage your publishing channels and their associated accounts.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{!channels || channels.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<Hash className="mx-auto h-12 w-12 text-gray-400" />
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No channels</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Get started by creating a new platform channel.
|
|
</p>
|
|
<div className="mt-6">
|
|
<p className="text-sm text-gray-500">
|
|
Channels are created during onboarding. If you need to create more channels, please go through the onboarding process again.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{channels.map((channel) => (
|
|
<div key={channel.id} className="bg-white overflow-hidden shadow rounded-lg">
|
|
<div className="p-6">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<Hash className="h-8 w-8 text-blue-500" />
|
|
</div>
|
|
<div className="ml-4 flex-1">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-medium text-gray-900 truncate">
|
|
{channel.display_name || channel.name}
|
|
</h3>
|
|
<button
|
|
onClick={() => handleToggle(channel.id)}
|
|
disabled={toggleMutation.isPending}
|
|
className="ml-2"
|
|
title={channel.is_active ? 'Deactivate channel' : 'Activate channel'}
|
|
>
|
|
{channel.is_active ? (
|
|
<ToggleRight className="h-6 w-6 text-green-500 hover:text-green-600" />
|
|
) : (
|
|
<ToggleLeft className="h-6 w-6 text-gray-400 hover:text-gray-500" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
{channel.display_name && channel.display_name !== channel.name && (
|
|
<p className="text-sm text-gray-500">@{channel.name}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<div className="flex items-center text-sm text-gray-500">
|
|
<Globe className="flex-shrink-0 mr-1.5 h-4 w-4" />
|
|
Channel ID: {channel.channel_id}
|
|
</div>
|
|
|
|
{channel.description && (
|
|
<p className="mt-2 text-sm text-gray-600">{channel.description}</p>
|
|
)}
|
|
|
|
<div className="mt-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center text-sm text-gray-500">
|
|
<Users className="flex-shrink-0 mr-1.5 h-4 w-4" />
|
|
<span>
|
|
{channel.platform_accounts?.length || 0} account{(channel.platform_accounts?.length || 0) !== 1 ? 's' : ''} linked
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={() => setShowAccountModal({ channelId: channel.id, channelName: channel.display_name || channel.name })}
|
|
className="text-blue-500 hover:text-blue-600"
|
|
title="Manage accounts"
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
className="text-gray-400 hover:text-gray-500"
|
|
title="View channel"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{channel.platform_accounts && channel.platform_accounts.length > 0 && (
|
|
<div className="space-y-1">
|
|
{channel.platform_accounts.map((account) => (
|
|
<div key={account.id} className="flex items-center justify-between text-xs bg-gray-50 rounded px-2 py-1">
|
|
<span className="text-gray-700">@{account.username}</span>
|
|
<button
|
|
onClick={() => handleDetachAccount(channel.id, account.id)}
|
|
disabled={detachAccountMutation.isPending}
|
|
className="text-red-400 hover:text-red-600"
|
|
title="Remove account"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 pt-4 border-t border-gray-200">
|
|
<div className="flex items-center justify-between">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
channel.is_active
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{channel.is_active ? 'Active' : 'Inactive'}
|
|
</span>
|
|
<div className="text-xs text-gray-500">
|
|
Created {new Date(channel.created_at).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Account Management Modal */}
|
|
{showAccountModal && (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={() => setShowAccountModal(null)}></div>
|
|
|
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
|
|
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<div className="sm:flex sm:items-start">
|
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
<Link2 className="h-6 w-6 text-blue-600" />
|
|
</div>
|
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
Manage Accounts for {showAccountModal.channelName}
|
|
</h3>
|
|
<div className="mt-4">
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
Select a platform account to link to this channel:
|
|
</p>
|
|
|
|
{accounts && accounts.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{accounts
|
|
.filter(account => !channels?.find(c => c.id === showAccountModal.channelId)?.platform_accounts?.some(pa => pa.id === account.id))
|
|
.map((account) => (
|
|
<button
|
|
key={account.id}
|
|
onClick={() => handleAttachAccount(showAccountModal.channelId, account.id)}
|
|
disabled={attachAccountMutation.isPending}
|
|
className="w-full text-left px-3 py-2 border border-gray-200 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">@{account.username}</p>
|
|
{account.display_name && (
|
|
<p className="text-xs text-gray-500">{account.display_name}</p>
|
|
)}
|
|
</div>
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
account.is_active
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{account.is_active ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
|
|
{accounts.filter(account => !channels?.find(c => c.id === showAccountModal.channelId)?.platform_accounts?.some(pa => pa.id === account.id)).length === 0 && (
|
|
<p className="text-sm text-gray-500 text-center py-4">
|
|
All available accounts are already linked to this channel.
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-gray-500 text-center py-4">
|
|
No platform accounts available. Create a platform account first.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
<button
|
|
onClick={() => setShowAccountModal(null)}
|
|
className="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Channels; |