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