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>