Shveiauto commited on
Commit
89507b1
·
verified ·
1 Parent(s): a92e4d4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +426 -338
app.py CHANGED
@@ -25,7 +25,7 @@ REPO_ID = os.getenv("HF_REPO_ID", "Kgshop/tontalent2")
25
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
26
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
 
28
- TELEGRAM_BOT_TOKEN = "7549355625:AAGhdbf6x1JEzpH0mUtuxTF83Soi7MFVNZ8"
29
 
30
  DOWNLOAD_RETRIES = 3
31
  DOWNLOAD_DELAY = 5
@@ -66,8 +66,8 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
66
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
67
  except Exception as create_e:
68
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
69
- success = False
70
- break
71
  else:
72
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
73
  except Exception as e:
@@ -202,7 +202,7 @@ MAIN_APP_TEMPLATE = '''
202
  <html lang="en">
203
  <head>
204
  <meta charset="UTF-8">
205
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
206
  <title>TonTalent</title>
207
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
208
  <style>
@@ -229,14 +229,12 @@ MAIN_APP_TEMPLATE = '''
229
  overscroll-behavior-y: none;
230
  -webkit-font-smoothing: antialiased;
231
  -moz-osx-font-smoothing: grayscale;
232
- display: flex;
233
- flex-direction: column;
234
- min-height: 100vh;
235
  }
236
- .app-container { display: flex; flex-direction: column; flex-grow: 1; }
237
  .header {
238
  background-color: var(--tg-theme-header-bg-color);
239
- padding: 10px 15px;
240
  text-align: center;
241
  font-weight: 600;
242
  font-size: 17px;
@@ -245,22 +243,43 @@ MAIN_APP_TEMPLATE = '''
245
  top: 0;
246
  z-index: 100;
247
  }
248
- .user-info {
249
- padding: 8px 15px;
250
- background-color: var(--tg-theme-bg-color);
251
- font-size: 13px;
252
- text-align: left;
253
- color: var(--tg-theme-text-color);
 
254
  border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  display: flex;
256
  align-items: center;
 
 
 
257
  }
258
- .user-info img { width: 28px; height: 28px; border-radius: 50%; vertical-align: middle; margin-right: 10px; }
 
259
 
260
- .tabs { display: flex; background-color: var(--tg-theme-header-bg-color); padding: 0px 5px; border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color); }
261
  .tab-button {
262
  flex: 1;
263
- padding: 12px 5px;
264
  text-align: center;
265
  cursor: pointer;
266
  background: none;
@@ -268,35 +287,37 @@ MAIN_APP_TEMPLATE = '''
268
  color: var(--tg-theme-hint-color);
269
  font-size: 15px;
270
  font-weight: 500;
271
- border-bottom: 2.5px solid transparent;
272
  transition: color 0.2s ease, border-bottom-color 0.2s ease;
273
- position: relative;
 
 
 
274
  }
275
- .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); font-weight: 600;}
276
- .content { flex-grow: 1; padding: 10px; overflow-y: auto; }
 
 
 
 
277
  .list-item {
278
  background-color: var(--tg-theme-section-bg-color);
279
  border-radius: 10px;
280
  padding: 12px 15px;
281
- margin-bottom: 10px;
282
- box-shadow: 0 2px 6px rgba(0,0,0,0.08);
283
  cursor: pointer;
284
- transition: background-color 0.15s ease, transform 0.15s ease;
285
- animation: fadeIn 0.4s ease-out;
286
- }
287
- @keyframes fadeIn {
288
- from { opacity: 0; transform: translateY(8px); }
289
- to { opacity: 1; transform: translateY(0); }
290
  }
291
- .list-item:active { background-color: var(--tg-theme-secondary-bg-color); transform: scale(0.98); }
292
- .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
293
- .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); line-height: 1.4; }
294
- .list-item .meta { font-size: 12px; color: var(--tg-theme-hint-color); margin-top: 8px; }
295
 
296
  .form-container { padding: 15px; background-color: var(--tg-theme-section-bg-color); }
297
- .form-group { margin-bottom: 18px; }
298
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
299
- .form-group input, .form-group textarea {
300
  width: 100%;
301
  padding: 12px;
302
  border: 1px solid var(--tg-theme-secondary-bg-color);
@@ -305,16 +326,16 @@ MAIN_APP_TEMPLATE = '''
305
  background-color: var(--tg-theme-bg-color);
306
  color: var(--tg-theme-text-color);
307
  box-sizing: border-box;
308
- transition: border-color 0.2s ease;
309
- }
310
- .form-group input:focus, .form-group textarea:focus {
311
- border-color: var(--tg-theme-link-color);
312
- outline: none;
313
  }
 
314
  .form-group textarea { min-height: 100px; resize: vertical; }
 
315
  .fab {
316
  position: fixed;
317
- bottom: 20px;
318
  right: 20px;
319
  width: 56px;
320
  height: 56px;
@@ -324,59 +345,65 @@ MAIN_APP_TEMPLATE = '''
324
  display: flex;
325
  align-items: center;
326
  justify-content: center;
327
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
 
328
  cursor: pointer;
329
  z-index: 1000;
330
  border: none;
331
- transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
332
  }
333
  .fab:active { transform: scale(0.92); }
334
- .fab svg { width: 28px; height: 28px; fill: currentColor; }
335
 
336
- .detail-view { padding: 15px; background-color: var(--tg-theme-section-bg-color); animation: fadeIn 0.3s ease-out; }
337
- .detail-view h2 { margin-top: 0; margin-bottom: 15px; font-size: 22px; font-weight: 600; color: var(--tg-theme-text-color); }
338
- .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; color: var(--tg-theme-text-color); }
339
- .detail-view p strong { font-weight: 500; color: var(--tg-theme-section-header-text-color); display: block; margin-bottom: 3px; font-size: 14px;}
340
- .detail-view .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 20px; }
341
  .detail-view a { color: var(--tg-theme-link-color); text-decoration: none; }
342
  .detail-view a:hover { text-decoration: underline; }
343
- .delete-post-button {
344
- background-color: var(--tg-theme-destructive-text-color);
345
- color: var(--tg-theme-button-text-color);
346
- padding: 12px 15px; border: none; border-radius: 8px; cursor: pointer;
347
- margin-top: 20px; width: 100%; font-size: 16px; font-weight: 500;
348
- transition: background-color 0.2s ease;
 
 
 
349
  }
350
- .delete-post-button:active { background-color: color-mix(in srgb, var(--tg-theme-destructive-text-color) 80%, black); }
351
-
352
-
 
353
  .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
354
  .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 8px; }
355
-
356
- .profile-header { text-align: center; margin-bottom: 25px; }
357
- .profile-header img { width: 80px; height: 80px; border-radius: 50%; margin-bottom: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
358
- .profile-header h2 { margin: 5px 0; font-size: 20px; font-weight: 600; }
359
- .profile-header p { margin: 0; font-size: 15px; color: var(--tg-theme-hint-color); }
360
- .profile-section h3 { font-size: 18px; font-weight: 600; color: var(--tg-theme-text-color); margin-top: 25px; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid var(--tg-theme-secondary-bg-color); }
361
- .profile-section .list-item h4 {font-size: 16px; margin: 0 0 5px 0; font-weight: 500;}
362
-
363
  </style>
364
  </head>
365
  <body>
366
  <div class="app-container">
367
  <div class="header">TonTalent</div>
368
- <div class="user-info" id="userInfo">Loading user...</div>
 
369
  <div class="tabs">
370
- <button class="tab-button active" data-tab="resumes">Resumes</button>
371
- <button class="tab-button" data-tab="vacancies">Vacancies</button>
372
- <button class="tab-button" data-tab="freelance_offers">Freelance</button>
373
- <button class="tab-button" data-tab="my_activity">My Activity</button>
 
 
 
 
 
 
 
 
374
  </div>
375
  <div class="content" id="mainContent">
376
  <div class="loading">Loading content...</div>
377
  </div>
378
  <button class="fab" id="fabButton" title="Add New Item">
379
- <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
380
  </button>
381
  </div>
382
 
@@ -385,199 +412,301 @@ MAIN_APP_TEMPLATE = '''
385
  let currentUser = null;
386
  let currentView = 'resumes';
387
  let currentItem = null;
388
- const TABS_ORDER = ['resumes', 'vacancies', 'freelance_offers', 'my_activity'];
389
-
390
- let touchstartX = 0;
391
- let touchendX = 0;
392
- const SWIPE_THRESHOLD = 75;
393
 
394
  function applyThemeParams() {
395
- const style = document.documentElement.style;
396
- style.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
397
- style.setProperty('--tg-theme-text-color', tg.themeParams.text_color || '#000000');
398
- style.setProperty('--tg-theme-hint-color', tg.themeParams.hint_color || '#999999');
399
- style.setProperty('--tg-theme-link-color', tg.themeParams.link_color || '#007aff');
400
- style.setProperty('--tg-theme-button-color', tg.themeParams.button_color || '#007aff');
401
- style.setProperty('--tg-theme-button-text-color', tg.themeParams.button_text_color || '#ffffff');
402
- style.setProperty('--tg-theme-secondary-bg-color', tg.themeParams.secondary_bg_color || '#f0f0f0');
403
- style.setProperty('--tg-theme-header-bg-color', tg.themeParams.header_bg_color || tg.themeParams.secondary_bg_color || '#efeff4');
404
- style.setProperty('--tg-theme-section-bg-color', tg.themeParams.section_bg_color || tg.themeParams.bg_color || '#ffffff');
405
- style.setProperty('--tg-theme-section-header-text-color', tg.themeParams.section_header_text_color || tg.themeParams.hint_color || '#8e8e93');
406
- style.setProperty('--tg-theme-destructive-text-color', tg.themeParams.destructive_text_color || '#ff3b30');
407
- style.setProperty('--tg-theme-accent-text-color', tg.themeParams.accent_text_color || tg.themeParams.link_color || '#007aff');
 
 
 
 
 
 
 
 
408
  }
409
 
410
  async function apiCall(endpoint, method = 'GET', body = null) {
411
  const headers = { 'Content-Type': 'application/json' };
412
- if (tg.initData) headers['X-Telegram-Auth'] = tg.initData;
 
 
413
  const options = { method, headers };
414
  if (body) options.body = JSON.stringify(body);
415
-
416
- tg.MainButton.showProgress();
417
  try {
418
  const response = await fetch(endpoint, options);
419
  if (!response.ok) {
420
- const errorData = await response.json().catch(() => ({ error: 'Request failed without JSON body' }));
421
  throw new Error(errorData.error || `HTTP error ${response.status}`);
422
  }
423
  return response.json();
424
  } catch (error) {
425
  console.error('API Call Error:', error);
426
  tg.showAlert(error.message || 'An API error occurred.');
427
- tg.HapticFeedback.notificationOccurred('error');
428
  throw error;
429
- } finally {
430
- tg.MainButton.hideProgress();
431
  }
432
  }
433
 
434
  function renderList(items, type) {
435
- const contentDiv = document.getElementById('mainContent');
436
- if (!items || items.length === 0) {
437
- contentDiv.innerHTML = `<div class="empty-state">No ${type.replace('_', ' ')} found. Be the first to add one!</div>`;
438
- return;
439
- }
440
- contentDiv.innerHTML = items.map(item => `
441
- <div class="list-item" onclick="showDetailView('${type}', '${item.id}'); tg.HapticFeedback.selectionChanged();">
442
- <h3>${item.title || item.name || 'Untitled'}</h3>
443
- ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
444
- ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
445
- <p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>
446
- </div>
447
- `).join('');
 
 
 
 
 
 
448
  }
449
-
450
  function showDetailView(type, id) {
 
451
  tg.BackButton.show();
452
- tg.BackButton.onClick(() => { loadView(type); tg.HapticFeedback.impactOccurred('light'); });
453
- tg.MainButton.hide();
 
 
 
454
  document.getElementById('fabButton').style.display = 'none';
 
455
 
456
  apiCall(`/api/${type}/${id}`)
457
  .then(item => {
458
  currentItem = item;
459
- const contentDiv = document.getElementById('mainContent');
460
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
 
 
461
  if (type === 'resumes') {
462
  detailsHtml += `
463
- <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
464
- <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
465
- <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
466
- <p><strong>Contact:</strong> ${item.contact || ('@' + item.user_telegram_username)}</p>
467
- ${item.portfolio_link ? `<p><strong>Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" rel="noopener noreferrer">${item.portfolio_link}</a></p>` : ''}
468
  `;
469
  } else if (type === 'vacancies') {
470
  detailsHtml += `
471
- <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
472
- <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
473
- <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
474
- <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
475
- <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
476
- <p><strong>Contact/Apply:</strong> ${item.contact || ('@' + item.user_telegram_username)}</p>
477
  `;
478
  } else if (type === 'freelance_offers') {
479
  detailsHtml += `
480
- <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
481
- <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
482
- <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
483
- <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
484
- <p><strong>Contact:</strong> ${item.contact || ('@' + item.user_telegram_username)}</p>
485
  `;
486
  }
487
- detailsHtml += `<p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p></div>`;
488
- contentDiv.innerHTML = detailsHtml;
489
-
490
- if (currentUser && String(item.user_id) === String(currentUser.id)) {
491
- tg.MainButton.setText('Edit My Post');
492
- tg.MainButton.onClick(() => { showForm(type, item); tg.HapticFeedback.impactOccurred('medium'); });
493
- tg.MainButton.show();
494
-
495
- const detailViewDiv = contentDiv.querySelector('.detail-view');
496
- if (detailViewDiv) {
497
- const deleteButtonHtml = \`<button id="deletePostButton" class="delete-post-button">Delete Post</button>\`;
498
- detailViewDiv.insertAdjacentHTML('beforeend', deleteButtonHtml);
499
- document.getElementById('deletePostButton').addEventListener('click', () => {
500
- tg.HapticFeedback.impactOccurred('medium');
501
- handleDeletePost(type, item.id);
502
- });
503
- }
504
  }
 
 
505
  })
506
  .catch(err => {
507
- document.getElementById('mainContent').innerHTML = \`<div class="empty-state">Error loading details. Check connection.</div>\`;
508
  });
509
  }
510
-
511
- async function handleDeletePost(type, itemId) {
512
- tg.showConfirm(\`Are you sure you want to delete this ${type.slice(0,-1)}?\`, async (confirmed) => {
 
 
 
 
 
 
 
 
 
 
513
  if (confirmed) {
514
- try {
515
- await apiCall(\`/api/${type}/${itemId}\`, 'DELETE');
516
- tg.HapticFeedback.notificationOccurred('success');
517
- tg.showAlert(\`${type.slice(0,-1)} deleted successfully.\`);
518
- loadView(currentView);
519
- } catch (error) {
520
- tg.HapticFeedback.notificationOccurred('error');
521
- }
522
  }
523
  });
524
  }
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  function showForm(type, itemToEdit = null) {
 
527
  currentItem = itemToEdit;
528
  tg.BackButton.show();
529
  tg.BackButton.onClick(() => {
 
530
  if (itemToEdit) showDetailView(type, itemToEdit.id);
531
  else loadView(type);
532
- tg.HapticFeedback.impactOccurred('light');
533
  });
534
  document.getElementById('fabButton').style.display = 'none';
535
 
536
- const contentDiv = document.getElementById('mainContent');
537
- let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1).replace('_', ' ')}</h2>`;
538
-
539
- if (type === 'resumes') {
540
- formHtml += \`
541
- <div class="form-group"><label for="name">Full Name</label><input type="text" id="name" value="\${itemToEdit?.name || ''}" required></div>
542
- <div class="form-group"><label for="title">Job Title / Desired Position</label><input type="text" id="title" value="\${itemToEdit?.title || ''}" required></div>
543
- <div class="form-group"><label for="skills">Skills (comma separated)</label><textarea id="skills">\${itemToEdit?.skills || ''}</textarea></div>
544
- <div class="form-group"><label for="experience">Experience</label><textarea id="experience">\${itemToEdit?.experience || ''}</textarea></div>
545
- <div class="form-group"><label for="education">Education</label><textarea id="education">\${itemToEdit?.education || ''}</textarea></div>
546
- <div class="form-group"><label for="contact">Contact Info (e.g., email, or blank for Telegram)</label><input type="text" id="contact" value="\${itemToEdit?.contact || ''}"></div>
547
- <div class="form-group"><label for="portfolio_link">Portfolio Link (optional)</label><input type="url" id="portfolio_link" value="\${itemToEdit?.portfolio_link || ''}"></div>
548
- \`;
549
- } else if (type === 'vacancies') {
550
- formHtml += \`
551
- <div class="form-group"><label for="company_name">Company Name</label><input type="text" id="company_name" value="\${itemToEdit?.company_name || ''}" required></div>
552
- <div class="form-group"><label for="title">Job Title</label><input type="text" id="title" value="\${itemToEdit?.title || ''}" required></div>
553
- <div class="form-group"><label for="description">Description</label><textarea id="description">\${itemToEdit?.description || ''}</textarea></div>
554
- <div class="form-group"><label for="requirements">Requirements</label><textarea id="requirements">\${itemToEdit?.requirements || ''}</textarea></div>
555
- <div class="form-group"><label for="salary">Salary/Compensation</label><input type="text" id="salary" value="\${itemToEdit?.salary || ''}"></div>
556
- <div class="form-group"><label for="location">Location (e.g., Remote, City)</label><input type="text" id="location" value="\${itemToEdit?.location || ''}"></div>
557
- <div class="form-group"><label for="contact">Contact Info / How to Apply</label><textarea id="contact">\${itemToEdit?.contact || ''}</textarea></div>
558
- \`;
559
- } else if (type === 'freelance_offers') {
560
- formHtml += \`
561
- <div class="form-group"><label for="title">Project Title</label><input type="text" id="title" value="\${itemToEdit?.title || ''}" required></div>
562
- <div class="form-group"><label for="description">Description of Work</label><textarea id="description">\${itemToEdit?.description || ''}</textarea></div>
563
- <div class="form-group"><label for="budget">Budget</label><input type="text" id="budget" value="\${itemToEdit?.budget || ''}"></div>
564
- <div class="form-group"><label for="deadline">Expected Deadline</label><input type="text" id="deadline" value="\${itemToEdit?.deadline || ''}"></div>
565
- <div class="form-group"><label for="skills_needed">Skills Needed (comma separated)</label><textarea id="skills_needed">\${itemToEdit?.skills_needed || ''}</textarea></div>
566
- <div class="form-group"><label for="contact">Contact Info (or blank for Telegram)</label><input type="text" id="contact" value="\${itemToEdit?.contact || ''}"></div>
567
- \`;
568
- }
569
- formHtml += \`<div id="formError" class="error-message"></div></div>\`;
570
- contentDiv.innerHTML = formHtml;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
  tg.MainButton.setText(itemToEdit ? 'Save Changes' : 'Post');
573
  tg.MainButton.show();
574
- tg.MainButton.onClick(() => { handleSubmit(type, itemToEdit ? itemToEdit.id : null); tg.HapticFeedback.impactOccurred('medium'); });
575
  }
576
 
577
  function handleSubmit(type, itemId = null) {
578
  const payload = {};
579
  let isValid = true;
580
- document.getElementById('formError').textContent = '';
 
581
 
582
  if (type === 'resumes') {
583
  payload.name = document.getElementById('name').value.trim();
@@ -608,128 +737,63 @@ MAIN_APP_TEMPLATE = '''
608
  }
609
 
610
  if (!isValid) {
611
- document.getElementById('formError').textContent = 'Please fill in all required fields.';
612
  tg.HapticFeedback.notificationOccurred('error');
613
  return;
614
  }
615
 
 
 
616
  const method = itemId ? 'PUT' : 'POST';
617
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
618
 
619
  apiCall(endpoint, method, payload)
620
  .then(response => {
621
  tg.HapticFeedback.notificationOccurred('success');
 
 
622
  loadView(type);
623
  })
624
  .catch(err => {
625
- document.getElementById('formError').textContent = err.message || 'Failed to submit.';
 
 
626
  });
627
  }
628
 
629
  function loadView(tabName) {
630
  currentView = tabName;
631
- document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
632
- document.querySelector(\`.tab-button[data-tab="\${tabName}"]\`).classList.add('active');
633
 
634
- const contentDiv = document.getElementById('mainContent');
635
- contentDiv.innerHTML = \`<div class="loading">Loading \${tabName.replace('_', ' ')}...</div>\`;
636
  tg.BackButton.hide();
637
  tg.MainButton.hide();
638
-
639
- if (tabName === 'my_activity') {
640
- renderMyActivityPage();
641
- document.getElementById('fabButton').style.display = 'none';
642
- } else {
643
- document.getElementById('fabButton').style.display = 'flex';
644
- apiCall(`/api/${tabName}`)
645
- .then(data => renderList(data, tabName))
646
- .catch(err => {
647
- contentDiv.innerHTML = \`<div class="empty-state">Error loading \${tabName.replace('_', ' ')}. Please check your connection.</div>\`;
648
- });
649
- }
650
- }
651
-
652
- async function renderMyActivityPage() {
653
- const contentDiv = document.getElementById('mainContent');
654
- if (!currentUser) {
655
- contentDiv.innerHTML = \`<div class="empty-state">Please wait, authenticating... Or reload if this persists.</div>\`;
656
- return;
657
- }
658
- contentDiv.innerHTML = \`<div class="loading">Loading your activity...</div>\`;
659
-
660
- let activityHtml = \`<div class="detail-view profile-section">\`;
661
- activityHtml += \`
662
- <div class="profile-header">
663
- \${currentUser.photo_url ? \`<img src="\${currentUser.photo_url}" alt="avatar">\` : '<div style="width:80px;height:80px;border-radius:50%;background-color:var(--tg-theme-secondary-bg-color);display:flex;align-items:center;justify-content:center;margin:0 auto 10px;"><span style="font-size:30px;">\${(currentUser.first_name || 'U')[0]}</span></div>'}
664
- <h2>\${currentUser.first_name || ''} \${currentUser.last_name || ''}</h2>
665
- <p>@\${currentUser.username || 'anonymous'}</p>
666
- </div>
667
- \`;
668
-
669
- try {
670
- const [resumes, vacancies, offers] = await Promise.all([
671
- apiCall('/api/resumes').catch(() => []),
672
- apiCall('/api/vacancies').catch(() => []),
673
- apiCall('/api/freelance_offers').catch(() => [])
674
- ]);
675
-
676
- const myResumes = resumes.filter(r => String(r.user_id) === String(currentUser.id));
677
- const myVacancies = vacancies.filter(v => String(v.user_id) === String(currentUser.id));
678
- const myOffers = offers.filter(f => String(f.user_id) === String(currentUser.id));
679
-
680
- activityHtml += \`<h3>My Resumes (\${myResumes.length})</h3>\`;
681
- if (myResumes.length > 0) {
682
- activityHtml += myResumes.map(item => \`
683
- <div class="list-item" onclick="showDetailView('resumes', '\${item.id}'); tg.HapticFeedback.selectionChanged();">
684
- <h4>\${item.title || item.name}</h4>
685
- <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
686
- </div>\`).join('');
687
- } else { activityHtml += \`<p>You haven't posted any resumes.</p>\`; }
688
-
689
- activityHtml += \`<h3>My Vacancies (\${myVacancies.length})</h3>\`;
690
- if (myVacancies.length > 0) {
691
- activityHtml += myVacancies.map(item => \`
692
- <div class="list-item" onclick="showDetailView('vacancies', '\${item.id}'); tg.HapticFeedback.selectionChanged();">
693
- <h4>\${item.title} - \${item.company_name}</h4>
694
- <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
695
- </div>\`).join('');
696
- } else { activityHtml += \`<p>You haven't posted any vacancies.</p>\`; }
697
-
698
- activityHtml += \`<h3>My Freelance Offers (\${myOffers.length})</h3>\`;
699
- if (myOffers.length > 0) {
700
- activityHtml += myOffers.map(item => \`
701
- <div class="list-item" onclick="showDetailView('freelance_offers', '\${item.id}'); tg.HapticFeedback.selectionChanged();">
702
- <h4>\${item.title}</h4>
703
- <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
704
- </div>\`).join('');
705
- } else { activityHtml += \`<p>You haven't posted any freelance offers.</p>\`; }
706
- activityHtml += \`</div>\`;
707
- contentDiv.innerHTML = activityHtml;
708
 
709
- } catch (error) {
710
- contentDiv.innerHTML = \`<div class="empty-state">Error loading your activity.</div>\`;
711
- }
 
 
712
  }
713
 
714
  function handleSwipeGesture() {
715
- if (tg.BackButton.isVisible) return;
716
-
717
- const activeTabButton = document.querySelector('.tab-button.active');
718
- if (!activeTabButton) return;
719
 
720
- const currentIndex = TABS_ORDER.indexOf(activeTabButton.dataset.tab);
721
 
722
- if (touchendX < touchstartX - SWIPE_THRESHOLD) {
723
- if (currentIndex < TABS_ORDER.length - 1) {
724
- loadView(TABS_ORDER[currentIndex + 1]);
725
- tg.HapticFeedback.selectionChanged();
726
- }
727
  }
728
- if (touchendX > touchstartX + SWIPE_THRESHOLD) {
729
- if (currentIndex > 0) {
730
- loadView(TABS_ORDER[currentIndex - 1]);
731
- tg.HapticFeedback.selectionChanged();
732
- }
733
  }
734
  }
735
 
@@ -740,39 +804,57 @@ MAIN_APP_TEMPLATE = '''
740
  tg.enableClosingConfirmation();
741
 
742
  const userInfoDiv = document.getElementById('userInfo');
743
- userInfoDiv.innerHTML = \`<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="avatar" style="opacity:0;"> Authenticating...\`;
744
-
 
745
 
 
 
 
 
 
 
 
 
 
746
  try {
747
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
748
  currentUser = authResponse.user;
749
  if (currentUser) {
750
- let avatarHtml = '';
751
- if (currentUser.photo_url) {
752
- avatarHtml = \`<img src="\${currentUser.photo_url}" alt="avatar">\`;
753
- } else {
754
- avatarHtml = \`<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="avatar" style="display:none;">\`;
755
- }
756
- userInfoDiv.innerHTML = \`\${avatarHtml} \${currentUser.first_name || ''} (@\${currentUser.username || 'anonymous'})\`;
757
- } else {
758
- userInfoDiv.textContent = \`Welcome, \${tg.initDataUnsafe.user?.first_name || 'User'}! (@\${tg.initDataUnsafe.user?.username || 'anonymous'})\`;
 
 
759
  }
760
  } catch (error) {
761
  console.error("Auth error:", error);
762
- userInfoDiv.textContent = \`Auth failed. \${tg.initDataUnsafe.user?.first_name || 'User'}\`;
763
  tg.showAlert("Authentication with the server failed. Some features might not work correctly.");
764
  }
765
 
766
-
767
- document.querySelectorAll('.tab-button').forEach(button => {
768
- button.addEventListener('click', () => { loadView(button.dataset.tab); tg.HapticFeedback.impactOccurred('light'); });
 
 
 
 
 
 
769
  });
770
- document.getElementById('fabButton').addEventListener('click', () => { showForm(currentView); tg.HapticFeedback.impactOccurred('medium'); });
771
 
772
- document.body.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; }, { passive: true });
773
- document.body.addEventListener('touchend', e => { touchendX = e.changedTouches[0].screenX; handleSwipeGesture(); }, { passive: true });
774
-
775
- tg.onEvent('themeChanged', applyThemeParams);
 
776
 
777
  loadView('resumes');
778
  }
@@ -908,41 +990,46 @@ def auth_user():
908
  else:
909
  return jsonify({"error": "Authentication data not provided"}), 401
910
 
911
- is_valid, user_data_dict = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
912
 
913
- if not is_valid or not user_data_dict:
914
  return jsonify({"error": "Invalid authentication data"}), 403
915
 
916
  data = load_data()
917
  users = data.get('users', {})
918
- user_id_str = str(user_data_dict.get('id'))
919
 
920
  if user_id_str not in users:
921
  users[user_id_str] = {
922
- 'id': user_data_dict.get('id'),
923
- 'first_name': user_data_dict.get('first_name'),
924
- 'last_name': user_data_dict.get('last_name'),
925
- 'username': user_data_dict.get('username'),
926
- 'language_code': user_data_dict.get('language_code'),
927
- 'photo_url': user_data_dict.get('photo_url'),
928
  'first_seen': datetime.now().isoformat()
929
  }
930
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
931
- if 'photo_url' in user_data_dict: # Ensure photo_url is updated if changed
932
- users[user_id_str]['photo_url'] = user_data_dict.get('photo_url')
 
 
 
 
933
 
934
  data['users'] = users
935
  save_data(data)
936
 
937
  return jsonify({"message": "User authenticated", "user": users[user_id_str]}), 200
938
 
939
- def get_authenticated_user(request_headers):
940
- auth_data_str = request_headers.get('X-Telegram-Auth')
941
  if not auth_data_str:
942
  return None
943
- is_valid, user_data_dict = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
944
- if is_valid and user_data_dict:
945
- return user_data_dict
 
946
  return None
947
 
948
  @app.route('/api/<item_type>', methods=['GET'])
@@ -965,8 +1052,8 @@ def get_item(item_type, item_id):
965
 
966
  @app.route('/api/<item_type>', methods=['POST'])
967
  def create_item(item_type):
968
- user = get_authenticated_user(request.headers)
969
- if not user:
970
  return jsonify({"error": "Authentication required"}), 401
971
 
972
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
@@ -978,8 +1065,8 @@ def create_item(item_type):
978
 
979
  new_item = {
980
  "id": str(uuid.uuid4()),
981
- "user_id": str(user.get('id')),
982
- "user_telegram_username": user.get('username', 'unknown'),
983
  "timestamp": datetime.now().isoformat(),
984
  }
985
 
@@ -1020,8 +1107,8 @@ def create_item(item_type):
1020
 
1021
  @app.route('/api/<item_type>/<item_id>', methods=['PUT'])
1022
  def update_item(item_type, item_id):
1023
- user = get_authenticated_user(request.headers)
1024
- if not user: return jsonify({"error": "Authentication required"}), 401
1025
 
1026
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
1027
  return jsonify({"error": "Invalid item type"}), 400
@@ -1040,7 +1127,8 @@ def update_item(item_type, item_id):
1040
  if item_index == -1: return jsonify({"error": "Item not found"}), 404
1041
 
1042
  original_item = items_list[item_index]
1043
- if str(original_item.get('user_id')) != str(user.get('id')):
 
1044
  return jsonify({"error": "Forbidden: You can only edit your own items"}), 403
1045
 
1046
  updated_item = original_item.copy()
@@ -1082,8 +1170,8 @@ def update_item(item_type, item_id):
1082
 
1083
  @app.route('/api/<item_type>/<item_id>', methods=['DELETE'])
1084
  def delete_item(item_type, item_id):
1085
- user = get_authenticated_user(request.headers)
1086
- if not user: return jsonify({"error": "Authentication required"}), 401
1087
 
1088
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
1089
  return jsonify({"error": "Invalid item type"}), 400
@@ -1095,7 +1183,7 @@ def delete_item(item_type, item_id):
1095
  item_to_delete = next((i for i in items_list if i['id'] == item_id), None)
1096
  if not item_to_delete: return jsonify({"error": "Item not found"}), 404
1097
 
1098
- if str(item_to_delete.get('user_id')) != str(user.get('id')):
1099
  return jsonify({"error": "Forbidden: You can only delete your own items"}), 403
1100
 
1101
  data[item_type] = [i for i in items_list if i['id'] != item_id]
 
25
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
26
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
 
28
+ TELEGRAM_BOT_TOKEN = "7549355625:AAGhdbf6x1JEzpH0mUtuxTF83Soi7MFVNZ8" # Replace with your actual bot token if needed
29
 
30
  DOWNLOAD_RETRIES = 3
31
  DOWNLOAD_DELAY = 5
 
66
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
67
  except Exception as create_e:
68
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
69
+ success = True
70
+ break
71
  else:
72
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
73
  except Exception as e:
 
202
  <html lang="en">
203
  <head>
204
  <meta charset="UTF-8">
205
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
206
  <title>TonTalent</title>
207
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
208
  <style>
 
229
  overscroll-behavior-y: none;
230
  -webkit-font-smoothing: antialiased;
231
  -moz-osx-font-smoothing: grayscale;
232
+ line-height: 1.4;
 
 
233
  }
234
+ .app-container { display: flex; flex-direction: column; min-height: 100vh; min-height: -webkit-fill-available; }
235
  .header {
236
  background-color: var(--tg-theme-header-bg-color);
237
+ padding: 12px 15px;
238
  text-align: center;
239
  font-weight: 600;
240
  font-size: 17px;
 
243
  top: 0;
244
  z-index: 100;
245
  }
246
+ .user-info {
247
+ display: flex;
248
+ align-items: center;
249
+ padding: 10px 15px;
250
+ background-color: var(--tg-theme-section-bg-color);
251
+ font-size: 14px;
252
+ color: var(--tg-theme-text-color);
253
  border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color);
254
+ }
255
+ .profile-avatar {
256
+ width: 40px;
257
+ height: 40px;
258
+ border-radius: 50%;
259
+ margin-right: 12px;
260
+ object-fit: cover;
261
+ background-color: var(--tg-theme-secondary-bg-color);
262
+ }
263
+ .profile-avatar-placeholder {
264
+ width: 40px;
265
+ height: 40px;
266
+ border-radius: 50%;
267
+ margin-right: 12px;
268
+ background-color: var(--tg-theme-button-color);
269
+ color: var(--tg-theme-button-text-color);
270
  display: flex;
271
  align-items: center;
272
+ justify-content: center;
273
+ font-weight: 600;
274
+ font-size: 18px;
275
  }
276
+ .user-info-text strong { font-weight: 500; display: block; }
277
+ .user-info-text span { font-size: 13px; color: var(--tg-theme-hint-color); }
278
 
279
+ .tabs { display: flex; background-color: var(--tg-theme-section-bg-color); padding: 8px 10px; border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color); }
280
  .tab-button {
281
  flex: 1;
282
+ padding: 10px 5px;
283
  text-align: center;
284
  cursor: pointer;
285
  background: none;
 
287
  color: var(--tg-theme-hint-color);
288
  font-size: 15px;
289
  font-weight: 500;
290
+ border-bottom: 2px solid transparent;
291
  transition: color 0.2s ease, border-bottom-color 0.2s ease;
292
+ display: flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ gap: 6px;
296
  }
297
+ .tab-button svg { width: 18px; height: 18px; fill: currentColor; }
298
+ .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); }
299
+ .content { flex-grow: 1; padding: 10px; overflow-y: auto; transition: opacity 0.2s ease-in-out; }
300
+ .content.fade-out { opacity: 0; }
301
+ .content.fade-in { opacity: 1; }
302
+
303
  .list-item {
304
  background-color: var(--tg-theme-section-bg-color);
305
  border-radius: 10px;
306
  padding: 12px 15px;
307
+ margin-bottom: 8px;
308
+ box-shadow: 0 1px 2px rgba(0,0,0,0.03), 0 2px 6px rgba(0,0,0,0.03);
309
  cursor: pointer;
310
+ transition: transform 0.1s ease-out, background-color 0.2s ease;
 
 
 
 
 
311
  }
312
+ .list-item:active { transform: scale(0.97); background-color: var(--tg-theme-secondary-bg-color); }
313
+ .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 500; color: var(--tg-theme-text-color); }
314
+ .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); }
315
+ .list-item .meta { font-size: 12px; color: var(--tg-theme-hint-color); margin-top: 6px; }
316
 
317
  .form-container { padding: 15px; background-color: var(--tg-theme-section-bg-color); }
318
+ .form-group { margin-bottom: 16px; }
319
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
320
+ .form-group input, .form-group textarea, .form-group select {
321
  width: 100%;
322
  padding: 12px;
323
  border: 1px solid var(--tg-theme-secondary-bg-color);
 
326
  background-color: var(--tg-theme-bg-color);
327
  color: var(--tg-theme-text-color);
328
  box-sizing: border-box;
329
+ -webkit-appearance: none;
330
+ -moz-appearance: none;
331
+ appearance: none;
 
 
332
  }
333
+ .form-group input:focus, .form-group textarea:focus { border-color: var(--tg-theme-link-color); box-shadow: 0 0 0 2px rgba(var(--tg-theme-link-color-rgb), 0.2); outline: none; }
334
  .form-group textarea { min-height: 100px; resize: vertical; }
335
+
336
  .fab {
337
  position: fixed;
338
+ bottom: calc(20px + env(safe-area-inset-bottom));
339
  right: 20px;
340
  width: 56px;
341
  height: 56px;
 
345
  display: flex;
346
  align-items: center;
347
  justify-content: center;
348
+ font-size: 28px;
349
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.1);
350
  cursor: pointer;
351
  z-index: 1000;
352
  border: none;
353
+ transition: transform 0.15s ease-out;
354
  }
355
  .fab:active { transform: scale(0.92); }
356
+ .fab svg { width: 24px; height: 24px; fill: currentColor; }
357
 
358
+ .detail-view { padding: 15px; background-color: var(--tg-theme-section-bg-color); }
359
+ .detail-view h2 { margin-top: 0; font-size: 22px; font-weight: 600; color: var(--tg-theme-text-color); margin-bottom: 12px; }
360
+ .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; }
361
+ .detail-view strong { font-weight: 500; color: var(--tg-theme-text-color); } /* Make strong stand out more */
362
+ .detail-view .meta-label { color: var(--tg-theme-hint-color); font-weight: normal; }
363
  .detail-view a { color: var(--tg-theme-link-color); text-decoration: none; }
364
  .detail-view a:hover { text-decoration: underline; }
365
+ .item-actions { margin-top: 20px; display: flex; gap: 10px; justify-content: flex-start; padding-bottom: 10px; }
366
+ .action-button {
367
+ padding: 10px 18px;
368
+ border-radius: 8px;
369
+ border: none;
370
+ font-weight: 500;
371
+ font-size: 15px;
372
+ cursor: pointer;
373
+ transition: opacity 0.2s;
374
  }
375
+ .action-button:active { opacity: 0.7; }
376
+ .edit-button { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
377
+ .delete-button { background-color: var(--tg-theme-destructive-text-color); color: var(--tg-theme-button-text-color); }
378
+
379
  .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
380
  .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 8px; }
 
 
 
 
 
 
 
 
381
  </style>
382
  </head>
383
  <body>
384
  <div class="app-container">
385
  <div class="header">TonTalent</div>
386
+ <div class="user-info" id="userInfo"><div class="profile-avatar-placeholder">?</div><div class="user-info-text"><span>Loading user...</span></div></div>
387
+
388
  <div class="tabs">
389
+ <button class="tab-button active" data-tab="resumes">
390
+ <svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
391
+ Resumes
392
+ </button>
393
+ <button class="tab-button" data-tab="vacancies">
394
+ <svg viewBox="0 0 24 24"><path d="M20 6h-4V4c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-6 0h-4V4h4v2z"/></svg>
395
+ Vacancies
396
+ </button>
397
+ <button class="tab-button" data-tab="freelance_offers">
398
+ <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 12.17l7.59-7.59L19 6l-9 9z"/></svg>
399
+ Freelance
400
+ </button>
401
  </div>
402
  <div class="content" id="mainContent">
403
  <div class="loading">Loading content...</div>
404
  </div>
405
  <button class="fab" id="fabButton" title="Add New Item">
406
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
407
  </button>
408
  </div>
409
 
 
412
  let currentUser = null;
413
  let currentView = 'resumes';
414
  let currentItem = null;
415
+ const mainContent = document.getElementById('mainContent');
416
+ const tabButtons = Array.from(document.querySelectorAll('.tab-button'));
417
+ let touchStartX = 0;
418
+ let touchEndX = 0;
419
+ const swipeThreshold = 70;
420
 
421
  function applyThemeParams() {
422
+ const root = document.documentElement;
423
+ root.style.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
424
+ root.style.setProperty('--tg-theme-text-color', tg.themeParams.text_color || '#000000');
425
+ root.style.setProperty('--tg-theme-hint-color', tg.themeParams.hint_color || '#999999');
426
+ root.style.setProperty('--tg-theme-link-color', tg.themeParams.link_color || '#007aff');
427
+ root.style.setProperty('--tg-theme-button-color', tg.themeParams.button_color || '#007aff');
428
+ root.style.setProperty('--tg-theme-button-text-color', tg.themeParams.button_text_color || '#ffffff');
429
+ root.style.setProperty('--tg-theme-secondary-bg-color', tg.themeParams.secondary_bg_color || '#f0f0f0');
430
+
431
+ root.style.setProperty('--tg-theme-header-bg-color', tg.themeParams.header_bg_color || tg.themeParams.secondary_bg_color || '#efeff4');
432
+ root.style.setProperty('--tg-theme-section-bg-color', tg.themeParams.section_bg_color || tg.themeParams.bg_color || '#ffffff');
433
+ root.style.setProperty('--tg-theme-section-header-text-color', tg.themeParams.section_header_text_color || tg.themeParams.hint_color || '#8e8e93');
434
+ root.style.setProperty('--tg-theme-destructive-text-color', tg.themeParams.destructive_text_color || '#ff3b30');
435
+ root.style.setProperty('--tg-theme-accent-text-color', tg.themeParams.accent_text_color || tg.themeParams.link_color || '#007aff');
436
+
437
+ try {
438
+ constlinkColorRGB = tg.themeParams.link_color.match(/\\w\\w/g).map(x => parseInt(x, 16));
439
+ root.style.setProperty('--tg-theme-link-color-rgb', `${linkColorRGB[0]}, ${linkColorRGB[1]}, ${linkColorRGB[2]}`);
440
+ } catch(e) {
441
+ root.style.setProperty('--tg-theme-link-color-rgb', '0, 122, 255');
442
+ }
443
  }
444
 
445
  async function apiCall(endpoint, method = 'GET', body = null) {
446
  const headers = { 'Content-Type': 'application/json' };
447
+ if (tg.initData) {
448
+ headers['X-Telegram-Auth'] = tg.initData;
449
+ }
450
  const options = { method, headers };
451
  if (body) options.body = JSON.stringify(body);
 
 
452
  try {
453
  const response = await fetch(endpoint, options);
454
  if (!response.ok) {
455
+ const errorData = await response.json().catch(() => ({ error: 'Request failed, server returned ' + response.status }));
456
  throw new Error(errorData.error || `HTTP error ${response.status}`);
457
  }
458
  return response.json();
459
  } catch (error) {
460
  console.error('API Call Error:', error);
461
  tg.showAlert(error.message || 'An API error occurred.');
 
462
  throw error;
 
 
463
  }
464
  }
465
 
466
  function renderList(items, type) {
467
+ mainContent.classList.remove('fade-in');
468
+ mainContent.classList.add('fade-out');
469
+
470
+ setTimeout(() => {
471
+ if (!items || items.length === 0) {
472
+ mainContent.innerHTML = `<div class="empty-state">No ${type} found. Be the first to add one!</div>`;
473
+ } else {
474
+ mainContent.innerHTML = items.map(item => `
475
+ <div class="list-item" onclick="showDetailView('${type}', '${item.id}')">
476
+ <h3>${item.title || item.name || 'Untitled'}</h3>
477
+ ${type === 'vacancies' && item.company_name ? `<p><span class="meta-label">Company:</span> ${item.company_name}</p>` : ''}
478
+ ${type === 'freelance_offers' && item.budget ? `<p><span class="meta-label">Budget:</span> ${item.budget}</p>` : ''}
479
+ <p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>
480
+ </div>
481
+ `).join('');
482
+ }
483
+ mainContent.classList.remove('fade-out');
484
+ mainContent.classList.add('fade-in');
485
+ }, 200);
486
  }
487
+
488
  function showDetailView(type, id) {
489
+ tg.HapticFeedback.impactOccurred('light');
490
  tg.BackButton.show();
491
+ tg.BackButton.onClick(() => {
492
+ tg.HapticFeedback.impactOccurred('light');
493
+ loadView(type);
494
+ tg.MainButton.hide();
495
+ });
496
  document.getElementById('fabButton').style.display = 'none';
497
+ mainContent.innerHTML = `<div class="loading">Loading details...</div>`;
498
 
499
  apiCall(`/api/${type}/${id}`)
500
  .then(item => {
501
  currentItem = item;
 
502
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
503
+ const nl2br = (str) => str ? str.replace(/\\n/g, '<br>') : 'N/A';
504
+
505
  if (type === 'resumes') {
506
  detailsHtml += `
507
+ <p><strong class="meta-label">Skills:</strong> ${item.skills || 'N/A'}</p>
508
+ <p><strong class="meta-label">Experience:</strong><br>${nl2br(item.experience)}</p>
509
+ <p><strong class="meta-label">Education:</strong><br>${nl2br(item.education)}</p>
510
+ <p><strong class="meta-label">Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
511
+ ${item.portfolio_link ? `<p><strong class="meta-label">Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" rel="noopener noreferrer">${item.portfolio_link}</a></p>` : ''}
512
  `;
513
  } else if (type === 'vacancies') {
514
  detailsHtml += `
515
+ <p><strong class="meta-label">Company:</strong> ${item.company_name || 'N/A'}</p>
516
+ <p><strong class="meta-label">Description:</strong><br>${nl2br(item.description)}</p>
517
+ <p><strong class="meta-label">Requirements:</strong><br>${nl2br(item.requirements)}</p>
518
+ <p><strong class="meta-label">Salary:</strong> ${item.salary || 'N/A'}</p>
519
+ <p><strong class="meta-label">Location:</strong> ${item.location || 'N/A'}</p>
520
+ <p><strong class="meta-label">Contact/Apply:</strong> ${nl2br(item.contact) || `@${item.user_telegram_username}`}</p>
521
  `;
522
  } else if (type === 'freelance_offers') {
523
  detailsHtml += `
524
+ <p><strong class="meta-label">Description:</strong><br>${nl2br(item.description)}</p>
525
+ <p><strong class="meta-label">Budget:</strong> ${item.budget || 'N/A'}</p>
526
+ <p><strong class="meta-label">Deadline:</strong> ${item.deadline || 'N/A'}</p>
527
+ <p><strong class="meta-label">Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
528
+ <p><strong class="meta-label">Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
529
  `;
530
  }
531
+ detailsHtml += `<p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
532
+
533
+ if (currentUser && item.user_id === String(currentUser.id)) {
534
+ detailsHtml += `
535
+ <div class="item-actions">
536
+ <button class="action-button edit-button" onclick="handleEditCurrentItem('${type}')">Edit Post</button>
537
+ <button class="action-button delete-button" onclick="confirmDeleteItem('${type}', '${item.id}')">Delete Post</button>
538
+ </div>
539
+ `;
 
 
 
 
 
 
 
 
540
  }
541
+ detailsHtml += `</div>`;
542
+ mainContent.innerHTML = detailsHtml;
543
  })
544
  .catch(err => {
545
+ mainContent.innerHTML = `<div class="empty-state">Error loading details. Please try again.</div>`;
546
  });
547
  }
548
+
549
+ function handleEditCurrentItem(type) {
550
+ tg.HapticFeedback.impactOccurred('light');
551
+ if (currentItem) {
552
+ showForm(type, currentItem);
553
+ } else {
554
+ tg.showAlert('Error: Item data not available for editing.');
555
+ }
556
+ }
557
+
558
+ function confirmDeleteItem(type, id) {
559
+ tg.HapticFeedback.impactOccurred('medium');
560
+ tg.showConfirm('Are you sure you want to delete this post?', (confirmed) => {
561
  if (confirmed) {
562
+ handleDeleteItem(type, id);
 
 
 
 
 
 
 
563
  }
564
  });
565
  }
566
 
567
+ function handleDeleteItem(type, id) {
568
+ tg.MainButton.showProgress();
569
+ apiCall(`/api/${type}/${id}`, 'DELETE')
570
+ .then(() => {
571
+ tg.HapticFeedback.notificationOccurred('success');
572
+ tg.MainButton.hideProgress();
573
+ tg.showAlert('Post deleted successfully.');
574
+ loadView(type);
575
+ })
576
+ .catch(err => {
577
+ tg.HapticFeedback.notificationOccurred('error');
578
+ tg.MainButton.hideProgress();
579
+ tg.showAlert(err.message || 'Failed to delete post.');
580
+ });
581
+ }
582
+
583
  function showForm(type, itemToEdit = null) {
584
+ tg.HapticFeedback.impactOccurred('light');
585
  currentItem = itemToEdit;
586
  tg.BackButton.show();
587
  tg.BackButton.onClick(() => {
588
+ tg.HapticFeedback.impactOccurred('light');
589
  if (itemToEdit) showDetailView(type, itemToEdit.id);
590
  else loadView(type);
591
+ tg.MainButton.hide();
592
  });
593
  document.getElementById('fabButton').style.display = 'none';
594
 
595
+ mainContent.classList.remove('fade-in');
596
+ mainContent.classList.add('fade-out');
597
+
598
+ setTimeout(() => {
599
+ let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1).replace('_', ' ')}</h2>`;
600
+
601
+ const val = (field) => itemToEdit?.[field] || '';
602
+
603
+ if (type === 'resumes') {
604
+ formHtml += `
605
+ <div class="form-group">
606
+ <label for="name">Full Name</label>
607
+ <input type="text" id="name" value="${val('name')}" required>
608
+ </div>
609
+ <div class="form-group">
610
+ <label for="title">Job Title / Desired Position</label>
611
+ <input type="text" id="title" value="${val('title')}" required>
612
+ </div>
613
+ <div class="form-group">
614
+ <label for="skills">Skills (comma separated)</label>
615
+ <textarea id="skills">${val('skills')}</textarea>
616
+ </div>
617
+ <div class="form-group">
618
+ <label for="experience">Experience</label>
619
+ <textarea id="experience">${val('experience')}</textarea>
620
+ </div>
621
+ <div class="form-group">
622
+ <label for="education">Education</label>
623
+ <textarea id="education">${val('education')}</textarea>
624
+ </div>
625
+ <div class="form-group">
626
+ <label for="contact">Contact Info (e.g., email, or leave blank to use Telegram)</label>
627
+ <input type="text" id="contact" value="${val('contact')}">
628
+ </div>
629
+ <div class="form-group">
630
+ <label for="portfolio_link">Portfolio Link (optional)</label>
631
+ <input type="url" id="portfolio_link" value="${val('portfolio_link')}">
632
+ </div>
633
+ `;
634
+ } else if (type === 'vacancies') {
635
+ formHtml += `
636
+ <div class="form-group">
637
+ <label for="company_name">Company Name</label>
638
+ <input type="text" id="company_name" value="${val('company_name')}" required>
639
+ </div>
640
+ <div class="form-group">
641
+ <label for="title">Job Title</label>
642
+ <input type="text" id="title" value="${val('title')}" required>
643
+ </div>
644
+ <div class="form-group">
645
+ <label for="description">Description</label>
646
+ <textarea id="description">${val('description')}</textarea>
647
+ </div>
648
+ <div class="form-group">
649
+ <label for="requirements">Requirements</label>
650
+ <textarea id="requirements">${val('requirements')}</textarea>
651
+ </div>
652
+ <div class="form-group">
653
+ <label for="salary">Salary/Compensation</label>
654
+ <input type="text" id="salary" value="${val('salary')}">
655
+ </div>
656
+ <div class="form-group">
657
+ <label for="location">Location (e.g., Remote, City)</label>
658
+ <input type="text" id="location" value="${val('location')}">
659
+ </div>
660
+ <div class="form-group">
661
+ <label for="contact">Contact Info / How to Apply</label>
662
+ <textarea id="contact">${val('contact')}</textarea>
663
+ </div>
664
+ `;
665
+ } else if (type === 'freelance_offers') {
666
+ formHtml += `
667
+ <div class="form-group">
668
+ <label for="title">Project Title</label>
669
+ <input type="text" id="title" value="${val('title')}" required>
670
+ </div>
671
+ <div class="form-group">
672
+ <label for="description">Description of Work</label>
673
+ <textarea id="description">${val('description')}</textarea>
674
+ </div>
675
+ <div class="form-group">
676
+ <label for="budget">Budget</label>
677
+ <input type="text" id="budget" value="${val('budget')}">
678
+ </div>
679
+ <div class="form-group">
680
+ <label for="deadline">Expected Deadline</label>
681
+ <input type="text" id="deadline" value="${val('deadline')}">
682
+ </div>
683
+ <div class="form-group">
684
+ <label for="skills_needed">Skills Needed (comma separated)</label>
685
+ <textarea id="skills_needed">${val('skills_needed')}</textarea>
686
+ </div>
687
+ <div class="form-group">
688
+ <label for="contact">Contact Info (or leave blank to use Telegram)</label>
689
+ <input type="text" id="contact" value="${val('contact')}">
690
+ </div>
691
+ `;
692
+ }
693
+ formHtml += `<div id="formError" class="error-message"></div></div>`;
694
+ mainContent.innerHTML = formHtml;
695
+ mainContent.classList.remove('fade-out');
696
+ mainContent.classList.add('fade-in');
697
+ }, 200);
698
+
699
 
700
  tg.MainButton.setText(itemToEdit ? 'Save Changes' : 'Post');
701
  tg.MainButton.show();
702
+ tg.MainButton.onClick(() => handleSubmit(type, itemToEdit ? itemToEdit.id : null));
703
  }
704
 
705
  function handleSubmit(type, itemId = null) {
706
  const payload = {};
707
  let isValid = true;
708
+ const formErrorDiv = document.getElementById('formError');
709
+ if(formErrorDiv) formErrorDiv.textContent = '';
710
 
711
  if (type === 'resumes') {
712
  payload.name = document.getElementById('name').value.trim();
 
737
  }
738
 
739
  if (!isValid) {
740
+ if(formErrorDiv) formErrorDiv.textContent = 'Please fill in all required fields (marked with * or by name).';
741
  tg.HapticFeedback.notificationOccurred('error');
742
  return;
743
  }
744
 
745
+ tg.MainButton.showProgress();
746
+
747
  const method = itemId ? 'PUT' : 'POST';
748
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
749
 
750
  apiCall(endpoint, method, payload)
751
  .then(response => {
752
  tg.HapticFeedback.notificationOccurred('success');
753
+ tg.MainButton.hideProgress();
754
+ tg.MainButton.hide();
755
  loadView(type);
756
  })
757
  .catch(err => {
758
+ tg.HapticFeedback.notificationOccurred('error');
759
+ tg.MainButton.hideProgress();
760
+ if(formErrorDiv) formErrorDiv.textContent = err.message || 'Failed to submit.';
761
  });
762
  }
763
 
764
  function loadView(tabName) {
765
  currentView = tabName;
766
+ tabButtons.forEach(btn => btn.classList.remove('active'));
767
+ document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active');
768
 
769
+ mainContent.innerHTML = `<div class="loading">Loading ${tabName}...</div>`;
 
770
  tg.BackButton.hide();
771
  tg.MainButton.hide();
772
+ document.getElementById('fabButton').style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
 
774
+ apiCall(`/api/${tabName}`)
775
+ .then(data => renderList(data, tabName))
776
+ .catch(err => {
777
+ mainContent.innerHTML = `<div class="empty-state">Error loading ${tabName}. Please try again.</div>`;
778
+ });
779
  }
780
 
781
  function handleSwipeGesture() {
782
+ const swipeDistance = touchEndX - touchStartX;
783
+ if (Math.abs(swipeDistance) < swipeThreshold) return;
 
 
784
 
785
+ let currentTabIndex = tabButtons.findIndex(btn => btn.classList.contains('active'));
786
 
787
+ if (swipeDistance < 0) {
788
+ currentTabIndex = (currentTabIndex + 1) % tabButtons.length;
789
+ } else {
790
+ currentTabIndex = (currentTabIndex - 1 + tabButtons.length) % tabButtons.length;
 
791
  }
792
+
793
+ const newTabName = tabButtons[currentTabIndex].dataset.tab;
794
+ if (newTabName !== currentView) {
795
+ tg.HapticFeedback.impactOccurred('light');
796
+ loadView(newTabName);
797
  }
798
  }
799
 
 
804
  tg.enableClosingConfirmation();
805
 
806
  const userInfoDiv = document.getElementById('userInfo');
807
+ let initialUserName = tg.initDataUnsafe.user?.first_name || 'User';
808
+ if (tg.initDataUnsafe.user?.last_name) initialUserName += ` ${tg.initDataUnsafe.user.last_name}`;
809
+ let initialUsername = tg.initDataUnsafe.user?.username || 'anonymous';
810
 
811
+ let initialProfileHtml = '';
812
+ if(tg.initDataUnsafe.user?.photo_url) {
813
+ initialProfileHtml += `<img src="${tg.initDataUnsafe.user.photo_url}" alt="${initialUserName}" class="profile-avatar">`;
814
+ } else {
815
+ initialProfileHtml += `<div class="profile-avatar-placeholder">${initialUserName.substring(0,1).toUpperCase()}</div>`;
816
+ }
817
+ initialProfileHtml += `<div class="user-info-text"><strong>${initialUserName}</strong><span>@${initialUsername} (Verifying...)</span></div>`;
818
+ userInfoDiv.innerHTML = initialProfileHtml;
819
+
820
  try {
821
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
822
  currentUser = authResponse.user;
823
  if (currentUser) {
824
+ let userName = currentUser.first_name;
825
+ if (currentUser.last_name) userName += ` ${currentUser.last_name}`;
826
+
827
+ let profileHtml = '';
828
+ if (currentUser.photo_url) {
829
+ profileHtml += `<img src="${currentUser.photo_url}" alt="${userName}" class="profile-avatar">`;
830
+ } else {
831
+ profileHtml += `<div class="profile-avatar-placeholder">${userName.substring(0,1).toUpperCase()}</div>`;
832
+ }
833
+ profileHtml += `<div class="user-info-text"><strong>${userName}</strong><span>@${currentUser.username || 'telegram_user'}</span></div>`;
834
+ userInfoDiv.innerHTML = profileHtml;
835
  }
836
  } catch (error) {
837
  console.error("Auth error:", error);
838
+ userInfoDiv.querySelector('.user-info-text span').textContent = `@${initialUsername} (Auth failed)`;
839
  tg.showAlert("Authentication with the server failed. Some features might not work correctly.");
840
  }
841
 
842
+ tabButtons.forEach(button => {
843
+ button.addEventListener('click', () => {
844
+ tg.HapticFeedback.impactOccurred('light');
845
+ loadView(button.dataset.tab);
846
+ });
847
+ });
848
+ document.getElementById('fabButton').addEventListener('click', () => {
849
+ tg.HapticFeedback.impactOccurred('heavy');
850
+ showForm(currentView);
851
  });
 
852
 
853
+ mainContent.addEventListener('touchstart', e => { touchStartX = e.changedTouches[0].screenX; }, { passive: true });
854
+ mainContent.addEventListener('touchend', e => {
855
+ touchEndX = e.changedTouches[0].screenX;
856
+ handleSwipeGesture();
857
+ }, { passive: true });
858
 
859
  loadView('resumes');
860
  }
 
990
  else:
991
  return jsonify({"error": "Authentication data not provided"}), 401
992
 
993
+ is_valid, user_data_from_tg = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
994
 
995
+ if not is_valid or not user_data_from_tg:
996
  return jsonify({"error": "Invalid authentication data"}), 403
997
 
998
  data = load_data()
999
  users = data.get('users', {})
1000
+ user_id_str = str(user_data_from_tg.get('id'))
1001
 
1002
  if user_id_str not in users:
1003
  users[user_id_str] = {
1004
+ 'id': user_data_from_tg.get('id'), # numeric ID from Telegram
1005
+ 'first_name': user_data_from_tg.get('first_name'),
1006
+ 'last_name': user_data_from_tg.get('last_name'),
1007
+ 'username': user_data_from_tg.get('username'),
1008
+ 'language_code': user_data_from_tg.get('language_code'),
1009
+ 'photo_url': user_data_from_tg.get('photo_url'),
1010
  'first_seen': datetime.now().isoformat()
1011
  }
1012
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
1013
+ # Ensure photo_url is updated if it changed
1014
+ users[user_id_str]['photo_url'] = user_data_from_tg.get('photo_url', users[user_id_str].get('photo_url'))
1015
+ users[user_id_str]['first_name'] = user_data_from_tg.get('first_name', users[user_id_str].get('first_name'))
1016
+ users[user_id_str]['last_name'] = user_data_from_tg.get('last_name', users[user_id_str].get('last_name'))
1017
+ users[user_id_str]['username'] = user_data_from_tg.get('username', users[user_id_str].get('username'))
1018
+
1019
 
1020
  data['users'] = users
1021
  save_data(data)
1022
 
1023
  return jsonify({"message": "User authenticated", "user": users[user_id_str]}), 200
1024
 
1025
+ def get_authenticated_user_from_request(req_headers):
1026
+ auth_data_str = req_headers.get('X-Telegram-Auth')
1027
  if not auth_data_str:
1028
  return None
1029
+ is_valid, user_data = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1030
+ if is_valid and user_data:
1031
+ # user_data contains 'id' (numeric), 'first_name', 'username', 'photo_url' etc.
1032
+ return user_data
1033
  return None
1034
 
1035
  @app.route('/api/<item_type>', methods=['GET'])
 
1052
 
1053
  @app.route('/api/<item_type>', methods=['POST'])
1054
  def create_item(item_type):
1055
+ authenticated_user_info = get_authenticated_user_from_request(request.headers)
1056
+ if not authenticated_user_info:
1057
  return jsonify({"error": "Authentication required"}), 401
1058
 
1059
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
 
1065
 
1066
  new_item = {
1067
  "id": str(uuid.uuid4()),
1068
+ "user_id": str(authenticated_user_info.get('id')), # Store as string
1069
+ "user_telegram_username": authenticated_user_info.get('username', 'unknown'),
1070
  "timestamp": datetime.now().isoformat(),
1071
  }
1072
 
 
1107
 
1108
  @app.route('/api/<item_type>/<item_id>', methods=['PUT'])
1109
  def update_item(item_type, item_id):
1110
+ authenticated_user_info = get_authenticated_user_from_request(request.headers)
1111
+ if not authenticated_user_info: return jsonify({"error": "Authentication required"}), 401
1112
 
1113
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
1114
  return jsonify({"error": "Invalid item type"}), 400
 
1127
  if item_index == -1: return jsonify({"error": "Item not found"}), 404
1128
 
1129
  original_item = items_list[item_index]
1130
+ # Compare user_id from item (string) with user_id from Telegram auth (numeric)
1131
+ if original_item.get('user_id') != str(authenticated_user_info.get('id')):
1132
  return jsonify({"error": "Forbidden: You can only edit your own items"}), 403
1133
 
1134
  updated_item = original_item.copy()
 
1170
 
1171
  @app.route('/api/<item_type>/<item_id>', methods=['DELETE'])
1172
  def delete_item(item_type, item_id):
1173
+ authenticated_user_info = get_authenticated_user_from_request(request.headers)
1174
+ if not authenticated_user_info: return jsonify({"error": "Authentication required"}), 401
1175
 
1176
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
1177
  return jsonify({"error": "Invalid item type"}), 400
 
1183
  item_to_delete = next((i for i in items_list if i['id'] == item_id), None)
1184
  if not item_to_delete: return jsonify({"error": "Item not found"}), 404
1185
 
1186
+ if item_to_delete.get('user_id') != str(authenticated_user_info.get('id')):
1187
  return jsonify({"error": "Forbidden: You can only delete your own items"}), 403
1188
 
1189
  data[item_type] = [i for i in items_list if i['id'] != item_id]