Shveiauto commited on
Commit
8e60fa0
·
verified ·
1 Parent(s): 508b4f1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -256
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 = os.getenv("TELEGRAM_BOT_TOKEN", "YOUR_TELEGRAM_BOT_TOKEN") # Replace with your actual bot token
29
 
30
  DOWNLOAD_RETRIES = 3
31
  DOWNLOAD_DELAY = 5
@@ -66,7 +66,7 @@ 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 = True # Consider it successful if 404 and we handle it (e.g. create empty)
70
  break
71
  else:
72
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
@@ -121,8 +121,8 @@ def load_data():
121
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
122
  if not isinstance(data, dict):
123
  logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
124
- raise FileNotFoundError # Trigger download if structure is wrong
125
- for key in default_data: # Ensure all top-level keys exist
126
  if key not in data: data[key] = default_data[key]
127
  return data
128
  except (FileNotFoundError, json.JSONDecodeError) as e:
@@ -136,7 +136,7 @@ def load_data():
136
  if not isinstance(data, dict):
137
  logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.")
138
  return default_data
139
- for key in default_data: # Ensure all top-level keys exist
140
  if key not in data: data[key] = default_data[key]
141
  return data
142
  except Exception as load_e:
@@ -144,7 +144,7 @@ def load_data():
144
  return default_data
145
  else:
146
  logging.error(f"Failed to download {DATA_FILE} from HF. Using empty default data structure.")
147
- if not os.path.exists(DATA_FILE): # Ensure a file exists even if download fails
148
  try:
149
  with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(default_data, f)
150
  logging.info(f"Created empty local file {DATA_FILE} after failed download.")
@@ -157,16 +157,14 @@ def save_data(data):
157
  if not isinstance(data, dict):
158
  logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
159
  return
160
- # Ensure essential keys are present before saving
161
  default_keys = {'resumes': [], 'vacancies': [], 'freelance_offers': [], 'users': {}}
162
  for key in default_keys:
163
- if key not in data:
164
- data[key] = default_keys[key]
165
 
166
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
167
  json.dump(data, file, ensure_ascii=False, indent=4)
168
  logging.info(f"Data successfully saved to {DATA_FILE}")
169
- upload_db_to_hf(specific_file=DATA_FILE) # Sync after every save
170
  except Exception as e:
171
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
172
 
@@ -195,7 +193,7 @@ def verify_telegram_auth_data(auth_data_str, bot_token):
195
  user_data = json.loads(params.get('user', '{}'))
196
  return True, user_data
197
  except json.JSONDecodeError:
198
- return False, None # User string malformed
199
  return False, None
200
 
201
 
@@ -265,7 +263,7 @@ MAIN_APP_TEMPLATE = '''
265
  border-radius: 50%;
266
  margin-right: 12px;
267
  object-fit: cover;
268
- background-color: var(--tg-theme-secondary-bg-color); /* Placeholder bg */
269
  }
270
  .user-info-bar span {
271
  font-size: 15px;
@@ -303,7 +301,6 @@ MAIN_APP_TEMPLATE = '''
303
  .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); }
304
  .list-item .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 8px; }
305
  .form-container, .detail-view { padding: 20px 15px; background-color: var(--tg-theme-section-bg-color); min-height: calc(100vh - 180px); /* Adjust based on header/tabs/userbar height */ }
306
- .form-container h2 { margin-top:0; margin-bottom: 20px; font-size: 20px; font-weight: 600;}
307
  .form-group { margin-bottom: 18px; }
308
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
309
  .form-group input, .form-group textarea {
@@ -341,7 +338,7 @@ MAIN_APP_TEMPLATE = '''
341
  }
342
  .fab:active { transform: scale(0.92); }
343
  .detail-view h2 { margin-top: 0; font-size: 22px; font-weight: 600; color: var(--tg-theme-text-color); margin-bottom: 15px; }
344
- .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; word-wrap: break-word; }
345
  .detail-view strong { font-weight: 500; color: var(--tg-theme-text-color); }
346
  .detail-view .meta-detail { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 20px; }
347
  .detail-view a { color: var(--tg-theme-link-color); text-decoration: none; }
@@ -359,7 +356,6 @@ MAIN_APP_TEMPLATE = '''
359
  font-weight: 500;
360
  cursor: pointer;
361
  text-align: center;
362
- box-sizing: border-box;
363
  transition: background-color 0.2s ease;
364
  }
365
  .button-destructive {
@@ -374,8 +370,8 @@ MAIN_APP_TEMPLATE = '''
374
  </head>
375
  <body>
376
  <div class="app-container">
377
- <div class="header">TonTalent</div>
378
- <div class="user-info-bar">
379
  <img id="userAvatar" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Avatar">
380
  <span id="userInfoText">Loading user...</span>
381
  </div>
@@ -393,12 +389,18 @@ MAIN_APP_TEMPLATE = '''
393
  <script>
394
  const tg = window.Telegram.WebApp;
395
  let currentUser = null;
396
- let currentView = 'resumes'; // Active main tab
397
- let currentItem = null;
398
- let previousViewContext = null; // For form back navigation: { type: 'tab'/'detail'/'myPosts', name: string, id?: string }
399
  const mainContent = document.getElementById('mainContent');
 
 
400
  const tabOrder = ['resumes', 'vacancies', 'freelance_offers'];
401
 
 
 
 
 
402
  function applyThemeParams() {
403
  const rootStyle = document.documentElement.style;
404
  rootStyle.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
@@ -437,33 +439,6 @@ MAIN_APP_TEMPLATE = '''
437
  }
438
  }
439
 
440
- function formatContactLink(contactValue, username) {
441
- let displayValue = contactValue;
442
- let linkUrl = '#';
443
-
444
- if (!contactValue && username) {
445
- displayValue = `@${username}`;
446
- linkUrl = `https://t.me/${username}`;
447
- return `<a href="${linkUrl}" target="_blank" rel="noopener noreferrer" style="color: var(--tg-theme-link-color);">${displayValue}</a>`;
448
- }
449
-
450
- if (contactValue) {
451
- if (contactValue.startsWith('@')) {
452
- const tgUsername = contactValue.substring(1);
453
- linkUrl = `https://t.me/${tgUsername}`;
454
- return `<a href="${linkUrl}" target="_blank" rel="noopener noreferrer" style="color: var(--tg-theme-link-color);">${displayValue}</a>`;
455
- } else if (contactValue.startsWith('http://') || contactValue.startsWith('https://')) {
456
- linkUrl = contactValue;
457
- return `<a href="${linkUrl}" target="_blank" rel="noopener noreferrer" style="color: var(--tg-theme-link-color);">${displayValue}</a>`;
458
- } else if (contactValue.includes('@') && contactValue.includes('.') && !contactValue.includes(' ')) { // Basic email check
459
- linkUrl = `mailto:${contactValue}`;
460
- return `<a href="${linkUrl}" style="color: var(--tg-theme-link-color);">${displayValue}</a>`;
461
- }
462
- return displayValue; // Plain text
463
- }
464
- return `@${username}` ? `<a href="https://t.me/${username}" target="_blank" rel="noopener noreferrer" style="color: var(--tg-theme-link-color);">@${username}</a>` : 'N/A';
465
- }
466
-
467
  function renderList(items, type) {
468
  mainContent.style.opacity = 0;
469
  if (!items || items.length === 0) {
@@ -474,7 +449,7 @@ MAIN_APP_TEMPLATE = '''
474
  <h3>${item.title || item.name || 'Untitled'}</h3>
475
  ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
476
  ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
477
- <p class="meta">Posted by: ${formatContactLink(null, item.user_telegram_username)} on ${new Date(item.timestamp).toLocaleDateString()}</p>
478
  </div>
479
  `).join('');
480
  }
@@ -486,27 +461,55 @@ MAIN_APP_TEMPLATE = '''
486
  showDetailView(type, id);
487
  }
488
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  function showDetailView(type, id) {
490
  mainContent.style.opacity = 0;
491
  tg.BackButton.show();
492
  tg.BackButton.onClick(() => {
493
  tg.HapticFeedback.impactOccurred('light');
494
- loadView(type); // Always go back to the tab view of this item's type
 
 
 
 
495
  });
496
  tg.MainButton.hide();
497
- document.getElementById('fabButton').style.display = 'none';
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
  if (type === 'resumes') {
504
  detailsHtml += `
505
  <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
506
  <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\\n/g, '<br>') : 'N/A'}</p>
