Update app.py
Browse files
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);
|
| 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"
|
| 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"
|
| 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:
|
| 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 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
| 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> ${
|
| 504 |
-
<p><strong>Experience:</strong><br>${item.experience ?
|
| 505 |
-
<p><strong>Education:</strong><br>${item.education ?
|
| 506 |
-
<p><strong>Contact:</strong> ${
|
| 507 |
-
${item.portfolio_link ? `<p><strong>Portfolio:</strong> ${
|
| 508 |
`;
|
| 509 |
} else if (type === 'vacancies') {
|
| 510 |
detailsHtml += `
|
| 511 |
-
<p><strong>Company:</strong> ${
|
| 512 |
-
<p><strong>Description:</strong><br>${item.description ?
|
| 513 |
-
<p><strong>Requirements:</strong><br>${item.requirements ?
|
| 514 |
-
<p><strong>Salary:</strong> ${
|
| 515 |
-
<p><strong>Location:</strong> ${
|
| 516 |
-
<p><strong>Contact/Apply:</strong> ${
|
| 517 |
`;
|
| 518 |
} else if (type === 'freelance_offers') {
|
| 519 |
detailsHtml += `
|
| 520 |
-
<p><strong>Description:</strong><br>${item.description ?
|
| 521 |
-
<p><strong>Budget:</strong> ${
|
| 522 |
-
<p><strong>Deadline:</strong> ${
|
| 523 |
-
<p><strong>Skills Needed:</strong> ${
|
| 524 |
-
<p><strong>Contact:</strong> ${
|
| 525 |
`;
|
| 526 |
}
|
| 527 |
-
detailsHtml += `<p class="meta-detail">Posted by: ${
|
| 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 (
|
| 562 |
-
|
| 563 |
-
|
| 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 (
|
| 664 |
-
|
| 665 |
-
} else
|
| 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
|
| 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 (
|
| 689 |
-
|
| 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
|
| 730 |
-
|
| 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);
|
| 738 |
-
window.customBackNavigation = null;
|
| 739 |
});
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 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']:
|