Shveiauto commited on
Commit
00700fb
·
verified ·
1 Parent(s): 4e9cca7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +341 -265
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" # User-provided
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:
@@ -229,41 +229,32 @@ 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
- min-height: calc(100vh - env(safe-area-inset-bottom)); /* iOS notch */
236
  }
237
- .app-container { display: flex; flex-direction: column; flex-grow: 1; }
238
- .app-header {
239
  background-color: var(--tg-theme-header-bg-color);
240
- padding: 10px 15px;
241
- border-bottom: 1px solid var(--tg-theme-secondary-bg-color);
 
 
 
242
  position: sticky;
243
  top: 0;
244
  z-index: 100;
245
  }
246
- .app-header-title { text-align: center; font-weight: 600; font-size: 17px; color: var(--tg-theme-text-color); }
247
- .profile-header {
 
 
248
  display: flex;
249
  align-items: center;
250
- padding: 10px 15px;
251
- background-color: var(--tg-theme-secondary-bg-color);
252
- }
253
- .profile-header img {
254
- width: 44px;
255
- height: 44px;
256
- border-radius: 50%;
257
- margin-right: 12px;
258
- object-fit: cover;
259
- border: 1px solid var(--tg-theme-hint-color);
260
- background-color: var(--tg-theme-bg-color); /* Placeholder bg */
261
  }
262
- .profile-header div { display: flex; flex-direction: column; justify-content: center; }
263
- #profileName { font-weight: 500; font-size: 16px; color: var(--tg-theme-text-color); }
264
- #profileUsername { font-size: 13px; color: var(--tg-theme-hint-color); }
265
 
266
- .tabs { display: flex; background-color: var(--tg-theme-secondary-bg-color); padding: 5px; border-bottom: 1px solid var(--tg-theme-secondary-bg-color); }
267
  .tab-button {
268
  flex: 1;
269
  padding: 12px 5px;
@@ -275,32 +266,36 @@ MAIN_APP_TEMPLATE = '''
275
  font-size: 15px;
276
  font-weight: 500;
277
  border-bottom: 3px solid transparent;
278
- transition: color 0.2s, border-bottom-color 0.2s;
279
- -webkit-tap-highlight-color: transparent;
 
 
 
 
280
  }
 
281
  .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); }
282
- .content {
283
- flex-grow: 1; padding: 15px;
284
- transition: opacity 0.25s ease-in-out;
285
- overflow-y: auto; /* Enable scrolling for content */
286
- -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
287
- }
288
- .content.loading-state { opacity: 0.5; }
289
  .list-item {
290
  background-color: var(--tg-theme-section-bg-color);
291
  border-radius: 10px;
292
- padding: 12px 15px;
293
- margin-bottom: 10px;
294
- box-shadow: 0 2px 6px rgba(0,0,0,0.08);
295
  cursor: pointer;
296
- transition: background-color 0.2s, transform 0.1s;
297
  }
298
- .list-item:active { background-color: var(--tg-theme-secondary-bg-color); transform: scale(0.99); }
299
- .list-item h3 { margin: 0 0 5px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
300
- .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-text-color); opacity: 0.8; }
301
- .list-item .meta { font-size: 12px; color: var(--tg-theme-hint-color); margin-top: 8px; }
302
 
303
- .form-container { padding: 15px; background-color: var(--tg-theme-section-bg-color); }
 
 
 
 
 
304
  .form-group { margin-bottom: 18px; }
305
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
306
  .form-group input, .form-group textarea {
@@ -312,10 +307,11 @@ MAIN_APP_TEMPLATE = '''
312
  background-color: var(--tg-theme-bg-color);
313
  color: var(--tg-theme-text-color);
314
  box-sizing: border-box;
315
- transition: border-color 0.2s;
316
  }
317
  .form-group input:focus, .form-group textarea:focus {
318
  border-color: var(--tg-theme-link-color);
 
319
  outline: none;
320
  }
321
  .form-group textarea { min-height: 100px; resize: vertical; }
@@ -333,65 +329,64 @@ MAIN_APP_TEMPLATE = '''
333
  align-items: center;
334
  justify-content: center;
335
  font-size: 28px;
336
- line-height: 1;
337
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
338
  cursor: pointer;
339
  z-index: 1000;
340
  border: none;
341
- transition: transform 0.15s ease-out, background-color 0.2s;
342
- -webkit-tap-highlight-color: transparent;
343
  }
344
- .fab:active { transform: scale(0.92); background-color: color-mix(in srgb, var(--tg-theme-button-color) 90%, black); }
 
 
 
 
 
 
345
 
346
- .detail-view { padding: 15px; background-color: var(--tg-theme-section-bg-color); }
347
- .detail-view h2 { margin-top: 0; font-size: 22px; font-weight: 600; color: var(--tg-theme-text-color); margin-bottom: 15px;}
348
- .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; color: var(--tg-theme-text-color); }
349
- .detail-view strong { font-weight: 600; color: var(--tg-theme-section-header-text-color); }
350
- .detail-view a { color: var(--tg-theme-link-color); text-decoration: none; }
351
- .detail-view a:hover { text-decoration: underline; }
352
- .detail-view .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 15px; }
353
-
354
- .action-buttons { margin-top: 25px; display: flex; gap: 12px; }
355
- .action-buttons button {
356
- flex-grow: 1;
357
  padding: 12px 15px;
 
 
 
358
  border-radius: 8px;
359
  font-size: 16px;
360
  font-weight: 500;
 
361
  cursor: pointer;
362
- border: none;
363
- transition: background-color 0.2s, transform 0.1s;
364
- -webkit-tap-highlight-color: transparent;
365
  }
366
- .action-buttons button:active { transform: scale(0.98); }
367
- .button-edit { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
368
- .button-delete { background-color: var(--tg-theme-destructive-text-color); color: var(--tg-theme-button-text-color); }
369
-
370
- .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
371
- .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 10px; text-align: center;}
372
  </style>
373
  </head>
374
  <body>
375
  <div class="app-container">
376
- <div class="app-header">
377
- <div class="app-header-title">TonTalent</div>
378
- </div>
379
- <div class="profile-header">
380
- <img id="userAvatar" src="#" alt="User Avatar" style="display: none;">
381
- <div>
382
- <span id="profileName">Loading...</span>
383
- <span id="profileUsername"></span>
384
- </div>
385
- </div>
386
  <div class="tabs">
387
- <button class="tab-button active" data-tab="resumes">Resumes</button>
388
- <button class="tab-button" data-tab="vacancies">Vacancies</button>
389
- <button class="tab-button" data-tab="freelance_offers">Freelance</button>
 
 
 
 
 
 
 
 
 
390
  </div>
391
  <div class="content" id="mainContent">
392
  <div class="loading">Loading content...</div>
393
  </div>
394
- <button class="fab" id="fabButton" title="Add New Item">+</button>
 
 
395
  </div>
396
 
397
  <script>
@@ -399,26 +394,27 @@ MAIN_APP_TEMPLATE = '''
399
  let currentUser = null;
400
  let currentView = 'resumes';
401
  let currentItem = null;
402
-
403
- let touchstartX = 0;
404
- let touchendX = 0;
405
- const swipeThreshold = 75;
406
- const mainContentEl = document.getElementById('mainContent');
407
 
