Shveiauto commited on
Commit
508b4f1
·
verified ·
1 Parent(s): 609f6e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -184
app.py CHANGED
@@ -25,7 +25,7 @@ REPO_ID = os.getenv("HF_REPO_ID", "Kgshop/tontalent2")
25
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
26
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
 
28
- TELEGRAM_BOT_TOKEN = "7549355625:AAGhdbf6x1JEzpH0mUtuxTF83Soi7MFVNZ8"
29
 
30
  DOWNLOAD_RETRIES = 3
31
  DOWNLOAD_DELAY = 5
@@ -66,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
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
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,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:
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):
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,14 +157,16 @@ 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
  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,7 +195,7 @@ def verify_telegram_auth_data(auth_data_str, bot_token):
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
 
@@ -249,11 +251,13 @@ MAIN_APP_TEMPLATE = '''
249
  padding: 10px 15px;
250
  background-color: var(--tg-theme-section-bg-color);
251
  border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color);
 
 
252
  cursor: pointer;
253
  transition: background-color 0.2s ease;
254
  }
255
- .user-info-bar:active {
256
- background-color: color-mix(in srgb, var(--tg-theme-section-bg-color) 90%, var(--tg-theme-hint-color));
257
  }
258
  .user-info-bar img {
259
  width: 40px;
@@ -261,7 +265,7 @@ MAIN_APP_TEMPLATE = '''
261
  border-radius: 50%;
262
  margin-right: 12px;
263
  object-fit: cover;
264
- background-color: var(--tg-theme-secondary-bg-color);
265
  }
266
  .user-info-bar span {
267
  font-size: 15px;
@@ -298,7 +302,8 @@ MAIN_APP_TEMPLATE = '''
298
  .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
299
  .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); }
300
  .list-item .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 8px; }
301
- .form-container, .detail-view { padding: 20px 15px; background-color: var(--tg-theme-section-bg-color); min-height: calc(100vh - 180px); }
 
302
  .form-group { margin-bottom: 18px; }
303
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
304
  .form-group input, .form-group textarea {
@@ -336,7 +341,7 @@ MAIN_APP_TEMPLATE = '''
336
  }
337
  .fab:active { transform: scale(0.92); }
338
  .detail-view h2 { margin-top: 0; font-size: 22px; font-weight: 600; color: var(--tg-theme-text-color); margin-bottom: 15px; }
339
- .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; }
340
  .detail-view strong { font-weight: 500; color: var(--tg-theme-text-color); }
341
  .detail-view .meta-detail { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 20px; }
342
  .detail-view a { color: var(--tg-theme-link-color); text-decoration: none; }
@@ -354,6 +359,7 @@ MAIN_APP_TEMPLATE = '''
354
  font-weight: 500;
355
  cursor: pointer;
356
  text-align: center;
 
357
  transition: background-color 0.2s ease;
358
  }
359
  .button-destructive {
@@ -387,9 +393,9 @@ MAIN_APP_TEMPLATE = '''
387
  <script>
388
  const tg = window.Telegram.WebApp;
389
  let currentUser = null;
390
- let currentView = 'resumes';
391
  let currentItem = null;
392
- let currentAppContext = 'main'; // 'main' or 'profile'
393
  const mainContent = document.getElementById('mainContent');
394
  const tabOrder = ['resumes', 'vacancies', 'freelance_offers'];
395
 
@@ -431,38 +437,31 @@ MAIN_APP_TEMPLATE = '''
431
  }
432
  }
433
 
434
- function makeClickable(text, isForPostedBy = false) {
435
- if (!text || text.trim() === '' || text.toLowerCase() === 'n/a') {
436
- return text || 'N/A';
437
- }
438
-
439
- let displayText = text.trim();
440
 
441
- if (displayText.startsWith('@')) {
442
- const username = displayText.substring(1);
443
- if (username.toLowerCase() === 'anonymous' && isForPostedBy) {
444
- return '@anonymous';
445
- }
446
- if (/^[a-zA-Z0-9_]{5,32}$/.test(username)) {
447
- const teleLink = `https://t.me/${username}`;
448
- return `<a href="${teleLink}" onclick="tg.openTelegramLink('${teleLink}'); return false;">${displayText}</a>`;
449
- } else {
450
- return displayText;
451
- }
452
- }
453
-
454
- if (displayText.toLowerCase().startsWith('http://') || displayText.toLowerCase().startsWith('https://')) {
455
- try {
456
- new URL(displayText);
457
- return `<a href="${displayText}" onclick="tg.openLink('${displayText}'); return false;">${displayText}</a>`;
458
- } catch (_) { /* Invalid URL, fall through */ }
459
  }
460
 
461
- if (/\\S+@\\S+\\.\\S+/.test(displayText)) {
462
- return `<a href="mailto:${displayText}">${displayText}</a>`;
 
 
 
 
 
 
 
 
 
 
 
463
  }
464
-
465
- return displayText;
466
  }
467
 
468
  function renderList(items, type) {
@@ -475,7 +474,7 @@ MAIN_APP_TEMPLATE = '''
475
  <h3>${item.title || item.name || 'Untitled'}</h3>
476
  ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
477
  ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
478
- <p class="meta">Posted by: ${makeClickable('@' + (item.user_telegram_username || 'anonymous'), true)} on ${new Date(item.timestamp).toLocaleDateString()}</p>
479
  </div>
480
  `).join('');
481
  }
@@ -492,11 +491,7 @@ MAIN_APP_TEMPLATE = '''
492
  tg.BackButton.show();
493
  tg.BackButton.onClick(() => {
494
  tg.HapticFeedback.impactOccurred('light');
495
- if (currentAppContext === 'profile') {
496
- showMyProfileView();
497
- } else {
498
- loadView(currentView);
499
- }
500
  });
501
  tg.MainButton.hide();
502
  document.getElementById('fabButton').style.display = 'none';
@@ -505,24 +500,13 @@ MAIN_APP_TEMPLATE = '''
505
  .then(item => {
506
  currentItem = item;
507
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
508
-
509
- let contactToDisplay;
510
- if (item.contact && item.contact.trim() !== '') {
511
- contactToDisplay = item.contact.trim();
512
- } else if (item.user_telegram_username && item.user_telegram_username.trim() !== '') {
513
- contactToDisplay = `@${item.user_telegram_username.trim()}`;
514
- } else {
515
- contactToDisplay = 'N/A';
516
- }
517
- const postedByUsername = item.user_telegram_username ? `@${item.user_telegram_username.trim()}` : '@anonymous';
518
-
519
  if (type === 'resumes') {
520
  detailsHtml += `
521
  <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
522
  <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\\n/g, '<br>') : 'N/A'}</p>
523
  <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\\n/g, '<br>') : 'N/A'}</p>
524
- <p><strong>Contact:</strong> ${makeClickable(contactToDisplay)}</p>
525
- ${item.portfolio_link ? `<p><strong>Portfolio:</strong> ${makeClickable(item.portfolio_link)}</p>` : ''}
526
  `;
527
  } else if (type === 'vacancies') {
528
  detailsHtml += `
@@ -531,7 +515,7 @@ MAIN_APP_TEMPLATE = '''
531
  <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\\n/g, '<br>') : 'N/A'}</p>
532
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
533
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
534
- <p><strong>Contact/Apply:</strong> ${makeClickable(contactToDisplay)}</p>
535
  `;
536
  } else if (type === 'freelance_offers') {
537
  detailsHtml += `
@@ -539,10 +523,10 @@ MAIN_APP_TEMPLATE = '''
539
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
540
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
541
  <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
542
- <p><strong>Contact:</strong> ${makeClickable(contactToDisplay)}</p>
543
  `;
544
  }
545
- detailsHtml += `<p class="meta-detail">Posted by: ${makeClickable(postedByUsername, true)} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
546
 
547
  if (currentUser && item.user_id === String(currentUser.id)) {
548
  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>`;
@@ -554,7 +538,7 @@ MAIN_APP_TEMPLATE = '''
554
  if (currentUser && item.user_id === String(currentUser.id)) {
555
  document.getElementById('editItemButton')?.addEventListener('click', () => {
556
  tg.HapticFeedback.impactOccurred('light');
557
- showForm(type, item);
558
  });
559
  document.getElementById('deleteItemButton')?.addEventListener('click', () => {
560
  tg.HapticFeedback.impactOccurred('light');
@@ -569,15 +553,30 @@ MAIN_APP_TEMPLATE = '''
569
  });
570
  }
571
 
572
- function showForm(type, itemToEdit = null) {
573
  mainContent.style.opacity = 0;
574
  currentItem = itemToEdit;
 
 
 
575
  tg.BackButton.show();
576
  tg.BackButton.onClick(() => {
577
  tg.HapticFeedback.impactOccurred('light');
578
- if (itemToEdit) showDetailView(type, itemToEdit.id);
579
- else if (currentAppContext === 'profile') showMyProfileView();
580
- else loadView(type);
 
 
 
 
 
 
 
 
 
 
 
 
581
  });
582
  document.getElementById('fabButton').style.display = 'none';
583
 
@@ -589,7 +588,7 @@ MAIN_APP_TEMPLATE = '''
589
  <div class="form-group"><label for="skills">Skills (comma separated)</label><textarea id="skills">${itemToEdit?.skills || ''}</textarea></div>
590
  <div class="form-group"><label for="experience">Experience</label><textarea id="experience">${itemToEdit?.experience || ''}</textarea></div>
591
  <div class="form-group"><label for="education">Education</label><textarea id="education">${itemToEdit?.education || ''}</textarea></div>
592
- <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>
593
  <div class="form-group"><label for="portfolio_link">Portfolio Link (optional)</label><input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}"></div>
594
  `;
595
  } else if (type === 'vacancies') {
@@ -609,7 +608,7 @@ MAIN_APP_TEMPLATE = '''
609
  <div class="form-group"><label for="budget">Budget</label><input type="text" id="budget" value="${itemToEdit?.budget || ''}"></div>
610
  <div class="form-group"><label for="deadline">Expected Deadline</label><input type="text" id="deadline" value="${itemToEdit?.deadline || ''}"></div>
611
  <div class="form-group"><label for="skills_needed">Skills Needed (comma separated)</label><textarea id="skills_needed">${itemToEdit?.skills_needed || ''}</textarea></div>
612
- <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>
613
  `;
614
  }
615
  formHtml += `<div id="formError" class="error-message"></div></div>`;
@@ -671,8 +670,8 @@ MAIN_APP_TEMPLATE = '''
671
  tg.HapticFeedback.notificationOccurred('success');
672
  tg.MainButton.hideProgress();
673
  tg.MainButton.hide();
674
- if (currentAppContext === 'profile') {
675
- showMyProfileView();
676
  } else {
677
  loadView(type);
678
  }
@@ -684,7 +683,7 @@ MAIN_APP_TEMPLATE = '''
684
  });
685
  }
686
 
687
- function handleDeleteItem(type, itemId) {
688
  tg.showConfirm('Are you sure you want to delete this post?', (confirmed) => {
689
  if (confirmed) {
690
  tg.HapticFeedback.impactOccurred('medium');
@@ -692,10 +691,10 @@ MAIN_APP_TEMPLATE = '''
692
  .then(() => {
693
  tg.HapticFeedback.notificationOccurred('success');
694
  tg.showAlert('Post deleted successfully.');
695
- if (currentAppContext === 'profile') {
696
- showMyProfileView();
697
  } else {
698
- loadView(type);
699
  }
700
  })
701
  .catch(err => {
@@ -710,10 +709,16 @@ MAIN_APP_TEMPLATE = '''
710
 
711
  function loadView(tabName, fromSwipe = false) {
712
  if (!fromSwipe) tg.HapticFeedback.impactOccurred('light');
713
- currentView = tabName;
714
- currentAppContext = 'main';
 
 
 
 
 
715
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
716
- document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active');
 
717
 
718
  mainContent.style.opacity = 0;
719
  mainContent.innerHTML = `<div class="loading">Loading ${tabName}...</div>`;
@@ -729,83 +734,106 @@ MAIN_APP_TEMPLATE = '''
729
  setTimeout(() => { mainContent.style.opacity = 1; }, 50);
730
  });
731
  }
732
-
733
- function showMyProfileView() {
734
- currentAppContext = 'profile';
735
  mainContent.style.opacity = 0;
736
- mainContent.innerHTML = \`<div class="loading">Loading your posts...</div>\`;
737
-
738
- document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
739
- document.getElementById('fabButton').style.display = 'none';
740
- tg.MainButton.hide();
741
-
742
  tg.BackButton.show();
743
  tg.BackButton.onClick(() => {
744
  tg.HapticFeedback.impactOccurred('light');
745
- loadView(currentView);
746
  });
 
 
 
747
 
748
- apiCall('/api/me/items')
749
- .then(data => {
750
- let profileHtml = '<div class="profile-view" style="padding-top: 10px;">';
751
- profileHtml += \`<h1 style="font-size: 20px; font-weight: 600; color: var(--tg-theme-text-color); margin: 0 15px 15px; padding-bottom: 10px; border-bottom: 1px solid var(--tg-theme-secondary-bg-color);">My Posts</h1>\`;
752
- let hasItems = false;
753
-
754
- const renderSection = (items, type, title) => {
755
- if (items && items.length > 0) {
756
- hasItems = true;
757
- profileHtml += \`<h2 style="font-size: 18px; font-weight: 600; color: var(--tg-theme-text-color); margin: 20px 15px 10px;">My \${title}</h2>\`;
758
- profileHtml += items.map(item => \`
759
- <div class="list-item" onclick="handleItemClick('\${type}', '\${item.id}')">
760
- <h3>\${item.title || item.name || 'Untitled'}</h3>
761
- <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
762
- </div>
763
- \`).join('');
764
- }
765
- };
766
-
767
- renderSection(data.resumes, 'resumes', 'Resumes');
768
- renderSection(data.vacancies, 'vacancies', 'Vacancies');
769
- renderSection(data.freelance_offers, 'freelance_offers', 'Freelance Offers');
770
 
771
- if (!hasItems) {
772
- profileHtml += \`<div class="empty-state" style="padding: 20px 15px;">You haven't posted anything yet.</div>\`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  }
774
-
775
- profileHtml += '</div>';
776
- mainContent.innerHTML = profileHtml;
777
  setTimeout(() => { mainContent.style.opacity = 1; }, 50);
778
  })
779
  .catch(err => {
780
- mainContent.innerHTML = \`<div class="empty-state">Error loading your posts.</div>\`;
781
  setTimeout(() => { mainContent.style.opacity = 1; }, 50);
782
  });
783
  }
784
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
  let touchstartX = 0;
786
  let touchendX = 0;
787
  const swipeThreshold = 70;
788
 
789
  mainContent.addEventListener('touchstart', e => {
790
- touchstartX = e.changedTouches[0].screenX;
 
 
 
 
 
791
  }, { passive: true });
792
 
793
  mainContent.addEventListener('touchend', e => {
 
794
  touchendX = e.changedTouches[0].screenX;
795
  handleSwipeGesture();
 
796
  });
797
 
798
  function handleSwipeGesture() {
799
- if (currentAppContext !== 'main') return; // Only allow swipe on main tab view
800
  const swipeLength = touchendX - touchstartX;
801
  if (Math.abs(swipeLength) < swipeThreshold) return;
802
 
803
  let currentIndex = tabOrder.indexOf(currentView);
 
 
804
  let newIndex;
805
 
806
- if (touchendX < touchstartX) {
807
  newIndex = (currentIndex + 1) % tabOrder.length;
808
- } else {
809
  newIndex = (currentIndex - 1 + tabOrder.length) % tabOrder.length;
810
  }
811
 
@@ -825,48 +853,51 @@ MAIN_APP_TEMPLATE = '''
825
 
826
  const userInfoText = document.getElementById('userInfoText');
827
  const userAvatar = document.getElementById('userAvatar');
 
828
 
829
- userInfoText.textContent = `Welcome, ${tg.initDataUnsafe.user?.first_name || 'User'}!`;
 
830
  if (tg.initDataUnsafe.user?.username) {
831
- userInfoText.textContent += ` (@${tg.initDataUnsafe.user.username})`;
832
  }
833
 
834
  try {
835
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
836
- currentUser = authResponse.user;
837
  if (currentUser) {
838
- userInfoText.textContent = `${currentUser.first_name || ''} ${currentUser.last_name || ''}`.trim();
839
- if (currentUser.username) userInfoText.textContent += ` (@${currentUser.username})`;
840
  if (currentUser.photo_url) {
841
  userAvatar.src = currentUser.photo_url;
842
  } else {
843
- userAvatar.style.display = 'none';
844
  }
 
 
 
 
 
 
 
 
 
845
  }
846
  } catch (error) {
847
  console.error("Auth error:", error);
848
  userInfoText.textContent = `Auth failed. Using basic info.`;
849
- tg.showAlert("Authentication with the server failed. Some features might be limited.");
850
  }
851
-
852
- document.querySelector('.user-info-bar').addEventListener('click', () => {
853
- if (currentUser) {
854
- tg.HapticFeedback.impactOccurred('light');
855
- showMyProfileView();
856
- } else {
857
- tg.showAlert('Please wait, user data is loading or authentication failed.');
858
- }
859
- });
860
 
861
  document.querySelectorAll('.tab-button').forEach(button => {
862
  button.addEventListener('click', () => loadView(button.dataset.tab));
863
  });
864
  document.getElementById('fabButton').addEventListener('click', () => {
865
  tg.HapticFeedback.impactOccurred('medium');
866
- showForm(currentView);
 
867
  });
868
 
869
- loadView('resumes');
870
  }
871
 
872
  init();
@@ -993,7 +1024,7 @@ def main_app_view():
993
  @app.route('/api/auth_user', methods=['POST'])
994
  def auth_user():
995
  auth_data_str = request.headers.get('X-Telegram-Auth')
996
- if not auth_data_str:
997
  init_data_payload = request.json.get('init_data')
998
  if init_data_payload:
999
  auth_data_str = init_data_payload
@@ -1003,15 +1034,16 @@ def auth_user():
1003
  is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1004
 
1005
  if not is_valid or not user_data_from_auth:
 
1006
  return jsonify({"error": "Invalid authentication data"}), 403
1007
 
1008
  data = load_data()
1009
- users = data.get('users', {})
1010
  user_id_str = str(user_data_from_auth.get('id'))
1011
 
1012
  if user_id_str not in users:
1013
  users[user_id_str] = {
1014
- 'id': user_data_from_auth.get('id'),
1015
  'first_name': user_data_from_auth.get('first_name'),
1016
  'last_name': user_data_from_auth.get('last_name'),
1017
  'username': user_data_from_auth.get('username'),
@@ -1019,9 +1051,16 @@ def auth_user():
1019
  'photo_url': user_data_from_auth.get('photo_url'),
1020
  'first_seen': datetime.now().isoformat()
1021
  }
 
1022
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
1023
- if user_data_from_auth.get('photo_url'):
1024
  users[user_id_str]['photo_url'] = user_data_from_auth.get('photo_url')
 
 
 
 
 
 
1025
 
1026
  data['users'] = users
1027
  save_data(data)
@@ -1034,36 +1073,11 @@ def get_authenticated_user_details(request_headers):
1034
  return None
1035
  is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1036
  if is_valid and user_data_from_auth:
1037
- data = load_data()
1038
  user_id_str = str(user_data_from_auth.get('id'))
1039
- return data.get('users', {}).get(user_id_str)
1040
  return None
1041
 
1042
- @app.route('/api/me/items', methods=['GET'])
1043
- def get_my_items():
1044
- user = get_authenticated_user_details(request.headers)
1045
- if not user:
1046
- return jsonify({"error": "Authentication required or user not found in DB"}), 401
1047
-
1048
- data = load_data()
1049
- user_id_str = str(user.get('id'))
1050
-
1051
- my_items = {
1052
- 'resumes': [],
1053
- 'vacancies': [],
1054
- 'freelance_offers': []
1055
- }
1056
-
1057
- for item_type_key in ['resumes', 'vacancies', 'freelance_offers']:
1058
- for item_data in data.get(item_type_key, []):
1059
- if str(item_data.get('user_id')) == user_id_str:
1060
- my_items[item_type_key].append(item_data)
1061
-
1062
- for item_type_key in my_items:
1063
- my_items[item_type_key] = sorted(my_items[item_type_key], key=lambda x: x.get('timestamp', ''), reverse=True)
1064
-
1065
- return jsonify(my_items), 200
1066
-
1067
  @app.route('/api/<item_type>', methods=['GET'])
1068
  def get_items(item_type):
1069
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
@@ -1082,10 +1096,36 @@ def get_item(item_type, item_id):
1082
  return jsonify(item), 200
1083
  return jsonify({"error": "Item not found"}), 404
1084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
  @app.route('/api/<item_type>', methods=['POST'])
1086
  def create_item(item_type):
1087
  user = get_authenticated_user_details(request.headers)
1088
- if not user:
1089
  return jsonify({"error": "Authentication required or user not found in DB"}), 401
1090
 
1091
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
@@ -1097,8 +1137,8 @@ def create_item(item_type):
1097
 
1098
  new_item = {
1099
  "id": str(uuid.uuid4()),
1100
- "user_id": str(user.get('id')),
1101
- "user_telegram_username": user.get('username', 'unknown'),
1102
  "timestamp": datetime.now().isoformat(),
1103
  }
1104
 
@@ -1159,23 +1199,30 @@ def update_item(item_type, item_id):
1159
  if item_index == -1: return jsonify({"error": "Item not found"}), 404
1160
 
1161
  original_item = items_list[item_index]
 
1162
  if str(original_item.get('user_id')) != str(user.get('id')):
1163
  return jsonify({"error": "Forbidden: You can only edit your own items"}), 403
1164
 
1165
- updated_item = original_item.copy()
1166
- updated_item['updated_timestamp'] = datetime.now().isoformat()
1167
 
 
1168
  if item_type == 'resumes':
 
 
 
1169
  updated_item.update({
1170
  "name": req_data.get('name', original_item.get('name')),
1171
  "title": req_data.get('title', original_item.get('title')),
1172
  "skills": req_data.get('skills', original_item.get('skills')),
1173
  "experience": req_data.get('experience', original_item.get('experience')),
1174
  "education": req_data.get('education', original_item.get('education')),
1175
- "contact": req_data.get('contact', original_item.get('contact')),
1176
  "portfolio_link": req_data.get('portfolio_link', original_item.get('portfolio_link'))
1177
  })
1178
  elif item_type == 'vacancies':
 
 
1179
  updated_item.update({
1180
  "company_name": req_data.get('company_name', original_item.get('company_name')),
1181
  "title": req_data.get('title', original_item.get('title')),
@@ -1186,6 +1233,7 @@ def update_item(item_type, item_id):
1186
  "contact": req_data.get('contact', original_item.get('contact'))
1187
  })
1188
  elif item_type == 'freelance_offers':
 
1189
  updated_item.update({
1190
  "title": req_data.get('title', original_item.get('title')),
1191
  "description": req_data.get('description', original_item.get('description')),
@@ -1214,6 +1262,7 @@ def delete_item(item_type, item_id):
1214
  item_to_delete = next((i for i in items_list if i['id'] == item_id), None)
1215
  if not item_to_delete: return jsonify({"error": "Item not found"}), 404
1216
 
 
1217
  if str(item_to_delete.get('user_id')) != str(user.get('id')):
1218
  return jsonify({"error": "Forbidden: You can only delete your own items"}), 403
1219
 
@@ -1222,11 +1271,14 @@ def delete_item(item_type, item_id):
1222
  if len(data[item_type]) < original_length:
1223
  save_data(data)
1224
  return jsonify({"message": "Item deleted successfully"}), 200
 
1225
  return jsonify({"error": "Item not found or deletion failed"}), 404
1226
 
1227
 
1228
  @app.route('/admin', methods=['GET'])
1229
  def admin_panel():
 
 
1230
  data = load_data()
1231
  return render_template_string(ADMIN_TEMPLATE,
1232
  resumes=sorted(data.get('resumes', []), key=lambda x: x.get('timestamp', ''), reverse=True),
@@ -1235,6 +1287,7 @@ def admin_panel():
1235
 
1236
  @app.route('/admin/delete', methods=['POST'])
1237
  def admin_delete_item():
 
1238
  item_type = request.form.get('item_type')
1239
  item_id = request.form.get('item_id')
1240
 
@@ -1249,16 +1302,17 @@ def admin_delete_item():
1249
 
1250
  if len(data[item_type]) < original_length:
1251
  save_data(data)
1252
- flash(f'{item_type.capitalize()[:-1]} deleted successfully.', 'success')
1253
  else:
1254
  flash('Item not found or already deleted.', 'warning')
1255
  return redirect(url_for('admin_panel'))
1256
 
1257
  @app.route('/admin/force_upload', methods=['POST'])
1258
  def force_upload_admin():
 
1259
  logging.info("Admin forcing upload to Hugging Face...")
1260
  try:
1261
- upload_db_to_hf()
1262
  flash("Data successfully uploaded to Hugging Face.", 'success')
1263
  except Exception as e:
1264
  logging.error(f"Error during forced upload: {e}", exc_info=True)
@@ -1267,11 +1321,12 @@ def force_upload_admin():
1267
 
1268
  @app.route('/admin/force_download', methods=['POST'])
1269
  def force_download_admin():
 
1270
  logging.info("Admin forcing download from Hugging Face...")
1271
  try:
1272
- if download_db_from_hf():
1273
  flash("Data successfully downloaded from Hugging Face. Local files updated.", 'success')
1274
- load_data()
1275
  else:
1276
  flash("Failed to download data from Hugging Face. Check logs.", 'error')
1277
  except Exception as e:
@@ -1282,8 +1337,11 @@ def force_download_admin():
1282
 
1283
  if __name__ == '__main__':
1284
  logging.info("Application starting up. Performing initial data load/download...")
 
 
 
1285
  download_db_from_hf()
1286
- load_data()
1287
  logging.info("Initial data load complete.")
1288
 
1289
  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 = 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
  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
  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
  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
  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
  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
  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
 
 
251
  padding: 10px 15px;
252
  background-color: var(--tg-theme-section-bg-color);
253
  border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color);
254
+ }
255
+ .user-info-bar.clickable {
256
  cursor: pointer;
257
  transition: background-color 0.2s ease;
258
  }
259
+ .user-info-bar.clickable:active {
260
+ background-color: var(--tg-theme-secondary-bg-color);
261
  }
262
  .user-info-bar img {
263
  width: 40px;
 
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;
 
302
  .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
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
  }
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
  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 {
 
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
 
 
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) {
 
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
  }
 
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';
 
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
  <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
  <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
  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
  });
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
 
 
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
  <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
  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
  }
 
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
  .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 => {
 
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>`;
 
734
  setTimeout(() => { mainContent.style.opacity = 1; }, 50);
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
+
805
  let touchstartX = 0;
806
  let touchendX = 0;
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() {
 
826
  const swipeLength = touchendX - touchstartX;
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
 
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
  @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
  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
  '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
  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'])
1082
  def get_items(item_type):
1083
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
 
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
 
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
  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
  "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
  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
  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
 
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
 
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
 
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
 
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: