├── LICENSE ├── README.md ├── api-page ├── 404.html ├── 500.html ├── index.html ├── notifications.json ├── script.js └── styles.css ├── image.png ├── index.js ├── package.json ├── src ├── api │ ├── ai │ │ ├── ai-hydromind.js │ │ └── ai-luminai.js │ ├── random │ │ └── random-bluearchive.js │ └── search │ │ └── search-youtube.js ├── banner.jpg ├── icon.png └── settings.json └── vercel.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Randy Yuan Kurnianto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Falcon API UI 2 | 3 | A modern, clean, and user-friendly interface for browsing and testing Falcon API endpoints. 4 | 5 | ![Falcon API UI Screenshot](image.png) 6 | 7 | ## Features 8 | 9 | - 🌓 **Light/Dark Mode**: Toggle between light and dark themes with automatic preference saving 10 | - 🔍 **Smart Search**: Quickly find endpoints by name or description 11 | - 📱 **Responsive Design**: Works perfectly on desktop, tablet, and mobile devices 12 | - 🔄 **API Status Indicators**: Visual indicators showing the status of each endpoint (ready, error, update) 13 | - 📋 **Copy to Clipboard**: One-click copying of API endpoints and responses 14 | - 📊 **JSON Highlighting**: Beautifully formatted JSON responses with syntax highlighting 15 | - 📝 **Detailed Parameter Forms**: Clearly labeled input fields with tooltips for parameter descriptions 16 | 17 | ## Getting Started 18 | 19 | ### Prerequisites 20 | 21 | - Web server (Apache, Nginx, etc.) 22 | - Modern web browser 23 | 24 | ### Installation 25 | 26 | 1. Clone this repository to your web server: 27 | ```bash 28 | git clone https://github.com/FlowFalcon/falcon-api-ui.git 29 | ``` 30 | 31 | 2. Configure your API endpoints in `settings.json` (see Configuration section below) 32 | 33 | 3. Access the UI through your web server (e.g., `https://your-domain.com/falcon-api-ui/`) 34 | 35 | ## Configuration 36 | 37 | All API endpoints and categories are configured in the `settings.json` file. The structure is as follows: 38 | 39 | ```json 40 | { 41 | "name": "Falcon-Api", 42 | "version": "v1.2", 43 | "description": "Simple and easy to use API.", 44 | "bannerImage": "/src/banner.jpg", 45 | "header": { 46 | "status": "Online!" 47 | }, 48 | "apiSettings": { 49 | "creator": "FlowFalcon", 50 | "apikey": ["falcon-api"] 51 | }, 52 | "categories": [ 53 | { 54 | "name": "Category Name", 55 | "image": "/api/placeholder/800/200", 56 | "items": [ 57 | { 58 | "name": "Endpoint Name", 59 | "desc": "Endpoint description", 60 | "path": "/api/endpoint?param=", 61 | "status": "ready", // Can be "ready", "error", or "update" 62 | "params": { 63 | "param": "Description of the parameter" 64 | } 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | ``` 71 | 72 | ### Adding a New Endpoint 73 | 74 | To add a new endpoint: 75 | 76 | 1. Find the appropriate category in the `categories` array or create a new one 77 | 2. Add a new object to the `items` array with the following properties: 78 | - `name`: Display name of the endpoint 79 | - `desc`: Brief description of what the endpoint does 80 | - `path`: The API path, including any query parameters 81 | - `status`: Status of the endpoint (`"ready"`, `"error"`, or `"update"`) 82 | - `params`: Object containing parameter names as keys and descriptions as values 83 | 84 | Example: 85 | ```json 86 | { 87 | "name": "User Info", 88 | "desc": "Get user information by ID", 89 | "path": "/api/user?id=", 90 | "status": "ready", 91 | "params": { 92 | "id": "User ID number" 93 | } 94 | } 95 | ``` 96 | 97 | ## Customization 98 | 99 | ### Theme Colors 100 | 101 | You can customize the colors by modifying the CSS variables in the `styles.css` file: 102 | 103 | ```css 104 | :root { 105 | --primary-color: #4361ee; 106 | --secondary-color: #3a86ff; 107 | --accent-color: #4cc9f0; 108 | /* Additional color variables... */ 109 | } 110 | ``` 111 | 112 | ### Banner Image 113 | 114 | Change the banner image by updating the `bannerImage` property in `settings.json`: 115 | 116 | ```json 117 | { 118 | "bannerImage": "/path/to/your/banner.jpg" 119 | } 120 | ``` 121 | 122 | ## Browser Support 123 | 124 | - Chrome (latest) 125 | - Firefox (latest) 126 | - Safari (latest) 127 | - Edge (latest) 128 | 129 | ## Contributing 130 | 131 | Contributions are welcome! Please feel free to submit a Pull Request. 132 | 133 | ## License 134 | 135 | This project is licensed under the MIT License - see the LICENSE file for details. 136 | 137 | ## Acknowledgements 138 | 139 | - [Font Awesome](https://fontawesome.com/) for icons 140 | - [Bootstrap](https://getbootstrap.com/) for layout components 141 | - [Inter Font](https://fonts.google.com/specimen/Inter) for typography 142 | 143 | --- 144 | 145 | Created with ❤️ by [FlowFalcon](https://github.com/FlowFalcon) 146 | -------------------------------------------------------------------------------- /api-page/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 7 | 8 | 404 9 | 29 | 30 | 31 |
32 |
33 |

404

34 | 35 |

Page Not Found-!!

36 |
37 |
38 | 39 | -------------------------------------------------------------------------------- /api-page/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 7 | 8 | 404 9 | 29 | 30 | 31 |
32 |
33 |

500

34 | 35 |

There Is Something Wrong-!!

36 |
37 |
38 | 39 | -------------------------------------------------------------------------------- /api-page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Falcon API 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 27 |

Memuat...

28 |
29 |
30 | 31 | 57 | 58 |
59 | 81 | 82 |
83 |
84 |
85 |
86 |

Falcon API

87 |
v1.0
88 |
89 | 90 |

Antarmuka dokumentasi API yang simpel dan mudah disesuaikan.

91 | 96 |
97 | 98 | 106 |
107 | 108 |
109 |

API yang Tersedia

110 |

Jelajahi koleksi API kami yang powerful dan mudah digunakan.

111 | 112 |
113 |
114 |
115 |
116 | 117 | 142 |
143 | 144 | 197 | 198 |
199 | 209 |
210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /api-page/notifications.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "#01", 4 | "date": "2025-06-015", 5 | "title": "Update Tampilan API!", 6 | "message": "Sekarang ada update baru dari UI API, Silahkan cek informasinya di saluran WhatsApp kami" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /api-page/script.js: -------------------------------------------------------------------------------- 1 | // Pastikan DOM sudah dimuat sepenuhnya sebelum menjalankan skrip 2 | document.addEventListener('DOMContentLoaded', async () => { 3 | // Selektor Elemen DOM Utama 4 | const DOM = { 5 | loadingScreen: document.getElementById("loadingScreen"), 6 | body: document.body, 7 | sideNav: document.querySelector('.side-nav'), 8 | mainWrapper: document.querySelector('.main-wrapper'), 9 | navCollapseBtn: document.querySelector('.nav-collapse-btn'), 10 | menuToggle: document.querySelector('.menu-toggle'), 11 | themeToggle: document.getElementById('themeToggle'), 12 | searchInput: document.getElementById('searchInput'), 13 | clearSearchBtn: document.getElementById('clearSearch'), 14 | apiContent: document.getElementById('apiContent'), 15 | notificationToast: document.getElementById('notificationToast'), // Toast untuk notifikasi umum 16 | notificationBell: document.getElementById('notificationBell'), // Tombol lonceng 17 | notificationBadge: document.getElementById('notificationBadge'), // Badge merah 18 | modal: { 19 | instance: null, // Akan diinisialisasi nanti 20 | element: document.getElementById('apiResponseModal'), 21 | label: document.getElementById('apiResponseModalLabel'), 22 | desc: document.getElementById('apiResponseModalDesc'), 23 | content: document.getElementById('apiResponseContent'), 24 | container: document.getElementById('responseContainer'), 25 | endpoint: document.getElementById('apiEndpoint'), 26 | spinner: document.getElementById('apiResponseLoading'), 27 | queryInputContainer: document.getElementById('apiQueryInputContainer'), 28 | submitBtn: document.getElementById('submitQueryBtn'), 29 | copyEndpointBtn: document.getElementById('copyEndpoint'), 30 | copyResponseBtn: document.getElementById('copyResponse') 31 | }, 32 | // Elemen yang diisi dari settings.json 33 | pageTitle: document.getElementById('page'), 34 | wm: document.getElementById('wm'), 35 | appName: document.getElementById('name'), 36 | sideNavName: document.getElementById('sideNavName'), 37 | versionBadge: document.getElementById('version'), 38 | versionHeaderBadge: document.getElementById('versionHeader'), 39 | appDescription: document.getElementById('description'), 40 | dynamicImage: document.getElementById('dynamicImage'), // ID untuk gambar banner di hero section 41 | apiLinksContainer: document.getElementById('apiLinks') 42 | }; 43 | 44 | let settings = {}; // Untuk menyimpan data dari settings.json 45 | let currentApiData = null; // Untuk menyimpan data API yang sedang ditampilkan di modal 46 | let allNotifications = []; // Untuk menyimpan semua notifikasi dari JSON 47 | 48 | // --- Fungsi Utilitas --- 49 | const showToast = (message, type = 'info', title = 'Notifikasi') => { 50 | if (!DOM.notificationToast) return; 51 | const toastBody = DOM.notificationToast.querySelector('.toast-body'); 52 | const toastTitleEl = DOM.notificationToast.querySelector('.toast-title'); 53 | const toastIcon = DOM.notificationToast.querySelector('.toast-icon'); 54 | 55 | toastBody.textContent = message; 56 | toastTitleEl.textContent = title; 57 | 58 | const typeConfig = { 59 | success: { color: 'var(--success-color)', icon: 'fa-check-circle' }, 60 | error: { color: 'var(--error-color)', icon: 'fa-exclamation-circle' }, 61 | info: { color: 'var(--primary-color)', icon: 'fa-info-circle' }, 62 | notification: { color: 'var(--accent-color)', icon: 'fa-bell' } 63 | }; 64 | 65 | const config = typeConfig[type] || typeConfig.info; 66 | 67 | DOM.notificationToast.style.borderLeftColor = config.color; 68 | toastIcon.className = `toast-icon fas ${config.icon} me-2`; 69 | toastIcon.style.color = config.color; 70 | 71 | let bsToast = bootstrap.Toast.getInstance(DOM.notificationToast); 72 | if (!bsToast) { 73 | bsToast = new bootstrap.Toast(DOM.notificationToast); 74 | } 75 | bsToast.show(); 76 | }; 77 | 78 | const copyToClipboard = async (text, btnElement) => { 79 | if (!navigator.clipboard) { 80 | showToast('Browser tidak mendukung penyalinan ke clipboard.', 'error'); 81 | return; 82 | } 83 | try { 84 | await navigator.clipboard.writeText(text); 85 | const originalIcon = btnElement.innerHTML; 86 | btnElement.innerHTML = ''; 87 | btnElement.classList.add('copy-success'); 88 | showToast('Berhasil disalin ke clipboard!', 'success'); 89 | 90 | setTimeout(() => { 91 | btnElement.innerHTML = originalIcon; 92 | btnElement.classList.remove('copy-success'); 93 | }, 1500); 94 | } catch (err) { 95 | showToast('Gagal menyalin teks: ' + err.message, 'error'); 96 | } 97 | }; 98 | 99 | const debounce = (func, delay) => { 100 | let timeout; 101 | return (...args) => { 102 | clearTimeout(timeout); 103 | timeout = setTimeout(() => func.apply(this, args), delay); 104 | }; 105 | }; 106 | 107 | // --- Fungsi Notifikasi --- 108 | const loadNotifications = async () => { 109 | try { 110 | const response = await fetch('/notifications.json'); 111 | if (!response.ok) throw new Error(`Gagal memuat notifikasi: ${response.status}`); 112 | allNotifications = await response.json(); 113 | updateNotificationBadge(); 114 | } catch (error) { 115 | console.error('Error loading notifications:', error); 116 | } 117 | }; 118 | 119 | const getSessionReadNotificationIds = () => { 120 | const ids = sessionStorage.getItem('sessionReadNotificationIds'); 121 | return ids ? JSON.parse(ids) : []; 122 | }; 123 | 124 | const addSessionReadNotificationId = (id) => { 125 | let ids = getSessionReadNotificationIds(); 126 | if (!ids.includes(id)) { 127 | ids.push(id); 128 | sessionStorage.setItem('sessionReadNotificationIds', JSON.stringify(ids)); 129 | } 130 | }; 131 | 132 | const updateNotificationBadge = () => { 133 | if (!DOM.notificationBadge || !allNotifications.length) { 134 | if(DOM.notificationBadge) DOM.notificationBadge.classList.remove('active'); 135 | return; 136 | } 137 | 138 | const today = new Date(); 139 | today.setHours(0, 0, 0, 0); 140 | 141 | const sessionReadIds = getSessionReadNotificationIds(); 142 | 143 | const unreadNotifications = allNotifications.filter(notif => { 144 | const notificationDate = new Date(notif.date); 145 | notificationDate.setHours(0, 0, 0, 0); 146 | return !notif.read && notificationDate <= today && !sessionReadIds.includes(notif.id); 147 | }); 148 | 149 | if (unreadNotifications.length > 0) { 150 | DOM.notificationBadge.classList.add('active'); 151 | DOM.notificationBell.setAttribute('aria-label', `Notifikasi (${unreadNotifications.length} belum dibaca)`); 152 | } else { 153 | DOM.notificationBadge.classList.remove('active'); 154 | DOM.notificationBell.setAttribute('aria-label', 'Tidak ada notifikasi baru'); 155 | } 156 | }; 157 | 158 | const handleNotificationBellClick = () => { 159 | const today = new Date(); 160 | today.setHours(0, 0, 0, 0); 161 | const sessionReadIds = getSessionReadNotificationIds(); 162 | 163 | const notificationsToShow = allNotifications.filter(notif => { 164 | const notificationDate = new Date(notif.date); 165 | notificationDate.setHours(0, 0, 0, 0); 166 | return !notif.read && notificationDate <= today && !sessionReadIds.includes(notif.id); 167 | }); 168 | 169 | if (notificationsToShow.length > 0) { 170 | notificationsToShow.forEach(notif => { 171 | showToast(notif.message, 'notification', `Notifikasi (${new Date(notif.date).toLocaleDateString('id-ID')})`); 172 | addSessionReadNotificationId(notif.id); 173 | }); 174 | } else { 175 | showToast('Tidak ada notifikasi baru saat ini.', 'info'); 176 | } 177 | 178 | updateNotificationBadge(); 179 | }; 180 | 181 | // --- Inisialisasi dan Event Listener Utama --- 182 | const init = async () => { 183 | setupEventListeners(); 184 | initTheme(); 185 | initSideNav(); 186 | initModal(); 187 | await loadNotifications(); 188 | 189 | try { 190 | const response = await fetch('/src/settings.json'); 191 | if (!response.ok) throw new Error(`Gagal memuat pengaturan: ${response.status}`); 192 | settings = await response.json(); 193 | populatePageContent(); 194 | renderApiCategories(); 195 | observeApiItems(); 196 | } catch (error) { 197 | console.error('Error loading settings:', error); 198 | showToast(`Gagal memuat pengaturan: ${error.message}`, 'error'); 199 | displayErrorState("Tidak dapat memuat konfigurasi API."); 200 | } finally { 201 | hideLoadingScreen(); 202 | } 203 | }; 204 | 205 | const setupEventListeners = () => { 206 | if (DOM.navCollapseBtn) DOM.navCollapseBtn.addEventListener('click', toggleSideNavCollapse); 207 | if (DOM.menuToggle) DOM.menuToggle.addEventListener('click', toggleSideNavMobile); 208 | if (DOM.themeToggle) DOM.themeToggle.addEventListener('change', handleThemeToggle); 209 | if (DOM.searchInput) DOM.searchInput.addEventListener('input', debounce(handleSearch, 300)); 210 | if (DOM.clearSearchBtn) DOM.clearSearchBtn.addEventListener('click', clearSearch); 211 | 212 | if (DOM.notificationBell) DOM.notificationBell.addEventListener('click', handleNotificationBellClick); 213 | 214 | if (DOM.apiContent) DOM.apiContent.addEventListener('click', handleApiGetButtonClick); 215 | 216 | if (DOM.modal.copyEndpointBtn) DOM.modal.copyEndpointBtn.addEventListener('click', () => copyToClipboard(DOM.modal.endpoint.textContent, DOM.modal.copyEndpointBtn)); 217 | if (DOM.modal.copyResponseBtn) DOM.modal.copyResponseBtn.addEventListener('click', () => copyToClipboard(DOM.modal.content.textContent, DOM.modal.copyResponseBtn)); 218 | if (DOM.modal.submitBtn) DOM.modal.submitBtn.addEventListener('click', handleSubmitQuery); 219 | 220 | window.addEventListener('scroll', handleScroll); 221 | document.addEventListener('click', closeSideNavOnClickOutside); 222 | }; 223 | 224 | // --- Manajemen Loading Screen --- 225 | const hideLoadingScreen = () => { 226 | if (!DOM.loadingScreen) return; 227 | const loadingDots = DOM.loadingScreen.querySelector(".loading-dots"); 228 | if (loadingDots && loadingDots.intervalId) clearInterval(loadingDots.intervalId); 229 | 230 | DOM.loadingScreen.classList.add('fade-out'); 231 | setTimeout(() => { 232 | DOM.loadingScreen.style.display = "none"; 233 | DOM.body.classList.remove("no-scroll"); 234 | }, 500); 235 | }; 236 | 237 | const animateLoadingDots = () => { 238 | const loadingDots = DOM.loadingScreen.querySelector(".loading-dots"); 239 | if (loadingDots) { 240 | loadingDots.intervalId = setInterval(() => { 241 | if (loadingDots.textContent.length >= 3) { 242 | loadingDots.textContent = '.'; 243 | } else { 244 | loadingDots.textContent += '.'; 245 | } 246 | }, 500); 247 | } 248 | }; 249 | animateLoadingDots(); 250 | 251 | // --- Manajemen Tema --- 252 | const initTheme = () => { 253 | const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 254 | const savedTheme = localStorage.getItem('darkMode'); 255 | if (savedTheme === 'true' || (savedTheme === null && prefersDark)) { 256 | DOM.body.classList.add('dark-mode'); 257 | if (DOM.themeToggle) DOM.themeToggle.checked = true; 258 | } 259 | }; 260 | 261 | const handleThemeToggle = () => { 262 | DOM.body.classList.toggle('dark-mode'); 263 | const isDarkMode = DOM.body.classList.contains('dark-mode'); 264 | localStorage.setItem('darkMode', isDarkMode); 265 | showToast(`Beralih ke mode ${isDarkMode ? 'gelap' : 'terang'}`, 'success'); 266 | }; 267 | 268 | // --- Manajemen Navigasi Samping --- 269 | const initSideNav = () => { 270 | if (DOM.sideNav && DOM.navCollapseBtn) { 271 | const isCollapsed = DOM.sideNav.classList.contains('collapsed'); 272 | DOM.navCollapseBtn.setAttribute('aria-expanded', !isCollapsed); 273 | } 274 | }; 275 | 276 | const toggleSideNavCollapse = () => { 277 | if (!DOM.sideNav || !DOM.mainWrapper || !DOM.navCollapseBtn) return; 278 | DOM.sideNav.classList.toggle('collapsed'); 279 | DOM.mainWrapper.classList.toggle('nav-collapsed'); 280 | const isExpanded = !DOM.sideNav.classList.contains('collapsed'); 281 | DOM.navCollapseBtn.setAttribute('aria-expanded', isExpanded); 282 | }; 283 | 284 | const toggleSideNavMobile = () => { 285 | if (!DOM.sideNav || !DOM.menuToggle) return; 286 | DOM.sideNav.classList.toggle('active'); 287 | const isActive = DOM.sideNav.classList.contains('active'); 288 | DOM.menuToggle.setAttribute('aria-expanded', isActive); 289 | }; 290 | 291 | const closeSideNavOnClickOutside = (e) => { 292 | if (!DOM.sideNav || !DOM.menuToggle) return; 293 | if (window.innerWidth < 992 && 294 | !DOM.sideNav.contains(e.target) && 295 | !DOM.menuToggle.contains(e.target) && 296 | DOM.sideNav.classList.contains('active')) { 297 | DOM.sideNav.classList.remove('active'); 298 | DOM.menuToggle.setAttribute('aria-expanded', 'false'); 299 | } 300 | }; 301 | 302 | const handleScroll = () => { 303 | const scrollPosition = window.scrollY; 304 | const headerElement = document.querySelector('.main-header'); 305 | const headerHeight = headerElement ? parseInt(getComputedStyle(headerElement).height) : 70; 306 | 307 | document.querySelectorAll('section[id]').forEach(section => { 308 | const sectionTop = section.offsetTop - headerHeight - 20; 309 | const sectionHeight = section.offsetHeight; 310 | const sectionId = section.getAttribute('id'); 311 | 312 | const navLink = document.querySelector(`.side-nav-link[href="#${sectionId}"]`); 313 | if (navLink) { 314 | if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) { 315 | document.querySelectorAll('.side-nav-link.active').forEach(l => { 316 | l.classList.remove('active'); 317 | l.removeAttribute('aria-current'); 318 | }); 319 | navLink.classList.add('active'); 320 | navLink.setAttribute('aria-current', 'page'); 321 | } 322 | } 323 | }); 324 | }; 325 | 326 | // --- Inisialisasi Modal --- 327 | const initModal = () => { 328 | if (DOM.modal.element) { 329 | DOM.modal.instance = new bootstrap.Modal(DOM.modal.element); 330 | } 331 | }; 332 | 333 | // --- Pengisian Konten Halaman --- 334 | const setPageContent = (element, value, fallback = '') => { 335 | if (element) element.textContent = value || fallback; 336 | }; 337 | 338 | const setPageAttribute = (element, attribute, value, fallback = '') => { 339 | if (element) element.setAttribute(attribute, value || fallback); 340 | }; 341 | 342 | const populatePageContent = () => { 343 | if (!settings || Object.keys(settings).length === 0) return; 344 | 345 | const currentYear = new Date().getFullYear(); 346 | const creator = settings.apiSettings?.creator || 'FlowFalcon'; 347 | 348 | setPageContent(DOM.pageTitle, settings.name, "Falcon API"); 349 | setPageContent(DOM.wm, `© ${currentYear} ${creator}. Semua hak dilindungi.`); 350 | setPageContent(DOM.appName, settings.name, "Falcon API"); 351 | setPageContent(DOM.sideNavName, settings.name || "API"); 352 | setPageContent(DOM.versionBadge, settings.version, "v1.0"); 353 | setPageContent(DOM.versionHeaderBadge, settings.header?.status, "Aktif!"); 354 | setPageContent(DOM.appDescription, settings.description, "Dokumentasi API simpel dan mudah digunakan."); 355 | 356 | // Mengatur gambar banner 357 | if (DOM.dynamicImage) { 358 | if (settings.bannerImage) { 359 | DOM.dynamicImage.src = settings.bannerImage; 360 | DOM.dynamicImage.alt = settings.name ? `${settings.name} Banner` : "API Banner"; 361 | DOM.dynamicImage.style.display = ''; // Pastikan gambar ditampilkan jika ada path 362 | } else { 363 | // Jika tidak ada bannerImage di settings, gunakan fallback default dan tampilkan 364 | DOM.dynamicImage.src = '/src/banner.jpg'; 365 | DOM.dynamicImage.alt = "API Banner Default"; 366 | DOM.dynamicImage.style.display = ''; 367 | } 368 | DOM.dynamicImage.onerror = () => { 369 | DOM.dynamicImage.src = '/src/banner.jpg'; // Fallback jika error loading 370 | DOM.dynamicImage.alt = "API Banner Fallback"; 371 | DOM.dynamicImage.style.display = ''; // Pastikan tetap tampil 372 | showToast('Gagal memuat gambar banner, menggunakan gambar default.', 'warning'); 373 | }; 374 | } 375 | 376 | if (DOM.apiLinksContainer) { 377 | DOM.apiLinksContainer.innerHTML = ''; 378 | const defaultLinks = [{ url: "https://github.com/FlowFalcon/Falcon-Api-UI", name: "Lihat di GitHub", icon: "fab fa-github" }]; 379 | const linksToRender = settings.links?.length ? settings.links : defaultLinks; 380 | 381 | linksToRender.forEach(({ url, name, icon }, index) => { 382 | const link = document.createElement('a'); 383 | link.href = url; 384 | link.target = '_blank'; 385 | link.rel = 'noopener noreferrer'; 386 | link.className = 'api-link btn btn-primary'; 387 | link.style.animationDelay = `${index * 0.1}s`; 388 | link.setAttribute('aria-label', name); 389 | 390 | const iconElement = document.createElement('i'); 391 | iconElement.className = icon || 'fas fa-external-link-alt'; 392 | iconElement.setAttribute('aria-hidden', 'true'); 393 | 394 | link.appendChild(iconElement); 395 | link.appendChild(document.createTextNode(` ${name}`)); 396 | DOM.apiLinksContainer.appendChild(link); 397 | }); 398 | } 399 | }; 400 | 401 | // --- Render Kategori dan Item API --- 402 | const renderApiCategories = () => { 403 | if (!DOM.apiContent || !settings.categories || !settings.categories.length) { 404 | displayErrorState("Tidak ada kategori API yang ditemukan."); 405 | return; 406 | } 407 | DOM.apiContent.innerHTML = ''; 408 | 409 | settings.categories.forEach((category, categoryIndex) => { 410 | const sortedItems = category.items.sort((a, b) => a.name.localeCompare(b.name)); 411 | 412 | const categorySection = document.createElement('section'); 413 | categorySection.id = `category-${category.name.toLowerCase().replace(/\s+/g, '-')}`; 414 | categorySection.className = 'category-section'; 415 | categorySection.style.animationDelay = `${categoryIndex * 0.15}s`; 416 | categorySection.setAttribute('aria-labelledby', `category-title-${categoryIndex}`); 417 | 418 | const categoryHeader = document.createElement('h3'); 419 | categoryHeader.id = `category-title-${categoryIndex}`; 420 | categoryHeader.className = 'category-header'; 421 | 422 | if (category.icon) { 423 | const iconEl = document.createElement('i'); 424 | iconEl.className = `${category.icon} me-2`; 425 | iconEl.setAttribute('aria-hidden', 'true'); 426 | categoryHeader.appendChild(iconEl); 427 | } 428 | categoryHeader.appendChild(document.createTextNode(category.name)); 429 | categorySection.appendChild(categoryHeader); 430 | 431 | if (category.image) { 432 | const img = document.createElement('img'); 433 | img.src = category.image; 434 | img.alt = `${category.name} banner`; 435 | img.className = 'category-image img-fluid rounded mb-3 shadow-sm'; 436 | img.loading = 'lazy'; 437 | categorySection.appendChild(img); 438 | } 439 | 440 | const itemsRow = document.createElement('div'); 441 | itemsRow.className = 'row'; 442 | 443 | sortedItems.forEach((item, itemIndex) => { 444 | const itemCol = document.createElement('div'); 445 | itemCol.className = 'col-12 col-md-6 col-lg-4 api-item'; 446 | itemCol.dataset.name = item.name; 447 | itemCol.dataset.desc = item.desc; 448 | itemCol.dataset.category = category.name; 449 | itemCol.style.animationDelay = `${itemIndex * 0.05 + 0.2}s`; 450 | 451 | const apiCard = document.createElement('article'); 452 | apiCard.className = 'api-card h-100'; 453 | apiCard.setAttribute('aria-labelledby', `api-title-${categoryIndex}-${itemIndex}`); 454 | 455 | const cardInfo = document.createElement('div'); 456 | cardInfo.className = 'api-card-info'; 457 | 458 | const itemTitle = document.createElement('h5'); 459 | itemTitle.id = `api-title-${categoryIndex}-${itemIndex}`; 460 | itemTitle.className = 'mb-1'; 461 | itemTitle.textContent = item.name; 462 | 463 | const itemDesc = document.createElement('p'); 464 | itemDesc.className = 'text-muted mb-0'; 465 | itemDesc.textContent = item.desc; 466 | 467 | cardInfo.appendChild(itemTitle); 468 | cardInfo.appendChild(itemDesc); 469 | 470 | const actionsDiv = document.createElement('div'); 471 | actionsDiv.className = 'api-actions mt-auto'; 472 | 473 | const getBtn = document.createElement('button'); 474 | getBtn.type = 'button'; 475 | getBtn.className = 'btn get-api-btn btn-sm'; 476 | getBtn.innerHTML = ' GET'; 477 | getBtn.dataset.apiPath = item.path; 478 | getBtn.dataset.apiName = item.name; 479 | getBtn.dataset.apiDesc = item.desc; 480 | if (item.params) getBtn.dataset.apiParams = JSON.stringify(item.params); 481 | if (item.innerDesc) getBtn.dataset.apiInnerDesc = item.innerDesc; 482 | getBtn.setAttribute('aria-label', `Dapatkan detail untuk ${item.name}`); 483 | 484 | const status = item.status || "ready"; 485 | const statusConfig = { 486 | ready: { class: "status-ready", icon: "fa-circle", text: "Ready" }, 487 | error: { class: "status-error", icon: "fa-exclamation-triangle", text: "Error" }, 488 | update: { class: "status-update", icon: "fa-arrow-up", text: "Update" } 489 | }; 490 | const currentStatus = statusConfig[status] || statusConfig.ready; 491 | 492 | if (status === 'error' || status === 'update') { 493 | getBtn.disabled = true; 494 | apiCard.classList.add('api-card-unavailable'); 495 | getBtn.title = `API ini sedang dalam status '${status}', sementara tidak dapat digunakan.`; 496 | } 497 | 498 | const statusIndicator = document.createElement('div'); 499 | statusIndicator.className = `api-status ${currentStatus.class}`; 500 | statusIndicator.title = `Status: ${currentStatus.text}`; 501 | statusIndicator.innerHTML = `${currentStatus.text}`; 502 | 503 | actionsDiv.appendChild(getBtn); 504 | actionsDiv.appendChild(statusIndicator); 505 | 506 | apiCard.appendChild(cardInfo); 507 | apiCard.appendChild(actionsDiv); 508 | itemCol.appendChild(apiCard); 509 | itemsRow.appendChild(itemCol); 510 | }); 511 | 512 | categorySection.appendChild(itemsRow); 513 | DOM.apiContent.appendChild(categorySection); 514 | }); 515 | initializeTooltips(); 516 | }; 517 | 518 | const displayErrorState = (message) => { 519 | if (!DOM.apiContent) return; 520 | DOM.apiContent.innerHTML = ` 521 |
522 | 523 |

${message}

524 |

Silakan coba muat ulang halaman atau hubungi administrator.

525 | 528 |
529 | `; 530 | }; 531 | 532 | // --- Fungsi Pencarian --- 533 | const handleSearch = () => { 534 | if (!DOM.searchInput || !DOM.apiContent) return; 535 | const searchTerm = DOM.searchInput.value.toLowerCase().trim(); 536 | DOM.clearSearchBtn.classList.toggle('visible', searchTerm.length > 0); 537 | 538 | const apiItems = DOM.apiContent.querySelectorAll('.api-item'); 539 | let visibleCategories = new Set(); 540 | 541 | apiItems.forEach(item => { 542 | const name = (item.dataset.name || '').toLowerCase(); 543 | const desc = (item.dataset.desc || '').toLowerCase(); 544 | const category = (item.dataset.category || '').toLowerCase(); 545 | const matches = name.includes(searchTerm) || desc.includes(searchTerm) || category.includes(searchTerm); 546 | 547 | item.style.display = matches ? '' : 'none'; 548 | if (matches) { 549 | visibleCategories.add(item.closest('.category-section')); 550 | } 551 | }); 552 | 553 | DOM.apiContent.querySelectorAll('.category-section').forEach(section => { 554 | section.style.display = visibleCategories.has(section) ? '' : 'none'; 555 | }); 556 | 557 | const noResultsMsg = DOM.apiContent.querySelector('#noResultsMessage') || createNoResultsMessage(); 558 | const allHidden = Array.from(visibleCategories).length === 0 && searchTerm.length > 0; 559 | 560 | if (allHidden) { 561 | noResultsMsg.querySelector('span').textContent = `"${searchTerm}"`; 562 | noResultsMsg.style.display = 'flex'; 563 | } else { 564 | noResultsMsg.style.display = 'none'; 565 | } 566 | }; 567 | 568 | const clearSearch = () => { 569 | if (!DOM.searchInput) return; 570 | DOM.searchInput.value = ''; 571 | DOM.searchInput.focus(); 572 | handleSearch(); 573 | DOM.searchInput.classList.add('shake-animation'); 574 | setTimeout(() => DOM.searchInput.classList.remove('shake-animation'), 400); 575 | }; 576 | 577 | const createNoResultsMessage = () => { 578 | let noResultsMsg = document.getElementById('noResultsMessage'); 579 | if (!noResultsMsg) { 580 | noResultsMsg = document.createElement('div'); 581 | noResultsMsg.id = 'noResultsMessage'; 582 | noResultsMsg.className = 'no-results-message flex-column align-items-center justify-content-center p-5 text-center'; 583 | noResultsMsg.style.display = 'none'; 584 | noResultsMsg.innerHTML = ` 585 | 586 |

Tidak ada hasil untuk

587 | 590 | `; 591 | DOM.apiContent.appendChild(noResultsMsg); 592 | document.getElementById('clearSearchFromMsg').addEventListener('click', clearSearch); 593 | } 594 | return noResultsMsg; 595 | }; 596 | 597 | // --- Penanganan Klik Tombol API --- 598 | const handleApiGetButtonClick = (event) => { 599 | const getApiBtn = event.target.closest('.get-api-btn'); 600 | if (!getApiBtn || getApiBtn.disabled) return; 601 | 602 | getApiBtn.classList.add('pulse-animation'); 603 | setTimeout(() => getApiBtn.classList.remove('pulse-animation'), 300); 604 | 605 | currentApiData = { 606 | path: getApiBtn.dataset.apiPath, 607 | name: getApiBtn.dataset.apiName, 608 | desc: getApiBtn.dataset.apiDesc, 609 | params: getApiBtn.dataset.apiParams ? JSON.parse(getApiBtn.dataset.apiParams) : null, 610 | innerDesc: getApiBtn.dataset.apiInnerDesc 611 | }; 612 | 613 | setupModalForApi(currentApiData); 614 | DOM.modal.instance.show(); 615 | }; 616 | 617 | const setupModalForApi = (apiData) => { 618 | DOM.modal.label.textContent = apiData.name; 619 | DOM.modal.desc.textContent = apiData.desc; 620 | DOM.modal.content.innerHTML = ''; 621 | DOM.modal.endpoint.textContent = `${window.location.origin}${apiData.path.split('?')[0]}`; 622 | 623 | DOM.modal.spinner.classList.add('d-none'); 624 | DOM.modal.content.classList.add('d-none'); 625 | DOM.modal.container.classList.add('d-none'); 626 | DOM.modal.endpoint.classList.remove('d-none'); 627 | 628 | DOM.modal.queryInputContainer.innerHTML = ''; 629 | DOM.modal.submitBtn.classList.add('d-none'); 630 | DOM.modal.submitBtn.disabled = true; 631 | DOM.modal.submitBtn.innerHTML = 'Kirim'; 632 | 633 | const paramsFromPath = new URLSearchParams(apiData.path.split('?')[1]); 634 | const paramKeys = Array.from(paramsFromPath.keys()); 635 | 636 | if (paramKeys.length > 0) { 637 | const paramContainer = document.createElement('div'); 638 | paramContainer.className = 'param-container'; 639 | 640 | const formTitle = document.createElement('h6'); 641 | formTitle.className = 'param-form-title'; 642 | formTitle.innerHTML = ' Parameter'; 643 | paramContainer.appendChild(formTitle); 644 | 645 | paramKeys.forEach(paramKey => { 646 | const paramGroup = document.createElement('div'); 647 | paramGroup.className = 'param-group mb-3'; 648 | 649 | const labelContainer = document.createElement('div'); 650 | labelContainer.className = 'param-label-container'; 651 | 652 | const label = document.createElement('label'); 653 | label.className = 'form-label'; 654 | label.textContent = paramKey; 655 | label.htmlFor = `param-${paramKey}`; 656 | 657 | const requiredSpan = document.createElement('span'); 658 | requiredSpan.className = 'required-indicator ms-1'; 659 | requiredSpan.textContent = '*'; 660 | label.appendChild(requiredSpan); 661 | labelContainer.appendChild(label); 662 | 663 | if (apiData.params && apiData.params[paramKey]) { 664 | const tooltipIcon = document.createElement('i'); 665 | tooltipIcon.className = 'fas fa-info-circle param-info ms-1'; 666 | tooltipIcon.setAttribute('data-bs-toggle', 'tooltip'); 667 | tooltipIcon.setAttribute('data-bs-placement', 'top'); 668 | tooltipIcon.title = apiData.params[paramKey]; 669 | labelContainer.appendChild(tooltipIcon); 670 | } 671 | paramGroup.appendChild(labelContainer); 672 | 673 | const inputContainer = document.createElement('div'); 674 | inputContainer.className = 'input-container'; 675 | const inputField = document.createElement('input'); 676 | inputField.type = 'text'; 677 | inputField.className = 'form-control custom-input'; 678 | inputField.id = `param-${paramKey}`; 679 | inputField.placeholder = `Masukkan ${paramKey}...`; 680 | inputField.dataset.param = paramKey; 681 | inputField.required = true; 682 | inputField.autocomplete = "off"; 683 | inputField.addEventListener('input', validateModalInputs); 684 | inputContainer.appendChild(inputField); 685 | paramGroup.appendChild(inputContainer); 686 | paramContainer.appendChild(paramGroup); 687 | }); 688 | 689 | if (apiData.innerDesc) { 690 | const innerDescDiv = document.createElement('div'); 691 | innerDescDiv.className = 'inner-desc mt-3'; 692 | innerDescDiv.innerHTML = ` ${apiData.innerDesc.replace(/\n/g, '
')}`; 693 | paramContainer.appendChild(innerDescDiv); 694 | } 695 | 696 | DOM.modal.queryInputContainer.appendChild(paramContainer); 697 | DOM.modal.submitBtn.classList.remove('d-none'); 698 | initializeTooltips(DOM.modal.queryInputContainer); 699 | } else { 700 | handleApiRequest(`${window.location.origin}${apiData.path}`, apiData.name); 701 | } 702 | }; 703 | 704 | const validateModalInputs = () => { 705 | const inputs = DOM.modal.queryInputContainer.querySelectorAll('input[required]'); 706 | const allFilled = Array.from(inputs).every(input => input.value.trim() !== ''); 707 | DOM.modal.submitBtn.disabled = !allFilled; 708 | DOM.modal.submitBtn.classList.toggle('btn-active', allFilled); 709 | 710 | inputs.forEach(input => { 711 | if (input.value.trim()) input.classList.remove('is-invalid'); 712 | }); 713 | const errorMsg = DOM.modal.queryInputContainer.querySelector('.alert.alert-danger.fade-in'); 714 | if (errorMsg && allFilled) { 715 | errorMsg.classList.replace('fade-in', 'fade-out'); 716 | setTimeout(() => errorMsg.remove(), 300); 717 | } 718 | }; 719 | 720 | const handleSubmitQuery = async () => { 721 | if (!currentApiData) return; 722 | 723 | const inputs = DOM.modal.queryInputContainer.querySelectorAll('input'); 724 | const newParams = new URLSearchParams(); 725 | let isValid = true; 726 | 727 | inputs.forEach(input => { 728 | if (input.required && !input.value.trim()) { 729 | isValid = false; 730 | input.classList.add('is-invalid'); 731 | input.parentElement.classList.add('shake-animation'); 732 | setTimeout(() => input.parentElement.classList.remove('shake-animation'), 500); 733 | } else { 734 | input.classList.remove('is-invalid'); 735 | if (input.value.trim()) newParams.append(input.dataset.param, input.value.trim()); 736 | } 737 | }); 738 | 739 | if (!isValid) { 740 | let errorMsg = DOM.modal.queryInputContainer.querySelector('.alert.alert-danger'); 741 | if (!errorMsg) { 742 | errorMsg = document.createElement('div'); 743 | errorMsg.className = 'alert alert-danger mt-3'; 744 | errorMsg.setAttribute('role', 'alert'); 745 | DOM.modal.queryInputContainer.appendChild(errorMsg); 746 | } 747 | errorMsg.innerHTML = ' Harap isi semua kolom yang wajib diisi.'; 748 | errorMsg.classList.remove('fade-out'); 749 | errorMsg.classList.add('fade-in'); 750 | 751 | DOM.modal.submitBtn.classList.add('shake-animation'); 752 | setTimeout(() => DOM.modal.submitBtn.classList.remove('shake-animation'), 500); 753 | return; 754 | } 755 | 756 | DOM.modal.submitBtn.disabled = true; 757 | DOM.modal.submitBtn.innerHTML = ' Memproses...'; 758 | 759 | const apiUrlWithParams = `${window.location.origin}${currentApiData.path.split('?')[0]}?${newParams.toString()}`; 760 | DOM.modal.endpoint.textContent = apiUrlWithParams; 761 | 762 | if (DOM.modal.queryInputContainer.firstChild) { 763 | DOM.modal.queryInputContainer.firstChild.classList.add('fade-out'); 764 | setTimeout(() => { 765 | if (DOM.modal.queryInputContainer.firstChild) DOM.modal.queryInputContainer.firstChild.style.display = 'none'; 766 | }, 300); 767 | } 768 | 769 | await handleApiRequest(apiUrlWithParams, currentApiData.name); 770 | }; 771 | 772 | const handleApiRequest = async (apiUrl, apiName) => { 773 | DOM.modal.spinner.classList.remove('d-none'); 774 | DOM.modal.container.classList.add('d-none'); 775 | DOM.modal.content.innerHTML = ''; 776 | 777 | try { 778 | const controller = new AbortController(); 779 | const timeoutId = setTimeout(() => controller.abort(), 20000); 780 | 781 | const response = await fetch(apiUrl, { signal: controller.signal }); 782 | clearTimeout(timeoutId); 783 | 784 | if (!response.ok) { 785 | const errorData = await response.json().catch(() => ({ message: response.statusText })); 786 | throw new Error(`HTTP error! Status: ${response.status} - ${errorData.message || response.statusText}`); 787 | } 788 | 789 | const contentType = response.headers.get('Content-Type'); 790 | if (contentType && contentType.includes('image/')) { 791 | const blob = await response.blob(); 792 | const imageUrl = URL.createObjectURL(blob); 793 | const img = document.createElement('img'); 794 | img.src = imageUrl; 795 | img.alt = apiName; 796 | img.className = 'response-image img-fluid rounded shadow-sm fade-in'; 797 | 798 | const downloadBtn = document.createElement('a'); 799 | downloadBtn.href = imageUrl; 800 | downloadBtn.download = `${apiName.toLowerCase().replace(/\s+/g, '-')}.${blob.type.split('/')[1] || 'png'}`; 801 | downloadBtn.className = 'btn btn-primary mt-3 w-100'; 802 | downloadBtn.innerHTML = ' Unduh Gambar'; 803 | 804 | DOM.modal.content.appendChild(img); 805 | DOM.modal.content.appendChild(downloadBtn); 806 | 807 | } else if (contentType && contentType.includes('application/json')) { 808 | const data = await response.json(); 809 | const formattedJson = syntaxHighlightJson(JSON.stringify(data, null, 2)); 810 | DOM.modal.content.innerHTML = formattedJson; 811 | if (JSON.stringify(data, null, 2).split('\n').length > 20) { 812 | addCodeFolding(DOM.modal.content); 813 | } 814 | } else { 815 | const textData = await response.text(); 816 | DOM.modal.content.textContent = textData || "Respons tidak memiliki konten atau format tidak dikenal."; 817 | } 818 | 819 | DOM.modal.container.classList.remove('d-none'); 820 | DOM.modal.content.classList.remove('d-none'); 821 | DOM.modal.container.classList.add('slide-in-bottom'); 822 | showToast(`Berhasil mengambil data untuk ${apiName}`, 'success'); 823 | 824 | } catch (error) { 825 | console.error("API Request Error:", error); 826 | const errorHtml = ` 827 |
828 | 829 |
Terjadi Kesalahan
830 |

${error.message || 'Tidak dapat mengambil data dari server.'}

831 | ${currentApiData && currentApiData.path.split('?')[1] ? 832 | `` : ''} 835 |
`; 836 | DOM.modal.content.innerHTML = errorHtml; 837 | DOM.modal.container.classList.remove('d-none'); 838 | DOM.modal.content.classList.remove('d-none'); 839 | showToast('Gagal mengambil data. Periksa detail di modal.', 'error'); 840 | 841 | const retryBtn = DOM.modal.content.querySelector('.retry-query-btn'); 842 | if (retryBtn) { 843 | retryBtn.onclick = () => { 844 | if (DOM.modal.queryInputContainer.firstChild) { 845 | DOM.modal.queryInputContainer.firstChild.style.display = ''; 846 | DOM.modal.queryInputContainer.firstChild.classList.remove('fade-out'); 847 | } 848 | DOM.modal.submitBtn.disabled = false; 849 | DOM.modal.submitBtn.innerHTML = 'Kirim'; 850 | DOM.modal.container.classList.add('d-none'); 851 | }; 852 | } 853 | 854 | } finally { 855 | DOM.modal.spinner.classList.add('d-none'); 856 | if (DOM.modal.submitBtn) { 857 | const hasParams = currentApiData && currentApiData.path && currentApiData.path.includes('?'); 858 | const hasError = DOM.modal.content.querySelector('.error-container'); 859 | const hasRetryButton = DOM.modal.content.querySelector('.retry-query-btn'); 860 | 861 | if (!hasParams || (hasError && !hasRetryButton)) { 862 | DOM.modal.submitBtn.disabled = !hasParams; 863 | DOM.modal.submitBtn.innerHTML = 'Kirim'; 864 | } 865 | } 866 | } 867 | }; 868 | 869 | // --- Fungsi Pembantu untuk Tampilan Kode --- 870 | const syntaxHighlightJson = (json) => { 871 | json = json.replace(/&/g, '&').replace(//g, '>'); 872 | return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => { 873 | let cls = 'json-number'; 874 | if (/^"/.test(match)) { 875 | cls = /:$/.test(match) ? 'json-key' : 'json-string'; 876 | } else if (/true|false/.test(match)) { 877 | cls = 'json-boolean'; 878 | } else if (/null/.test(match)) { 879 | cls = 'json-null'; 880 | } 881 | return `${match}`; 882 | }); 883 | }; 884 | 885 | const addCodeFolding = (container) => { 886 | const lines = container.innerHTML.split('\n'); 887 | let currentLevel = 0; 888 | let foldableHtml = ''; 889 | let inFoldableBlock = false; 890 | 891 | lines.forEach((line, index) => { 892 | const trimmedLine = line.trim(); 893 | if (trimmedLine.endsWith('{') || trimmedLine.endsWith('[')) { 894 | if (currentLevel === 0) { 895 | foldableHtml += `
${line}( Lipat)
`; 896 | inFoldableBlock = true; 897 | } else { 898 | foldableHtml += line + '\n'; 899 | } 900 | currentLevel++; 901 | } else if (trimmedLine.startsWith('}') || trimmedLine.startsWith(']')) { 902 | currentLevel--; 903 | foldableHtml += line + '\n'; 904 | if (currentLevel === 0 && inFoldableBlock) { 905 | foldableHtml += '
'; 906 | inFoldableBlock = false; 907 | } 908 | } else { 909 | foldableHtml += line + (index === lines.length - 1 ? '' : '\n'); 910 | } 911 | }); 912 | container.innerHTML = foldableHtml; 913 | 914 | container.querySelectorAll('.code-fold-trigger').forEach(trigger => { 915 | trigger.addEventListener('click', () => toggleFold(trigger)); 916 | trigger.addEventListener('keydown', (e) => { 917 | if (e.key === 'Enter' || e.key === ' ') { 918 | e.preventDefault(); 919 | toggleFold(trigger); 920 | } 921 | }); 922 | }); 923 | }; 924 | 925 | const toggleFold = (trigger) => { 926 | const content = trigger.nextElementSibling; 927 | const isFolded = trigger.dataset.folded === 'true'; 928 | const indicator = trigger.querySelector('.fold-indicator'); 929 | 930 | if (isFolded) { 931 | content.style.maxHeight = content.scrollHeight + "px"; 932 | trigger.dataset.folded = "false"; 933 | trigger.setAttribute('aria-expanded', 'true'); 934 | indicator.innerHTML = '( Tutup)'; 935 | } else { 936 | content.style.maxHeight = "0px"; 937 | trigger.dataset.folded = "true"; 938 | trigger.setAttribute('aria-expanded', 'false'); 939 | indicator.innerHTML = '( Buka)'; 940 | } 941 | }; 942 | 943 | // --- Observasi Item API untuk Animasi --- 944 | const observeApiItems = () => { 945 | const observer = new IntersectionObserver((entries) => { 946 | entries.forEach(entry => { 947 | if (entry.isIntersecting) { 948 | entry.target.classList.add('in-view', 'slideInUp'); 949 | observer.unobserve(entry.target); 950 | } 951 | }); 952 | }, { threshold: 0.1 }); 953 | 954 | document.querySelectorAll('.api-item:not(.in-view)').forEach(item => { 955 | observer.observe(item); 956 | }); 957 | }; 958 | 959 | // --- Inisialisasi Tooltip --- 960 | const initializeTooltips = (parentElement = document) => { 961 | const tooltipTriggerList = [].slice.call(parentElement.querySelectorAll('[data-bs-toggle="tooltip"]')); 962 | tooltipTriggerList.map(tooltipTriggerEl => { 963 | const existingTooltip = bootstrap.Tooltip.getInstance(tooltipTriggerEl); 964 | if (existingTooltip) { 965 | existingTooltip.dispose(); 966 | } 967 | return new bootstrap.Tooltip(tooltipTriggerEl); 968 | }); 969 | }; 970 | 971 | // Jalankan inisialisasi utama 972 | init(); 973 | }); -------------------------------------------------------------------------------- /api-page/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Color Palette - Purple Gradient Theme */ 3 | --primary-color: #6c5ce7; 4 | --primary-hover: #5649d1; 5 | --secondary-color: #a29bfe; 6 | --accent-color: #00cec9; 7 | --success-color: #00b894; 8 | --error-color: #ff7675; 9 | --warning-color: #fdcb6e; 10 | 11 | /* Light Mode */ 12 | --background-color: #f8f9fd; 13 | --card-background: #ffffff; 14 | --text-color: #2d3436; 15 | --text-muted: #636e72; 16 | --border-color: rgba(0, 0, 0, 0.08); 17 | --highlight-color: rgba(108, 92, 231, 0.1); 18 | 19 | /* UI Elements */ 20 | --border-radius-sm: 8px; 21 | --border-radius: 12px; 22 | --border-radius-lg: 20px; 23 | --shadow: 0 10px 20px rgba(0, 0, 0, 0.05); 24 | --hover-shadow: 0 15px 30px rgba(0, 0, 0, 0.1); 25 | --card-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 26 | 27 | /* Animation */ 28 | --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); 29 | --hover-transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1); 30 | --hover-transform: translateY(-5px); 31 | 32 | /* Spacing */ 33 | --space-xs: 4px; 34 | --space-sm: 8px; 35 | --space-md: 16px; 36 | --space-lg: 24px; 37 | --space-xl: 32px; 38 | --space-xxl: 48px; 39 | 40 | /* Layout */ 41 | --side-nav-width: 260px; 42 | --side-nav-collapsed-width: 80px; 43 | --header-height: 70px; 44 | 45 | /* Background values for rgba */ 46 | --background-color-rgb: 248, 249, 253; /* Light mode background RGB */ 47 | } 48 | 49 | .dark-mode { 50 | --primary-color: #a29bfe; 51 | --primary-hover: #8983d8; 52 | --secondary-color: #6c5ce7; 53 | --accent-color: #00cec9; 54 | --background-color: #1a1b2e; 55 | --card-background: #252640; 56 | --text-color: #e5e5e5; 57 | --text-muted: #b2becd; 58 | --border-color: rgba(255, 255, 255, 0.08); 59 | --highlight-color: rgba(162, 155, 254, 0.2); 60 | --shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 61 | --hover-shadow: 0 15px 35px rgba(0, 0, 0, 0.3); 62 | --card-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 63 | --background-color-rgb: 26, 27, 46; /* Dark mode background RGB */ 64 | } 65 | 66 | * { 67 | box-sizing: border-box; 68 | margin: 0; 69 | padding: 0; 70 | } 71 | 72 | html { 73 | scroll-behavior: smooth; 74 | font-size: 16px; 75 | scroll-padding-top: calc(var(--header-height) + 20px); 76 | } 77 | 78 | body { 79 | font-family: 'Outfit', sans-serif; 80 | color: var(--text-color); 81 | background-color: var(--background-color); 82 | line-height: 1.6; 83 | transition: var(--transition); 84 | overflow-x: hidden; 85 | } 86 | 87 | body.no-scroll { 88 | overflow: hidden; 89 | } 90 | 91 | /* Typography */ 92 | h1, h2, h3, h4, h5, h6 { 93 | font-weight: 700; 94 | letter-spacing: -0.02em; 95 | margin-bottom: 0.75em; 96 | color: var(--text-color); 97 | } 98 | 99 | p { 100 | margin-bottom: 1.25rem; 101 | } 102 | 103 | a { 104 | color: var(--primary-color); 105 | text-decoration: none; 106 | transition: var(--transition); 107 | } 108 | 109 | a:hover { 110 | color: var(--primary-hover); 111 | text-decoration: underline; 112 | } 113 | 114 | /* Gradient Text */ 115 | .gradient-text { 116 | background: linear-gradient(135deg, var(--primary-color), var(--accent-color)); 117 | -webkit-background-clip: text; 118 | -webkit-text-fill-color: transparent; 119 | background-clip: text; 120 | text-fill-color: transparent; 121 | background-size: 300% 300%; 122 | animation: gradient-shift 8s ease infinite; 123 | } 124 | 125 | @keyframes gradient-shift { 126 | 0% { background-position: 0% 50%; } 127 | 50% { background-position: 100% 50%; } 128 | 100% { background-position: 0% 50%; } 129 | } 130 | 131 | /* Buttons */ 132 | .btn { 133 | font-weight: 500; 134 | border-radius: var(--border-radius-sm); 135 | transition: var(--hover-transition); 136 | padding: 0.6rem 1.35rem; 137 | display: inline-flex; 138 | align-items: center; 139 | justify-content: center; 140 | gap: 0.5rem; 141 | border: none; 142 | cursor: pointer; 143 | line-height: 1.2; 144 | } 145 | 146 | .btn:focus, .btn:focus-visible { 147 | outline: none; 148 | box-shadow: 0 0 0 3px var(--highlight-color); 149 | } 150 | 151 | .btn-primary { 152 | background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); 153 | color: white; 154 | box-shadow: 0 4px 10px rgba(108, 92, 231, 0.2); 155 | } 156 | 157 | .btn-primary:hover { 158 | transform: var(--hover-transform); 159 | box-shadow: 0 6px 15px rgba(108, 92, 231, 0.3); 160 | color: white; 161 | } 162 | 163 | .btn-primary:active { 164 | transform: translateY(-2px); 165 | } 166 | 167 | .btn-primary:disabled { 168 | background: linear-gradient(135deg, #b0b5f3, #c4d9f3); 169 | cursor: not-allowed; 170 | opacity: 0.7; 171 | transform: none; 172 | box-shadow: none; 173 | } 174 | 175 | /* Badge */ 176 | .badge-pill { 177 | display: inline-block; 178 | background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); 179 | color: white; 180 | padding: 0.3rem 0.85rem; 181 | font-size: 0.8rem; 182 | border-radius: 100px; 183 | font-weight: 600; 184 | box-shadow: 0 4px 10px rgba(108, 92, 231, 0.3); 185 | white-space: nowrap; 186 | } 187 | 188 | /* Animations */ 189 | @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } 190 | @keyframes slideInUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } 191 | @keyframes slideInRight { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } 192 | @keyframes slideInLeft { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } 193 | @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } 194 | @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } 195 | @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } 196 | @keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-10px); } 100% { transform: translateY(0px); } } 197 | @keyframes dash { 0% { stroke-dasharray: 1, 200; stroke-dashoffset: 0; } 50% { stroke-dasharray: 89, 200; stroke-dashoffset: -35; } 100% { stroke-dasharray: 89, 200; stroke-dashoffset: -124; } } 198 | @keyframes rotate-spinner { 100% { transform: rotate(360deg); } } 199 | 200 | /* Layout Structure */ 201 | .main-wrapper { 202 | min-height: 100vh; 203 | margin-left: var(--side-nav-width); 204 | transition: margin-left var(--transition); 205 | display: flex; 206 | flex-direction: column; 207 | } 208 | 209 | .main-wrapper.nav-collapsed { 210 | margin-left: var(--side-nav-collapsed-width); 211 | } 212 | 213 | /* Side Navigation */ 214 | .side-nav { 215 | position: fixed; 216 | left: 0; 217 | top: 0; 218 | height: 100vh; 219 | width: var(--side-nav-width); 220 | background-color: var(--card-background); 221 | box-shadow: var(--shadow); 222 | z-index: 1000; 223 | transition: width var(--transition), transform var(--transition); 224 | display: flex; 225 | flex-direction: column; 226 | padding: var(--space-lg) 0; 227 | overflow-x: hidden; 228 | } 229 | 230 | .side-nav.collapsed { 231 | width: var(--side-nav-collapsed-width); 232 | } 233 | 234 | .side-nav-logo { 235 | display: flex; 236 | align-items: center; 237 | padding: 0 var(--space-lg); 238 | margin-bottom: var(--space-xl); 239 | gap: var(--space-sm); 240 | } 241 | 242 | .side-nav-logo #sideNavName { 243 | font-size: 1.5rem; 244 | font-weight: 800; 245 | color: var(--primary-color); 246 | transition: opacity var(--transition), transform var(--transition); 247 | white-space: nowrap; 248 | } 249 | 250 | .side-nav.collapsed .side-nav-logo { 251 | justify-content: center; 252 | padding: 0; 253 | } 254 | 255 | .side-nav.collapsed .side-nav-logo #sideNavName, 256 | .side-nav.collapsed #versionHeader { 257 | opacity: 0; 258 | transform: translateX(-10px); 259 | pointer-events: none; 260 | display: none; 261 | } 262 | 263 | .side-nav-links { 264 | display: flex; 265 | flex-direction: column; 266 | flex-grow: 1; 267 | overflow-y: auto; 268 | scrollbar-width: thin; 269 | scrollbar-color: var(--primary-color) transparent; 270 | } 271 | .side-nav-links::-webkit-scrollbar { width: 5px; } 272 | .side-nav-links::-webkit-scrollbar-thumb { background-color: var(--primary-color); border-radius: 10px; } 273 | 274 | 275 | .side-nav-link { 276 | display: flex; 277 | align-items: center; 278 | padding: var(--space-md) var(--space-lg); 279 | color: var(--text-muted); 280 | transition: var(--transition); 281 | margin: var(--space-xs) 0; 282 | border-left: 4px solid transparent; 283 | gap: var(--space-md); 284 | white-space: nowrap; 285 | } 286 | 287 | .side-nav-link i { 288 | font-size: 1.35rem; 289 | min-width: 28px; 290 | text-align: center; 291 | transition: transform 0.2s ease; 292 | } 293 | 294 | .side-nav-link:hover i, 295 | .side-nav-link.active i { 296 | transform: scale(1.1); 297 | } 298 | 299 | .side-nav-link span { 300 | transition: opacity var(--transition), transform var(--transition); 301 | } 302 | 303 | .side-nav-link:hover, .side-nav-link.active { 304 | color: var(--primary-color); 305 | background-color: var(--highlight-color); 306 | border-left-color: var(--primary-color); 307 | } 308 | 309 | .side-nav.collapsed .side-nav-link { 310 | justify-content: center; 311 | padding: var(--space-md) 0; 312 | } 313 | 314 | .side-nav.collapsed .side-nav-link span { 315 | opacity: 0; 316 | transform: translateX(-10px); 317 | pointer-events: none; 318 | display: none; 319 | } 320 | 321 | .nav-collapse-btn { 322 | position: absolute; 323 | top: calc(var(--header-height) + 20px); 324 | right: -16px; 325 | width: 32px; 326 | height: 32px; 327 | background-color: var(--primary-color); 328 | color: white; 329 | border-radius: 50%; 330 | display: flex; 331 | align-items: center; 332 | justify-content: center; 333 | cursor: pointer; 334 | box-shadow: var(--shadow); 335 | transition: var(--transition); 336 | z-index: 10; 337 | } 338 | 339 | .nav-collapse-btn:hover { 340 | transform: scale(1.1) rotate(180deg); 341 | } 342 | 343 | .side-nav.collapsed .nav-collapse-btn i { 344 | transform: rotate(180deg); 345 | } 346 | 347 | /* Header */ 348 | .main-header { 349 | position: sticky; 350 | top: 0; 351 | height: var(--header-height); 352 | background-color: var(--card-background); 353 | box-shadow: var(--shadow); 354 | display: flex; 355 | align-items: center; 356 | padding: 0 var(--space-lg); 357 | z-index: 100; 358 | } 359 | 360 | .menu-toggle { 361 | background: none; 362 | border: none; 363 | color: var(--text-color); 364 | font-size: 1.5rem; 365 | cursor: pointer; 366 | display: none; 367 | margin-right: var(--space-md); 368 | padding: var(--space-sm); 369 | border-radius: 50%; 370 | transition: background-color var(--transition); 371 | } 372 | .menu-toggle:hover { 373 | background-color: var(--highlight-color); 374 | } 375 | 376 | 377 | .search-container { 378 | max-width: 500px; 379 | width: 100%; 380 | margin: 0 var(--space-lg); 381 | position: relative; 382 | } 383 | 384 | .input-group { 385 | box-shadow: var(--shadow); 386 | border-radius: var(--border-radius); 387 | overflow: hidden; 388 | display: flex; 389 | align-items: center; 390 | background-color: var(--background-color); 391 | transition: var(--hover-transition); 392 | } 393 | 394 | .input-group:focus-within { 395 | box-shadow: var(--hover-shadow); 396 | transform: translateY(-2px); 397 | } 398 | 399 | .input-group-text { 400 | background-color: transparent; 401 | border: none; 402 | color: var(--primary-color); 403 | font-size: 1.2rem; 404 | padding: 0 var(--space-md); 405 | } 406 | 407 | #searchInput { 408 | border: none; 409 | padding: var(--space-md) 0; 410 | font-size: 1rem; 411 | background-color: transparent; 412 | color: var(--text-color); 413 | flex-grow: 1; 414 | min-width: 0; 415 | } 416 | #searchInput:focus { box-shadow: none; outline: none; } 417 | #searchInput::placeholder { color: var(--text-muted); opacity: 0.7; } 418 | 419 | .clear-search { 420 | position: absolute; 421 | right: var(--space-sm); 422 | top: 50%; 423 | transform: translateY(-50%); 424 | background: none; 425 | border: none; 426 | color: var(--text-muted); 427 | cursor: pointer; 428 | opacity: 0; 429 | pointer-events: none; 430 | transition: var(--transition); 431 | z-index: 2; 432 | width: 32px; 433 | height: 32px; 434 | display: flex; 435 | align-items: center; 436 | justify-content: center; 437 | border-radius: 50%; 438 | } 439 | .clear-search.visible { 440 | opacity: 1; 441 | pointer-events: auto; 442 | } 443 | .clear-search:hover { color: var(--primary-color); background-color: var(--highlight-color); } 444 | 445 | .header-actions { 446 | margin-left: auto; 447 | display: flex; 448 | align-items: center; 449 | gap: var(--space-sm); 450 | } 451 | 452 | .notification-bell { 453 | position: relative; 454 | width: 40px; 455 | height: 40px; 456 | display: flex; 457 | align-items: center; 458 | justify-content: center; 459 | color: var(--text-muted); 460 | font-size: 1.2rem; 461 | cursor: pointer; 462 | transition: var(--transition); 463 | border-radius: 50%; 464 | background: none; 465 | border: none; 466 | } 467 | .notification-bell:hover { color: var(--primary-color); background-color: var(--highlight-color); } 468 | 469 | .notification-badge { 470 | position: absolute; 471 | top: 8px; 472 | right: 8px; 473 | width: 10px; 474 | height: 10px; 475 | background-color: var(--error-color); 476 | border-radius: 50%; 477 | border: 1.5px solid var(--card-background); 478 | display: none; 479 | } 480 | .notification-badge.active { 481 | display: block; 482 | } 483 | 484 | /* Hero Section */ 485 | .hero-section { 486 | display: flex; 487 | align-items: center; 488 | padding: var(--space-xxl) var(--space-xl); /* Padding untuk konten dan visual */ 489 | position: relative; 490 | overflow: hidden; 491 | min-height: calc(100vh - var(--header-height)); /* Tinggi minimal untuk mengisi viewport */ 492 | /* Hanya gradient, tanpa background image url('/src/icons.png') */ 493 | background: linear-gradient(135deg, rgba(var(--background-color-rgb),0.85), rgba(var(--background-color-rgb),1) 70%); 494 | /* background-blend-mode tidak diperlukan lagi jika hanya gradient */ 495 | } 496 | 497 | .dark-mode .hero-section { 498 | /* Hanya gradient untuk dark mode */ 499 | background: linear-gradient(135deg, rgba(var(--background-color-rgb),0.9), rgba(var(--background-color-rgb),1) 70%); 500 | } 501 | 502 | .hero-content { 503 | flex: 1; /* Mengambil ruang yang tersedia */ 504 | max-width: 650px; 505 | animation: slideInLeft 0.8s ease-out; 506 | position: relative; 507 | z-index: 5; /* Konten di atas elemen lain jika ada tumpang tindih */ 508 | /* Pengaturan text-align dan margin disesuaikan oleh flex container .hero-section */ 509 | } 510 | 511 | .hero-heading { 512 | display: flex; 513 | align-items: center; 514 | flex-wrap: wrap; 515 | gap: var(--space-md); 516 | margin-bottom: var(--space-lg); 517 | /* justify-content: flex-start; /* Default untuk item flex */ 518 | } 519 | 520 | #name { 521 | font-size: clamp(2.5rem, 5vw, 3.8rem); 522 | font-weight: 800; 523 | margin-bottom: 0; 524 | } 525 | 526 | .hero-description { 527 | font-size: clamp(1rem, 2.5vw, 1.25rem); 528 | color: var(--text-muted); 529 | margin-bottom: var(--space-xl); 530 | line-height: 1.7; 531 | } 532 | 533 | .hero-actions { 534 | display: flex; 535 | flex-wrap: wrap; 536 | gap: var(--space-md); 537 | /* justify-content: flex-start; /* Default untuk item flex */ 538 | } 539 | 540 | .hero-actions a { 541 | display: inline-flex; 542 | align-items: center; 543 | gap: var(--space-sm); 544 | color: white; 545 | text-decoration: none; 546 | font-weight: 500; 547 | position: relative; 548 | padding: var(--space-md) var(--space-lg); 549 | border-radius: var(--border-radius); 550 | background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); 551 | box-shadow: var(--shadow); 552 | transition: var(--hover-transition); 553 | overflow: hidden; 554 | } 555 | .hero-actions a:hover { transform: var(--hover-transform); box-shadow: var(--hover-shadow); } 556 | .hero-actions a::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0)); transform: translateX(-100%); transition: 0.5s; } 557 | .hero-actions a:hover::before { transform: translateX(100%); } 558 | 559 | /* Visual banner di sisi kanan */ 560 | .hero-visual { 561 | flex: 1; /* Mengambil ruang yang tersedia */ 562 | position: relative; 563 | min-height: 300px; /* Beri tinggi minimal agar tidak hilang jika banner kecil */ 564 | max-height: 500px; /* Batasi tinggi maksimal banner */ 565 | animation: slideInRight 0.8s ease-out; 566 | z-index: 2; 567 | display: flex; 568 | align-items: center; 569 | justify-content: center; 570 | padding-left: var(--space-lg); /* Beri sedikit jarak dari konten teks */ 571 | } 572 | 573 | .banner-container { 574 | width: 100%; /* Lebar banner container mengisi .hero-visual */ 575 | max-width: 500px; /* Batasi lebar maksimal banner */ 576 | aspect-ratio: 16/9; 577 | border-radius: var(--border-radius-lg); 578 | overflow: hidden; 579 | box-shadow: var(--shadow); 580 | transition: var(--transition); 581 | transform: rotate(3deg); 582 | position: relative; 583 | z-index: 3; 584 | } 585 | .banner-container:hover { transform: rotate(0deg) translateY(-10px); box-shadow: var(--hover-shadow); } 586 | 587 | .banner { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; } 588 | .banner-container:hover .banner { transform: scale(1.05); } 589 | 590 | .shape { 591 | position: absolute; 592 | border-radius: 50%; 593 | background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); 594 | opacity: 0.1; 595 | animation: float 10s infinite alternate; 596 | display: block; 597 | } 598 | .shape-1 { width: 200px; height: 200px; top: -50px; right: 80px; animation-delay: 0s; } 599 | .shape-2 { width: 150px; height: 150px; bottom: 0; right: 20%; animation-delay: 2s; background: linear-gradient(135deg, var(--accent-color), var(--primary-color)); } 600 | .shape-3 { width: 80px; height: 80px; bottom: 30%; right: 10%; animation-delay: 4s; background: linear-gradient(135deg, var(--secondary-color), var(--accent-color)); } 601 | 602 | 603 | /* API Section */ 604 | .api-section { 605 | padding: var(--space-xxl) var(--space-xl); /* Padding atas dan bawah yang cukup */ 606 | background-color: var(--background-color); 607 | } 608 | 609 | .section-title { 610 | font-size: clamp(2rem, 4vw, 2.8rem); 611 | margin-bottom: var(--space-md); 612 | position: relative; 613 | display: inline-block; 614 | color: var(--text-color); 615 | } 616 | .section-title::after { content: ''; position: absolute; left: 0; bottom: -8px; width: 60px; height: 4px; background: linear-gradient(to right, var(--primary-color), var(--accent-color)); border-radius: 4px; } 617 | 618 | .section-description { 619 | font-size: clamp(1rem, 2vw, 1.1rem); 620 | color: var(--text-muted); 621 | margin-bottom: var(--space-xl); 622 | max-width: 800px; 623 | } 624 | 625 | /* Category Section */ 626 | .category-section { 627 | margin-bottom: var(--space-xxl); 628 | animation: slideInUp 0.6s ease-in-out both; 629 | } 630 | 631 | .category-header { 632 | font-size: clamp(1.5rem, 3vw, 2rem); 633 | font-weight: 700; 634 | margin-bottom: var(--space-lg); 635 | color: var(--text-color); 636 | position: relative; 637 | display: inline-flex; 638 | align-items: center; 639 | gap: var(--space-sm); 640 | padding-left: var(--space-sm); 641 | border-left: 4px solid var(--primary-color); 642 | } 643 | 644 | .category-image { 645 | width: 100%; 646 | height: 200px; 647 | object-fit: cover; 648 | border-radius: var(--border-radius); 649 | margin-bottom: var(--space-lg); 650 | box-shadow: var(--shadow); 651 | transition: var(--hover-transition); 652 | } 653 | .category-image:hover { transform: scale(1.02); box-shadow: var(--hover-shadow); } 654 | 655 | .row { 656 | display: flex; 657 | flex-wrap: wrap; 658 | margin: 0 calc(var(--space-md) * -0.5); 659 | } 660 | 661 | /* API Cards */ 662 | .api-item { 663 | margin-bottom: var(--space-lg); 664 | padding: 0 calc(var(--space-md) * 0.5); 665 | transition: var(--hover-transition); 666 | opacity: 0; 667 | transform: translateY(20px); 668 | width: 100%; 669 | } 670 | 671 | @media (min-width: 768px) { .api-item { width: 50%; } } 672 | @media (min-width: 992px) { .api-item { width: 33.3333%; } } 673 | 674 | 675 | .api-item.in-view { opacity: 1; transform: translateY(0); } 676 | 677 | .api-card { 678 | padding: var(--space-lg); 679 | background-color: var(--card-background); 680 | color: var(--text-color); 681 | border-radius: var(--border-radius); 682 | min-height: 150px; 683 | display: flex; 684 | flex-direction: column; 685 | justify-content: space-between; 686 | box-shadow: var(--card-shadow); 687 | transition: var(--hover-transition); 688 | overflow: hidden; 689 | border: 1px solid var(--border-color); 690 | position: relative; 691 | height: 100%; 692 | } 693 | .api-card::after { content: ''; position: absolute; top: 0; right: 0; width: 0; height: 0; border-style: solid; border-width: 0 50px 50px 0; border-color: transparent var(--highlight-color) transparent transparent; transition: var(--transition); opacity: 0; } 694 | .api-card:hover { box-shadow: var(--hover-shadow); transform: translateY(-5px); } 695 | .api-card:hover::after { opacity: 1; } 696 | 697 | /* Styling for unavailable API cards (status error or update) */ 698 | .api-card.api-card-unavailable { 699 | position: relative; 700 | } 701 | 702 | .api-card.api-card-unavailable::before { 703 | content: ''; 704 | position: absolute; 705 | top: 0; 706 | left: 0; 707 | width: 100%; 708 | height: 100%; 709 | background-color: rgba(0, 0, 0, 0.35); 710 | border-radius: inherit; 711 | z-index: 1; 712 | pointer-events: none; 713 | } 714 | 715 | .api-card.api-card-unavailable > * { 716 | position: relative; 717 | z-index: 2; 718 | } 719 | 720 | 721 | .api-card-info { 722 | margin-bottom: var(--space-md); 723 | } 724 | 725 | .api-card h5 { 726 | font-size: 1.15rem; 727 | font-weight: 600; 728 | margin-bottom: var(--space-sm); 729 | transition: var(--transition); 730 | color: var(--text-color); 731 | } 732 | 733 | .api-card .text-muted { 734 | color: var(--text-muted) !important; 735 | font-size: 0.9rem; 736 | overflow: hidden; 737 | text-overflow: ellipsis; 738 | display: -webkit-box; 739 | -webkit-line-clamp: 3; 740 | -webkit-box-orient: vertical; 741 | line-height: 1.5; 742 | margin-bottom: 0; 743 | } 744 | 745 | .api-actions { 746 | display: flex; 747 | justify-content: space-between; 748 | align-items: center; 749 | margin-top: auto; 750 | } 751 | 752 | .get-api-btn { 753 | background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); 754 | color: white; 755 | border: none; 756 | border-radius: var(--border-radius-sm); 757 | padding: 0.5rem 1rem; 758 | transition: var(--hover-transition); 759 | font-weight: 500; 760 | box-shadow: var(--shadow); 761 | position: relative; 762 | overflow: hidden; 763 | font-size: 0.9rem; 764 | } 765 | .get-api-btn::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0)); transform: translateX(-100%); transition: 0.5s; } 766 | .get-api-btn:hover { transform: translateY(-3px); box-shadow: var(--hover-shadow); } 767 | .get-api-btn:hover::before { transform: translateX(100%); } 768 | 769 | /* Styles for disabled GET button due to API status */ 770 | .get-api-btn[disabled] { 771 | background: var(--text-muted) !important; 772 | color: var(--background-color) !important; 773 | opacity: 0.65 !important; 774 | cursor: not-allowed !important; 775 | box-shadow: none !important; 776 | transform: none !important; 777 | } 778 | .get-api-btn[disabled]::before { 779 | display: none !important; 780 | } 781 | 782 | 783 | .api-status { 784 | display: inline-flex; 785 | align-items: center; 786 | gap: 5px; 787 | font-size: 0.75rem; 788 | font-weight: 500; 789 | padding: 0.3rem 0.7rem; 790 | border-radius: 50px; 791 | white-space: nowrap; 792 | } 793 | .status-ready { background-color: rgba(0, 184, 148, 0.15); color: var(--success-color); } 794 | .status-error { background-color: rgba(255, 118, 117, 0.15); color: var(--error-color); } 795 | .status-update { background-color: rgba(253, 203, 110, 0.15); color: var(--warning-color); } 796 | .api-status i { font-size: 0.65rem; } 797 | .status-ready i { font-size: 0.6rem; } 798 | .api-status span { margin-left: 3px; } 799 | 800 | /* No Results Message */ 801 | .no-results-message { 802 | display: flex; 803 | flex-direction: column; 804 | align-items: center; 805 | justify-content: center; 806 | padding: var(--space-xxl) var(--space-lg); 807 | text-align: center; 808 | animation: fadeIn 0.4s ease-in-out; 809 | background-color: var(--card-background); 810 | border-radius: var(--border-radius); 811 | box-shadow: var(--card-shadow); 812 | margin-top: var(--space-xl); 813 | } 814 | .no-results-message i { font-size: 3rem; color: var(--text-muted); margin-bottom: var(--space-lg); opacity: 0.5; } 815 | .no-results-message p { font-size: 1.2rem; color: var(--text-muted); margin-bottom: var(--space-lg); } 816 | .no-results-message span { font-weight: 600; color: var(--text-color); } 817 | .no-results-message .btn { background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: white; border: none; padding: 0.6rem 1.5rem; border-radius: var(--border-radius-sm); transition: var(--transition); } 818 | .no-results-message .btn:hover { transform: var(--hover-transform); box-shadow: var(--hover-shadow); } 819 | 820 | /* Footer */ 821 | .main-footer { 822 | margin-top: auto; 823 | padding: var(--space-lg) var(--space-xl); 824 | background-color: var(--card-background); 825 | color: var(--text-muted); 826 | border-top: 1px solid var(--border-color); 827 | } 828 | .footer-content { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: var(--space-lg); } 829 | .copyright { font-size: 0.9rem; } 830 | .footer-middle { display: flex; align-items: center; } 831 | .theme-switcher { display: flex; align-items: center; gap: var(--space-sm); font-size: 0.9rem; color: var(--text-muted); } 832 | .switch { position: relative; display: inline-block; width: 50px; height: 24px; } 833 | .switch input { opacity: 0; width: 0; height: 0; } 834 | .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; } 835 | .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: white; transition: .4s; } 836 | input:checked + .slider { background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); } 837 | input:focus + .slider { box-shadow: 0 0 1px var(--primary-color); } 838 | input:checked + .slider:before { transform: translateX(26px); } 839 | .slider.round { border-radius: 34px; } 840 | .slider.round:before { border-radius: 50%; } 841 | .footer-links { display: flex; gap: var(--space-md); } 842 | .footer-link { display: inline-flex; align-items: center; gap: var(--space-sm); color: var(--text-muted); text-decoration: none; transition: var(--transition); padding: var(--space-sm) var(--space-md); border-radius: var(--border-radius-sm); font-size: 0.9rem; } 843 | .footer-link:hover { color: var(--primary-color); background-color: var(--highlight-color); } 844 | 845 | /* Modal */ 846 | .modal-content { background-color: var(--card-background); color: var(--text-color); border: none; border-radius: var(--border-radius); box-shadow: var(--shadow); padding: var(--space-lg); overflow: hidden; } 847 | .modal-header { border-bottom: 1px solid var(--border-color); padding-bottom: var(--space-md); } 848 | .modal-title { font-weight: 700; color: var(--text-color); font-size: 1.25rem; } 849 | .modal-desc { color: var(--text-muted); font-size: 0.9rem; margin-top: var(--space-xs); } 850 | .btn-close { color: var(--text-color); opacity: 0.7; transition: var(--transition); background: none; border: none; font-size: 1.2rem; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; } 851 | .btn-close:hover { opacity: 1; color: var(--primary-color); background-color: var(--highlight-color); } 852 | .modal-body { max-height: 70vh; overflow-y: auto; padding: var(--space-lg) 0; scrollbar-width: thin; scrollbar-color: var(--primary-color) var(--card-background); } 853 | .modal-body::-webkit-scrollbar { width: 8px; } 854 | .modal-body::-webkit-scrollbar-track { background: var(--card-background); border-radius: 10px; } 855 | .modal-body::-webkit-scrollbar-thumb { background: linear-gradient(var(--primary-color), var(--secondary-color)); border-radius: 10px; } 856 | .endpoint-container, .response-container { margin-bottom: var(--space-lg); animation: slideInUp 0.4s ease-in-out; } 857 | .endpoint-label, .response-label { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-sm); font-weight: 600; color: var(--text-color); } 858 | .copy-btn { background: none; border: none; color: var(--text-muted); cursor: pointer; transition: var(--transition); font-size: 1rem; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } 859 | .copy-btn:hover { color: var(--primary-color); background-color: var(--highlight-color); } 860 | .copy-success { color: var(--success-color) !important; } 861 | .code-block { background-color: var(--background-color); padding: var(--space-md); border-radius: var(--border-radius); color: var(--text-color); font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.9rem; margin-bottom: var(--space-md); overflow-x: auto; border: 1px solid var(--border-color); position: relative; line-height: 1.6; } 862 | 863 | /* Query Input Container */ 864 | .query-input-container { margin-bottom: var(--space-lg); } 865 | .param-container { margin-bottom: var(--space-lg); animation: slideInUp 0.4s ease-in-out; background-color: var(--background-color); border-radius: var(--border-radius); padding: var(--space-lg); border: 1px solid var(--border-color); } 866 | .param-form-title { margin-bottom: var(--space-lg); font-weight: 600; color: var(--text-color); display: flex; align-items: center; gap: var(--space-sm); font-size: 1.1rem; } 867 | .param-form-title i { color: var(--primary-color); } 868 | .param-group { margin-bottom: var(--space-lg); position: relative; } 869 | .param-label-container { display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm); } 870 | .form-label { color: var(--text-color); font-weight: 500; margin-bottom: 0; } 871 | .required-indicator { color: var(--error-color); font-weight: bold; } 872 | .param-info { color: var(--text-muted); font-size: 0.9rem; cursor: help; transition: var(--transition); width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } 873 | .param-info:hover { color: var(--primary-color); background-color: var(--highlight-color); } 874 | .input-container { position: relative; } 875 | .custom-input { background-color: var(--card-background); border: 1px solid var(--border-color); color: var(--text-color); padding: 0.75rem 1rem; border-radius: var(--border-radius-sm); transition: var(--transition); width: 100%; font-size: 0.95rem; } 876 | .custom-input:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px var(--highlight-color); } 877 | .custom-input.is-invalid { border-color: var(--error-color); box-shadow: 0 0 0 3px rgba(255, 118, 117, 0.1); } 878 | .shake-animation { animation: shake 0.4s ease-in-out; } 879 | .inner-desc { background-color: var(--highlight-color); color: var(--text-color); padding: var(--space-md); border-radius: var(--border-radius-sm); font-size: 0.9rem; margin-top: var(--space-lg); display: flex; align-items: flex-start; gap: var(--space-sm); border-left: 3px solid var(--primary-color); } 880 | .inner-desc i { color: var(--primary-color); margin-top: 2px; } 881 | 882 | /* Loading Spinner - New Style */ 883 | #apiResponseLoading { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 180px; gap: var(--space-md); } 884 | #apiResponseLoading p { color: var(--text-muted); font-weight: 500; } 885 | .spinner-logo { animation: rotate-spinner 2s linear infinite; } 886 | .spinner-path { stroke: var(--background-color); stroke-linecap: round; } 887 | .spinner-animation { stroke: var(--primary-color); stroke-linecap: round; animation: dash 1.5s ease-in-out infinite; } 888 | 889 | /* Loading Screen */ 890 | #loadingScreen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--background-color); z-index: 9999; display: flex; justify-content: center; align-items: center; flex-direction: column; } 891 | .spinner-wrapper { text-align: center; } 892 | .spinner-wrapper p { color: var(--text-color); font-weight: 500; margin-top: var(--space-lg); font-size: 1.1rem; letter-spacing: 1px; } 893 | .loading-dots { display: inline-block; width: 20px; text-align: left; } 894 | .fade-out { opacity: 0; transition: opacity 0.5s ease; } 895 | 896 | /* Toast Notification */ 897 | .toast-container { position: fixed; bottom: var(--space-lg); right: var(--space-lg); z-index: 1060; } 898 | .toast { background-color: var(--card-background); color: var(--text-color); border: none; border-radius: var(--border-radius-sm); box-shadow: var(--shadow); overflow: hidden; border-left: 4px solid var(--primary-color); min-width: 320px; } 899 | .toast-header { background-color: var(--card-background); color: var(--text-color); border-bottom: 1px solid var(--border-color); padding: var(--space-sm) var(--space-md); } 900 | .toast-icon { color: var(--primary-color); } 901 | .toast-title { font-weight: 600; } 902 | .toast-body { padding: var(--space-md); font-size: 0.9rem; } 903 | 904 | /* JSON Syntax Highlighting */ 905 | .json-string { color: var(--success-color); } .json-number { color: var(--accent-color); } .json-boolean { color: var(--primary-color); } .json-null { color: var(--error-color); } .json-key { color: var(--warning-color); } 906 | .dark-mode .json-string { color: #7ee787; } .dark-mode .json-number { color: #79c0ff; } .dark-mode .json-boolean { color: #ff7b72; } .dark-mode .json-null { color: #ff7b72; } .dark-mode .json-key { color: #ffa657; } 907 | 908 | /* Responsive Styles */ 909 | @media (max-width: 1200px) { 910 | .hero-section { 911 | flex-direction: column; 912 | text-align: center; 913 | padding: var(--space-xl) var(--space-lg); 914 | } 915 | .hero-content { 916 | max-width: 100%; 917 | margin-bottom: var(--space-xl); 918 | text-align: center; 919 | } 920 | .hero-heading { justify-content: center; } 921 | .hero-actions { justify-content: center; } 922 | .hero-visual { width: 100%; max-width: 500px; padding-left: 0; /* Hapus padding kiri pada mode kolom */ } 923 | } 924 | 925 | @media (max-width: 992px) { 926 | .main-wrapper { margin-left: 0; } 927 | .side-nav { transform: translateX(-100%); box-shadow: 0 0 20px rgba(0,0,0,0.2); } 928 | .side-nav.active { transform: translateX(0); } 929 | .menu-toggle { display: flex; } 930 | .search-container { max-width: 350px; margin: 0 auto 0 var(--space-md); } 931 | .api-section, .hero-section { padding: var(--space-xl) var(--space-md); } 932 | .modal-dialog { margin: var(--space-sm); max-width: calc(100% - (var(--space-sm) * 2)); } 933 | .api-item { width: 50%; } 934 | .nav-collapse-btn { display: none; } 935 | } 936 | 937 | @media (max-width: 768px) { 938 | html { font-size: 15px; } 939 | .hero-actions { flex-direction: column; align-items: stretch; } 940 | .hero-actions a { width: 100%; text-align: center; } 941 | .search-container { max-width: none; margin: 0 var(--space-sm); } 942 | .footer-content { flex-direction: column; text-align: center; } 943 | .category-header { font-size: 1.8rem; } 944 | .api-item { width: 100%; } 945 | .main-header { padding: 0 var(--space-md); } 946 | .main-footer { padding: var(--space-md) var(--space-lg); } 947 | .footer-links { flex-direction: column; align-items: center; } 948 | } 949 | 950 | @media (max-width: 576px) { 951 | .api-section, .hero-section { padding: var(--space-lg) var(--space-sm); } 952 | .section-title { font-size: 1.8rem; } 953 | .section-description { font-size: 0.95rem; } 954 | .get-api-btn { padding: 0.6rem 1rem; font-size: 0.85rem; } 955 | .api-actions { flex-direction: row; justify-content: space-between; margin-top: var(--space-sm); } 956 | .toast-container { left: var(--space-md); right: var(--space-md); bottom: var(--space-md); } 957 | .toast { min-width: unset; width: calc(100% - (var(--space-md) * 2)); } 958 | .modal-body { max-height: 65vh; } 959 | .modal-content { padding: var(--space-md); } 960 | } 961 | 962 | /* Additional Styles for Error Container in Modal */ 963 | .error-container { 964 | display: flex; 965 | align-items: center; 966 | gap: var(--space-md); 967 | padding: var(--space-lg); 968 | background-color: rgba(var(--error-color), 0.1); 969 | border: 1px solid rgba(var(--error-color), 0.3); 970 | border-radius: var(--border-radius); 971 | color: var(--error-color); 972 | } 973 | .error-icon { 974 | font-size: 1.8rem; 975 | } 976 | .error-message h6 { 977 | font-weight: 600; 978 | margin-bottom: var(--space-xs); 979 | color: var(--error-color); 980 | } 981 | .error-message p { 982 | font-size: 0.9rem; 983 | margin-bottom: var(--space-sm); 984 | color: var(--error-color); 985 | opacity: 0.9; 986 | } 987 | .retry-btn { 988 | background-color: var(--error-color); 989 | color: white; 990 | border: none; 991 | padding: var(--space-xs) var(--space-md); 992 | font-size: 0.85rem; 993 | border-radius: var(--border-radius-sm); 994 | } 995 | .retry-btn:hover { 996 | background-color: darken(var(--error-color), 10%); 997 | } 998 | 999 | /* Code folding styles */ 1000 | .code-fold-trigger { 1001 | cursor: pointer; 1002 | padding: 5px; 1003 | background-color: rgba(var(--primary-color-rgb), 0.05); 1004 | border-radius: var(--border-radius-sm); 1005 | margin-bottom: 2px; 1006 | display: flex; 1007 | justify-content: space-between; 1008 | align-items: center; 1009 | } 1010 | .code-fold-trigger:hover { 1011 | background-color: rgba(var(--primary-color-rgb), 0.1); 1012 | } 1013 | .code-fold-content { 1014 | padding-left: 20px; 1015 | border-left: 2px solid var(--highlight-color); 1016 | overflow: hidden; 1017 | max-height: 1000px; 1018 | transition: max-height 0.3s ease-in-out; 1019 | } 1020 | .code-fold-trigger.folded + .code-fold-content { 1021 | max-height: 0; 1022 | } 1023 | .fold-indicator { 1024 | font-size: 0.8em; 1025 | color: var(--text-muted); 1026 | margin-left: 10px; 1027 | } 1028 | .fold-indicator i { 1029 | transition: transform 0.3s ease; 1030 | margin-right: 5px; 1031 | } 1032 | .code-fold-trigger.folded .fold-indicator i { 1033 | transform: rotate(-90deg); 1034 | } 1035 | 1036 | /* Search input focus animation */ 1037 | .input-group.search-focused { 1038 | box-shadow: 0 0 0 3px var(--highlight-color), var(--hover-shadow) !important; 1039 | } 1040 | 1041 | /* Button active state */ 1042 | .btn-active { 1043 | transform: translateY(-2px); 1044 | box-shadow: 0 2px 5px rgba(0,0,0,0.15) !important; 1045 | } 1046 | 1047 | /* Pulse animation for button click */ 1048 | .pulse-animation { 1049 | animation: pulse 0.3s ease-out; 1050 | } 1051 | 1052 | /* Slide in bottom animation for response */ 1053 | .slide-in-bottom { 1054 | animation: slideInUp 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; 1055 | } -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlowFalcon/Falcon-Api-UI/3d09a86d6c50edb43f7acf5c09c3a28a32f95e70/image.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const chalk = require('chalk'); 3 | const fs = require('fs'); 4 | const cors = require('cors'); 5 | const path = require('path'); 6 | 7 | const app = express(); 8 | const PORT = process.env.PORT || 4000; 9 | 10 | app.enable("trust proxy"); 11 | app.set("json spaces", 2); 12 | 13 | app.use(express.json()); 14 | app.use(express.urlencoded({ extended: false })); 15 | app.use(cors()); 16 | app.use('/', express.static(path.join(__dirname, 'api-page'))); 17 | app.use('/src', express.static(path.join(__dirname, 'src'))); 18 | 19 | const settingsPath = path.join(__dirname, './src/settings.json'); 20 | const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); 21 | 22 | app.use((req, res, next) => { 23 | const originalJson = res.json; 24 | res.json = function (data) { 25 | if (data && typeof data === 'object') { 26 | const responseData = { 27 | status: data.status, 28 | creator: settings.apiSettings.creator || "Created Using Rynn UI", 29 | ...data 30 | }; 31 | return originalJson.call(this, responseData); 32 | } 33 | return originalJson.call(this, data); 34 | }; 35 | next(); 36 | }); 37 | 38 | // Api Route 39 | let totalRoutes = 0; 40 | const apiFolder = path.join(__dirname, './src/api'); 41 | fs.readdirSync(apiFolder).forEach((subfolder) => { 42 | const subfolderPath = path.join(apiFolder, subfolder); 43 | if (fs.statSync(subfolderPath).isDirectory()) { 44 | fs.readdirSync(subfolderPath).forEach((file) => { 45 | const filePath = path.join(subfolderPath, file); 46 | if (path.extname(file) === '.js') { 47 | require(filePath)(app); 48 | totalRoutes++; 49 | console.log(chalk.bgHex('#FFFF99').hex('#333').bold(` Loaded Route: ${path.basename(file)} `)); 50 | } 51 | }); 52 | } 53 | }); 54 | console.log(chalk.bgHex('#90EE90').hex('#333').bold(' Load Complete! ✓ ')); 55 | console.log(chalk.bgHex('#90EE90').hex('#333').bold(` Total Routes Loaded: ${totalRoutes} `)); 56 | 57 | app.get('/', (req, res) => { 58 | res.sendFile(path.join(__dirname, 'api-page', 'index.html')); 59 | }); 60 | 61 | app.use((req, res, next) => { 62 | res.status(404).sendFile(process.cwd() + "/api-page/404.html"); 63 | }); 64 | 65 | app.use((err, req, res, next) => { 66 | console.error(err.stack); 67 | res.status(500).sendFile(process.cwd() + "/api-page/500.html"); 68 | }); 69 | 70 | app.listen(PORT, () => { 71 | console.log(chalk.bgHex('#90EE90').hex('#333').bold(` Server is running on port ${PORT} `)); 72 | }); 73 | 74 | module.exports = app; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rynn-ui", 3 | "version": "1.0.0", 4 | "description": "A simple and customizable API documentation interface built with Express.js.", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "scripts": { 8 | "start": "node --no-deprecation index.js" 9 | }, 10 | "author": "Rynn", 11 | "license": "MIT", 12 | "dependencies": { 13 | "axios": "^1.6.8", 14 | "body-parser": "^1.20.3", 15 | "cheerio": "^1.0.0", 16 | "cors": "^2.8.5", 17 | "chalk": "^4.1.2", 18 | "express": "^4.19.2", 19 | "fs": "^0.0.1-security", 20 | "https": "^1.0.0", 21 | "node-fetch": "^2.6.1", 22 | "yt-search": "^2.12.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/api/ai/ai-hydromind.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const FormData = require('form-data'); 3 | module.exports = function(app) { 4 | async function hydromind(content, model) { 5 | const form = new FormData(); 6 | form.append('content', content); 7 | form.append('model', model); 8 | const { data } = await axios.post('https://mind.hydrooo.web.id/v1/chat/', form, { 9 | headers: { 10 | ...form.getHeaders(), 11 | } 12 | }) 13 | return data; 14 | } 15 | app.get('/ai/hydromind', async (req, res) => { 16 | try { 17 | const { text, model } = req.query; 18 | if (!text || !model) { 19 | return res.status(400).json({ status: false, error: 'Text and Model is required' }); 20 | } 21 | const { result } = await hydromind(text, model); 22 | res.status(200).json({ 23 | status: true, 24 | result 25 | }); 26 | } catch (error) { 27 | res.status(500).json({ status: false, error: error.message }); 28 | } 29 | }); 30 | } -------------------------------------------------------------------------------- /src/api/ai/ai-luminai.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | module.exports = function(app) { 3 | async function fetchContent(content) { 4 | try { 5 | const response = await axios.post('https://luminai.my.id/', { content }); 6 | return response.data; 7 | } catch (error) { 8 | console.error("Error fetching content from LuminAI:", error); 9 | throw error; 10 | } 11 | } 12 | app.get('/ai/luminai', async (req, res) => { 13 | try { 14 | const { text } = req.query; 15 | if (!text) { 16 | return res.status(400).json({ status: false, error: 'Text is required' }); 17 | } 18 | const { result } = await fetchContent(text); 19 | res.status(200).json({ 20 | status: true, 21 | result 22 | }); 23 | } catch (error) { 24 | res.status(500).json({ status: false, error: error.message }); 25 | } 26 | }); 27 | }; -------------------------------------------------------------------------------- /src/api/random/random-bluearchive.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | module.exports = function(app) { 3 | async function bluearchive() { 4 | try { 5 | const { data } = await axios.get(`https://raw.githubusercontent.com/rynxzyy/blue-archive-r-img/refs/heads/main/links.json`) 6 | const response = await axios.get(data[Math.floor(data.length * Math.random())], { responseType: 'arraybuffer' }); 7 | return Buffer.from(response.data); 8 | } catch (error) { 9 | throw error; 10 | } 11 | } 12 | app.get('/random/ba', async (req, res) => { 13 | try { 14 | const pedo = await bluearchive(); 15 | res.writeHead(200, { 16 | 'Content-Type': 'image/png', 17 | 'Content-Length': pedo.length, 18 | }); 19 | res.end(pedo); 20 | } catch (error) { 21 | res.status(500).send(`Error: ${error.message}`); 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/api/search/search-youtube.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | const yts = require('yt-search'); 3 | app.get('/search/youtube', async (req, res) => { 4 | const { q } = req.query; 5 | if (!q) { 6 | return res.status(400).json({ status: false, error: 'Query is required' }); 7 | } 8 | try { 9 | const ytResults = await yts.search(q); 10 | const ytTracks = ytResults.videos.map(video => ({ 11 | title: video.title, 12 | channel: video.author.name, 13 | duration: video.duration.timestamp, 14 | imageUrl: video.thumbnail, 15 | link: video.url 16 | })); 17 | res.status(200).json({ 18 | status: true, 19 | result: ytTracks 20 | }); 21 | } catch (error) { 22 | res.status(500).json({ status: false, error: error.message }); 23 | } 24 | }); 25 | } -------------------------------------------------------------------------------- /src/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlowFalcon/Falcon-Api-UI/3d09a86d6c50edb43f7acf5c09c3a28a32f95e70/src/banner.jpg -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlowFalcon/Falcon-Api-UI/3d09a86d6c50edb43f7acf5c09c3a28a32f95e70/src/icon.png -------------------------------------------------------------------------------- /src/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Falcon-Api UI", 3 | "version": "v1.0.0", 4 | "description": "Simple and easy to use API.", 5 | "bannerImage": "/src/banner.jpg", 6 | "header": { 7 | "status": "Online!" 8 | }, 9 | "apiSettings": { 10 | "creator": "FlowFalcon", 11 | "apikey": ["falcon-api"] 12 | }, 13 | "categories": [ 14 | { 15 | "name": "AI (Artificial Intelligence)", 16 | "items": [ 17 | { 18 | "name": "LuminAI", 19 | "desc": "Talk with luminai", 20 | "path": "/ai/luminai?text=", 21 | "status": "ready", 22 | "params": { 23 | "text": "Text for luminai to respond to" 24 | } 25 | }, 26 | { 27 | "name": "HydroMind", 28 | "desc": "See the list of supported AI models here: https://mind.hydrooo.web.id", 29 | "path": "/ai/hydromind?text=&model=", 30 | "status": "ready", 31 | "params": { 32 | "text": "Teks atau perintah untuk chat AI", 33 | "model":"1See the list of supported AI models here: https://mind.hydrooo.web.id" 34 | } 35 | } 36 | ] 37 | }, 38 | { 39 | "name": "Random", 40 | "items": [ 41 | { 42 | "name": "Blue Archive", 43 | "desc": "Blue Archive Random Images", 44 | "path": "/random/ba", 45 | "status": "ready" 46 | } 47 | ] 48 | }, 49 | { 50 | "name": "Search Tools", 51 | "items": [ 52 | { 53 | "name": "YouTube", 54 | "desc": "Video search", 55 | "path": "/search/youtube?q=", 56 | "status": "ready", 57 | "params": { 58 | "q": "Search query" 59 | } 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "index.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "/index.js" 13 | } 14 | ], 15 | "env": { 16 | "NODE_ENV": "production" 17 | } 18 | } 19 | --------------------------------------------------------------------------------