408
  function applyThemeParams() {
409
  const root = document.documentElement;
410
- root.style.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
411
- root.style.setProperty('--tg-theme-text-color', tg.themeParams.text_color || '#000000');
412
- root.style.setProperty('--tg-theme-hint-color', tg.themeParams.hint_color || '#999999');
413
- root.style.setProperty('--tg-theme-link-color', tg.themeParams.link_color || '#007aff');
414
- root.style.setProperty('--tg-theme-button-color', tg.themeParams.button_color || '#007aff');
415
- root.style.setProperty('--tg-theme-button-text-color', tg.themeParams.button_text_color || '#ffffff');
416
- root.style.setProperty('--tg-theme-secondary-bg-color', tg.themeParams.secondary_bg_color || '#f0f0f0');
417
- root.style.setProperty('--tg-theme-header-bg-color', tg.themeParams.header_bg_color || tg.themeParams.secondary_bg_color || '#efeff4');
418
- root.style.setProperty('--tg-theme-section-bg-color', tg.themeParams.section_bg_color || tg.themeParams.bg_color || '#ffffff');
419
- root.style.setProperty('--tg-theme-section-header-text-color', tg.themeParams.section_header_text_color || tg.themeParams.hint_color || '#8e8e93');
420
- root.style.setProperty('--tg-theme-destructive-text-color', tg.themeParams.destructive_text_color || '#ff3b30');
421
- root.style.setProperty('--tg-theme-accent-text-color', tg.themeParams.accent_text_color || tg.themeParams.link_color || '#007aff');
 
 
 
 
 
422
  }
423
 
424
  async function apiCall(endpoint, method = 'GET', body = null) {
@@ -428,23 +424,28 @@ MAIN_APP_TEMPLATE = '''
428
  }
429
  const options = { method, headers };
430
  if (body) options.body = JSON.stringify(body);
 
 
431
  try {
432
  const response = await fetch(endpoint, options);
433
  if (!response.ok) {
434
- const errorData = await response.json().catch(() => ({ error: 'Request failed with status: ' + response.status }));
435
  throw new Error(errorData.error || `HTTP error ${response.status}`);
436
  }
437
  return response.json();
438
  } catch (error) {
439
  console.error('API Call Error:', error);
440
  tg.showAlert(error.message || 'An API error occurred.');
 
441
  throw error;
 
 
442
  }
443
  }
444
 
445
  function renderList(items, type) {
446
  const contentDiv = document.getElementById('mainContent');
447
- contentDiv.classList.remove('loading-state');
448
  if (!items || items.length === 0) {
449
  contentDiv.innerHTML = `<div class="empty-state">No ${type} found. Be the first to add one!</div>`;
450
  return;
@@ -460,151 +461,198 @@ MAIN_APP_TEMPLATE = '''
460
  }
461
 
462
  function showDetailView(type, id) {
 
463
  tg.BackButton.show();
464
  tg.BackButton.onClick(() => {
465
  loadView(type);
466
- tg.HapticFeedback.selectionChanged();
467
  });
468
  tg.MainButton.hide();
469
  document.getElementById('fabButton').style.display = 'none';
470
- mainContentEl.classList.add('loading-state');
471
 
472
  apiCall(`/api/${type}/${id}`)
473
  .then(item => {
474
  currentItem = item;
475
- mainContentEl.classList.remove('loading-state');
476
  const contentDiv = document.getElementById('mainContent');
 
477
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
478
-
479
- const formatText = (text) => text ? String(text).replace(/\\n/g, '<br>').replace(/\n/g, '<br>') : 'N/A';
480
-
481
  if (type === 'resumes') {
482
  detailsHtml += `
483
- <p><strong>Skills:</strong> ${formatText(item.skills)}</p>
484
- <p><strong>Experience:</strong><br>${formatText(item.experience)}</p>
485
- <p><strong>Education:</strong><br>${formatText(item.education)}</p>
486
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
487
- ${item.portfolio_link ? `<p><strong>Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" rel="noopener noreferrer">${item.portfolio_link}</a></p>` : ''}
488
  `;
489
  } else if (type === 'vacancies') {
490
  detailsHtml += `
491
  <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
492
- <p><strong>Description:</strong><br>${formatText(item.description)}</p>
493
- <p><strong>Requirements:</strong><br>${formatText(item.requirements)}</p>
494
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
495
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
496
- <p><strong>Contact/Apply:</strong> ${formatText(item.contact) || `@${item.user_telegram_username}`}</p>
497
  `;
498
  } else if (type === 'freelance_offers') {
499
  detailsHtml += `
500
- <p><strong>Description:</strong><br>${formatText(item.description)}</p>
501
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
502
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
503
- <p><strong>Skills Needed:</strong> ${formatText(item.skills_needed)}</p>
504
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
505
  `;
506
  }
507
- detailsHtml += `<p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
508
 
509
- let actionButtonsHtml = '';
510
  if (currentUser && item.user_id === String(currentUser.id)) {
511
- actionButtonsHtml = `
512
- <div class="action-buttons">
513
- <button class="button-edit" onclick="handleEditItem('${type}', '${item.id}')">Edit</button>
514
- <button class="button-delete" onclick="handleDeleteItemPrompt('${type}', '${item.id}')">Delete</button>
515
  </div>
516
  `;
 
 
 
 
 
 
517
  }
518
- detailsHtml += actionButtonsHtml;
519
  detailsHtml += `</div>`;
520
  contentDiv.innerHTML = detailsHtml;
 
 
 
 
521
  })
522
  .catch(err => {
523
- mainContentEl.classList.remove('loading-state');
524
  document.getElementById('mainContent').innerHTML = `<div class="empty-state">Error loading details. Please try again.</div>`;
525
  });
526
  }
527
-
528
- function handleEditItem(type, id) {
529
- tg.HapticFeedback.impactOccurred('light');
530
- if (currentItem && currentItem.id === id) {
531
- showForm(type, currentItem);
532
- } else {
533
- apiCall(`/api/${type}/${id}`).then(itemData => {
534
- currentItem = itemData;
535
- showForm(type, itemData);
536
- }).catch(err => tg.showAlert('Could not load item for editing.'));
537
- }
538
- }
539
 
540
- function handleDeleteItemPrompt(type, id) {
541
- tg.HapticFeedback.impactOccurred('light');
542
- tg.showConfirm(`Are you sure you want to delete this ${type.slice(0,-1)}? This action cannot be undone.`, (confirmed) => {
543
  if (confirmed) {
544
- tg.HapticFeedback.impactOccurred('heavy');
545
- actuallyDeleteItem(type, id);
 
 
 
 
 
 
 
 
 
 
 
546
  }
547
  });
548
  }
549
-
550
- function actuallyDeleteItem(type, id) {
551
- tg.MainButton.setText('Deleting...').showProgress().disable();
552
- apiCall(`/api/${type}/${id}`, 'DELETE')
553
- .then(response => {
554
- tg.MainButton.hideProgress().enable().hide();
555
- tg.HapticFeedback.notificationOccurred('success');
556
- tg.showAlert(response.message || `${type.slice(0,-1)} deleted successfully.`);
557
- loadView(type);
558
- })
559
- .catch(err => {
560
- tg.MainButton.hideProgress().enable().hide();
561
- tg.HapticFeedback.notificationOccurred('error');
562
- tg.showAlert(err.message || `Failed to delete ${type.slice(0,-1)}.`);
563
- });
564
- }
565
-
566
  function showForm(type, itemToEdit = null) {
 
567
  currentItem = itemToEdit;
568
  tg.BackButton.show();
569
  tg.BackButton.onClick(() => {
570
- tg.HapticFeedback.selectionChanged();
571
  if (itemToEdit) showDetailView(type, itemToEdit.id);
572
  else loadView(type);
573
  });
574
  document.getElementById('fabButton').style.display = 'none';
575
- mainContentEl.classList.remove('loading-state');
576
 
577
  const contentDiv = document.getElementById('mainContent');
578
- let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1)}</h2>`;
 
579
 
580
  if (type === 'resumes') {
581
  formHtml += `
582
- <div class="form-group"><label for="name">Full Name</label><input type="text" id="name" value="${itemToEdit?.name || ''}" required></div>
583
- <div class="form-group"><label for="title">Job Title / Desired Position</label><input type="text" id="title" value="${itemToEdit?.title || ''}" required></div>
584
- <div class="form-group"><label for="skills">Skills (comma separated)</label><textarea id="skills">${itemToEdit?.skills || ''}</textarea></div>
585
- <div class="form-group"><label for="experience">Experience</label><textarea id="experience">${itemToEdit?.experience || ''}</textarea></div>
586
- <div class="form-group"><label for="education">Education</label><textarea id="education">${itemToEdit?.education || ''}</textarea></div>
587
- <div class="form-group"><label for="contact">Contact Info (e.g., email, or leave blank to use Telegram)</label><input type="text" id="contact" value="${itemToEdit?.contact || ''}"></div>
588
- <div class="form-group"><label for="portfolio_link">Portfolio Link (optional)</label><input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  `;
590
  } else if (type === 'vacancies') {
591
  formHtml += `
592
- <div class="form-group"><label for="company_name">Company Name</label><input type="text" id="company_name" value="${itemToEdit?.company_name || ''}" required></div>
593
- <div class="form-group"><label for="title">Job Title</label><input type="text" id="title" value="${itemToEdit?.title || ''}" required></div>
594
- <div class="form-group"><label for="description">Description</label><textarea id="description">${itemToEdit?.description || ''}</textarea></div>
595
- <div class="form-group"><label for="requirements">Requirements</label><textarea id="requirements">${itemToEdit?.requirements || ''}</textarea></div>
596
- <div class="form-group"><label for="salary">Salary/Compensation</label><input type="text" id="salary" value="${itemToEdit?.salary || ''}"></div>
597
- <div class="form-group"><label for="location">Location (e.g., Remote, City)</label><input type="text" id="location" value="${itemToEdit?.location || ''}"></div>
598
- <div class="form-group"><label for="contact">Contact Info / How to Apply</label><textarea id="contact">${itemToEdit?.contact || ''}</textarea></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  `;
600
  } else if (type === 'freelance_offers') {
601
  formHtml += `
602
- <div class="form-group"><label for="title">Project Title</label><input type="text" id="title" value="${itemToEdit?.title || ''}" required></div>
603
- <div class="form-group"><label for="description">Description of Work</label><textarea id="description">${itemToEdit?.description || ''}</textarea></div>
604
- <div class="form-group"><label for="budget">Budget</label><input type="text" id="budget" value="${itemToEdit?.budget || ''}"></div>
605
- <div class="form-group"><label for="deadline">Expected Deadline</label><input type="text" id="deadline" value="${itemToEdit?.deadline || ''}"></div>
606
- <div class="form-group"><label for="skills_needed">Skills Needed (comma separated)</label><textarea id="skills_needed">${itemToEdit?.skills_needed || ''}</textarea></div>
607
- <div class="form-group"><label for="contact">Contact Info (or leave blank to use Telegram)</label><input type="text" id="contact" value="${itemToEdit?.contact || ''}"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  `;
609
  }
610
  formHtml += `<div id="formError" class="error-message"></div></div>`;
@@ -649,12 +697,12 @@ MAIN_APP_TEMPLATE = '''
649
  }
650
 
651
  if (!isValid) {
652
- document.getElementById('formError').textContent = 'Please fill in all required fields.';
653
  tg.HapticFeedback.notificationOccurred('error');
654
  return;
655
  }
656
 
657
- tg.MainButton.showProgress().disable();
658
 
659
  const method = itemId ? 'PUT' : 'POST';
660
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
@@ -662,59 +710,85 @@ MAIN_APP_TEMPLATE = '''
662
  apiCall(endpoint, method, payload)
663
  .then(response => {
664
  tg.HapticFeedback.notificationOccurred('success');
665
- tg.MainButton.hideProgress().enable();
666
  loadView(type);
 
667
  })
668
  .catch(err => {
669
  tg.HapticFeedback.notificationOccurred('error');
670
- tg.MainButton.hideProgress().enable();
671
- document.getElementById('formError').textContent = err.message || 'Failed to submit.';
672
  });
673
  }
674
 
675
- function loadView(tabName) {
676
  currentView = tabName;
677
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
678
- document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active');
 
679
 
680
- mainContentEl.innerHTML = `<div class="loading">Loading ${tabName}...</div>`;
681
- mainContentEl.classList.add('loading-state');
 
 
 
682
  tg.BackButton.hide();
683
  tg.MainButton.hide();
684
  document.getElementById('fabButton').style.display = 'block';
 
 
685
 
686
  apiCall(`/api/${tabName}`)
687
  .then(data => renderList(data, tabName))
688
  .catch(err => {
689
- mainContentEl.classList.remove('loading-state');
690
- mainContentEl.innerHTML = `<div class="empty-state">Error loading ${tabName}. Please try again.</div>`;
691
  });
692
  }
693
 
694
- mainContentEl.addEventListener('touchstart', function(event) {
695
- touchstartX = event.changedTouches[0].screenX;
696
- }, { passive: true });
697
-
698
- mainContentEl.addEventListener('touchend', function(event) {
699
- touchendX = event.changedTouches[0].screenX;
700
- handleSwipeGesture();
701
- }, { passive: true });
702
-
703
- function handleSwipeGesture() {
704
- const tabs = ['resumes', 'vacancies', 'freelance_offers'];
705
- const currentIndex = tabs.indexOf(currentView);
706
- let direction = 0;
707
-
708
- if (touchendX < touchstartX - swipeThreshold) { direction = 1; } // Swiped left
709
- if (touchendX > touchstartX + swipeThreshold) { direction = -1; } // Swiped right
 
 
710
 
711
- if (direction !== 0) {
712
- const nextIndex = currentIndex + direction;
713
- if (nextIndex >= 0 && nextIndex < tabs.length) {
714
- loadView(tabs[nextIndex]);
715
- tg.HapticFeedback.selectionChanged();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  }
717
  }
 
 
718
  }
719
 
720
  async function init() {
@@ -723,52 +797,44 @@ MAIN_APP_TEMPLATE = '''
723
  tg.onEvent('themeChanged', applyThemeParams);
724
  tg.expand();
725
  tg.enableClosingConfirmation();
726
- tg.setHeaderColor(tg.themeParams.secondary_bg_color || '#f0f0f0');
727
 
728
-
729
- const profileNameEl = document.getElementById('profileName');
730
- const profileUsernameEl = document.getElementById('profileUsername');
731
- const userAvatarEl = document.getElementById('userAvatar');
732
 
733
  try {
734
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
735
  currentUser = authResponse.user;
736
  if (currentUser) {
737
- profileNameEl.textContent = `${currentUser.first_name || ''} ${currentUser.last_name || ''}`.trim() || 'User';
738
- if (currentUser.username) profileUsernameEl.textContent = `@${currentUser.username}`;
739
- else profileUsernameEl.textContent = 'Authenticated';
740
-
741
- if (currentUser.photo_url) {
742
- userAvatarEl.src = currentUser.photo_url;
743
- userAvatarEl.style.display = 'block';
744
- userAvatarEl.onerror = () => { userAvatarEl.style.display = 'none'; };
745
- } else { userAvatarEl.style.display = 'none'; }
746
- } else { throw new Error("No user data from auth"); }
747
  } catch (error) {
748
  console.error("Auth error:", error);
749
- const unsafeUser = tg.initDataUnsafe.user;
750
- profileNameEl.textContent = `${unsafeUser?.first_name || ''} ${unsafeUser?.last_name || ''}`.trim() || 'User';
751
- if (unsafeUser?.username) profileUsernameEl.textContent = `@${unsafeUser.username}`;
752
- else profileUsernameEl.textContent = 'Guest (Limited Access)';
753
- userAvatarEl.style.display = 'none';
754
- if (error.message !== "No user data from auth") { // Avoid double alert if it's just missing data
755
- tg.showAlert("Authentication with the server failed. Some features might be limited.");
756
- }
757
  }
758
 
 
759
  document.querySelectorAll('.tab-button').forEach(button => {
760
- button.addEventListener('click', () => {
761
- loadView(button.dataset.tab);
762
- tg.HapticFeedback.selectionChanged();
763
- });
764
  });
765
  document.getElementById('fabButton').addEventListener('click', () => {
766
  showForm(currentView);
767
  tg.HapticFeedback.impactOccurred('medium');
768
  });
769
 
770
- loadView('resumes');
 
 
 
 
 
771
  }
 
772
  init();
773
  </script>
774
  </body>
@@ -900,28 +966,31 @@ def auth_user():
900
  else:
901
  return jsonify({"error": "Authentication data not provided"}), 401
902
 
903
- is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
904
 
905
- if not is_valid or not user_data_from_auth:
906
  return jsonify({"error": "Invalid authentication data"}), 403
907
 
908
  data = load_data()
909
  users = data.get('users', {})
910
- user_id_str = str(user_data_from_auth.get('id'))
911
 
912
  if user_id_str not in users:
913
  users[user_id_str] = {
914
- 'id': user_data_from_auth.get('id'), # This is an int
915
- 'first_name': user_data_from_auth.get('first_name'),
916
- 'last_name': user_data_from_auth.get('last_name'),
917
- 'username': user_data_from_auth.get('username'),
918
- 'language_code': user_data_from_auth.get('language_code'),
919
- 'photo_url': user_data_from_auth.get('photo_url'),
920
  'first_seen': datetime.now().isoformat()
921
  }
922
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
923
- if user_data_from_auth.get('photo_url'): # Update photo_url if changed
924
- users[user_id_str]['photo_url'] = user_data_from_auth.get('photo_url')
 
 
 
925
  data['users'] = users
926
  save_data(data)
927
 
@@ -933,7 +1002,7 @@ def get_authenticated_user(request_headers):
933
  return None
934
  is_valid, user_data = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
935
  if is_valid and user_data:
936
- return user_data # user_data['id'] is an int here
937
  return None
938
 
939
  @app.route('/api/<item_type>', methods=['GET'])
@@ -956,7 +1025,7 @@ def get_item(item_type, item_id):
956
 
957
  @app.route('/api/<item_type>', methods=['POST'])
958
  def create_item(item_type):
959
- user = get_authenticated_user(request.headers) # user['id'] is int
960
  if not user:
961
  return jsonify({"error": "Authentication required"}), 401
962
 
@@ -969,7 +1038,7 @@ def create_item(item_type):
969
 
970
  new_item = {
971
  "id": str(uuid.uuid4()),
972
- "user_id": str(user.get('id')), # Store as string
973
  "user_telegram_username": user.get('username', 'unknown'),
974
  "timestamp": datetime.now().isoformat(),
975
  }
@@ -1011,7 +1080,7 @@ def create_item(item_type):
1011
 
1012
  @app.route('/api/<item_type>/<item_id>', methods=['PUT'])
1013
  def update_item(item_type, item_id):
1014
- user = get_authenticated_user(request.headers) # user['id'] is int
1015
  if not user: return jsonify({"error": "Authentication required"}), 401
1016
 
1017
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
@@ -1031,14 +1100,16 @@ def update_item(item_type, item_id):
1031
  if item_index == -1: return jsonify({"error": "Item not found"}), 404
1032
 
1033
  original_item = items_list[item_index]
1034
- # original_item['user_id'] is string, user.get('id') is int
1035
- if original_item.get('user_id') != str(user.get('id')):
1036
  return jsonify({"error": "Forbidden: You can only edit your own items"}), 403
1037
 
1038
  updated_item = original_item.copy()
1039
  updated_item['updated_timestamp'] = datetime.now().isoformat()
1040
 
1041
  if item_type == 'resumes':
 
 
 
1042
  updated_item.update({
1043
  "name": req_data.get('name', original_item.get('name')),
1044
  "title": req_data.get('title', original_item.get('title')),
@@ -1049,6 +1120,9 @@ def update_item(item_type, item_id):
1049
  "portfolio_link": req_data.get('portfolio_link', original_item.get('portfolio_link'))
1050
  })
1051
  elif item_type == 'vacancies':
 
 
 
1052
  updated_item.update({
1053
  "company_name": req_data.get('company_name', original_item.get('company_name')),
1054
  "title": req_data.get('title', original_item.get('title')),
@@ -1059,7 +1133,10 @@ def update_item(item_type, item_id):
1059
  "contact": req_data.get('contact', original_item.get('contact'))
1060
  })
1061
  elif item_type == 'freelance_offers':
1062
- updated_item.update({
 
 
 
1063
  "title": req_data.get('title', original_item.get('title')),
1064
  "description": req_data.get('description', original_item.get('description')),
1065
  "budget": req_data.get('budget', original_item.get('budget')),
@@ -1074,7 +1151,7 @@ def update_item(item_type, item_id):
1074
 
1075
  @app.route('/api/<item_type>/<item_id>', methods=['DELETE'])
1076
  def delete_item(item_type, item_id):
1077
- user = get_authenticated_user(request.headers) # user['id'] is int
1078
  if not user: return jsonify({"error": "Authentication required"}), 401
1079
 
1080
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
@@ -1087,8 +1164,7 @@ def delete_item(item_type, item_id):
1087
  item_to_delete = next((i for i in items_list if i['id'] == item_id), None)
1088
  if not item_to_delete: return jsonify({"error": "Item not found"}), 404
1089
 
1090
- # item_to_delete['user_id'] is string, user.get('id') is int
1091
- if item_to_delete.get('user_id') != str(user.get('id')):
1092
  return jsonify({"error": "Forbidden: You can only delete your own items"}), 403
1093
 
1094
  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" # Placeholder, user should replace
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 = 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:
 
229
  overscroll-behavior-y: none;
230
  -webkit-font-smoothing: antialiased;
231
  -moz-osx-font-smoothing: grayscale;
232
+ transition: background-color 0.3s, color 0.3s;
 
 
 
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;
241
+ border-bottom: 0.5px solid var(--tg-theme-hint-color, #c8c7cb);
242
  position: sticky;
243
  top: 0;
244
  z-index: 100;
245
  }
246
+ .user-info {
247
+ padding: 12px 15px;
248
+ background-color: var(--tg-theme-secondary-bg-color);
249
+ font-size: 14px;
250
  display: flex;
251
  align-items: center;
252
+ border-bottom: 0.5px solid var(--tg-theme-hint-color, #c8c7cb);
 
 
 
 
 
 
 
 
 
 
253
  }
254
+ .user-info img { width: 36px; height: 36px; border-radius: 50%; margin-right: 10px; object-fit: cover; }
255
+ .user-info span { font-weight: 500; }
 
256
 
257
+ .tabs { display: flex; background-color: var(--tg-theme-secondary-bg-color); padding: 8px 5px 0; }
258
  .tab-button {
259
  flex: 1;
260
  padding: 12px 5px;
 
266
  font-size: 15px;
267
  font-weight: 500;
268
  border-bottom: 3px solid transparent;
269
+ transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out;
270
+ display: flex;
271
+ flex-direction: column;
272
+ align-items: center;
273
+ justify-content: center;
274
+ gap: 4px;
275
  }
276
+ .tab-button svg { width: 22px; height: 22px; fill: currentColor; }
277
  .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); }
278
+ .content { flex-grow: 1; padding: 15px; overflow-y: auto; -webkit-overflow-scrolling: touch; }
 
 
 
 
 
 
279
  .list-item {
280
  background-color: var(--tg-theme-section-bg-color);
281
  border-radius: 10px;
282
+ padding: 15px;
283
+ margin-bottom: 12px;
284
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
285
  cursor: pointer;
286
+ transition: transform 0.1s ease-out, background-color 0.2s;
287
  }
288
+ .list-item:active { transform: scale(0.98); background-color: var(--tg-theme-secondary-bg-color); }
289
+ .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
290
+ .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); line-height: 1.4; }
291
+ .list-item .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 8px; }
292
 
293
+ .form-container, .detail-view {
294
+ padding: 15px; background-color: var(--tg-theme-section-bg-color);
295
+ animation: fadeIn 0.3s ease-out;
296
+ }
297
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
298
+
299
  .form-group { margin-bottom: 18px; }
300
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
301
  .form-group input, .form-group textarea {
 
307
  background-color: var(--tg-theme-bg-color);
308
  color: var(--tg-theme-text-color);
309
  box-sizing: border-box;
310
+ transition: border-color 0.2s, box-shadow 0.2s;
311
  }
312
  .form-group input:focus, .form-group textarea:focus {
313
  border-color: var(--tg-theme-link-color);
314
+ box-shadow: 0 0 0 2px var(--tg-theme-link-color, #007aff30);
315
  outline: none;
316
  }
317
  .form-group textarea { min-height: 100px; resize: vertical; }
 
329
  align-items: center;
330
  justify-content: center;
331
  font-size: 28px;
 
332
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
333
  cursor: pointer;
334
  z-index: 1000;
335
  border: none;
336
+ transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
 
337
  }
338
+ .fab:active { transform: scale(0.9); }
339
+ .fab svg { width: 24px; height: 24px; fill: currentColor; }
340
+
341
+ .detail-view h2 { margin-top: 0; font-size: 22px; font-weight: 700; color: var(--tg-theme-text-color); margin-bottom: 15px; }
342
+ .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; }
343
+ .detail-view strong { font-weight: 600; color: var(--tg-theme-text-color); }
344
+ .detail-view .meta-detail { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 15px; padding-top: 10px; border-top: 1px solid var(--tg-theme-secondary-bg-color); }
345
 
346
+ .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
347
+ .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 8px; text-align: center; }
348
+ .owner-actions { margin-top: 25px; padding-top: 15px; border-top: 1px solid var(--tg-theme-secondary-bg-color); }
349
+ .button-destructive {
350
+ display: block;
351
+ width: 100%;
 
 
 
 
 
352
  padding: 12px 15px;
353
+ background-color: var(--tg-theme-destructive-text-color);
354
+ color: white;
355
+ border: none;
356
  border-radius: 8px;
357
  font-size: 16px;
358
  font-weight: 500;
359
+ text-align: center;
360
  cursor: pointer;
361
+ transition: background-color 0.2s;
 
 
362
  }
363
+ .button-destructive:active { background-color: color-mix(in srgb, var(--tg-theme-destructive-text-color), #000000 20%); }
 
 
 
 
 
364
  </style>
365
  </head>
366
  <body>
367
  <div class="app-container">
368
+ <div class="header">TonTalent</div>
369
+ <div class="user-info" id="userInfo"><span class="placeholder">Loading user...</span></div>
 
 
 
 
 
 
 
 
370
  <div class="tabs">
371
+ <button class="tab-button active" data-tab="resumes">
372
+ <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>
373
+ Resumes
374
+ </button>
375
+ <button class="tab-button" data-tab="vacancies">
376
+ <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>
377
+ Vacancies
378
+ </button>
379
+ <button class="tab-button" data-tab="freelance_offers">
380
+ <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 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v2h-2zm0 4h2v6h-2z"/></svg>
381
+ Freelance
382
+ </button>
383
  </div>
384
  <div class="content" id="mainContent">
385
  <div class="loading">Loading content...</div>
386
  </div>
387
+ <button class="fab" id="fabButton" title="Add New Item">
388
+ <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
389
+ </button>
390
  </div>
391
 
392
  <script>
 
394
  let currentUser = null;
395
  let currentView = 'resumes';
396
  let currentItem = null;
397
+ const tabOrder = ['resumes', 'vacancies', 'freelance_offers'];
 
 
 
 
398
 
399
  function applyThemeParams() {
400
  const root = document.documentElement;
401
+ const themeParams = tg.themeParams;
402
+ root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || '#ffffff');
403
+ root.style.setProperty('--tg-theme-text-color', themeParams.text_color || '#000000');
404
+ root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || '#999999');
405
+ root.style.setProperty('--tg-theme-link-color', themeParams.link_color || '#007aff');
406
+ root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#007aff');
407
+ root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
408
+ root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#f0f0f0');
409
+ root.style.setProperty('--tg-theme-header-bg-color', themeParams.header_bg_color || themeParams.secondary_bg_color || '#efeff4');
410
+ root.style.setProperty('--tg-theme-section-bg-color', themeParams.section_bg_color || themeParams.bg_color || '#ffffff');
411
+ root.style.setProperty('--tg-theme-section-header-text-color', themeParams.section_header_text_color || themeParams.hint_color || '#8e8e93');
412
+ root.style.setProperty('--tg-theme-destructive-text-color', themeParams.destructive_text_color || '#ff3b30');
413
+ root.style.setProperty('--tg-theme-accent-text-color', themeParams.accent_text_color || themeParams.link_color || '#007aff');
414
+
415
+ if (themeParams.bg_color) {
416
+ tg.setHeaderColor(themeParams.header_bg_color || themeParams.secondary_bg_color || '#efeff4');
417
+ }
418
  }
419
 
420
  async function apiCall(endpoint, method = 'GET', body = null) {
 
424
  }
425
  const options = { method, headers };
426
  if (body) options.body = JSON.stringify(body);
427
+
428
+ tg.MainButton.showProgress(false);
429
  try {
430
  const response = await fetch(endpoint, options);
431
  if (!response.ok) {
432
+ const errorData = await response.json().catch(() => ({ error: 'Request failed, server might be unavailable.' }));
433
  throw new Error(errorData.error || `HTTP error ${response.status}`);
434
  }
435
  return response.json();
436
  } catch (error) {
437
  console.error('API Call Error:', error);
438
  tg.showAlert(error.message || 'An API error occurred.');
439
+ tg.HapticFeedback.notificationOccurred('error');
440
  throw error;
441
+ } finally {
442
+ tg.MainButton.hideProgress();
443
  }
444
  }
445
 
446
  function renderList(items, type) {
447
  const contentDiv = document.getElementById('mainContent');
448
+ contentDiv.scrollTop = 0;
449
  if (!items || items.length === 0) {
450
  contentDiv.innerHTML = `<div class="empty-state">No ${type} found. Be the first to add one!</div>`;
451
  return;
 
461
  }
462
 
463
  function showDetailView(type, id) {
464
+ tg.HapticFeedback.impactOccurred('light');
465
  tg.BackButton.show();
466
  tg.BackButton.onClick(() => {
467
  loadView(type);
468
+ tg.HapticFeedback.impactOccurred('light');
469
  });
470
  tg.MainButton.hide();
471
  document.getElementById('fabButton').style.display = 'none';
 
472
 
473
  apiCall(`/api/${type}/${id}`)
474
  .then(item => {
475
  currentItem = item;
 
476
  const contentDiv = document.getElementById('mainContent');
477
+ contentDiv.scrollTop = 0;
478
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
 
 
 
479
  if (type === 'resumes') {
480
  detailsHtml += `
481
+ <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
482
+ <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\n/g, '<br>') : 'N/A'}</p>
483
+ <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\n/g, '<br>') : 'N/A'}</p>
484
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
485
+ ${item.portfolio_link ? `<p><strong>Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" style="color: var(--tg-theme-link-color);">${item.portfolio_link}</a></p>` : ''}
486
  `;
487
  } else if (type === 'vacancies') {
488
  detailsHtml += `
489
  <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
490
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\n/g, '<br>') : 'N/A'}</p>
491
+ <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\n/g, '<br>') : 'N/A'}</p>
492
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
493
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
494
+ <p><strong>Contact/Apply:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
495
  `;
496
  } else if (type === 'freelance_offers') {
497
  detailsHtml += `
498
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\n/g, '<br>') : 'N/A'}</p>
499
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
500
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
501
+ <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
502
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
503
  `;
504
  }
505
+ detailsHtml += `<p class="meta-detail">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
506
 
 
507
  if (currentUser && item.user_id === String(currentUser.id)) {
508
+ detailsHtml += `
509
+ <div class="owner-actions">
510
+ <button id="deleteItemButton" class="button-destructive">Delete Post</button>
 
511
  </div>
512
  `;
513
+ tg.MainButton.setText('Edit My Post');
514
+ tg.MainButton.onClick(() => {
515
+ showForm(type, item);
516
+ tg.HapticFeedback.impactOccurred('light');
517
+ });
518
+ tg.MainButton.show();
519
  }
 
520
  detailsHtml += `</div>`;
521
  contentDiv.innerHTML = detailsHtml;
522
+
523
+ if (currentUser && item.user_id === String(currentUser.id)) {
524
+ document.getElementById('deleteItemButton')?.addEventListener('click', () => confirmDeleteItem(type, item.id));
525
+ }
526
  })
527
  .catch(err => {
 
528
  document.getElementById('mainContent').innerHTML = `<div class="empty-state">Error loading details. Please try again.</div>`;
529
  });
530
  }
 
 
 
 
 
 
 
 
 
 
 
 
531
 
532
+ function confirmDeleteItem(type, itemId) {
533
+ tg.HapticFeedback.impactOccurred('medium');
534
+ tg.showConfirm("Are you sure you want to delete this post? This action cannot be undone.", (confirmed) => {
535
  if (confirmed) {
536
+ tg.HapticFeedback.impactOccurred('light');
537
+ apiCall(`/api/${type}/${itemId}`, 'DELETE')
538
+ .then(() => {
539
+ tg.HapticFeedback.notificationOccurred('success');
540
+ tg.showAlert('Post deleted successfully.');
541
+ loadView(type);
542
+ })
543
+ .catch(err => {
544
+ tg.HapticFeedback.notificationOccurred('error');
545
+ tg.showAlert('Failed to delete post: ' + err.message);
546
+ });
547
+ } else {
548
+ tg.HapticFeedback.impactOccurred('light');
549
  }
550
  });
551
  }
552
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  function showForm(type, itemToEdit = null) {
554
+ tg.HapticFeedback.impactOccurred('light');
555
  currentItem = itemToEdit;
556
  tg.BackButton.show();
557
  tg.BackButton.onClick(() => {
558
+ tg.HapticFeedback.impactOccurred('light');
559
  if (itemToEdit) showDetailView(type, itemToEdit.id);
560
  else loadView(type);
561
  });
562
  document.getElementById('fabButton').style.display = 'none';
 
563
 
564
  const contentDiv = document.getElementById('mainContent');
565
+ contentDiv.scrollTop = 0;
566
+ let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.charAt(0).toUpperCase() + type.slice(1, -1)}</h2>`;
567
 
568
  if (type === 'resumes') {
569
  formHtml += `
570
+ <div class="form-group">
571
+ <label for="name">Full Name *</label>
572
+ <input type="text" id="name" value="${itemToEdit?.name || ''}" required placeholder="e.g., John Doe">
573
+ </div>
574
+ <div class="form-group">
575
+ <label for="title">Job Title / Desired Position *</label>
576
+ <input type="text" id="title" value="${itemToEdit?.title || ''}" required placeholder="e.g., Software Engineer">
577
+ </div>
578
+ <div class="form-group">
579
+ <label for="skills">Skills (comma separated)</label>
580
+ <textarea id="skills" placeholder="e.g., Python, JavaScript, Project Management">${itemToEdit?.skills || ''}</textarea>
581
+ </div>
582
+ <div class="form-group">
583
+ <label for="experience">Experience</label>
584
+ <textarea id="experience" placeholder="Describe your work experience...">${itemToEdit?.experience || ''}</textarea>
585
+ </div>
586
+ <div class="form-group">
587
+ <label for="education">Education</label>
588
+ <textarea id="education" placeholder="Your educational background...">${itemToEdit?.education || ''}</textarea>
589
+ </div>
590
+ <div class="form-group">
591
+ <label for="contact">Contact Info (email, etc. Optional - uses Telegram if blank)</label>
592
+ <input type="text" id="contact" value="${itemToEdit?.contact || ''}" placeholder="e.g., mail@example.com">
593
+ </div>
594
+ <div class="form-group">
595
+ <label for="portfolio_link">Portfolio Link (optional)</label>
596
+ <input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}" placeholder="e.g., https://github.com/johndoe">
597
+ </div>
598
  `;
599
  } else if (type === 'vacancies') {
600
  formHtml += `
601
+ <div class="form-group">
602
+ <label for="company_name">Company Name *</label>
603
+ <input type="text" id="company_name" value="${itemToEdit?.company_name || ''}" required placeholder="e.g., Tech Solutions Inc.">
604
+ </div>
605
+ <div class="form-group">
606
+ <label for="title">Job Title *</label>
607
+ <input type="text" id="title" value="${itemToEdit?.title || ''}" required placeholder="e.g., Marketing Manager">
608
+ </div>
609
+ <div class="form-group">
610
+ <label for="description">Description</label>
611
+ <textarea id="description" placeholder="Job details, responsibilities...">${itemToEdit?.description || ''}</textarea>
612
+ </div>
613
+ <div class="form-group">
614
+ <label for="requirements">Requirements</label>
615
+ <textarea id="requirements" placeholder="Required skills and qualifications...">${itemToEdit?.requirements || ''}</textarea>
616
+ </div>
617
+ <div class="form-group">
618
+ <label for="salary">Salary/Compensation</label>
619
+ <input type="text" id="salary" value="${itemToEdit?.salary || ''}" placeholder="e.g., $5000/month or Competitive">
620
+ </div>
621
+ <div class="form-group">
622
+ <label for="location">Location</label>
623
+ <input type="text" id="location" value="${itemToEdit?.location || ''}" placeholder="e.g., Remote, New York">
624
+ </div>
625
+ <div class="form-group">
626
+ <label for="contact">Contact Info / How to Apply</label>
627
+ <textarea id="contact" placeholder="e.g., Apply via email@company.com or link">${itemToEdit?.contact || ''}</textarea>
628
+ </div>
629
  `;
630
  } else if (type === 'freelance_offers') {
631
  formHtml += `
632
+ <div class="form-group">
633
+ <label for="title">Project Title *</label>
634
+ <input type="text" id="title" value="${itemToEdit?.title || ''}" required placeholder="e.g., Design a new logo">
635
+ </div>
636
+ <div class="form-group">
637
+ <label for="description">Description of Work</label>
638
+ <textarea id="description" placeholder="Detailed project scope...">${itemToEdit?.description || ''}</textarea>
639
+ </div>
640
+ <div class="form-group">
641
+ <label for="budget">Budget</label>
642
+ <input type="text" id="budget" value="${itemToEdit?.budget || ''}" placeholder="e.g., $500 or Negotiable">
643
+ </div>
644
+ <div class="form-group">
645
+ <label for="deadline">Expected Deadline</label>
646
+ <input type="text" id="deadline" value="${itemToEdit?.deadline || ''}" placeholder="e.g., 2 weeks or 2024-12-31">
647
+ </div>
648
+ <div class="form-group">
649
+ <label for="skills_needed">Skills Needed (comma separated)</label>
650
+ <textarea id="skills_needed" placeholder="e.g., Graphic Design, Adobe Illustrator">${itemToEdit?.skills_needed || ''}</textarea>
651
+ </div>
652
+ <div class="form-group">
653
+ <label for="contact">Contact Info (Optional - uses Telegram if blank)</label>
654
+ <input type="text" id="contact" value="${itemToEdit?.contact || ''}" placeholder="e.g., project@example.com">
655
+ </div>
656
  `;
657
  }
658
  formHtml += `<div id="formError" class="error-message"></div></div>`;
 
697
  }
698
 
699
  if (!isValid) {
700
+ document.getElementById('formError').textContent = 'Please fill in all required fields (marked with *).';
701
  tg.HapticFeedback.notificationOccurred('error');
702
  return;
703
  }
704
 
705
+ tg.MainButton.showProgress();
706
 
707
  const method = itemId ? 'PUT' : 'POST';
708
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
 
710
  apiCall(endpoint, method, payload)
711
  .then(response => {
712
  tg.HapticFeedback.notificationOccurred('success');
713
+ tg.MainButton.hideProgress();
714
  loadView(type);
715
+ tg.showAlert(itemId ? 'Successfully updated!' : 'Successfully posted!');
716
  })
717
  .catch(err => {
718
  tg.HapticFeedback.notificationOccurred('error');
719
+ tg.MainButton.hideProgress();
720
+ document.getElementById('formError').textContent = err.message || 'Failed to submit. Please try again.';
721
  });
722
  }
723
 
724
+ function loadView(tabName, noHaptic = false) {
725
  currentView = tabName;
726
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
727
+ const activeTabButton = document.querySelector(`.tab-button[data-tab="${tabName}"]`);
728
+ if (activeTabButton) activeTabButton.classList.add('active');
729
 
730
+ if (!noHaptic) tg.HapticFeedback.selectionChanged();
731
+
732
+ const contentDiv = document.getElementById('mainContent');
733
+ contentDiv.innerHTML = `<div class="loading">Loading ${tabName}...</div>`;
734
+ contentDiv.scrollTop = 0;
735
  tg.BackButton.hide();
736
  tg.MainButton.hide();
737
  document.getElementById('fabButton').style.display = 'block';
738
+ document.getElementById('fabButton').innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>';
739
+
740
 
741
  apiCall(`/api/${tabName}`)
742
  .then(data => renderList(data, tabName))
743
  .catch(err => {
744
+ document.getElementById('mainContent').innerHTML = `<div class="empty-state">Error loading ${tabName}. Please check your connection and try again.</div>`;
 
745
  });
746
  }
747
 
748
+ let touchStartX = 0;
749
+ let touchEndX = 0;
750
+ const swipeThreshold = 50;
751
+
752
+ function handleTouchStart(event) {
753
+ if (event.touches.length === 1) {
754
+ touchStartX = event.touches[0].clientX;
755
+ }
756
+ }
757
+
758
+ function handleTouchMove(event) {
759
+ if (event.touches.length === 1) {
760
+ touchEndX = event.touches[0].clientX;
761
+ }
762
+ }
763
+
764
+ function handleTouchEnd() {
765
+ if (touchStartX === 0 || touchEndX === 0) return;
766
 
767
+ const diffX = touchStartX - touchEndX;
768
+ const mainContent = document.getElementById('mainContent');
769
+ const isFormOrDetailActive = mainContent.querySelector('.form-container') || mainContent.querySelector('.detail-view');
770
+
771
+ if (isFormOrDetailActive) { // Disable swipe on form/detail for now
772
+ touchStartX = 0;
773
+ touchEndX = 0;
774
+ return;
775
+ }
776
+
777
+
778
+ if (Math.abs(diffX) > swipeThreshold) {
779
+ const currentIndex = tabOrder.indexOf(currentView);
780
+ if (diffX > 0) { // Swiped left
781
+ if (currentIndex < tabOrder.length - 1) {
782
+ loadView(tabOrder[currentIndex + 1]);
783
+ }
784
+ } else { // Swiped right
785
+ if (currentIndex > 0) {
786
+ loadView(tabOrder[currentIndex - 1]);
787
+ }
788
  }
789
  }
790
+ touchStartX = 0;
791
+ touchEndX = 0;
792
  }
793
 
794
  async function init() {
 
797
  tg.onEvent('themeChanged', applyThemeParams);
798
  tg.expand();
799
  tg.enableClosingConfirmation();
 
800
 
801
+ const userInfoDiv = document.getElementById('userInfo');
802
+ const defaultUserText = `Welcome, ${tg.initDataUnsafe.user?.first_name || 'User'}! (@${tg.initDataUnsafe.user?.username || 'anonymous'})`;
803
+ let avatarHtml = tg.initDataUnsafe.user?.photo_url ? `<img src="${tg.initDataUnsafe.user.photo_url}" alt="avatar">` : '<div style="width:36px; height:36px; border-radius:50%; background-color:var(--tg-theme-hint-color); margin-right:10px; display:flex; align-items:center; justify-content:center; font-size:18px; color:var(--tg-theme-button-text-color);">${(tg.initDataUnsafe.user?.first_name || "U").charAt(0)}</div>';
804
+ userInfoDiv.innerHTML = `${avatarHtml}<span>${defaultUserText}</span>`;
805
 
806
  try {
807
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
808
  currentUser = authResponse.user;
809
  if (currentUser) {
810
+ let userDisplayName = currentUser.first_name;
811
+ if (currentUser.last_name) userDisplayName += ` ${currentUser.last_name}`;
812
+ const avatar = currentUser.photo_url ? `<img src="${currentUser.photo_url}" alt="avatar">` : `<div style="width:36px; height:36px; border-radius:50%; background-color:var(--tg-theme-hint-color); margin-right:10px; display:flex; align-items:center; justify-content:center; font-size:18px; color:var(--tg-theme-button-text-color);">${(currentUser.first_name || "U").charAt(0)}</div>`;
813
+ userInfoDiv.innerHTML = `${avatar}<span>${userDisplayName} (@${currentUser.username || 'id'+currentUser.id})</span>`;
814
+ }
 
 
 
 
 
815
  } catch (error) {
816
  console.error("Auth error:", error);
817
+ userInfoDiv.innerHTML = `${avatarHtml}<span>Auth failed. Limited functionality.</span>`;
818
+ tg.showAlert("Authentication with the server failed. Some features might not work correctly.");
 
 
 
 
 
 
819
  }
820
 
821
+
822
  document.querySelectorAll('.tab-button').forEach(button => {
823
+ button.addEventListener('click', () => loadView(button.dataset.tab));
 
 
 
824
  });
825
  document.getElementById('fabButton').addEventListener('click', () => {
826
  showForm(currentView);
827
  tg.HapticFeedback.impactOccurred('medium');
828
  });
829
 
830
+ const mainContentEl = document.getElementById('mainContent');
831
+ mainContentEl.addEventListener('touchstart', handleTouchStart, { passive: true });
832
+ mainContentEl.addEventListener('touchmove', handleTouchMove, { passive: true });
833
+ mainContentEl.addEventListener('touchend', handleTouchEnd, { passive: true });
834
+
835
+ loadView('resumes', true); // Initial load without haptic
836
  }
837
+
838
  init();
839
  </script>
840
  </body>
 
966
  else:
967
  return jsonify({"error": "Authentication data not provided"}), 401
968
 
969
+ is_valid, user_data_from_tg = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
970
 
971
+ if not is_valid or not user_data_from_tg:
972
  return jsonify({"error": "Invalid authentication data"}), 403
973
 
974
  data = load_data()
975
  users = data.get('users', {})
976
+ user_id_str = str(user_data_from_tg.get('id'))
977
 
978
  if user_id_str not in users:
979
  users[user_id_str] = {
980
+ 'id': user_data_from_tg.get('id'),
981
+ 'first_name': user_data_from_tg.get('first_name'),
982
+ 'last_name': user_data_from_tg.get('last_name'),
983
+ 'username': user_data_from_tg.get('username'),
984
+ 'language_code': user_data_from_tg.get('language_code'),
985
+ 'photo_url': user_data_from_tg.get('photo_url'),
986
  'first_seen': datetime.now().isoformat()
987
  }
988
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
989
+ users[user_id_str]['first_name'] = user_data_from_tg.get('first_name', users[user_id_str].get('first_name'))
990
+ users[user_id_str]['last_name'] = user_data_from_tg.get('last_name', users[user_id_str].get('last_name'))
991
+ users[user_id_str]['username'] = user_data_from_tg.get('username', users[user_id_str].get('username'))
992
+ users[user_id_str]['photo_url'] = user_data_from_tg.get('photo_url', users[user_id_str].get('photo_url'))
993
+
994
  data['users'] = users
995
  save_data(data)
996
 
 
1002
  return None
1003
  is_valid, user_data = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1004
  if is_valid and user_data:
1005
+ return user_data
1006
  return None
1007
 
1008
  @app.route('/api/<item_type>', methods=['GET'])
 
1025
 
1026
  @app.route('/api/<item_type>', methods=['POST'])
1027
  def create_item(item_type):
1028
+ user = get_authenticated_user(request.headers)
1029
  if not user:
1030
  return jsonify({"error": "Authentication required"}), 401
1031
 
 
1038
 
1039
  new_item = {
1040
  "id": str(uuid.uuid4()),
1041
+ "user_id": str(user.get('id')),
1042
  "user_telegram_username": user.get('username', 'unknown'),
1043
  "timestamp": datetime.now().isoformat(),
1044
  }
 
1080
 
1081
  @app.route('/api/<item_type>/<item_id>', methods=['PUT'])
1082
  def update_item(item_type, item_id):
1083
+ user = get_authenticated_user(request.headers)
1084
  if not user: return jsonify({"error": "Authentication required"}), 401
1085
 
1086
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
 
1100
  if item_index == -1: return jsonify({"error": "Item not found"}), 404
1101
 
1102
  original_item = items_list[item_index]
1103
+ if str(original_item.get('user_id')) != str(user.get('id')):
 
1104
  return jsonify({"error": "Forbidden: You can only edit your own items"}), 403
1105
 
1106
  updated_item = original_item.copy()
1107
  updated_item['updated_timestamp'] = datetime.now().isoformat()
1108
 
1109
  if item_type == 'resumes':
1110
+ required_fields = ['name', 'title']
1111
+ for field in required_fields:
1112
+ if not req_data.get(field): return jsonify({"error": f"Missing field: {field} in update"}), 400
1113
  updated_item.update({
1114
  "name": req_data.get('name', original_item.get('name')),
1115
  "title": req_data.get('title', original_item.get('title')),
 
1120
  "portfolio_link": req_data.get('portfolio_link', original_item.get('portfolio_link'))
1121
  })
1122
  elif item_type == 'vacancies':
1123
+ required_fields = ['company_name', 'title']
1124
+ for field in required_fields:
1125
+ if not req_data.get(field): return jsonify({"error": f"Missing field: {field} in update"}), 400
1126
  updated_item.update({
1127
  "company_name": req_data.get('company_name', original_item.get('company_name')),
1128
  "title": req_data.get('title', original_item.get('title')),
 
1133
  "contact": req_data.get('contact', original_item.get('contact'))
1134
  })
1135
  elif item_type == 'freelance_offers':
1136
+ required_fields = ['title']
1137
+ for field in required_fields:
1138
+ if not req_data.get(field): return jsonify({"error": f"Missing field: {field} in update"}), 400
1139
+ updated_item.update({
1140
  "title": req_data.get('title', original_item.get('title')),
1141
  "description": req_data.get('description', original_item.get('description')),
1142
  "budget": req_data.get('budget', original_item.get('budget')),
 
1151
 
1152
  @app.route('/api/<item_type>/<item_id>', methods=['DELETE'])
1153
  def delete_item(item_type, item_id):
1154
+ user = get_authenticated_user(request.headers)
1155
  if not user: return jsonify({"error": "Authentication required"}), 401
1156
 
1157
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
 
1164
  item_to_delete = next((i for i in items_list if i['id'] == item_id), None)
1165
  if not item_to_delete: return jsonify({"error": "Item not found"}), 404
1166
 
1167
+ if str(item_to_delete.get('user_id')) != str(user.get('id')):
 
1168
  return jsonify({"error": "Forbidden: You can only delete your own items"}), 403
1169
 
1170
  data[item_type] = [i for i in items_list if i['id'] != item_id]