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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -155
app.py CHANGED
@@ -250,6 +250,10 @@ MAIN_APP_TEMPLATE = '''
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
  }
254
  .user-info-bar img {
255
  width: 40px;
@@ -294,7 +298,7 @@ MAIN_APP_TEMPLATE = '''
294
  .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
295
  .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); }
296
  .list-item .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 8px; }
297
- .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 */ }
298
  .form-group { margin-bottom: 18px; }
299
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
300
  .form-group input, .form-group textarea {
@@ -351,7 +355,6 @@ MAIN_APP_TEMPLATE = '''
351
  cursor: pointer;
352
  text-align: center;
353
  transition: background-color 0.2s ease;
354
- box-sizing: border-box;
355
  }
356
  .button-destructive {
357
  background-color: var(--tg-theme-destructive-text-color);
@@ -366,11 +369,11 @@ MAIN_APP_TEMPLATE = '''
366
  <body>
367
  <div class="app-container">
368
  <div class="header">TonTalent</div>
369
- <div class="user-info-bar" id="userInfoBar">
370
  <img id="userAvatar" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Avatar">
371
  <span id="userInfoText">Loading user...</span>
372
  </div>
373
- <div class="tabs" id="tabsContainer">
374
  <button class="tab-button active" data-tab="resumes">Resumes</button>
375
  <button class="tab-button" data-tab="vacancies">Vacancies</button>
376
  <button class="tab-button" data-tab="freelance_offers">Freelance</button>
@@ -386,11 +389,9 @@ MAIN_APP_TEMPLATE = '''
386
  let currentUser = null;
387
  let currentView = 'resumes';
388
  let currentItem = null;
 
389
  const mainContent = document.getElementById('mainContent');
390
  const tabOrder = ['resumes', 'vacancies', 'freelance_offers'];
391
-
392
- window.myEditablePosts = {};
393
- window.customBackNavigation = null;
394
 
395
  function applyThemeParams() {
396
  const rootStyle = document.documentElement.style;
@@ -430,6 +431,40 @@ MAIN_APP_TEMPLATE = '''
430
  }
431
  }
432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  function renderList(items, type) {
434
  mainContent.style.opacity = 0;
435
  if (!items || items.length === 0) {
@@ -440,7 +475,7 @@ MAIN_APP_TEMPLATE = '''
440
  <h3>${item.title || item.name || 'Untitled'}</h3>
441
  ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
442
  ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
443
- <p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>
444
  </div>
445
  `).join('');
446
  }
@@ -452,79 +487,62 @@ MAIN_APP_TEMPLATE = '''
452
  showDetailView(type, id);
453
  }
454
 
455
- function renderContactLink(contact, username) {
456
- let contactValue = contact || (username ? `@${username}` : '');
457
- if (!contactValue) return 'N/A';
458
-
459
- if (contactValue.startsWith('@')) {
460
- const tgUsername = contactValue.substring(1);
461
- return `<a href="https://t.me/${tgUsername}" target="_blank" rel="noopener noreferrer">${contactValue}</a>`;
462
- } else if (contactValue.startsWith('http://') || contactValue.startsWith('https://')) {
463
- try {
464
- const url = new URL(contactValue); // Validate URL
465
- return `<a href="${url.href}" target="_blank" rel="noopener noreferrer">${url.href}</a>`;
466
- } catch (e) {
467
- return escapeHtml(contactValue); // Invalid URL, display as text
468
- }
469
- } else {
470
- return escapeHtml(contactValue); // Plain text or email
471
- }
472
- }
473
-
474
- function escapeHtml(unsafe) {
475
- if (typeof unsafe !== 'string') return '';
476
- return unsafe
477
- .replace(/&/g, "&")
478
- .replace(/</g, "<")
479
- .replace(/>/g, ">")
480
- .replace(/"/g, """)
481
- .replace(/'/g, "'");
482
- }
483
-
484
-
485
  function showDetailView(type, id) {
486
  mainContent.style.opacity = 0;
487
  tg.BackButton.show();
488
  tg.BackButton.onClick(() => {
489
  tg.HapticFeedback.impactOccurred('light');
490
- loadView(type);
491
- window.customBackNavigation = null;
 
 
 
492
  });
493
  tg.MainButton.hide();
494
  document.getElementById('fabButton').style.display = 'none';
495
- document.getElementById('tabsContainer').style.display = 'none';
496
 
497
  apiCall(`/api/${type}/${id}`)
498
  .then(item => {
499
  currentItem = item;
500
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
 
 
 
 
 
 
 
 
 
 
 
501
  if (type === 'resumes') {
502
  detailsHtml += `
503
- <p><strong>Skills:</strong> ${escapeHtml(item.skills) || 'N/A'}</p>
504
- <p><strong>Experience:</strong><br>${item.experience ? escapeHtml(item.experience).replace(/\r\n|\r|\n/g, '<br>') : 'N/A'}</p>
505
- <p><strong>Education:</strong><br>${item.education ? escapeHtml(item.education).replace(/\r\n|\r|\n/g, '<br>') : 'N/A'}</p>
506
- <p><strong>Contact:</strong> ${renderContactLink(item.contact, item.user_telegram_username)}</p>
507
- ${item.portfolio_link ? `<p><strong>Portfolio:</strong> ${renderContactLink(item.portfolio_link)}</p>` : ''}
508
  `;
509
  } else if (type === 'vacancies') {
510
  detailsHtml += `
511
- <p><strong>Company:</strong> ${escapeHtml(item.company_name) || 'N/A'}</p>
512
- <p><strong>Description:</strong><br>${item.description ? escapeHtml(item.description).replace(/\r\n|\r|\n/g, '<br>') : 'N/A'}</p>
513
- <p><strong>Requirements:</strong><br>${item.requirements ? escapeHtml(item.requirements).replace(/\r\n|\r|\n/g, '<br>') : 'N/A'}</p>
514
- <p><strong>Salary:</strong> ${escapeHtml(item.salary) || 'N/A'}</p>
515
- <p><strong>Location:</strong> ${escapeHtml(item.location) || 'N/A'}</p>
516
- <p><strong>Contact/Apply:</strong> ${renderContactLink(item.contact, item.user_telegram_username)}</p>
517
  `;
518
  } else if (type === 'freelance_offers') {
519
  detailsHtml += `
520
- <p><strong>Description:</strong><br>${item.description ? escapeHtml(item.description).replace(/\r\n|\r|\n/g, '<br>') : 'N/A'}</p>
521
- <p><strong>Budget:</strong> ${escapeHtml(item.budget) || 'N/A'}</p>
522
- <p><strong>Deadline:</strong> ${escapeHtml(item.deadline) || 'N/A'}</p>
523
- <p><strong>Skills Needed:</strong> ${escapeHtml(item.skills_needed) || 'N/A'}</p>
524
- <p><strong>Contact:</strong> ${renderContactLink(item.contact, item.user_telegram_username)}</p>
525
  `;
526
  }
527
- detailsHtml += `<p class="meta-detail">Posted by: ${renderContactLink(null, item.user_telegram_username)} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
528
 
529
  if (currentUser && item.user_id === String(currentUser.id)) {
530
  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>`;
@@ -536,7 +554,6 @@ MAIN_APP_TEMPLATE = '''
536
  if (currentUser && item.user_id === String(currentUser.id)) {
537
  document.getElementById('editItemButton')?.addEventListener('click', () => {
538
  tg.HapticFeedback.impactOccurred('light');
539
- window.customBackNavigation = () => showDetailView(type, item.id);
540
  showForm(type, item);
541
  });
542
  document.getElementById('deleteItemButton')?.addEventListener('click', () => {
@@ -558,17 +575,11 @@ MAIN_APP_TEMPLATE = '''
558
  tg.BackButton.show();
559
  tg.BackButton.onClick(() => {
560
  tg.HapticFeedback.impactOccurred('light');
561
- if (window.customBackNavigation) {
562
- window.customBackNavigation();
563
- } else if (itemToEdit) {
564
- showDetailView(type, itemToEdit.id);
565
- } else {
566
- loadView(type);
567
- }
568
- window.customBackNavigation = null;
569
  });
570
  document.getElementById('fabButton').style.display = 'none';
571
- document.getElementById('tabsContainer').style.display = 'none';
572
 
573
  let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1)}</h2>`;
574
  if (type === 'resumes') {
@@ -660,15 +671,11 @@ MAIN_APP_TEMPLATE = '''
660
  tg.HapticFeedback.notificationOccurred('success');
661
  tg.MainButton.hideProgress();
662
  tg.MainButton.hide();
663
- if (window.customBackNavigation && typeof window.customBackNavigation === 'function' && window.customBackNavigation.name === 'showMyPostsView') {
664
- showMyPostsView();
665
- } else if (window.customBackNavigation && typeof window.customBackNavigation === 'function' && window.customBackNavigation.name.startsWith('showDetailView')) {
666
- window.customBackNavigation(); // To refresh detail view after edit
667
- }
668
- else {
669
  loadView(type);
670
  }
671
- window.customBackNavigation = null;
672
  })
673
  .catch(err => {
674
  tg.HapticFeedback.notificationOccurred('error');
@@ -677,7 +684,7 @@ MAIN_APP_TEMPLATE = '''
677
  });
678
  }
679
 
680
- function handleDeleteItem(type, itemId, cameFromMyPosts = false) {
681
  tg.showConfirm('Are you sure you want to delete this post?', (confirmed) => {
682
  if (confirmed) {
683
  tg.HapticFeedback.impactOccurred('medium');
@@ -685,12 +692,11 @@ MAIN_APP_TEMPLATE = '''
685
  .then(() => {
686
  tg.HapticFeedback.notificationOccurred('success');
687
  tg.showAlert('Post deleted successfully.');
688
- if (cameFromMyPosts) {
689
- showMyPostsView();
690
  } else {
691
- loadView(type);
692
  }
693
- window.customBackNavigation = null;
694
  })
695
  .catch(err => {
696
  tg.HapticFeedback.notificationOccurred('error');
@@ -705,6 +711,7 @@ MAIN_APP_TEMPLATE = '''
705
  function loadView(tabName, fromSwipe = false) {
706
  if (!fromSwipe) tg.HapticFeedback.impactOccurred('light');
707
  currentView = tabName;
 
708
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
709
  document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active');
710
 
@@ -714,9 +721,6 @@ MAIN_APP_TEMPLATE = '''
714
  tg.BackButton.hide();
715
  tg.MainButton.hide();
716
  document.getElementById('fabButton').style.display = 'block';
717
- document.getElementById('tabsContainer').style.display = 'flex';
718
- window.customBackNavigation = null;
719
-
720
 
721
  apiCall(`/api/${tabName}`)
722
  .then(data => renderList(data, tabName))
@@ -725,88 +729,74 @@ MAIN_APP_TEMPLATE = '''
725
  setTimeout(() => { mainContent.style.opacity = 1; }, 50);
726
  });
727
  }
728
-
729
- function showMyPostsView() {
730
- tg.HapticFeedback.impactOccurred('light');
731
  mainContent.style.opacity = 0;
732
- mainContent.innerHTML = `<div class="loading">Loading your posts...</div>`;
733
-
 
 
 
 
734
  tg.BackButton.show();
735
  tg.BackButton.onClick(() => {
736
  tg.HapticFeedback.impactOccurred('light');
737
- loadView(currentView); // Go back to the current active tab
738
- window.customBackNavigation = null;
739
  });
740
- tg.MainButton.hide();
741
- document.getElementById('fabButton').style.display = 'none';
742
- document.getElementById('tabsContainer').style.display = 'none';
743
-
744
- Promise.all([
745
- apiCall('/api/resumes'),
746
- apiCall('/api/vacancies'),
747
- apiCall('/api/freelance_offers')
748
- ]).then(([resumes, vacancies, freelance_offers_list]) => {
749
- const myPostsRaw = [];
750
- if (currentUser && currentUser.id) {
751
- const userIdStr = String(currentUser.id);
752
- resumes.forEach(item => { if(item.user_id === userIdStr) myPostsRaw.push({...item, type: 'resumes'}); });
753
- vacancies.forEach(item => { if(item.user_id === userIdStr) myPostsRaw.push({...item, type: 'vacancies'}); });
754
- freelance_offers_list.forEach(item => { if(item.user_id === userIdStr) myPostsRaw.push({...item, type: 'freelance_offers'}); });
755
- }
756
- myPostsRaw.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
757
-
758
- window.myEditablePosts = {};
759
- myPostsRaw.forEach(p => window.myEditablePosts[p.id] = p);
760
-
761
- let html = `<div class="form-container" id="myPostsListContainer"><h2>My Posts</h2>`;
762
- if (myPostsRaw.length === 0) {
763
- html += `<p class="empty-state">You haven't posted anything yet.</p>`;
764
- } else {
765
- html += myPostsRaw.map(item => `
766
- <div class="list-item" style="cursor:default;">
767
- <h3>${escapeHtml(item.title || item.name)} (${escapeHtml(item.type.slice(0,-1))})</h3>
768
- <p class="meta">Posted on ${new Date(item.timestamp).toLocaleDateString()}</p>
769
- <div style="margin-top: 10px; display: flex; gap: 10px;">
770
- <button class="action-button" style="background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); margin-top:0; flex:1;" onclick="editMyPostFromList('${item.id}')">Edit</button>
771
- <button class="action-button button-destructive" style="margin-top:0; flex:1;" onclick="handleDeleteItem('${item.type}', '${item.id}', true)">Delete</button>
772
- </div>
773
- </div>
774
- `).join('');
775
- }
776
- html += `</div>`;
777
- mainContent.innerHTML = html;
778
- setTimeout(() => { mainContent.style.opacity = 1; }, 50);
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
- function editMyPostFromList(itemId) {
786
- tg.HapticFeedback.impactOccurred('light');
787
- const item = window.myEditablePosts[itemId];
788
- if (item) {
789
- window.customBackNavigation = showMyPostsView;
790
- showForm(item.type, item);
791
- }
792
  }
793
-
794
  let touchstartX = 0;
795
  let touchendX = 0;
796
  const swipeThreshold = 70;
797
 
798
  mainContent.addEventListener('touchstart', e => {
799
- if (document.getElementById('tabsContainer').style.display !== 'flex') return;
800
  touchstartX = e.changedTouches[0].screenX;
801
  }, { passive: true });
802
 
803
  mainContent.addEventListener('touchend', e => {
804
- if (document.getElementById('tabsContainer').style.display !== 'flex') return;
805
  touchendX = e.changedTouches[0].screenX;
806
  handleSwipeGesture();
807
  });
808
 
809
  function handleSwipeGesture() {
 
810
  const swipeLength = touchendX - touchstartX;
811
  if (Math.abs(swipeLength) < swipeThreshold) return;
812
 
@@ -850,30 +840,31 @@ MAIN_APP_TEMPLATE = '''
850
  if (currentUser.photo_url) {
851
  userAvatar.src = currentUser.photo_url;
852
  } else {
853
- userAvatar.style.display = 'none';
854
  }
855
  }
856
  } catch (error) {
857
  console.error("Auth error:", error);
858
  userInfoText.textContent = `Auth failed. Using basic info.`;
 
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
- window.customBackNavigation = null;
867
  showForm(currentView);
868
  });
869
- document.getElementById('userInfoBar').addEventListener('click', () => {
870
- if (currentUser) {
871
- tg.HapticFeedback.impactOccurred('light');
872
- showMyPostsView();
873
- } else {
874
- tg.showAlert('Please wait for user authentication to complete.');
875
- }
876
- });
877
 
878
  loadView('resumes');
879
  }
@@ -1048,6 +1039,31 @@ def get_authenticated_user_details(request_headers):
1048
  return data.get('users', {}).get(user_id_str)
1049
  return None
1050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
  @app.route('/api/<item_type>', methods=['GET'])
1052
  def get_items(item_type):
1053
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
 
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;
 
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 {
 
355
  cursor: pointer;
356
  text-align: center;
357
  transition: background-color 0.2s ease;
 
358
  }
359
  .button-destructive {
360
  background-color: var(--tg-theme-destructive-text-color);
 
369
  <body>
370
  <div class="app-container">
371
  <div class="header">TonTalent</div>
372
+ <div class="user-info-bar">
373
  <img id="userAvatar" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Avatar">
374
  <span id="userInfoText">Loading user...</span>
375
  </div>
376
+ <div class="tabs">
377
  <button class="tab-button active" data-tab="resumes">Resumes</button>
378
  <button class="tab-button" data-tab="vacancies">Vacancies</button>
379
  <button class="tab-button" data-tab="freelance_offers">Freelance</button>
 
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
 
396
  function applyThemeParams() {
397
  const rootStyle = document.documentElement.style;
 
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) {
469
  mainContent.style.opacity = 0;
470
  if (!items || items.length === 0) {
 
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
  }
 
487
  showDetailView(type, id);
488
  }
489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  function showDetailView(type, id) {
491
  mainContent.style.opacity = 0;
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';
 
503
 
504
  apiCall(`/api/${type}/${id}`)
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 += `
529
+ <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
530
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n/g, '<br>') : 'N/A'}</p>
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 += `
538
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n/g, '<br>') : 'N/A'}</p>
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
  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', () => {
 
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
 
584
  let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1)}</h2>`;
585
  if (type === 'resumes') {
 
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
  }
 
679
  })
680
  .catch(err => {
681
  tg.HapticFeedback.notificationOccurred('error');
 
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
  .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 => {
702
  tg.HapticFeedback.notificationOccurred('error');
 
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
 
 
721
  tg.BackButton.hide();
722
  tg.MainButton.hide();
723
  document.getElementById('fabButton').style.display = 'block';
 
 
 
724
 
725
  apiCall(`/api/${tabName}`)
726
  .then(data => renderList(data, tabName))
 
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
 
 
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
  }
 
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']: