dependabot/npm_and_yarn/katex-0.16.27
copilot/add-i18n-support-for-english
dependabot/npm_and_yarn/astro-5.16.4
dependabot/npm_and_yarn/katex-0.16.27
dependabot/npm_and_yarn/prettier-3.7.4
dependabot/npm_and_yarn/tailwindcss-4.1.16
dependabot/npm_and_yarn/tailwindcss/vite-4.1.16
dependabot/npm_and_yarn/typescript-eslint-8.46.2
dependabot/npm_and_yarn/typescript-eslint/parser-8.46.2
development
main
| 1 | --- |
| 2 | import IndexLayout from '@/layouts/IndexLayout.astro' |
| 3 | import { SITE } from '@/config' |
| 4 | import { Image } from 'astro:assets' |
| 5 | |
| 6 | // Fetch desktop app releases at build time (will be fetched client-side for dynamic updates) |
| 7 | const desktopDownloadsApiUrl = '/api/download-desktop-app' |
| 8 | --- |
| 9 | |
| 10 | <IndexLayout |
| 11 | title={`Unduh - ${SITE.title}`} |
| 12 | description="Hubungi tim PBJ Komplit untuk pertanyaan, saran, atau feedback" |
| 13 | > |
| 14 | <div class="contact-page prose"> |
| 15 | <h1>Unduh Aplikasi PBJ Komplit Bahasa Jawa</h1> |
| 16 | |
| 17 | <p> |
| 18 | Aplikasi PBJ Komplit Bahasa Jawa tersedia dalam berbagai platform, mulai dari iOS, Android, |
| 19 | macOS, Windows, dan Linux. |
| 20 | </p> |
| 21 | |
| 22 | <div class="contact-info"> |
| 23 | <h2>Unduh</h2> |
| 24 | <a href="https://apps.apple.com/se/app/pepak-basa-jawa-komplit/id6752241812"> |
| 25 | <Image |
| 26 | src="/images/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg" |
| 27 | alt="PBJ Komplit Apple AppStore page" |
| 28 | width="150" |
| 29 | height="50" |
| 30 | /> |
| 31 | </a> |
| 32 | <a href="https://play.google.com/store/apps/details?id=com.theblangkon.pepakbasajawa"> |
| 33 | <Image |
| 34 | src="/images/GetItOnGooglePlay_Badge_Web_color_English.png" |
| 35 | alt="PBJ Komplit Google PlayStore page" |
| 36 | width="150" |
| 37 | height="50" |
| 38 | /> |
| 39 | </a> |
| 40 | |
| 41 | <h2>Desktop Apps</h2> |
| 42 | <div id="desktop-downloads"> |
| 43 | <p>Loading desktop app downloads...</p> |
| 44 | </div> |
| 45 | |
| 46 | <h2>Bantuan</h2> |
| 47 | <p>Butuh bantuan? Hubungi kami:</p> |
| 48 | <div class="email-section"> |
| 49 | <a href="mailto:splitfire@setoelkahfi.se" class="email-link"> splitfire@setoelkahfi.se </a> |
| 50 | </div> |
| 51 | </div> |
| 52 | </div> |
| 53 | </IndexLayout> |
| 54 | |
| 55 | <script> |
| 56 | interface DownloadLink { |
| 57 | platform: string |
| 58 | architecture?: string |
| 59 | url: string |
| 60 | filename: string |
| 61 | size: number |
| 62 | version: string |
| 63 | } |
| 64 | |
| 65 | interface DownloadsResponse { |
| 66 | version: string |
| 67 | name: string |
| 68 | published_at: string |
| 69 | downloads: DownloadLink[] |
| 70 | } |
| 71 | |
| 72 | function formatBytes(bytes: number): string { |
| 73 | if (bytes === 0) return '0 Bytes' |
| 74 | const k = 1024 |
| 75 | const sizes = ['Bytes', 'KB', 'MB', 'GB'] |
| 76 | const i = Math.floor(Math.log(bytes) / Math.log(k)) |
| 77 | return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i] |
| 78 | } |
| 79 | |
| 80 | function getPlatformIcon(platform: string): string { |
| 81 | switch (platform.toLowerCase()) { |
| 82 | case 'macos': |
| 83 | return '🍎' |
| 84 | case 'windows': |
| 85 | return '🪟' |
| 86 | case 'linux': |
| 87 | return '🐧' |
| 88 | default: |
| 89 | return '💻' |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | async function loadDesktopDownloads() { |
| 94 | const container = document.getElementById('desktop-downloads') |
| 95 | if (!container) return |
| 96 | |
| 97 | try { |
| 98 | const response = await fetch('/api/download-desktop-app') |
| 99 | if (!response.ok) { |
| 100 | throw new Error('Failed to fetch downloads') |
| 101 | } |
| 102 | |
| 103 | const data: DownloadsResponse = await response.json() |
| 104 | |
| 105 | if (!data.downloads || data.downloads.length === 0) { |
| 106 | container.innerHTML = '<p>No desktop downloads available at the moment.</p>' |
| 107 | return |
| 108 | } |
| 109 | |
| 110 | // Group downloads by platform |
| 111 | const groupedDownloads = data.downloads.reduce( |
| 112 | (acc, download) => { |
| 113 | if (!acc[download.platform]) { |
| 114 | acc[download.platform] = [] |
| 115 | } |
| 116 | acc[download.platform].push(download) |
| 117 | return acc |
| 118 | }, |
| 119 | {} as Record<string, DownloadLink[]> |
| 120 | ) |
| 121 | |
| 122 | let html = `<div class="version-info">Latest Version: ${data.version}</div>` |
| 123 | html += '<div class="downloads-grid">' |
| 124 | |
| 125 | // Sort platforms: macOS, Windows, Linux |
| 126 | const platformOrder = ['macOS', 'Windows', 'Linux'] |
| 127 | const sortedPlatforms = Object.keys(groupedDownloads).sort( |
| 128 | (a, b) => platformOrder.indexOf(a) - platformOrder.indexOf(b) |
| 129 | ) |
| 130 | |
| 131 | for (const platform of sortedPlatforms) { |
| 132 | const downloads = groupedDownloads[platform] |
| 133 | html += ` |
| 134 | <div class="platform-section"> |
| 135 | <h3>${getPlatformIcon(platform)} ${platform}</h3> |
| 136 | <div class="download-items"> |
| 137 | ` |
| 138 | |
| 139 | for (const download of downloads) { |
| 140 | html += ` |
| 141 | <a href="${download.url}" class="download-item"> |
| 142 | <div class="download-info"> |
| 143 | <span class="download-name">${download.architecture || 'Download'}</span> |
| 144 | <span class="download-details">${download.filename} • ${formatBytes(download.size)}</span> |
| 145 | </div> |
| 146 | <span class="download-button">⬇️ Download</span> |
| 147 | </a> |
| 148 | ` |
| 149 | } |
| 150 | |
| 151 | html += ` |
| 152 | </div> |
| 153 | </div> |
| 154 | ` |
| 155 | } |
| 156 | |
| 157 | html += '</div>' |
| 158 | container.innerHTML = html |
| 159 | } catch (error) { |
| 160 | console.error('Error loading desktop downloads:', error) |
| 161 | container.innerHTML = |
| 162 | '<p class="error-message">Unable to load desktop downloads. Please try again later.</p>' |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | // Load downloads when page is ready |
| 167 | if (document.readyState === 'loading') { |
| 168 | document.addEventListener('DOMContentLoaded', loadDesktopDownloads) |
| 169 | } else { |
| 170 | loadDesktopDownloads() |
| 171 | } |
| 172 | </script> |
| 173 | |
| 174 | <style> |
| 175 | .contact-page { |
| 176 | max-width: 100%; |
| 177 | margin: 0 auto; |
| 178 | padding: 2rem 0; |
| 179 | } |
| 180 | |
| 181 | .contact-info { |
| 182 | margin: 2rem 0; |
| 183 | } |
| 184 | |
| 185 | .email-section { |
| 186 | margin: 1.5rem 0; |
| 187 | padding: 1rem; |
| 188 | background: var(--code-bg); |
| 189 | border-radius: 8px; |
| 190 | border-left: 4px solid var(--accent); |
| 191 | } |
| 192 | |
| 193 | .email-link { |
| 194 | font-size: 1.25rem; |
| 195 | font-weight: 600; |
| 196 | color: var(--accent); |
| 197 | text-decoration: none; |
| 198 | word-break: break-all; |
| 199 | } |
| 200 | |
| 201 | .email-link:hover { |
| 202 | text-decoration: underline; |
| 203 | } |
| 204 | |
| 205 | .topics { |
| 206 | margin: 2rem 0; |
| 207 | } |
| 208 | |
| 209 | .topics ul { |
| 210 | list-style-type: disc; |
| 211 | margin-left: 1.5rem; |
| 212 | } |
| 213 | |
| 214 | .topics li { |
| 215 | margin: 0.5rem 0; |
| 216 | line-height: 1.6; |
| 217 | } |
| 218 | |
| 219 | .app-info, |
| 220 | .community { |
| 221 | margin: 2rem 0; |
| 222 | } |
| 223 | |
| 224 | .app-info a { |
| 225 | color: var(--accent); |
| 226 | text-decoration: none; |
| 227 | } |
| 228 | |
| 229 | .app-info a:hover { |
| 230 | text-decoration: underline; |
| 231 | } |
| 232 | |
| 233 | .footer-note { |
| 234 | margin: 3rem 0 1rem; |
| 235 | padding: 1.5rem; |
| 236 | background: var(--selection); |
| 237 | border-radius: 8px; |
| 238 | text-align: center; |
| 239 | } |
| 240 | |
| 241 | .footer-note p { |
| 242 | margin: 0.5rem 0; |
| 243 | } |
| 244 | |
| 245 | #desktop-downloads { |
| 246 | margin: 1.5rem 0; |
| 247 | } |
| 248 | |
| 249 | .version-info { |
| 250 | padding: 0.75rem 1rem; |
| 251 | background: var(--selection); |
| 252 | border-radius: 6px; |
| 253 | margin-bottom: 1.5rem; |
| 254 | font-weight: 600; |
| 255 | color: var(--accent); |
| 256 | } |
| 257 | |
| 258 | .downloads-grid { |
| 259 | display: grid; |
| 260 | gap: 2rem; |
| 261 | margin-top: 1.5rem; |
| 262 | } |
| 263 | |
| 264 | .platform-section { |
| 265 | border: 1px solid var(--border); |
| 266 | border-radius: 8px; |
| 267 | padding: 1.5rem; |
| 268 | background: var(--code-bg); |
| 269 | } |
| 270 | |
| 271 | .platform-section h3 { |
| 272 | margin: 0 0 1rem 0; |
| 273 | font-size: 1.5rem; |
| 274 | color: var(--heading-color); |
| 275 | } |
| 276 | |
| 277 | .download-items { |
| 278 | display: flex; |
| 279 | flex-direction: column; |
| 280 | gap: 0.75rem; |
| 281 | } |
| 282 | |
| 283 | .download-item { |
| 284 | display: flex; |
| 285 | justify-content: space-between; |
| 286 | align-items: center; |
| 287 | padding: 1rem; |
| 288 | background: var(--bg); |
| 289 | border: 1px solid var(--border); |
| 290 | border-radius: 6px; |
| 291 | text-decoration: none; |
| 292 | transition: all 0.2s ease; |
| 293 | } |
| 294 | |
| 295 | .download-item:hover { |
| 296 | border-color: var(--accent); |
| 297 | transform: translateY(-2px); |
| 298 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
| 299 | } |
| 300 | |
| 301 | .download-info { |
| 302 | display: flex; |
| 303 | flex-direction: column; |
| 304 | gap: 0.25rem; |
| 305 | } |
| 306 | |
| 307 | .download-name { |
| 308 | font-weight: 600; |
| 309 | color: var(--heading-color); |
| 310 | font-size: 1.1rem; |
| 311 | } |
| 312 | |
| 313 | .download-details { |
| 314 | font-size: 0.875rem; |
| 315 | color: var(--text-muted); |
| 316 | } |
| 317 | |
| 318 | .download-button { |
| 319 | padding: 0.5rem 1rem; |
| 320 | background: var(--accent); |
| 321 | color: white; |
| 322 | border-radius: 4px; |
| 323 | font-weight: 600; |
| 324 | white-space: nowrap; |
| 325 | } |
| 326 | |
| 327 | .error-message { |
| 328 | color: #e74c3c; |
| 329 | padding: 1rem; |
| 330 | background: rgba(231, 76, 60, 0.1); |
| 331 | border-radius: 6px; |
| 332 | border-left: 4px solid #e74c3c; |
| 333 | } |
| 334 | |
| 335 | @media (max-width: 768px) { |
| 336 | .contact-page { |
| 337 | padding: 1rem 0; |
| 338 | } |
| 339 | |
| 340 | .email-link { |
| 341 | font-size: 1.1rem; |
| 342 | } |
| 343 | |
| 344 | .download-item { |
| 345 | flex-direction: column; |
| 346 | align-items: flex-start; |
| 347 | gap: 1rem; |
| 348 | } |
| 349 | |
| 350 | .download-button { |
| 351 | width: 100%; |
| 352 | text-align: center; |
| 353 | } |
| 354 | |
| 355 | .platform-section { |
| 356 | padding: 1rem; |
| 357 | } |
| 358 | } |
| 359 | </style> |