├── 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 | 
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 |
24 |
25 |
26 |
27 |
Memuat...
28 |
29 |
30 |
31 |
57 |
58 |
59 |
60 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
80 |
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 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
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 |
145 |
146 |
147 |
156 |
157 |
158 |
159 | Endpoint
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
Memproses permintaan...
176 |
177 |
178 |
179 |
180 | Respons
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
205 |
206 | Ini adalah pesan notifikasi.
207 |
208 |
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 |
526 | Muat Ulang
527 |
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 |
588 | Hapus Pencarian
589 |
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 | `
833 | Coba Lagi
834 | ` : ''}
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 |
--------------------------------------------------------------------------------