507
  <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\\n/g, '<br>') : 'N/A'}</p>
508
- <p><strong>Contact:</strong> ${formatContactLink(item.contact, item.user_telegram_username)}</p>
509
- ${item.portfolio_link ? `<p><strong>Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" rel="noopener noreferrer" style="color: var(--tg-theme-link-color);">${item.portfolio_link}</a></p>` : ''}
510
  `;
511
  } else if (type === 'vacancies') {
512
  detailsHtml += `
@@ -515,7 +518,7 @@ MAIN_APP_TEMPLATE = '''
515
  <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\\n/g, '<br>') : 'N/A'}</p>
516
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
517
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
518
- <p><strong>Contact/Apply:</strong> ${formatContactLink(item.contact, item.user_telegram_username)}</p>
519
  `;
520
  } else if (type === 'freelance_offers') {
521
  detailsHtml += `
@@ -523,10 +526,10 @@ MAIN_APP_TEMPLATE = '''
523
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
524
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
525
  <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
526
- <p><strong>Contact:</strong> ${formatContactLink(item.contact, item.user_telegram_username)}</p>
527
  `;
528
  }
529
- detailsHtml += `<p class="meta-detail">Posted by: ${formatContactLink(null, item.user_telegram_username)} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
530
 
531
  if (currentUser && item.user_id === String(currentUser.id)) {
532
  detailsHtml += `<button id="editItemButton" class="action-button" style="background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); margin-top: 25px;">Edit Post</button>`;
@@ -538,7 +541,7 @@ MAIN_APP_TEMPLATE = '''
538
  if (currentUser && item.user_id === String(currentUser.id)) {
539
  document.getElementById('editItemButton')?.addEventListener('click', () => {
540
  tg.HapticFeedback.impactOccurred('light');
541
- showForm(type, item, { type: 'detail', name: type, id: item.id });
542
  });
543
  document.getElementById('deleteItemButton')?.addEventListener('click', () => {
544
  tg.HapticFeedback.impactOccurred('light');
@@ -553,32 +556,17 @@ MAIN_APP_TEMPLATE = '''
553
  });
554
  }
555
 
556
- function showForm(type, itemToEdit = null, callingContext = null) {
557
  mainContent.style.opacity = 0;
558
  currentItem = itemToEdit;
559
- // If callingContext is not provided, default to current tab for back navigation
560
- previousViewContext = callingContext || { type: 'tab', name: currentView };
561
-
562
  tg.BackButton.show();
563
  tg.BackButton.onClick(() => {
564
  tg.HapticFeedback.impactOccurred('light');
565
- if (previousViewContext) {
566
- if (previousViewContext.type === 'tab') {
567
- loadView(previousViewContext.name);
568
- } else if (previousViewContext.type === 'detail') {
569
- showDetailView(previousViewContext.name, previousViewContext.id);
570
- } else if (previousViewContext.type === 'myPosts') {
571
- showMyPostsView();
572
- } else {
573
- if (itemToEdit) showDetailView(type, itemToEdit.id);
574
- else loadView(type); // Fallback to type of form
575
- }
576
- } else {
577
- if (itemToEdit) showDetailView(type, itemToEdit.id);
578
- else loadView(type); // Fallback
579
- }
580
  });
581
- document.getElementById('fabButton').style.display = 'none';
582
 
583
  let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1)}</h2>`;
584
  if (type === 'resumes') {
@@ -588,7 +576,7 @@ MAIN_APP_TEMPLATE = '''
588
  <div class="form-group"><label for="skills">Skills (comma separated)</label><textarea id="skills">${itemToEdit?.skills || ''}</textarea></div>
589
  <div class="form-group"><label for="experience">Experience</label><textarea id="experience">${itemToEdit?.experience || ''}</textarea></div>
590
  <div class="form-group"><label for="education">Education</label><textarea id="education">${itemToEdit?.education || ''}</textarea></div>
591
- <div class="form-group"><label for="contact">Contact Info (e.g., email, link, or @username)</label><input type="text" id="contact" value="${itemToEdit?.contact || ''}"></div>
592
  <div class="form-group"><label for="portfolio_link">Portfolio Link (optional)</label><input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}"></div>
593
  `;
594
  } else if (type === 'vacancies') {
@@ -608,7 +596,7 @@ MAIN_APP_TEMPLATE = '''
608
  <div class="form-group"><label for="budget">Budget</label><input type="text" id="budget" value="${itemToEdit?.budget || ''}"></div>
609
  <div class="form-group"><label for="deadline">Expected Deadline</label><input type="text" id="deadline" value="${itemToEdit?.deadline || ''}"></div>
610
  <div class="form-group"><label for="skills_needed">Skills Needed (comma separated)</label><textarea id="skills_needed">${itemToEdit?.skills_needed || ''}</textarea></div>
611
- <div class="form-group"><label for="contact">Contact Info (e.g. email, link, or @username)</label><input type="text" id="contact" value="${itemToEdit?.contact || ''}"></div>
612
  `;
613
  }
614
  formHtml += `<div id="formError" class="error-message"></div></div>`;
@@ -670,11 +658,8 @@ MAIN_APP_TEMPLATE = '''
670
  tg.HapticFeedback.notificationOccurred('success');
671
  tg.MainButton.hideProgress();
672
  tg.MainButton.hide();
673
- if (previousViewContext && previousViewContext.type === 'myPosts') {
674
- showMyPostsView();
675
- } else {
676
- loadView(type);
677
- }
678
  })
679
  .catch(err => {
680
  tg.HapticFeedback.notificationOccurred('error');
@@ -683,7 +668,7 @@ MAIN_APP_TEMPLATE = '''
683
  });
684
  }
685
 
686
- function handleDeleteItem(type, itemId, successCallback = null) {
687
  tg.showConfirm('Are you sure you want to delete this post?', (confirmed) => {
688
  if (confirmed) {
689
  tg.HapticFeedback.impactOccurred('medium');
@@ -691,11 +676,8 @@ MAIN_APP_TEMPLATE = '''
691
  .then(() => {
692
  tg.HapticFeedback.notificationOccurred('success');
693
  tg.showAlert('Post deleted successfully.');
694
- if (successCallback) {
695
- successCallback();
696
- } else {
697
- loadView(type); // Default: reload current tab view
698
- }
699
  })
700
  .catch(err => {
701
  tg.HapticFeedback.notificationOccurred('error');
@@ -708,24 +690,22 @@ MAIN_APP_TEMPLATE = '''
708
  }
709
 
710
  function loadView(tabName, fromSwipe = false) {
711
- if (!fromSwipe) tg.HapticFeedback.impactOccurred('light');
712
- currentView = tabName; // This tracks the active main tab
713
 
714
- // If navigating to a main tab, this becomes the primary context for new forms
715
- if (tabOrder.includes(tabName)) {
716
- previousViewContext = { type: 'tab', name: tabName };
717
  }
 
718
 
719
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
720
- const activeTabButton = document.querySelector(`.tab-button[data-tab="${tabName}"]`);
721
- if (activeTabButton) activeTabButton.classList.add('active');
722
 
723
  mainContent.style.opacity = 0;
724
  mainContent.innerHTML = `<div class="loading">Loading ${tabName}...</div>`;
725
 
726
  tg.BackButton.hide();
727
  tg.MainButton.hide();
728
- document.getElementById('fabButton').style.display = 'block';
729
 
730
  apiCall(`/api/${tabName}`)
731
  .then(data => renderList(data, tabName))
@@ -735,70 +715,64 @@ MAIN_APP_TEMPLATE = '''
735
  });
736
  }
737
 
738
- function showMyPostsView() {
739
  mainContent.style.opacity = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
  tg.BackButton.show();
741
  tg.BackButton.onClick(() => {
742
  tg.HapticFeedback.impactOccurred('light');
743
- loadView(currentView || tabOrder[0]); // Go back to the last active tab or default
 
744
  });
745
- tg.MainButton.hide();
746
- document.getElementById('fabButton').style.display = 'none';
747
- document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); // Deactivate tabs visually
748
-
749
- mainContent.innerHTML = `<div class="form-container"><h2>My Publications</h2><div class="loading">Loading your posts...</div></div>`;
750
-
751
- apiCall(`/api/my_posts`)
752
- .then(posts => {
753
- if (!posts || posts.length === 0) {
754
- mainContent.innerHTML = `<div class="form-container" style="text-align:center;"><h2>My Publications</h2><div class="empty-state" style="padding:20px 0;">You haven't posted anything yet.</div></div>`;
755
- } else {
756
- let postsHtml = `<div class="form-container"><h2>My Publications</h2>`;
757
- postsHtml += posts.map(item => {
758
- let itemTypeDisplay = 'Item';
759
- if (item.item_type === 'resumes') itemTypeDisplay = 'Resume';
760
- else if (item.item_type === 'vacancies') itemTypeDisplay = 'Vacancy';
761
- else if (item.item_type === 'freelance_offers') itemTypeDisplay = 'Freelance Offer';
762
-
763
- return \`
764
- <div class="list-item" style="margin-left:0; margin-right:0; cursor:default;">
765
- <h3 style="margin-bottom:3px;">\${item.title || item.name || 'Untitled'}</h3>
766
- <p style="font-size:13px; color: var(--tg-theme-hint-color); margin-bottom:8px;">Type: \${itemTypeDisplay}</p>
767
- <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
768
- <div style="margin-top:10px; display:flex; gap:10px;">
769
- <button class="action-button" style="background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); margin:0; padding: 8px 12px; font-size: 14px; flex:1;"
770
- onclick="handleEditMyPost('\${item.item_type}', '\${item.id}')">Edit</button>
771
- <button class="action-button button-destructive" style="margin:0; padding: 8px 12px; font-size: 14px; flex:1;"
772
- onclick="handleDeleteMyPost('\${item.item_type}', '\${item.id}')">Delete</button>
773
- </div>
774
- </div>\`;
775
- }).join('');
776
- postsHtml += `</div>`;
777
- mainContent.innerHTML = postsHtml;
778
- }
779
- setTimeout(() => { mainContent.style.opacity = 1; }, 50);
780
- })
781
- .catch(err => {
782
- mainContent.innerHTML = `<div class="form-container"><h2>My Publications</h2><div class="empty-state">Error loading your posts.</div></div>`;
783
- setTimeout(() => { mainContent.style.opacity = 1; }, 50);
784
- });
785
- }
786
 
787
- function handleEditMyPost(type, id) {
788
- tg.HapticFeedback.impactOccurred('light');
789
- apiCall(`/api/\${type}/\${id}`) // Fetch full item data
790
- .then(item => {
791
- showForm(type, item, { type: 'myPosts' }); // Pass 'myPosts' context
792
- })
793
- .catch(err => {
794
- tg.showAlert("Could not load item for editing.");
795
- });
796
- }
797
-
798
- function handleDeleteMyPost(type, id) {
799
- tg.HapticFeedback.impactOccurred('light');
800
- handleDeleteItem(type, id, () => {
801
- showMyPostsView(); // Refresh My Posts view after deletion
 
 
 
 
 
 
 
802
  });
803
  }
804
 
@@ -807,19 +781,14 @@ MAIN_APP_TEMPLATE = '''
807
  const swipeThreshold = 70;
808
 
809
  mainContent.addEventListener('touchstart', e => {
810
- // Only allow swipe on main tab views, not in forms or detail views
811
- if (tabOrder.includes(currentView) && document.getElementById('fabButton').style.display !== 'none') {
812
- touchstartX = e.changedTouches[0].screenX;
813
- } else {
814
- touchstartX = 0; // Reset if not on a swipable view
815
- }
816
  }, { passive: true });
817
 
818
  mainContent.addEventListener('touchend', e => {
819
- if (touchstartX === 0) return; // Swipe not initiated on a valid view
820
  touchendX = e.changedTouches[0].screenX;
821
  handleSwipeGesture();
822
- touchstartX = 0; // Reset for next touch
823
  });
824
 
825
  function handleSwipeGesture() {
@@ -827,13 +796,12 @@ MAIN_APP_TEMPLATE = '''
827
  if (Math.abs(swipeLength) < swipeThreshold) return;
828
 
829
  let currentIndex = tabOrder.indexOf(currentView);
830
- if (currentIndex === -1) return; // Not on a main tab view
831
-
832
  let newIndex;
833
 
834
- if (touchendX < touchstartX) { // Swiped left
835
  newIndex = (currentIndex + 1) % tabOrder.length;
836
- } else { // Swiped right
837
  newIndex = (currentIndex - 1 + tabOrder.length) % tabOrder.length;
838
  }
839
 
@@ -853,51 +821,46 @@ MAIN_APP_TEMPLATE = '''
853
 
854
  const userInfoText = document.getElementById('userInfoText');
855
  const userAvatar = document.getElementById('userAvatar');
856
- const userInfoBar = document.querySelector('.user-info-bar');
857
 
858
- // Basic info from unsafe data first
859
- userInfoText.textContent = `Welcome, \${tg.initDataUnsafe.user?.first_name || 'User'}!`;
860
  if (tg.initDataUnsafe.user?.username) {
861
- userInfoText.textContent += ` (@\${tg.initDataUnsafe.user.username})`;
862
  }
863
 
864
  try {
865
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
866
- currentUser = authResponse.user; // Stored user data from our DB
867
  if (currentUser) {
868
- userInfoText.textContent = `\${currentUser.first_name || ''} \${currentUser.last_name || ''}`.trim();
869
- if (currentUser.username) userInfoText.textContent += ` (@\${currentUser.username})`;
 
 
870
  if (currentUser.photo_url) {
871
  userAvatar.src = currentUser.photo_url;
872
  } else {
873
- userAvatar.style.display = 'none'; // Or set a default placeholder image
874
  }
875
  userInfoBar.classList.add('clickable');
876
  userInfoBar.addEventListener('click', () => {
877
- if (currentUser) {
878
- tg.HapticFeedback.impactOccurred('light');
879
- showMyPostsView();
880
- }
881
- });
882
- } else {
883
- userInfoText.textContent = "Welcome! Tap to see your posts."; // Or other message
884
  }
885
  } catch (error) {
886
  console.error("Auth error:", error);
887
  userInfoText.textContent = `Auth failed. Using basic info.`;
888
- // Potentially tg.showAlert("Authentication with the server failed...");
889
  }
890
 
891
  document.querySelectorAll('.tab-button').forEach(button => {
892
  button.addEventListener('click', () => loadView(button.dataset.tab));
893
  });
894
- document.getElementById('fabButton').addEventListener('click', () => {
895
  tg.HapticFeedback.impactOccurred('medium');
896
- // Pass the current tab context for back navigation from the new form
897
- showForm(currentView, null, { type: 'tab', name: currentView });
898
  });
899
 
900
- loadView('resumes'); // Load initial view
901
  }
902
 
903
  init();
@@ -1024,7 +987,7 @@ def main_app_view():
1024
  @app.route('/api/auth_user', methods=['POST'])
1025
  def auth_user():
1026
  auth_data_str = request.headers.get('X-Telegram-Auth')
1027
- if not auth_data_str: # Fallback for testing if not sent in header
1028
  init_data_payload = request.json.get('init_data')
1029
  if init_data_payload:
1030
  auth_data_str = init_data_payload
@@ -1034,16 +997,15 @@ def auth_user():
1034
  is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1035
 
1036
  if not is_valid or not user_data_from_auth:
1037
- logging.warning(f"Invalid auth data received. Valid: {is_valid}, UserData: {user_data_from_auth is not None}")
1038
  return jsonify({"error": "Invalid authentication data"}), 403
1039
 
1040
  data = load_data()
1041
- users = data.get('users', {}) # Ensure 'users' key exists
1042
  user_id_str = str(user_data_from_auth.get('id'))
1043
 
1044
  if user_id_str not in users:
1045
  users[user_id_str] = {
1046
- 'id': user_data_from_auth.get('id'), # Store as integer if possible, but keep consistent as string if ID from TG is string
1047
  'first_name': user_data_from_auth.get('first_name'),
1048
  'last_name': user_data_from_auth.get('last_name'),
1049
  'username': user_data_from_auth.get('username'),
@@ -1051,16 +1013,9 @@ def auth_user():
1051
  'photo_url': user_data_from_auth.get('photo_url'),
1052
  'first_seen': datetime.now().isoformat()
1053
  }
1054
- # Always update last_seen and potentially other fields like photo_url or name
1055
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
1056
- if user_data_from_auth.get('photo_url') and users[user_id_str].get('photo_url') != user_data_from_auth.get('photo_url'):
1057
  users[user_id_str]['photo_url'] = user_data_from_auth.get('photo_url')
1058
- if user_data_from_auth.get('first_name') and users[user_id_str].get('first_name') != user_data_from_auth.get('first_name'):
1059
- users[user_id_str]['first_name'] = user_data_from_auth.get('first_name')
1060
- if user_data_from_auth.get('last_name') and users[user_id_str].get('last_name') != user_data_from_auth.get('last_name'):
1061
- users[user_id_str]['last_name'] = user_data_from_auth.get('last_name')
1062
- if user_data_from_auth.get('username') and users[user_id_str].get('username') != user_data_from_auth.get('username'):
1063
- users[user_id_str]['username'] = user_data_from_auth.get('username')
1064
 
1065
  data['users'] = users
1066
  save_data(data)
@@ -1073,9 +1028,9 @@ def get_authenticated_user_details(request_headers):
1073
  return None
1074
  is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1075
  if is_valid and user_data_from_auth:
1076
- data = load_data() # Load fresh data
1077
  user_id_str = str(user_data_from_auth.get('id'))
1078
- return data.get('users', {}).get(user_id_str) # Return our stored user data
1079
  return None
1080
 
1081
  @app.route('/api/<item_type>', methods=['GET'])
@@ -1096,36 +1051,10 @@ def get_item(item_type, item_id):
1096
  return jsonify(item), 200
1097
  return jsonify({"error": "Item not found"}), 404
1098
 
1099
- @app.route('/api/my_posts', methods=['GET'])
1100
- def get_my_posts():
1101
- user = get_authenticated_user_details(request.headers)
1102
- if not user:
1103
- return jsonify({"error": "Authentication required or user not found in DB"}), 401
1104
-
1105
- user_id_str = str(user.get('id'))
1106
- data = load_data()
1107
- my_posts = []
1108
-
1109
- for resume in data.get('resumes', []):
1110
- if str(resume.get('user_id')) == user_id_str:
1111
- my_posts.append({**resume, 'item_type': 'resumes'})
1112
-
1113
- for vacancy in data.get('vacancies', []):
1114
- if str(vacancy.get('user_id')) == user_id_str:
1115
- my_posts.append({**vacancy, 'item_type': 'vacancies'})
1116
-
1117
- for offer in data.get('freelance_offers', []):
1118
- if str(offer.get('user_id')) == user_id_str:
1119
- my_posts.append({**offer, 'item_type': 'freelance_offers'})
1120
-
1121
- my_posts_sorted = sorted(my_posts, key=lambda x: x.get('timestamp', ''), reverse=True)
1122
- return jsonify(my_posts_sorted), 200
1123
-
1124
-
1125
  @app.route('/api/<item_type>', methods=['POST'])
1126
  def create_item(item_type):
1127
  user = get_authenticated_user_details(request.headers)
1128
- if not user: # Make sure user is found in our DB
1129
  return jsonify({"error": "Authentication required or user not found in DB"}), 401
1130
 
1131
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
@@ -1137,8 +1066,8 @@ def create_item(item_type):
1137
 
1138
  new_item = {
1139
  "id": str(uuid.uuid4()),
1140
- "user_id": str(user.get('id')), # Use ID from our DB user object
1141
- "user_telegram_username": user.get('username', 'unknown'), # Use username from our DB user object
1142
  "timestamp": datetime.now().isoformat(),
1143
  }
1144
 
@@ -1199,30 +1128,23 @@ def update_item(item_type, item_id):
1199
  if item_index == -1: return jsonify({"error": "Item not found"}), 404
1200
 
1201
  original_item = items_list[item_index]
1202
- # Critical check: user can only edit their own items
1203
  if str(original_item.get('user_id')) != str(user.get('id')):
1204
  return jsonify({"error": "Forbidden: You can only edit your own items"}), 403
1205
 
1206
- updated_item = original_item.copy() # Keep original timestamp, user_id, etc.
1207
- updated_item['updated_timestamp'] = datetime.now().isoformat() # Add/update 'updated_timestamp'
1208
 
1209
- # Update fields based on item_type
1210
  if item_type == 'resumes':
1211
- # Check for required fields if they are being changed from empty
1212
- if 'name' in req_data and not req_data.get('name'): return jsonify({"error": "Missing field: name"}), 400
1213
- if 'title' in req_data and not req_data.get('title'): return jsonify({"error": "Missing field: title"}), 400
1214
  updated_item.update({
1215
  "name": req_data.get('name', original_item.get('name')),
1216
  "title": req_data.get('title', original_item.get('title')),
1217
  "skills": req_data.get('skills', original_item.get('skills')),
1218
  "experience": req_data.get('experience', original_item.get('experience')),
1219
  "education": req_data.get('education', original_item.get('education')),
1220
- "contact": req_data.get('contact', original_item.get('contact')), # Allow clearing contact
1221
  "portfolio_link": req_data.get('portfolio_link', original_item.get('portfolio_link'))
1222
  })
1223
  elif item_type == 'vacancies':
1224
- if 'company_name' in req_data and not req_data.get('company_name'): return jsonify({"error": "Missing field: company_name"}), 400
1225
- if 'title' in req_data and not req_data.get('title'): return jsonify({"error": "Missing field: title"}), 400
1226
  updated_item.update({
1227
  "company_name": req_data.get('company_name', original_item.get('company_name')),
1228
  "title": req_data.get('title', original_item.get('title')),
@@ -1233,7 +1155,6 @@ def update_item(item_type, item_id):
1233
  "contact": req_data.get('contact', original_item.get('contact'))
1234
  })
1235
  elif item_type == 'freelance_offers':
1236
- if 'title' in req_data and not req_data.get('title'): return jsonify({"error": "Missing field: title"}), 400
1237
  updated_item.update({
1238
  "title": req_data.get('title', original_item.get('title')),
1239
  "description": req_data.get('description', original_item.get('description')),
@@ -1262,7 +1183,6 @@ def delete_item(item_type, item_id):
1262
  item_to_delete = next((i for i in items_list if i['id'] == item_id), None)
1263
  if not item_to_delete: return jsonify({"error": "Item not found"}), 404
1264
 
1265
- # Critical check: user can only delete their own items
1266
  if str(item_to_delete.get('user_id')) != str(user.get('id')):
1267
  return jsonify({"error": "Forbidden: You can only delete your own items"}), 403
1268
 
@@ -1271,14 +1191,11 @@ def delete_item(item_type, item_id):
1271
  if len(data[item_type]) < original_length:
1272
  save_data(data)
1273
  return jsonify({"message": "Item deleted successfully"}), 200
1274
- # This case should ideally not be reached if item_to_delete was found
1275
  return jsonify({"error": "Item not found or deletion failed"}), 404
1276
 
1277
 
1278
  @app.route('/admin', methods=['GET'])
1279
  def admin_panel():
1280
- # Basic auth for admin for now, replace with something more secure for production
1281
- # For simplicity, this example doesn't have admin auth. Add it if needed.
1282
  data = load_data()
1283
  return render_template_string(ADMIN_TEMPLATE,
1284
  resumes=sorted(data.get('resumes', []), key=lambda x: x.get('timestamp', ''), reverse=True),
@@ -1287,7 +1204,6 @@ def admin_panel():
1287
 
1288
  @app.route('/admin/delete', methods=['POST'])
1289
  def admin_delete_item():
1290
- # Add admin authentication here
1291
  item_type = request.form.get('item_type')
1292
  item_id = request.form.get('item_id')
1293
 
@@ -1302,17 +1218,16 @@ def admin_delete_item():
1302
 
1303
  if len(data[item_type]) < original_length:
1304
  save_data(data)
1305
- flash(f'{item_type.capitalize()[:-1]} deleted successfully.', 'success') # e.g. Resume deleted
1306
  else:
1307
  flash('Item not found or already deleted.', 'warning')
1308
  return redirect(url_for('admin_panel'))
1309
 
1310
  @app.route('/admin/force_upload', methods=['POST'])
1311
  def force_upload_admin():
1312
- # Add admin authentication here
1313
  logging.info("Admin forcing upload to Hugging Face...")
1314
  try:
1315
- upload_db_to_hf() # Upload all SYNC_FILES
1316
  flash("Data successfully uploaded to Hugging Face.", 'success')
1317
  except Exception as e:
1318
  logging.error(f"Error during forced upload: {e}", exc_info=True)
@@ -1321,12 +1236,11 @@ def force_upload_admin():
1321
 
1322
  @app.route('/admin/force_download', methods=['POST'])
1323
  def force_download_admin():
1324
- # Add admin authentication here
1325
  logging.info("Admin forcing download from Hugging Face...")
1326
  try:
1327
- if download_db_from_hf(): # Download all SYNC_FILES
1328
  flash("Data successfully downloaded from Hugging Face. Local files updated.", 'success')
1329
- load_data() # Reload data into memory after download
1330
  else:
1331
  flash("Failed to download data from Hugging Face. Check logs.", 'error')
1332
  except Exception as e:
@@ -1337,11 +1251,8 @@ def force_download_admin():
1337
 
1338
  if __name__ == '__main__':
1339
  logging.info("Application starting up. Performing initial data load/download...")
1340
- # Attempt to download all sync files first. If DATA_FILE doesn't exist, download_db_from_hf
1341
- # will attempt to create an empty one if it's a 404.
1342
- # Then load_data will either load the downloaded file or the newly created empty one.
1343
  download_db_from_hf()
1344
- load_data() # This loads data into memory, using defaults if file is new/empty
1345
  logging.info("Initial data load complete.")
1346
 
1347
  if HF_TOKEN_WRITE:
 
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
  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...")
 
121
  logging.info(f"Local data loaded successfully from {DATA_FILE}")
122
  if not isinstance(data, dict):
123
  logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
124
+ raise FileNotFoundError
125
+ for key in default_data:
126
  if key not in data: data[key] = default_data[key]
127
  return data
128
  except (FileNotFoundError, json.JSONDecodeError) as e:
 
136
  if not isinstance(data, dict):
137
  logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.")
138
  return default_data
139
+ for key in default_data:
140
  if key not in data: data[key] = default_data[key]
141
  return data
142
  except Exception as load_e:
 
144
  return default_data
145
  else:
146
  logging.error(f"Failed to download {DATA_FILE} from HF. Using empty default data structure.")
147
+ if not os.path.exists(DATA_FILE):
148
  try:
149
  with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(default_data, f)
150
  logging.info(f"Created empty local file {DATA_FILE} after failed download.")
 
157
  if not isinstance(data, dict):
158
  logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
159
  return
 
160
  default_keys = {'resumes': [], 'vacancies': [], 'freelance_offers': [], 'users': {}}
161
  for key in default_keys:
162
+ if key not in data: data[key] = default_keys[key]
 
163
 
164
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
165
  json.dump(data, file, ensure_ascii=False, indent=4)
166
  logging.info(f"Data successfully saved to {DATA_FILE}")
167
+ upload_db_to_hf(specific_file=DATA_FILE)
168
  except Exception as e:
169
  logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
170
 
 
193
  user_data = json.loads(params.get('user', '{}'))
194
  return True, user_data
195
  except json.JSONDecodeError:
196
+ return False, None
197
  return False, None
198
 
199
 
 
263
  border-radius: 50%;
264
  margin-right: 12px;
265
  object-fit: cover;
266
+ background-color: var(--tg-theme-secondary-bg-color);
267
  }
268
  .user-info-bar span {
269
  font-size: 15px;
 
301
  .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); }
302
  .list-item .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 8px; }
303
  .form-container, .detail-view { padding: 20px 15px; background-color: var(--tg-theme-section-bg-color); min-height: calc(100vh - 180px); /* Adjust based on header/tabs/userbar height */ }
 
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 {
 
338
  }
339
  .fab:active { transform: scale(0.92); }
340
  .detail-view h2 { margin-top: 0; font-size: 22px; font-weight: 600; color: var(--tg-theme-text-color); margin-bottom: 15px; }
341
+ .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; }
342
  .detail-view strong { font-weight: 500; color: var(--tg-theme-text-color); }
343
  .detail-view .meta-detail { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 20px; }
344
  .detail-view a { color: var(--tg-theme-link-color); text-decoration: none; }
 
356
  font-weight: 500;
357
  cursor: pointer;
358
  text-align: center;
 
359
  transition: background-color 0.2s ease;
360
  }
361
  .button-destructive {
 
370
  </head>
371
  <body>
372
  <div class="app-container">
373
+ <div class="header" id="appHeader">TonTalent</div>
374
+ <div class="user-info-bar" id="userInfoBar">
375
  <img id="userAvatar" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Avatar">
376
  <span id="userInfoText">Loading user...</span>
377
  </div>
 
389
  <script>
390
  const tg = window.Telegram.WebApp;
391
  let currentUser = null;
392
+ let currentView = 'resumes';
393
+ let currentItem = null;
394
+ let previousViewBeforeMyPosts = 'resumes';
395
  const mainContent = document.getElementById('mainContent');
396
+ const fabButton = document.getElementById('fabButton');
397
+ const appHeader = document.getElementById('appHeader');
398
  const tabOrder = ['resumes', 'vacancies', 'freelance_offers'];
399
 
400
+ function capitalizeFirstLetter(string) {
401
+ return string.charAt(0).toUpperCase() + string.slice(1);
402
+ }
403
+
404
  function applyThemeParams() {
405
  const rootStyle = document.documentElement.style;
406
  rootStyle.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
 
439
  }
440
  }
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  function renderList(items, type) {
443
  mainContent.style.opacity = 0;
444
  if (!items || items.length === 0) {
 
449
  <h3>${item.title || item.name || 'Untitled'}</h3>
450
  ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
451
  ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
452
+ <p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>
453
  </div>
454
  `).join('');
455
  }
 
461
  showDetailView(type, id);
462
  }
463
 
464
+ function formatContactField(contactValue, userTelegramUsername) {
465
+ let displayContact = contactValue;
466
+ if (!displayContact && userTelegramUsername) {
467
+ return `<a href="tg://resolve?domain=${userTelegramUsername}" target="_blank" rel="noopener noreferrer">@${userTelegramUsername}</a>`;
468
+ }
469
+ if (displayContact) {
470
+ if (displayContact.startsWith('@')) {
471
+ const username = displayContact.substring(1);
472
+ return `<a href="tg://resolve?domain=${username}" target="_blank" rel="noopener noreferrer">${displayContact}</a>`;
473
+ } else if (displayContact.startsWith('http://') || displayContact.startsWith('https://')) {
474
+ return `<a href="${displayContact}" target="_blank" rel="noopener noreferrer">${displayContact}</a>`;
475
+ } else if (displayContact.includes('@') && displayContact.includes('.') && !displayContact.startsWith('http')) {
476
+ return `<a href="mailto:${displayContact}">${displayContact}</a>`;
477
+ }
478
+ }
479
+ return displayContact || 'N/A';
480
+ }
481
+
482
  function showDetailView(type, id) {
483
  mainContent.style.opacity = 0;
484
  tg.BackButton.show();
485
  tg.BackButton.onClick(() => {
486
  tg.HapticFeedback.impactOccurred('light');
487
+ if (currentView === 'my_posts') {
488
+ showMyPostsView();
489
+ } else {
490
+ loadView(type);
491
+ }
492
  });
493
  tg.MainButton.hide();
494
+ fabButton.style.display = 'none';
495
 
496
  apiCall(`/api/${type}/${id}`)
497
  .then(item => {
498
  currentItem = item;
499
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
500
+
501
+ const itemContact = formatContactField(item.contact, item.user_telegram_username);
502
+ const postedByLink = item.user_telegram_username
503
+ ? `<a href="tg://resolve?domain=${item.user_telegram_username}" target="_blank" rel="noopener noreferrer">@${item.user_telegram_username}</a>`
504
+ : 'anonymous';
505
+
506
  if (type === 'resumes') {
507
  detailsHtml += `
508
  <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
509
  <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\\n/g, '<br>') : 'N/A'}</p>
510
  <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\\n/g, '<br>') : 'N/A'}</p>
511
+ <p><strong>Contact:</strong> ${itemContact}</p>
512
+ ${item.portfolio_link ? `<p><strong>Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" rel="noopener noreferrer">${item.portfolio_link}</a></p>` : ''}
513
  `;
514
  } else if (type === 'vacancies') {
515
  detailsHtml += `
 
518
  <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\\n/g, '<br>') : 'N/A'}</p>
519
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
520
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
521
+ <p><strong>Contact/Apply:</strong> ${itemContact}</p>
522
  `;
523
  } else if (type === 'freelance_offers') {
524
  detailsHtml += `
 
526
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
527
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
528
  <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
529
+ <p><strong>Contact:</strong> ${itemContact}</p>
530
  `;
531
  }
532
+ detailsHtml += `<p class="meta-detail">Posted by: ${postedByLink} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
533
 
534
  if (currentUser && item.user_id === String(currentUser.id)) {
535
  detailsHtml += `<button id="editItemButton" class="action-button" style="background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); margin-top: 25px;">Edit Post</button>`;
 
541
  if (currentUser && item.user_id === String(currentUser.id)) {
542
  document.getElementById('editItemButton')?.addEventListener('click', () => {
543
  tg.HapticFeedback.impactOccurred('light');
544
+ showForm(type, item);
545
  });
546
  document.getElementById('deleteItemButton')?.addEventListener('click', () => {
547
  tg.HapticFeedback.impactOccurred('light');
 
556
  });
557
  }
558
 
559
+ function showForm(type, itemToEdit = null) {
560
  mainContent.style.opacity = 0;
561
  currentItem = itemToEdit;
 
 
 
562
  tg.BackButton.show();
563
  tg.BackButton.onClick(() => {
564
  tg.HapticFeedback.impactOccurred('light');
565
+ if (itemToEdit) showDetailView(type, itemToEdit.id);
566
+ else if (currentView === 'my_posts') showMyPostsView();
567
+ else loadView(type);
 
 
 
 
 
 
 
 
 
 
 
 
568
  });
569
+ fabButton.style.display = 'none';
570
 
571
  let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1)}</h2>`;
572
  if (type === 'resumes') {
 
576
  <div class="form-group"><label for="skills">Skills (comma separated)</label><textarea id="skills">${itemToEdit?.skills || ''}</textarea></div>
577
  <div class="form-group"><label for="experience">Experience</label><textarea id="experience">${itemToEdit?.experience || ''}</textarea></div>
578
  <div class="form-group"><label for="education">Education</label><textarea id="education">${itemToEdit?.education || ''}</textarea></div>
579
+ <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>
580
  <div class="form-group"><label for="portfolio_link">Portfolio Link (optional)</label><input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}"></div>
581
  `;
582
  } else if (type === 'vacancies') {
 
596
  <div class="form-group"><label for="budget">Budget</label><input type="text" id="budget" value="${itemToEdit?.budget || ''}"></div>
597
  <div class="form-group"><label for="deadline">Expected Deadline</label><input type="text" id="deadline" value="${itemToEdit?.deadline || ''}"></div>
598
  <div class="form-group"><label for="skills_needed">Skills Needed (comma separated)</label><textarea id="skills_needed">${itemToEdit?.skills_needed || ''}</textarea></div>
599
+ <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>
600
  `;
601
  }
602
  formHtml += `<div id="formError" class="error-message"></div></div>`;
 
658
  tg.HapticFeedback.notificationOccurred('success');
659
  tg.MainButton.hideProgress();
660
  tg.MainButton.hide();
661
+ if (currentView === 'my_posts') showMyPostsView();
662
+ else loadView(type);
 
 
 
663
  })
664
  .catch(err => {
665
  tg.HapticFeedback.notificationOccurred('error');
 
668
  });
669
  }
670
 
671
+ function handleDeleteItem(type, itemId) {
672
  tg.showConfirm('Are you sure you want to delete this post?', (confirmed) => {
673
  if (confirmed) {
674
  tg.HapticFeedback.impactOccurred('medium');
 
676
  .then(() => {
677
  tg.HapticFeedback.notificationOccurred('success');
678
  tg.showAlert('Post deleted successfully.');
679
+ if (currentView === 'my_posts') showMyPostsView();
680
+ else loadView(type);
 
 
 
681
  })
682
  .catch(err => {
683
  tg.HapticFeedback.notificationOccurred('error');
 
690
  }
691
 
692
  function loadView(tabName, fromSwipe = false) {
693
+ if (!fromSwipe && currentView !== tabName) tg.HapticFeedback.impactOccurred('light');
 
694
 
695
+ if (currentView === 'my_posts' && tabName !== 'my_posts') {
696
+ appHeader.textContent = 'TonTalent';
 
697
  }
698
+ currentView = tabName;
699
 
700
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
701
+ document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active');
 
702
 
703
  mainContent.style.opacity = 0;
704
  mainContent.innerHTML = `<div class="loading">Loading ${tabName}...</div>`;
705
 
706
  tg.BackButton.hide();
707
  tg.MainButton.hide();
708
+ fabButton.style.display = 'block';
709
 
710
  apiCall(`/api/${tabName}`)
711
  .then(data => renderList(data, tabName))
 
715
  });
716
  }
717
 
718
+ function renderMyPostsList(items) {
719
  mainContent.style.opacity = 0;
720
+ if (!items || items.length === 0) {
721
+ mainContent.innerHTML = `<div class="empty-state">You haven't posted anything yet.</div>`;
722
+ } else {
723
+ mainContent.innerHTML = items.map(item => `
724
+ <div class="list-item" onclick="handleItemClick('${item.type}', '${item.id}')">
725
+ <h3>${item.title || item.name || 'Untitled'} <span style="font-weight:normal; font-size: 0.8em; color: var(--tg-theme-hint-color);">(${capitalizeFirstLetter(item.type.slice(0,-1))})</span></h3>
726
+ ${item.type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
727
+ ${item.type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
728
+ <p class="meta">Posted on ${new Date(item.timestamp).toLocaleDateString()}${item.updated_timestamp ? ' (Edited: ' + new Date(item.updated_timestamp).toLocaleDateString() + ')' : ''}</p>
729
+ </div>
730
+ `).join('');
731
+ }
732
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
733
+ }
734
+
735
+ function showMyPostsView() {
736
+ tg.HapticFeedback.impactOccurred('light');
737
+ if (currentView !== 'my_posts') { // Store the view only if we are not already in my_posts
738
+ previousViewBeforeMyPosts = currentView;
739
+ }
740
+ currentView = 'my_posts';
741
+ appHeader.textContent = 'My Posts';
742
+
743
+ document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
744
+ fabButton.style.display = 'none';
745
+ tg.MainButton.hide();
746
+
747
  tg.BackButton.show();
748
  tg.BackButton.onClick(() => {
749
  tg.HapticFeedback.impactOccurred('light');
750
+ appHeader.textContent = 'TonTalent';
751
+ loadView(previousViewBeforeMyPosts);
752
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
 
754
+ mainContent.style.opacity = 0;
755
+ mainContent.innerHTML = '<div class="loading">Loading your posts...</div>';
756
+
757
+ Promise.all([
758
+ apiCall('/api/resumes'),
759
+ apiCall('/api/vacancies'),
760
+ apiCall('/api/freelance_offers')
761
+ ]).then(([resumes, vacancies, freelanceOffers]) => {
762
+ const myResumes = resumes.filter(item => String(item.user_id) === String(currentUser.id));
763
+ const myVacancies = vacancies.filter(item => String(item.user_id) === String(currentUser.id));
764
+ const myFreelanceOffers = freelanceOffers.filter(item => String(item.user_id) === String(currentUser.id));
765
+
766
+ const allMyPosts = [
767
+ ...myResumes.map(item => ({ ...item, type: 'resumes' })),
768
+ ...myVacancies.map(item => ({ ...item, type: 'vacancies' })),
769
+ ...myFreelanceOffers.map(item => ({ ...item, type: 'freelance_offers' }))
770
+ ].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
771
+
772
+ renderMyPostsList(allMyPosts);
773
+ }).catch(err => {
774
+ mainContent.innerHTML = '<div class="empty-state">Error loading your posts.</div>';
775
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
776
  });
777
  }
778
 
 
781
  const swipeThreshold = 70;
782
 
783
  mainContent.addEventListener('touchstart', e => {
784
+ if (currentView === 'my_posts') return; // Disable swipe on "My Posts" view
785
+ touchstartX = e.changedTouches[0].screenX;
 
 
 
 
786
  }, { passive: true });
787
 
788
  mainContent.addEventListener('touchend', e => {
789
+ if (currentView === 'my_posts') return; // Disable swipe on "My Posts" view
790
  touchendX = e.changedTouches[0].screenX;
791
  handleSwipeGesture();
 
792
  });
793
 
794
  function handleSwipeGesture() {
 
796
  if (Math.abs(swipeLength) < swipeThreshold) return;
797
 
798
  let currentIndex = tabOrder.indexOf(currentView);
799
+ if (currentIndex === -1) return; // Should not happen if currentView is one of the tabs
 
800
  let newIndex;
801
 
802
+ if (touchendX < touchstartX) {
803
  newIndex = (currentIndex + 1) % tabOrder.length;
804
+ } else {
805
  newIndex = (currentIndex - 1 + tabOrder.length) % tabOrder.length;
806
  }
807
 
 
821
 
822
  const userInfoText = document.getElementById('userInfoText');
823
  const userAvatar = document.getElementById('userAvatar');
824
+ const userInfoBar = document.getElementById('userInfoBar');
825
 
826
+ userInfoText.textContent = `Welcome, ${tg.initDataUnsafe.user?.first_name || 'User'}!`;
 
827
  if (tg.initDataUnsafe.user?.username) {
828
+ userInfoText.textContent += ` (@${tg.initDataUnsafe.user.username})`;
829
  }
830
 
831
  try {
832
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
833
+ currentUser = authResponse.user;
834
  if (currentUser) {
835
+ userInfoText.textContent = `${currentUser.first_name || ''} ${currentUser.last_name || ''}`.trim();
836
+ if (currentUser.username) userInfoText.textContent += ` (@${currentUser.username})`;
837
+ else userInfoText.textContent += ` (ID: ${currentUser.id})`;
838
+
839
  if (currentUser.photo_url) {
840
  userAvatar.src = currentUser.photo_url;
841
  } else {
842
+ // Keep default placeholder or hide
843
  }
844
  userInfoBar.classList.add('clickable');
845
  userInfoBar.addEventListener('click', () => {
846
+ if(currentUser) showMyPostsView();
847
+ });
 
 
 
 
 
848
  }
849
  } catch (error) {
850
  console.error("Auth error:", error);
851
  userInfoText.textContent = `Auth failed. Using basic info.`;
852
+ // tg.showAlert("Authentication with the server failed. Some features might be limited.");
853
  }
854
 
855
  document.querySelectorAll('.tab-button').forEach(button => {
856
  button.addEventListener('click', () => loadView(button.dataset.tab));
857
  });
858
+ fabButton.addEventListener('click', () => {
859
  tg.HapticFeedback.impactOccurred('medium');
860
+ showForm(currentView); // currentView should be one of the main tabs here
 
861
  });
862
 
863
+ loadView('resumes');
864
  }
865
 
866
  init();
 
987
  @app.route('/api/auth_user', methods=['POST'])
988
  def auth_user():
989
  auth_data_str = request.headers.get('X-Telegram-Auth')
990
+ if not auth_data_str:
991
  init_data_payload = request.json.get('init_data')
992
  if init_data_payload:
993
  auth_data_str = init_data_payload
 
997
  is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
998
 
999
  if not is_valid or not user_data_from_auth:
 
1000
  return jsonify({"error": "Invalid authentication data"}), 403
1001
 
1002
  data = load_data()
1003
+ users = data.get('users', {})
1004
  user_id_str = str(user_data_from_auth.get('id'))
1005
 
1006
  if user_id_str not in users:
1007
  users[user_id_str] = {
1008
+ 'id': user_data_from_auth.get('id'),
1009
  'first_name': user_data_from_auth.get('first_name'),
1010
  'last_name': user_data_from_auth.get('last_name'),
1011
  'username': user_data_from_auth.get('username'),
 
1013
  'photo_url': user_data_from_auth.get('photo_url'),
1014
  'first_seen': datetime.now().isoformat()
1015
  }
 
1016
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
1017
+ if user_data_from_auth.get('photo_url'):
1018
  users[user_id_str]['photo_url'] = user_data_from_auth.get('photo_url')
 
 
 
 
 
 
1019
 
1020
  data['users'] = users
1021
  save_data(data)
 
1028
  return None
1029
  is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1030
  if is_valid and user_data_from_auth:
1031
+ data = load_data()
1032
  user_id_str = str(user_data_from_auth.get('id'))
1033
+ return data.get('users', {}).get(user_id_str)
1034
  return None
1035
 
1036
  @app.route('/api/<item_type>', methods=['GET'])
 
1051
  return jsonify(item), 200
1052
  return jsonify({"error": "Item not found"}), 404
1053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  @app.route('/api/<item_type>', methods=['POST'])
1055
  def create_item(item_type):
1056
  user = get_authenticated_user_details(request.headers)
1057
+ if not user:
1058
  return jsonify({"error": "Authentication required or user not found in DB"}), 401
1059
 
1060
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
 
1066
 
1067
  new_item = {
1068
  "id": str(uuid.uuid4()),
1069
+ "user_id": str(user.get('id')),
1070
+ "user_telegram_username": user.get('username', 'unknown'),
1071
  "timestamp": datetime.now().isoformat(),
1072
  }
1073
 
 
1128
  if item_index == -1: return jsonify({"error": "Item not found"}), 404
1129
 
1130
  original_item = items_list[item_index]
 
1131
  if str(original_item.get('user_id')) != str(user.get('id')):
1132
  return jsonify({"error": "Forbidden: You can only edit your own items"}), 403
1133
 
1134
+ updated_item = original_item.copy()
1135
+ updated_item['updated_timestamp'] = datetime.now().isoformat()
1136
 
 
1137
  if item_type == 'resumes':
 
 
 
1138
  updated_item.update({
1139
  "name": req_data.get('name', original_item.get('name')),
1140
  "title": req_data.get('title', original_item.get('title')),
1141
  "skills": req_data.get('skills', original_item.get('skills')),
1142
  "experience": req_data.get('experience', original_item.get('experience')),
1143
  "education": req_data.get('education', original_item.get('education')),
1144
+ "contact": req_data.get('contact', original_item.get('contact')),
1145
  "portfolio_link": req_data.get('portfolio_link', original_item.get('portfolio_link'))
1146
  })
1147
  elif item_type == 'vacancies':
 
 
1148
  updated_item.update({
1149
  "company_name": req_data.get('company_name', original_item.get('company_name')),
1150
  "title": req_data.get('title', original_item.get('title')),
 
1155
  "contact": req_data.get('contact', original_item.get('contact'))
1156
  })
1157
  elif item_type == 'freelance_offers':
 
1158
  updated_item.update({
1159
  "title": req_data.get('title', original_item.get('title')),
1160
  "description": req_data.get('description', original_item.get('description')),
 
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 str(item_to_delete.get('user_id')) != str(user.get('id')):
1187
  return jsonify({"error": "Forbidden: You can only delete your own items"}), 403
1188
 
 
1191
  if len(data[item_type]) < original_length:
1192
  save_data(data)
1193
  return jsonify({"message": "Item deleted successfully"}), 200
 
1194
  return jsonify({"error": "Item not found or deletion failed"}), 404
1195
 
1196
 
1197
  @app.route('/admin', methods=['GET'])
1198
  def admin_panel():
 
 
1199
  data = load_data()
1200
  return render_template_string(ADMIN_TEMPLATE,
1201
  resumes=sorted(data.get('resumes', []), key=lambda x: x.get('timestamp', ''), reverse=True),
 
1204
 
1205
  @app.route('/admin/delete', methods=['POST'])
1206
  def admin_delete_item():
 
1207
  item_type = request.form.get('item_type')
1208
  item_id = request.form.get('item_id')
1209
 
 
1218
 
1219
  if len(data[item_type]) < original_length:
1220
  save_data(data)
1221
+ flash(f'{item_type.capitalize()[:-1]} deleted successfully.', 'success')
1222
  else:
1223
  flash('Item not found or already deleted.', 'warning')
1224
  return redirect(url_for('admin_panel'))
1225
 
1226
  @app.route('/admin/force_upload', methods=['POST'])
1227
  def force_upload_admin():
 
1228
  logging.info("Admin forcing upload to Hugging Face...")
1229
  try:
1230
+ upload_db_to_hf()
1231
  flash("Data successfully uploaded to Hugging Face.", 'success')
1232
  except Exception as e:
1233
  logging.error(f"Error during forced upload: {e}", exc_info=True)
 
1236
 
1237
  @app.route('/admin/force_download', methods=['POST'])
1238
  def force_download_admin():
 
1239
  logging.info("Admin forcing download from Hugging Face...")
1240
  try:
1241
+ if download_db_from_hf():
1242
  flash("Data successfully downloaded from Hugging Face. Local files updated.", 'success')
1243
+ load_data()
1244
  else:
1245
  flash("Failed to download data from Hugging Face. Check logs.", 'error')
1246
  except Exception as e:
 
1251
 
1252
  if __name__ == '__main__':
1253
  logging.info("Application starting up. Performing initial data load/download...")
 
 
 
1254
  download_db_from_hf()
1255
+ load_data()
1256
  logging.info("Initial data load complete.")
1257
 
1258
  if HF_TOKEN_WRITE: