feat: add ntfy and telegram notification workflows
This commit is contained in:
458
src/App.js
458
src/App.js
@@ -57,6 +57,16 @@ const buildSelectionRange = (start, end, minDate) => {
|
||||
};
|
||||
};
|
||||
|
||||
const defaultNotificationSettings = {
|
||||
ntfy: { enabled: false, topic: '', serverUrl: '' },
|
||||
telegram: { enabled: false, chatId: '' }
|
||||
};
|
||||
|
||||
const defaultNotificationCapabilities = {
|
||||
ntfy: { enabled: false, serverUrl: '', topicPrefix: '' },
|
||||
telegram: { enabled: false }
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [session, setSession] = useState(null);
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' });
|
||||
@@ -83,6 +93,13 @@ function App() {
|
||||
const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false);
|
||||
const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
|
||||
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
||||
const [notificationSettings, setNotificationSettings] = useState(defaultNotificationSettings);
|
||||
const [notificationCapabilities, setNotificationCapabilities] = useState(defaultNotificationCapabilities);
|
||||
const [notificationDirty, setNotificationDirty] = useState(false);
|
||||
const [notificationLoading, setNotificationLoading] = useState(false);
|
||||
const [notificationSaving, setNotificationSaving] = useState(false);
|
||||
const [notificationMessage, setNotificationMessage] = useState('');
|
||||
const [notificationError, setNotificationError] = useState('');
|
||||
const minSelectableDate = useMemo(() => startOfDay(new Date()), []);
|
||||
|
||||
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
||||
@@ -192,7 +209,20 @@ function App() {
|
||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||
description: slot?.description || ''
|
||||
}))
|
||||
: []
|
||||
: [],
|
||||
notifications: {
|
||||
ntfy: {
|
||||
enabled: !!raw.notifications?.ntfy?.enabled,
|
||||
serverUrl: raw.notifications?.ntfy?.serverUrl || 'https://ntfy.sh',
|
||||
topicPrefix: raw.notifications?.ntfy?.topicPrefix || '',
|
||||
username: raw.notifications?.ntfy?.username || '',
|
||||
password: raw.notifications?.ntfy?.password || ''
|
||||
},
|
||||
telegram: {
|
||||
enabled: !!raw.notifications?.telegram?.enabled,
|
||||
botToken: raw.notifications?.telegram?.botToken || ''
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -206,6 +236,13 @@ function App() {
|
||||
setAdminSettingsLoading(false);
|
||||
setAvailableCollapsed(true);
|
||||
setInitializing(false);
|
||||
setNotificationSettings(defaultNotificationSettings);
|
||||
setNotificationCapabilities(defaultNotificationCapabilities);
|
||||
setNotificationDirty(false);
|
||||
setNotificationError('');
|
||||
setNotificationMessage('');
|
||||
setNotificationLoading(false);
|
||||
setNotificationSaving(false);
|
||||
}, []);
|
||||
|
||||
const handleUnauthorized = useCallback(() => {
|
||||
@@ -290,6 +327,47 @@ function App() {
|
||||
[handleUnauthorized, session?.token]
|
||||
);
|
||||
|
||||
const loadNotificationSettings = useCallback(async () => {
|
||||
if (!session?.token) {
|
||||
return;
|
||||
}
|
||||
setNotificationLoading(true);
|
||||
setNotificationError('');
|
||||
try {
|
||||
const response = await authorizedFetch('/api/notifications/settings');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setNotificationSettings({
|
||||
ntfy: {
|
||||
enabled: Boolean(data?.settings?.ntfy?.enabled),
|
||||
topic: data?.settings?.ntfy?.topic || '',
|
||||
serverUrl: data?.settings?.ntfy?.serverUrl || ''
|
||||
},
|
||||
telegram: {
|
||||
enabled: Boolean(data?.settings?.telegram?.enabled),
|
||||
chatId: data?.settings?.telegram?.chatId || ''
|
||||
}
|
||||
});
|
||||
setNotificationCapabilities({
|
||||
ntfy: {
|
||||
enabled: Boolean(data?.capabilities?.ntfy?.enabled),
|
||||
serverUrl: data?.capabilities?.ntfy?.serverUrl || '',
|
||||
topicPrefix: data?.capabilities?.ntfy?.topicPrefix || ''
|
||||
},
|
||||
telegram: {
|
||||
enabled: Boolean(data?.capabilities?.telegram?.enabled)
|
||||
}
|
||||
});
|
||||
setNotificationDirty(false);
|
||||
} catch (err) {
|
||||
setNotificationError(`Benachrichtigungseinstellungen konnten nicht geladen werden: ${err.message}`);
|
||||
} finally {
|
||||
setNotificationLoading(false);
|
||||
}
|
||||
}, [authorizedFetch, session?.token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.token || !session.isAdmin) {
|
||||
setAdminSettings(null);
|
||||
@@ -445,6 +523,13 @@ function App() {
|
||||
}
|
||||
}, [session?.token, authorizedFetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.token) {
|
||||
return;
|
||||
}
|
||||
loadNotificationSettings();
|
||||
}, [session?.token, loadNotificationSettings]);
|
||||
|
||||
const syncStoresWithProgress = useCallback(
|
||||
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
|
||||
const effectiveToken = tokenOverride || session?.token;
|
||||
@@ -951,6 +1036,81 @@ function App() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleNotificationFieldChange = (channel, field, value) => {
|
||||
setNotificationSettings((prev) => {
|
||||
const nextChannel = {
|
||||
...prev[channel],
|
||||
[field]: value
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
[channel]: nextChannel
|
||||
};
|
||||
});
|
||||
setNotificationDirty(true);
|
||||
};
|
||||
|
||||
const saveNotificationSettings = async () => {
|
||||
if (!session?.token) {
|
||||
return;
|
||||
}
|
||||
setNotificationSaving(true);
|
||||
setNotificationError('');
|
||||
setNotificationMessage('');
|
||||
try {
|
||||
const response = await authorizedFetch('/api/notifications/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ notifications: notificationSettings })
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setNotificationSettings({
|
||||
ntfy: {
|
||||
enabled: Boolean(data?.ntfy?.enabled),
|
||||
topic: data?.ntfy?.topic || notificationSettings.ntfy.topic,
|
||||
serverUrl: data?.ntfy?.serverUrl || notificationSettings.ntfy.serverUrl
|
||||
},
|
||||
telegram: {
|
||||
enabled: Boolean(data?.telegram?.enabled),
|
||||
chatId: data?.telegram?.chatId || notificationSettings.telegram.chatId
|
||||
}
|
||||
});
|
||||
setNotificationDirty(false);
|
||||
setNotificationMessage('Benachrichtigungseinstellungen gespeichert.');
|
||||
setTimeout(() => setNotificationMessage(''), 4000);
|
||||
} catch (err) {
|
||||
setNotificationError(`Speichern der Benachrichtigungen fehlgeschlagen: ${err.message}`);
|
||||
} finally {
|
||||
setNotificationSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotificationTest = async (channel) => {
|
||||
if (!session?.token) {
|
||||
return;
|
||||
}
|
||||
setNotificationError('');
|
||||
setNotificationMessage('');
|
||||
try {
|
||||
const response = await authorizedFetch('/api/notifications/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ channel })
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
await response.json();
|
||||
setNotificationMessage('Testbenachrichtigung gesendet.');
|
||||
setTimeout(() => setNotificationMessage(''), 4000);
|
||||
} catch (err) {
|
||||
setNotificationError(`Testbenachrichtigung fehlgeschlagen: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminSettingChange = (field, value, isNumber = false) => {
|
||||
setAdminSettings((prev) => {
|
||||
if (!prev) {
|
||||
@@ -967,6 +1127,24 @@ function App() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleAdminNotificationChange = (channel, field, value) => {
|
||||
setAdminSettings((prev) => {
|
||||
if (!prev) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
notifications: {
|
||||
...(prev.notifications || {}),
|
||||
[channel]: {
|
||||
...(prev.notifications?.[channel] || {}),
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleIgnoredSlotChange = (index, field, value) => {
|
||||
setAdminSettings((prev) => {
|
||||
if (!prev) {
|
||||
@@ -1034,7 +1212,20 @@ function App() {
|
||||
ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
|
||||
storeId: slot.storeId || '',
|
||||
description: slot.description || ''
|
||||
}))
|
||||
})),
|
||||
notifications: {
|
||||
ntfy: {
|
||||
enabled: !!adminSettings.notifications?.ntfy?.enabled,
|
||||
serverUrl: adminSettings.notifications?.ntfy?.serverUrl || '',
|
||||
topicPrefix: adminSettings.notifications?.ntfy?.topicPrefix || '',
|
||||
username: adminSettings.notifications?.ntfy?.username || '',
|
||||
password: adminSettings.notifications?.ntfy?.password || ''
|
||||
},
|
||||
telegram: {
|
||||
enabled: !!adminSettings.notifications?.telegram?.enabled,
|
||||
botToken: adminSettings.notifications?.telegram?.botToken || ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await authorizedFetch('/api/admin/settings', {
|
||||
@@ -1240,6 +1431,165 @@ function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6 border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-800">Benachrichtigungen</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Erhalte Hinweise über ntfy oder Telegram, sobald Slots gefunden oder gebucht wurden.
|
||||
</p>
|
||||
</div>
|
||||
{notificationLoading && <span className="text-sm text-gray-500">Lade Einstellungen…</span>}
|
||||
</div>
|
||||
{notificationError && (
|
||||
<div className="mt-4 bg-red-100 border border-red-300 text-red-700 px-4 py-2 rounded">
|
||||
{notificationError}
|
||||
</div>
|
||||
)}
|
||||
{notificationMessage && (
|
||||
<div className="mt-4 bg-green-100 border border-green-300 text-green-700 px-4 py-2 rounded">
|
||||
{notificationMessage}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800">ntfy</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Push aufs Handy über die ntfy-App oder Browser (Themenkanal notwendig).
|
||||
</p>
|
||||
</div>
|
||||
<label className="inline-flex items-center space-x-2 text-sm text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationSettings.ntfy.enabled}
|
||||
onChange={(e) => handleNotificationFieldChange('ntfy', 'enabled', e.target.checked)}
|
||||
disabled={!notificationCapabilities.ntfy.enabled}
|
||||
className="h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
</div>
|
||||
{!notificationCapabilities.ntfy.enabled ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
Diese Option wurde vom Admin deaktiviert. Bitte frage nach, wenn du ntfy nutzen möchtest.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">Topic / Kanal</label>
|
||||
<input
|
||||
type="text"
|
||||
value={notificationSettings.ntfy.topic}
|
||||
onChange={(e) => handleNotificationFieldChange('ntfy', 'topic', e.target.value)}
|
||||
placeholder="z. B. mein-pickup-topic"
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={!notificationSettings.ntfy.enabled}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">
|
||||
Eigener Server (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={notificationSettings.ntfy.serverUrl}
|
||||
onChange={(e) => handleNotificationFieldChange('ntfy', 'serverUrl', e.target.value)}
|
||||
placeholder={notificationCapabilities.ntfy.serverUrl || 'https://ntfy.sh'}
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={!notificationSettings.ntfy.enabled}
|
||||
/>
|
||||
{notificationCapabilities.ntfy.topicPrefix && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Vom Admin vorgegebenes Präfix: {notificationCapabilities.ntfy.topicPrefix}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sendNotificationTest('ntfy')}
|
||||
disabled={!notificationSettings.ntfy.enabled || notificationSaving}
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Test senden
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800">Telegram</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Nutze den Bot des Admins, um Nachrichten direkt in Telegram zu erhalten.
|
||||
</p>
|
||||
</div>
|
||||
<label className="inline-flex items-center space-x-2 text-sm text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notificationSettings.telegram.enabled}
|
||||
onChange={(e) => handleNotificationFieldChange('telegram', 'enabled', e.target.checked)}
|
||||
disabled={!notificationCapabilities.telegram.enabled}
|
||||
className="h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
</div>
|
||||
{!notificationCapabilities.telegram.enabled ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
Telegram-Benachrichtigungen sind derzeit deaktiviert oder der Bot ist nicht konfiguriert.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">Chat-ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={notificationSettings.telegram.chatId}
|
||||
onChange={(e) => handleNotificationFieldChange('telegram', 'chatId', e.target.value)}
|
||||
placeholder="z. B. 123456789"
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
disabled={!notificationSettings.telegram.enabled}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Tipp: Schreibe dem Telegram-Bot und nutze @userinfobot oder ein Chat-ID-Tool.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sendNotificationTest('telegram')}
|
||||
disabled={!notificationSettings.telegram.enabled || notificationSaving}
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Test senden
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end mt-4 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadNotificationSettings}
|
||||
className="text-sm text-gray-600 hover:text-gray-900"
|
||||
disabled={notificationLoading}
|
||||
>
|
||||
Zurücksetzen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={saveNotificationSettings}
|
||||
disabled={!notificationDirty || notificationSaving}
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors disabled:opacity-60"
|
||||
>
|
||||
{notificationSaving ? 'Speichere...' : 'Benachrichtigungen speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-4 space-x-4">
|
||||
<a
|
||||
href="https://foodsharing.de/?page=dashboard"
|
||||
@@ -1586,6 +1936,110 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h2 className="text-lg font-semibold text-purple-900 mb-2">Benachrichtigungen</h2>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Lege fest, welche Kanäle zur Verfügung stehen und welche Zugangsdaten verwendet werden.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white border border-purple-100 rounded-lg p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-purple-900">ntfy</h3>
|
||||
<p className="text-xs text-gray-500">Server-basierte Push-Benachrichtigungen</p>
|
||||
</div>
|
||||
<label className="inline-flex items-center space-x-2 text-sm text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={adminSettings.notifications?.ntfy?.enabled || false}
|
||||
onChange={(e) => handleAdminNotificationChange('ntfy', 'enabled', e.target.checked)}
|
||||
className="h-4 w-4 text-purple-600 rounded focus:ring-purple-500"
|
||||
/>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">Server-URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={adminSettings.notifications?.ntfy?.serverUrl || ''}
|
||||
onChange={(e) => handleAdminNotificationChange('ntfy', 'serverUrl', e.target.value)}
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="https://ntfy.sh"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">Topic-Präfix</label>
|
||||
<input
|
||||
type="text"
|
||||
value={adminSettings.notifications?.ntfy?.topicPrefix || ''}
|
||||
onChange={(e) => handleAdminNotificationChange('ntfy', 'topicPrefix', e.target.value)}
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="optional"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Wird vor jedes User-Topic gesetzt (z. B. <code>pickup/</code>).
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">Benutzername</label>
|
||||
<input
|
||||
type="text"
|
||||
value={adminSettings.notifications?.ntfy?.username || ''}
|
||||
onChange={(e) => handleAdminNotificationChange('ntfy', 'username', e.target.value)}
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="optional"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
value={adminSettings.notifications?.ntfy?.password || ''}
|
||||
onChange={(e) => handleAdminNotificationChange('ntfy', 'password', e.target.value)}
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="optional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-purple-100 rounded-lg p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-purple-900">Telegram</h3>
|
||||
<p className="text-xs text-gray-500">Benachrichtigungen über einen Bot</p>
|
||||
</div>
|
||||
<label className="inline-flex items-center space-x-2 text-sm text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={adminSettings.notifications?.telegram?.enabled || false}
|
||||
onChange={(e) => handleAdminNotificationChange('telegram', 'enabled', e.target.checked)}
|
||||
className="h-4 w-4 text-purple-600 rounded focus:ring-purple-500"
|
||||
/>
|
||||
<span>Aktiv</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-600 mb-1">Bot Token</label>
|
||||
<input
|
||||
type="text"
|
||||
value={adminSettings.notifications?.telegram?.botToken || ''}
|
||||
onChange={(e) => handleAdminNotificationChange('telegram', 'botToken', e.target.value)}
|
||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="123456:ABCDEF"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Erstelle einen Bot via @BotFather und trage den Token hier ein.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-purple-200 rounded-lg bg-white p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold text-purple-900">Ignorierte Slots</h3>
|
||||
|
||||
Reference in New Issue
Block a user