= {
CHANNEL_IS_REQUIRED_SUB: {
label: 'admin.channelSubscriptions.globalSettings.channelRequired',
desc: 'admin.channelSubscriptions.globalSettings.channelRequiredDesc',
},
CHANNEL_DISABLE_TRIAL_ON_UNSUBSCRIBE: {
label: 'admin.channelSubscriptions.globalSettings.disableTrialOnUnsub',
desc: 'admin.channelSubscriptions.globalSettings.disableTrialOnUnsubDesc',
},
CHANNEL_REQUIRED_FOR_ALL: {
label: 'admin.channelSubscriptions.globalSettings.requiredForAll',
desc: 'admin.channelSubscriptions.globalSettings.requiredForAllDesc',
},
};
function GlobalSettingsSection() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const haptic = useHaptic();
const notify = useNotify();
const { data: settings, isLoading } = useQuery({
queryKey: ['admin-settings', 'CHANNEL'],
queryFn: () => adminSettingsApi.getSettings('CHANNEL'),
});
const updateSettingMutation = useMutation({
mutationFn: ({ key, value }: { key: string; value: unknown }) =>
adminSettingsApi.updateSetting(key, value),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-settings', 'CHANNEL'] });
haptic.impact('light');
},
onError: () => {
haptic.notification('error');
notify.error(t('common.error'));
},
});
const getSettingByKey = (key: string): SettingDefinition | undefined =>
settings?.find((s) => s.key === key);
const isSettingEnabled = (key: string): boolean => {
const setting = getSettingByKey(key);
if (!setting) return false;
return setting.current === true || setting.current === 'true';
};
const handleToggleSetting = (key: string) => {
const current = isSettingEnabled(key);
updateSettingMutation.mutate({ key, value: !current });
};
if (isLoading) {
return (
);
}
return (
{t('admin.channelSubscriptions.globalSettings.title')}
{CHANNEL_SETTING_KEYS.map((key) => {
const setting = getSettingByKey(key);
const i18n = SETTING_I18N_MAP[key];
const enabled = isSettingEnabled(key);
const isUpdating = updateSettingMutation.isPending;
const isReadOnly = setting?.read_only ?? false;
return (
{t(i18n.label)}
{t(i18n.desc)}
handleToggleSetting(key)}
disabled={isUpdating || isReadOnly || !setting}
/>
);
})}
);
}
// Channel card component
function ChannelCard({
channel,
onToggle,
onDelete,
onEdit,
onUpdate,
}: {
channel: RequiredChannel;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
onEdit: (channel: RequiredChannel) => void;
onUpdate: (id: number, data: UpdateChannelRequest) => void;
}) {
const { t } = useTranslation();
const displayName = channel.title || channel.channel_id;
const hasLink = !!channel.channel_link;
return (
{/* Status + sort order */}
{channel.is_active
? t('admin.channelSubscriptions.enabled')
: t('admin.channelSubscriptions.disabled')}
#{channel.id}
{t('admin.channelSubscriptions.sortOrder')}: {channel.sort_order}
{/* Title / channel_id */}
{displayName}
{/* Channel ID (if title exists, show ID separately) */}
{channel.title &&
{channel.channel_id}
}
{/* Link */}
{hasLink && (
)}
{/* Per-channel disable toggles */}
{t('admin.channelSubscriptions.perChannel.disableTrial')}
{t('admin.channelSubscriptions.perChannel.disableTrialDesc')}
onUpdate(channel.id, {
disable_trial_on_leave: !channel.disable_trial_on_leave,
})
}
/>
{t('admin.channelSubscriptions.perChannel.disablePaid')}
{t('admin.channelSubscriptions.perChannel.disablePaidDesc')}
onUpdate(channel.id, {
disable_paid_on_leave: !channel.disable_paid_on_leave,
})
}
/>
{/* Action buttons */}
{channel.is_active ? (
) : (
)}
);
}
// Shared form fields component
function ChannelFormFields({
channelId,
setChannelId,
title,
setTitle,
channelLink,
setChannelLink,
sortOrder,
setSortOrder,
showChannelId,
showSortOrder,
}: {
channelId: string;
setChannelId: (v: string) => void;
title: string;
setTitle: (v: string) => void;
channelLink: string;
setChannelLink: (v: string) => void;
sortOrder: string;
setSortOrder: (v: string) => void;
showChannelId: boolean;
showSortOrder: boolean;
}) {
const { t } = useTranslation();
return (
<>
{showChannelId && (
setChannelId(e.target.value)}
placeholder={t('admin.channelSubscriptions.form.channelIdHint')}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-sm text-dark-100 placeholder-dark-500 outline-none transition-colors focus:border-accent-500"
autoFocus
/>
)}
setTitle(e.target.value)}
placeholder={t('admin.channelSubscriptions.form.title')}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-sm text-dark-100 placeholder-dark-500 outline-none transition-colors focus:border-accent-500"
autoFocus={!showChannelId}
/>
setChannelLink(e.target.value)}
placeholder={t('admin.channelSubscriptions.form.channelLinkHint')}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-sm text-dark-100 placeholder-dark-500 outline-none transition-colors focus:border-accent-500"
/>
{showSortOrder && (
setSortOrder(e.target.value)}
placeholder="0"
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-sm text-dark-100 placeholder-dark-500 outline-none transition-colors focus:border-accent-500"
/>
)}
>
);
}
// Add channel form component
function AddChannelForm({
onSubmit,
onCancel,
isLoading,
}: {
onSubmit: (data: CreateChannelRequest) => void;
onCancel: () => void;
isLoading: boolean;
}) {
const { t } = useTranslation();
const [channelId, setChannelId] = useState('');
const [channelLink, setChannelLink] = useState('');
const [title, setTitle] = useState('');
const handleSubmit = () => {
if (!channelId.trim()) return;
onSubmit({
channel_id: channelId.trim(),
channel_link: channelLink.trim() || undefined,
title: title.trim() || undefined,
});
};
return (
{}}
showChannelId
showSortOrder={false}
/>
);
}
// Edit channel form component
function EditChannelForm({
channel,
onSubmit,
onCancel,
isLoading,
}: {
channel: RequiredChannel;
onSubmit: (id: number, data: UpdateChannelRequest) => void;
onCancel: () => void;
isLoading: boolean;
}) {
const { t } = useTranslation();
const [title, setTitle] = useState(channel.title ?? '');
const [channelLink, setChannelLink] = useState(channel.channel_link ?? '');
const [sortOrder, setSortOrder] = useState(String(channel.sort_order));
const handleSubmit = () => {
const updates: UpdateChannelRequest = {};
const newTitle = title.trim() || undefined;
const newLink = channelLink.trim() || undefined;
const newSort = parseInt(sortOrder, 10);
if (newTitle !== (channel.title ?? undefined))
updates.title = newTitle ?? (null as unknown as string);
if (newLink !== (channel.channel_link ?? undefined))
updates.channel_link = newLink ?? (null as unknown as string);
if (!isNaN(newSort) && newSort !== channel.sort_order) updates.sort_order = newSort;
onSubmit(channel.id, updates);
};
return (
{t('admin.channelSubscriptions.editing')}:{' '}
{channel.channel_id}
{}}
title={title}
setTitle={setTitle}
channelLink={channelLink}
setChannelLink={setChannelLink}
sortOrder={sortOrder}
setSortOrder={setSortOrder}
showChannelId={false}
showSortOrder
/>
);
}
// Main component
export default function AdminChannelSubscriptions() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const haptic = useHaptic();
const notify = useNotify();
const [showAddForm, setShowAddForm] = useState(false);
const [editingChannel, setEditingChannel] = useState(null);
// Fetch channels
const { data, isLoading, refetch } = useQuery({
queryKey: ['admin-channels'],
queryFn: adminChannelsApi.list,
});
const toggleMutation = useMutation({
mutationFn: (id: number) => adminChannelsApi.toggle(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-channels'] });
haptic.impact('light');
},
onError: () => {
haptic.notification('error');
notify.error(t('common.error'));
},
});
const deleteMutation = useMutation({
mutationFn: (id: number) => adminChannelsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-channels'] });
haptic.impact('medium');
},
onError: () => {
haptic.notification('error');
notify.error(t('common.error'));
},
});
const createMutation = useMutation({
mutationFn: (req: CreateChannelRequest) => adminChannelsApi.create(req),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-channels'] });
setShowAddForm(false);
haptic.impact('light');
},
onError: () => {
haptic.notification('error');
notify.error(t('common.error'));
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: UpdateChannelRequest }) =>
adminChannelsApi.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-channels'] });
setEditingChannel(null);
haptic.impact('light');
},
onError: () => {
haptic.notification('error');
notify.error(t('common.error'));
},
});
const handleToggle = (id: number) => {
toggleMutation.mutate(id);
};
const { confirm: confirmDialog } = useNativeDialog();
const handleDelete = async (id: number) => {
if (await confirmDialog(t('admin.channelSubscriptions.deleteConfirm'))) {
deleteMutation.mutate(id);
}
};
const handleCreate = (data: CreateChannelRequest) => {
createMutation.mutate(data);
};
const handleUpdate = (id: number, data: UpdateChannelRequest) => {
updateMutation.mutate({ id, data });
};
const handleEdit = (channel: RequiredChannel) => {
setEditingChannel(channel);
setShowAddForm(false);
};
const channels = data?.items ?? [];
return (
{/* Header */}
{t('admin.channelSubscriptions.title')}
{t('admin.channelSubscriptions.subtitle')}
{!showAddForm && !editingChannel && (
)}
{/* Global channel settings */}
{/* Add form */}
{showAddForm && (
setShowAddForm(false)}
isLoading={createMutation.isPending}
/>
)}
{/* Edit form */}
{editingChannel && (
setEditingChannel(null)}
isLoading={updateMutation.isPending}
/>
)}
{/* Channel list */}
{isLoading ? (
) : channels.length === 0 ? (
{t('admin.channelSubscriptions.empty')}
) : (
{channels.map((channel: RequiredChannel) => (
))}
)}
);
}