diff --git a/api/save_settings.php b/api/save_settings.php
new file mode 100644
index 0000000..2e4c890
--- /dev/null
+++ b/api/save_settings.php
@@ -0,0 +1,27 @@
+ false, 'error' => 'Invalid method']);
+ exit;
+}
+
+$data = [
+ 'discord_token' => $_POST['discord_token'] ?? '',
+ 'voice_channel_id' => $_POST['voice_channel_id'] ?? '',
+ 'sahur_time' => $_POST['sahur_time'] ?? '03:30',
+];
+
+try {
+ $db = db();
+ foreach ($data as $key => $value) {
+ $stmt = $db->prepare('INSERT INTO bot_settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?');
+ $stmt->execute([$key, $value, $value]);
+ }
+ echo json_encode(['success' => true]);
+} catch (PDOException $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+}
diff --git a/assets/audio/sahur.mp3 b/assets/audio/sahur.mp3
new file mode 100644
index 0000000..e69de29
diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..c4a9ada
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,126 @@
+:root {
+ --primary: #10b981;
+ --primary-hover: #059669;
+ --bg: #f8fafc;
+ --surface: #ffffff;
+ --text: #1e293b;
+ --text-muted: #64748b;
+ --border: #e2e8f0;
+ --radius: 4px;
+}
+
+body {
+ background-color: var(--bg);
+ color: var(--text);
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ line-height: 1.5;
+ margin: 0;
+}
+
+.container {
+ max-width: 800px;
+ margin: 40px auto;
+ padding: 0 20px;
+}
+
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 24px;
+ margin-bottom: 24px;
+}
+
+.card-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0 0 16px 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.form-group {
+ margin-bottom: 16px;
+}
+
+.form-label {
+ display: block;
+ font-size: 0.875rem;
+ font-weight: 500;
+ margin-bottom: 4px;
+ color: var(--text-muted);
+}
+
+.form-control {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 0.875rem;
+ transition: border-color 0.2s;
+ box-sizing: border-box;
+}
+
+.form-control:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ border-radius: var(--radius);
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+ border: 1px solid transparent;
+}
+
+.btn-primary {
+ background: var(--primary);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: var(--primary-hover);
+}
+
+.badge {
+ display: inline-flex;
+ padding: 2px 8px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border-radius: 9999px;
+ text-transform: uppercase;
+}
+
+.badge-disconnected {
+ background: #fee2e2;
+ color: #991b1b;
+}
+
+.badge-connected {
+ background: #dcfce7;
+ color: #166534;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 32px;
+}
+
+.header h1 {
+ font-size: 1.5rem;
+ margin: 0;
+}
+
+.audio-player {
+ width: 100%;
+ margin-top: 8px;
+}
diff --git a/assets/js/main.js b/assets/js/main.js
new file mode 100644
index 0000000..832d6bd
--- /dev/null
+++ b/assets/js/main.js
@@ -0,0 +1,54 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const configForm = document.getElementById('config-form');
+ const testAudioBtn = document.getElementById('test-audio-btn');
+ const audioPlayer = document.querySelector('.audio-player');
+
+ if (configForm) {
+ configForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const formData = new FormData(configForm);
+ const submitBtn = configForm.querySelector('button[type="submit"]');
+ const originalBtnText = submitBtn.textContent;
+
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Saving...';
+
+ try {
+ const response = await fetch('api/save_settings.php', {
+ method: 'POST',
+ body: formData
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ alert('Settings saved successfully!');
+ } else {
+ alert('Error: ' + result.error);
+ }
+ } catch (error) {
+ alert('Network error while saving settings.');
+ } finally {
+ submitBtn.disabled = false;
+ submitBtn.textContent = originalBtnText;
+ }
+ });
+ }
+
+ if (testAudioBtn && audioPlayer) {
+ testAudioBtn.addEventListener('click', () => {
+ if (audioPlayer.paused) {
+ audioPlayer.play();
+ testAudioBtn.textContent = 'Pause';
+ } else {
+ audioPlayer.pause();
+ testAudioBtn.textContent = 'Test Play Locally';
+ }
+ });
+
+ audioPlayer.addEventListener('ended', () => {
+ testAudioBtn.textContent = 'Test Play Locally';
+ });
+ }
+});
diff --git a/bot/index.js b/bot/index.js
new file mode 100644
index 0000000..42ad0e6
--- /dev/null
+++ b/bot/index.js
@@ -0,0 +1,50 @@
+const { Client, GatewayIntentBits, Collection } = require('discord.js');
+const { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus } = require('@discordjs/voice');
+const { CronJob } = require('cron');
+const path = require('path');
+
+const client = new Client({
+ intents: [
+ GatewayIntentBits.Guilds,
+ GatewayIntentBits.GuildVoiceStates,
+ GatewayIntentBits.GuildMessages,
+ GatewayIntentBits.MessageContent,
+ GatewayIntentBits.GuildMembers,
+ GatewayIntentBits.GuildPresences
+ ]
+});
+
+// Settings from DB or Env
+const VC_ID = process.env.VC_ID || '1457687430189682781';
+const AUDIO_PATH = path.join(__dirname, '../assets/audio/sahur.mp3');
+
+client.once('ready', () => {
+ console.log('Bot is ready!');
+
+ // Sahur Alarm: 03:30 Asia/Jakarta
+ new CronJob('30 03 * * *', async () => {
+ const guild = client.guilds.cache.first(); // Simplified for single server
+ const channel = guild.channels.cache.get(VC_ID);
+
+ if (channel) {
+ const connection = joinVoiceChannel({
+ channelId: channel.id,
+ guildId: guild.id,
+ adapterCreator: guild.voiceAdapterCreator,
+ });
+
+ const player = createAudioPlayer();
+ const resource = createAudioResource(AUDIO_PATH);
+
+ player.play(resource);
+ connection.subscribe(player);
+
+ player.on(AudioPlayerStatus.Idle, () => {
+ connection.destroy();
+ });
+ }
+ }, null, true, 'Asia/Jakarta');
+});
+
+client.login(process.env.DISCORD_TOKEN);
+
diff --git a/bot/package.json b/bot/package.json
new file mode 100644
index 0000000..9b141be
--- /dev/null
+++ b/bot/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "sahur-bot",
+ "version": "1.0.0",
+ "description": "Discord Sahur Bot",
+ "main": "index.js",
+ "dependencies": {
+ "discord.js": "^14.11.0",
+ "cron": "^2.3.1",
+ "@discordjs/voice": "^0.16.0",
+ "libsodium-wrappers": "^0.7.10",
+ "ffmpeg-static": "^5.1.0"
+ }
+}
diff --git a/db/migrations/001_create_bot_settings.sql b/db/migrations/001_create_bot_settings.sql
new file mode 100644
index 0000000..42c87b3
--- /dev/null
+++ b/db/migrations/001_create_bot_settings.sql
@@ -0,0 +1,5 @@
+CREATE TABLE IF NOT EXISTS bot_settings (
+ setting_key VARCHAR(255) PRIMARY KEY,
+ setting_value TEXT,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
diff --git a/index.php b/index.php
index 7205f3d..c77799d 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,97 @@
query("SELECT setting_key, setting_value FROM bot_settings");
+ while ($row = $stmt->fetch()) {
+ $settings[$row['setting_key']] = $row['setting_value'];
+ }
+} catch (PDOException $e) {
+ // Table might not exist yet or other DB error
+}
+
+$discordToken = $settings['discord_token'] ?? '';
+$voiceChannelId = $settings['voice_channel_id'] ?? '1457687430189682781';
+$sahurTime = $settings['sahur_time'] ?? '03:30';
+
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Discord Bot for Sahur Alarm and Music';
+$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Sahur Bot Command Center
+
-
-
+
+
-
-
-
Analyzing your requirements and generating your website…
-
-
Loading…
+
+
+
+
-
-
+
+
+
Sound Preview
+
This sound will be played in the voice channel at = htmlspecialchars($sahurTime) ?> WIB.
+
+
+
+
+
+
+
+
Bot Code Status
+
The discord.js v14 bot skeleton has been generated in the /bot directory.
+
+ # To start the bot:
+ cd bot
+ npm install
+ DISCORD_TOKEN="your_token" node index.js
+
+
+
+
+
+
+