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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +289 -358
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" # Placeholder, user should replace
29
 
30
  DOWNLOAD_RETRIES = 3
31
  DOWNLOAD_DELAY = 5
@@ -202,7 +202,7 @@ MAIN_APP_TEMPLATE = '''
202
  <html lang="en">
203
  <head>
204
  <meta charset="UTF-8">
205
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
206
  <title>TonTalent</title>
207
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
208
  <style>
@@ -229,32 +229,35 @@ MAIN_APP_TEMPLATE = '''
229
  overscroll-behavior-y: none;
230
  -webkit-font-smoothing: antialiased;
231
  -moz-osx-font-smoothing: grayscale;
232
- transition: background-color 0.3s, color 0.3s;
 
 
233
  }
234
- .app-container { display: flex; flex-direction: column; min-height: 100vh; min-height: -webkit-fill-available; }
235
  .header {
236
  background-color: var(--tg-theme-header-bg-color);
237
- padding: 12px 15px;
238
  text-align: center;
239
  font-weight: 600;
240
  font-size: 17px;
241
- border-bottom: 0.5px solid var(--tg-theme-hint-color, #c8c7cb);
242
  position: sticky;
243
  top: 0;
244
  z-index: 100;
245
  }
246
- .user-info {
247
- padding: 12px 15px;
248
- background-color: var(--tg-theme-secondary-bg-color);
249
- font-size: 14px;
 
 
 
250
  display: flex;
251
  align-items: center;
252
- border-bottom: 0.5px solid var(--tg-theme-hint-color, #c8c7cb);
253
  }
254
- .user-info img { width: 36px; height: 36px; border-radius: 50%; margin-right: 10px; object-fit: cover; }
255
- .user-info span { font-weight: 500; }
256
 
257
- .tabs { display: flex; background-color: var(--tg-theme-secondary-bg-color); padding: 8px 5px 0; }
258
  .tab-button {
259
  flex: 1;
260
  padding: 12px 5px;
@@ -265,37 +268,32 @@ MAIN_APP_TEMPLATE = '''
265
  color: var(--tg-theme-hint-color);
266
  font-size: 15px;
267
  font-weight: 500;
268
- border-bottom: 3px solid transparent;
269
- transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out;
270
- display: flex;
271
- flex-direction: column;
272
- align-items: center;
273
- justify-content: center;
274
- gap: 4px;
275
  }
276
- .tab-button svg { width: 22px; height: 22px; fill: currentColor; }
277
- .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); }
278
- .content { flex-grow: 1; padding: 15px; overflow-y: auto; -webkit-overflow-scrolling: touch; }
279
  .list-item {
280
  background-color: var(--tg-theme-section-bg-color);
281
  border-radius: 10px;
282
- padding: 15px;
283
- margin-bottom: 12px;
284
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
285
  cursor: pointer;
286
- transition: transform 0.1s ease-out, background-color 0.2s;
 
 
 
 
 
287
  }
288
- .list-item:active { transform: scale(0.98); background-color: var(--tg-theme-secondary-bg-color); }
289
  .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
290
  .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); line-height: 1.4; }
291
- .list-item .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 8px; }
292
 
293
- .form-container, .detail-view {
294
- padding: 15px; background-color: var(--tg-theme-section-bg-color);
295
- animation: fadeIn 0.3s ease-out;
296
- }
297
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
298
-
299
  .form-group { margin-bottom: 18px; }
300
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
301
  .form-group input, .form-group textarea {
@@ -307,18 +305,16 @@ MAIN_APP_TEMPLATE = '''
307
  background-color: var(--tg-theme-bg-color);
308
  color: var(--tg-theme-text-color);
309
  box-sizing: border-box;
310
- transition: border-color 0.2s, box-shadow 0.2s;
311
  }
312
  .form-group input:focus, .form-group textarea:focus {
313
  border-color: var(--tg-theme-link-color);
314
- box-shadow: 0 0 0 2px var(--tg-theme-link-color, #007aff30);
315
  outline: none;
316
  }
317
  .form-group textarea { min-height: 100px; resize: vertical; }
318
-
319
  .fab {
320
  position: fixed;
321
- bottom: calc(20px + env(safe-area-inset-bottom));
322
  right: 20px;
323
  width: 56px;
324
  height: 56px;
@@ -328,64 +324,59 @@ MAIN_APP_TEMPLATE = '''
328
  display: flex;
329
  align-items: center;
330
  justify-content: center;
331
- font-size: 28px;
332
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
333
  cursor: pointer;
334
  z-index: 1000;
335
  border: none;
336
- transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
337
  }
338
- .fab:active { transform: scale(0.9); }
339
- .fab svg { width: 24px; height: 24px; fill: currentColor; }
340
-
341
- .detail-view h2 { margin-top: 0; font-size: 22px; font-weight: 700; color: var(--tg-theme-text-color); margin-bottom: 15px; }
342
- .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; }
343
- .detail-view strong { font-weight: 600; color: var(--tg-theme-text-color); }
344
- .detail-view .meta-detail { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 15px; padding-top: 10px; border-top: 1px solid var(--tg-theme-secondary-bg-color); }
345
-
346
- .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
347
- .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 8px; text-align: center; }
348
- .owner-actions { margin-top: 25px; padding-top: 15px; border-top: 1px solid var(--tg-theme-secondary-bg-color); }
349
- .button-destructive {
350
- display: block;
351
- width: 100%;
352
- padding: 12px 15px;
353
  background-color: var(--tg-theme-destructive-text-color);
354
- color: white;
355
- border: none;
356
- border-radius: 8px;
357
- font-size: 16px;
358
- font-weight: 500;
359
- text-align: center;
360
- cursor: pointer;
361
- transition: background-color 0.2s;
362
  }
363
- .button-destructive:active { background-color: color-mix(in srgb, var(--tg-theme-destructive-text-color), #000000 20%); }
 
 
 
 
 
 
 
 
 
 
 
 
364
  </style>
365
  </head>
366
  <body>
367
  <div class="app-container">
368
  <div class="header">TonTalent</div>
369
- <div class="user-info" id="userInfo"><span class="placeholder">Loading user...</span></div>
370
  <div class="tabs">
371
- <button class="tab-button active" data-tab="resumes">
372
- <svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
373
- Resumes
374
- </button>
375
- <button class="tab-button" data-tab="vacancies">
376
- <svg viewBox="0 0 24 24"><path d="M20 6h-4V4c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-6 0h-4V4h4v2z"/></svg>
377
- Vacancies
378
- </button>
379
- <button class="tab-button" data-tab="freelance_offers">
380
- <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v2h-2zm0 4h2v6h-2z"/></svg>
381
- Freelance
382
- </button>
383
  </div>
384
  <div class="content" id="mainContent">
385
  <div class="loading">Loading content...</div>
386
  </div>
387
  <button class="fab" id="fabButton" title="Add New Item">
388
- <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
389
  </button>
390
  </div>
391
 
@@ -394,42 +385,39 @@ MAIN_APP_TEMPLATE = '''
394
  let currentUser = null;
395
  let currentView = 'resumes';
396
  let currentItem = null;
397
- const tabOrder = ['resumes', 'vacancies', 'freelance_offers'];
 
 
 
 
398
 
399
  function applyThemeParams() {
400
- const root = document.documentElement;
401
- const themeParams = tg.themeParams;
402
- root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || '#ffffff');
403
- root.style.setProperty('--tg-theme-text-color', themeParams.text_color || '#000000');
404
- root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || '#999999');
405
- root.style.setProperty('--tg-theme-link-color', themeParams.link_color || '#007aff');
406
- root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#007aff');
407
- root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
408
- root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#f0f0f0');
409
- root.style.setProperty('--tg-theme-header-bg-color', themeParams.header_bg_color || themeParams.secondary_bg_color || '#efeff4');
410
- root.style.setProperty('--tg-theme-section-bg-color', themeParams.section_bg_color || themeParams.bg_color || '#ffffff');
411
- root.style.setProperty('--tg-theme-section-header-text-color', themeParams.section_header_text_color || themeParams.hint_color || '#8e8e93');
412
- root.style.setProperty('--tg-theme-destructive-text-color', themeParams.destructive_text_color || '#ff3b30');
413
- root.style.setProperty('--tg-theme-accent-text-color', themeParams.accent_text_color || themeParams.link_color || '#007aff');
414
-
415
- if (themeParams.bg_color) {
416
- tg.setHeaderColor(themeParams.header_bg_color || themeParams.secondary_bg_color || '#efeff4');
417
- }
418
  }
419
 
420
  async function apiCall(endpoint, method = 'GET', body = null) {
421
  const headers = { 'Content-Type': 'application/json' };
422
- if (tg.initData) {
423
- headers['X-Telegram-Auth'] = tg.initData;
424
- }
425
  const options = { method, headers };
426
  if (body) options.body = JSON.stringify(body);
427
 
428
- tg.MainButton.showProgress(false);
429
  try {
430
  const response = await fetch(endpoint, options);
431
  if (!response.ok) {
432
- const errorData = await response.json().catch(() => ({ error: 'Request failed, server might be unavailable.' }));
433
  throw new Error(errorData.error || `HTTP error ${response.status}`);
434
  }
435
  return response.json();
@@ -445,13 +433,12 @@ MAIN_APP_TEMPLATE = '''
445
 
446
  function renderList(items, type) {
447
  const contentDiv = document.getElementById('mainContent');
448
- contentDiv.scrollTop = 0;
449
  if (!items || items.length === 0) {
450
- contentDiv.innerHTML = `<div class="empty-state">No ${type} found. Be the first to add one!</div>`;
451
  return;
452
  }
453
  contentDiv.innerHTML = items.map(item => `
454
- <div class="list-item" onclick="showDetailView('${type}', '${item.id}')">
455
  <h3>${item.title || item.name || 'Untitled'}</h3>
456
  ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
457
  ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
@@ -461,12 +448,8 @@ MAIN_APP_TEMPLATE = '''
461
  }
462
 
463
  function showDetailView(type, id) {
464
- tg.HapticFeedback.impactOccurred('light');
465
  tg.BackButton.show();
466
- tg.BackButton.onClick(() => {
467
- loadView(type);
468
- tg.HapticFeedback.impactOccurred('light');
469
- });
470
  tg.MainButton.hide();
471
  document.getElementById('fabButton').style.display = 'none';
472
 
@@ -474,193 +457,121 @@ MAIN_APP_TEMPLATE = '''
474
  .then(item => {
475
  currentItem = item;
476
  const contentDiv = document.getElementById('mainContent');
477
- contentDiv.scrollTop = 0;
478
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
479
  if (type === 'resumes') {
480
  detailsHtml += `
481
  <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
482
- <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\n/g, '<br>') : 'N/A'}</p>
483
- <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\n/g, '<br>') : 'N/A'}</p>
484
- <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
485
- ${item.portfolio_link ? `<p><strong>Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" style="color: var(--tg-theme-link-color);">${item.portfolio_link}</a></p>` : ''}
486
  `;
487
  } else if (type === 'vacancies') {
488
  detailsHtml += `
489
  <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
490
- <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\n/g, '<br>') : 'N/A'}</p>
491
- <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\n/g, '<br>') : 'N/A'}</p>
492
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
493
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
494
- <p><strong>Contact/Apply:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
495
  `;
496
  } else if (type === 'freelance_offers') {
497
  detailsHtml += `
498
- <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\n/g, '<br>') : 'N/A'}</p>
499
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
500
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
501
  <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
502
- <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
503
- `;
504
- }
505
- detailsHtml += `<p class="meta-detail">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
506
-
507
- if (currentUser && item.user_id === String(currentUser.id)) {
508
- detailsHtml += `
509
- <div class="owner-actions">
510
- <button id="deleteItemButton" class="button-destructive">Delete Post</button>
511
- </div>
512
  `;
513
- tg.MainButton.setText('Edit My Post');
514
- tg.MainButton.onClick(() => {
515
- showForm(type, item);
516
- tg.HapticFeedback.impactOccurred('light');
517
- });
518
- tg.MainButton.show();
519
  }
520
- detailsHtml += `</div>`;
521
  contentDiv.innerHTML = detailsHtml;
522
 
523
- if (currentUser && item.user_id === String(currentUser.id)) {
524
- document.getElementById('deleteItemButton')?.addEventListener('click', () => confirmDeleteItem(type, item.id));
 
 
 
 
 
 
 
 
 
 
 
 
525
  }
526
  })
527
  .catch(err => {
528
- document.getElementById('mainContent').innerHTML = `<div class="empty-state">Error loading details. Please try again.</div>`;
529
  });
530
  }
531
-
532
- function confirmDeleteItem(type, itemId) {
533
- tg.HapticFeedback.impactOccurred('medium');
534
- tg.showConfirm("Are you sure you want to delete this post? This action cannot be undone.", (confirmed) => {
535
  if (confirmed) {
536
- tg.HapticFeedback.impactOccurred('light');
537
- apiCall(`/api/${type}/${itemId}`, 'DELETE')
538
- .then(() => {
539
- tg.HapticFeedback.notificationOccurred('success');
540
- tg.showAlert('Post deleted successfully.');
541
- loadView(type);
542
- })
543
- .catch(err => {
544
- tg.HapticFeedback.notificationOccurred('error');
545
- tg.showAlert('Failed to delete post: ' + err.message);
546
- });
547
- } else {
548
- tg.HapticFeedback.impactOccurred('light');
549
  }
550
  });
551
  }
552
-
553
  function showForm(type, itemToEdit = null) {
554
- tg.HapticFeedback.impactOccurred('light');
555
  currentItem = itemToEdit;
556
  tg.BackButton.show();
557
  tg.BackButton.onClick(() => {
558
- tg.HapticFeedback.impactOccurred('light');
559
  if (itemToEdit) showDetailView(type, itemToEdit.id);
560
  else loadView(type);
 
561
  });
562
  document.getElementById('fabButton').style.display = 'none';
563
 
564
  const contentDiv = document.getElementById('mainContent');
565
- contentDiv.scrollTop = 0;
566
- let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.charAt(0).toUpperCase() + type.slice(1, -1)}</h2>`;
567
 
568
  if (type === 'resumes') {
569
- formHtml += `
570
- <div class="form-group">
571
- <label for="name">Full Name *</label>
572
- <input type="text" id="name" value="${itemToEdit?.name || ''}" required placeholder="e.g., John Doe">
573
- </div>
574
- <div class="form-group">
575
- <label for="title">Job Title / Desired Position *</label>
576
- <input type="text" id="title" value="${itemToEdit?.title || ''}" required placeholder="e.g., Software Engineer">
577
- </div>
578
- <div class="form-group">
579
- <label for="skills">Skills (comma separated)</label>
580
- <textarea id="skills" placeholder="e.g., Python, JavaScript, Project Management">${itemToEdit?.skills || ''}</textarea>
581
- </div>
582
- <div class="form-group">
583
- <label for="experience">Experience</label>
584
- <textarea id="experience" placeholder="Describe your work experience...">${itemToEdit?.experience || ''}</textarea>
585
- </div>
586
- <div class="form-group">
587
- <label for="education">Education</label>
588
- <textarea id="education" placeholder="Your educational background...">${itemToEdit?.education || ''}</textarea>
589
- </div>
590
- <div class="form-group">
591
- <label for="contact">Contact Info (email, etc. Optional - uses Telegram if blank)</label>
592
- <input type="text" id="contact" value="${itemToEdit?.contact || ''}" placeholder="e.g., mail@example.com">
593
- </div>
594
- <div class="form-group">
595
- <label for="portfolio_link">Portfolio Link (optional)</label>
596
- <input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}" placeholder="e.g., https://github.com/johndoe">
597
- </div>
598
- `;
599
  } else if (type === 'vacancies') {
600
- formHtml += `
601
- <div class="form-group">
602
- <label for="company_name">Company Name *</label>
603
- <input type="text" id="company_name" value="${itemToEdit?.company_name || ''}" required placeholder="e.g., Tech Solutions Inc.">
604
- </div>
605
- <div class="form-group">
606
- <label for="title">Job Title *</label>
607
- <input type="text" id="title" value="${itemToEdit?.title || ''}" required placeholder="e.g., Marketing Manager">
608
- </div>
609
- <div class="form-group">
610
- <label for="description">Description</label>
611
- <textarea id="description" placeholder="Job details, responsibilities...">${itemToEdit?.description || ''}</textarea>
612
- </div>
613
- <div class="form-group">
614
- <label for="requirements">Requirements</label>
615
- <textarea id="requirements" placeholder="Required skills and qualifications...">${itemToEdit?.requirements || ''}</textarea>
616
- </div>
617
- <div class="form-group">
618
- <label for="salary">Salary/Compensation</label>
619
- <input type="text" id="salary" value="${itemToEdit?.salary || ''}" placeholder="e.g., $5000/month or Competitive">
620
- </div>
621
- <div class="form-group">
622
- <label for="location">Location</label>
623
- <input type="text" id="location" value="${itemToEdit?.location || ''}" placeholder="e.g., Remote, New York">
624
- </div>
625
- <div class="form-group">
626
- <label for="contact">Contact Info / How to Apply</label>
627
- <textarea id="contact" placeholder="e.g., Apply via email@company.com or link">${itemToEdit?.contact || ''}</textarea>
628
- </div>
629
- `;
630
  } else if (type === 'freelance_offers') {
631
- formHtml += `
632
- <div class="form-group">
633
- <label for="title">Project Title *</label>
634
- <input type="text" id="title" value="${itemToEdit?.title || ''}" required placeholder="e.g., Design a new logo">
635
- </div>
636
- <div class="form-group">
637
- <label for="description">Description of Work</label>
638
- <textarea id="description" placeholder="Detailed project scope...">${itemToEdit?.description || ''}</textarea>
639
- </div>
640
- <div class="form-group">
641
- <label for="budget">Budget</label>
642
- <input type="text" id="budget" value="${itemToEdit?.budget || ''}" placeholder="e.g., $500 or Negotiable">
643
- </div>
644
- <div class="form-group">
645
- <label for="deadline">Expected Deadline</label>
646
- <input type="text" id="deadline" value="${itemToEdit?.deadline || ''}" placeholder="e.g., 2 weeks or 2024-12-31">
647
- </div>
648
- <div class="form-group">
649
- <label for="skills_needed">Skills Needed (comma separated)</label>
650
- <textarea id="skills_needed" placeholder="e.g., Graphic Design, Adobe Illustrator">${itemToEdit?.skills_needed || ''}</textarea>
651
- </div>
652
- <div class="form-group">
653
- <label for="contact">Contact Info (Optional - uses Telegram if blank)</label>
654
- <input type="text" id="contact" value="${itemToEdit?.contact || ''}" placeholder="e.g., project@example.com">
655
- </div>
656
- `;
657
  }
658
- formHtml += `<div id="formError" class="error-message"></div></div>`;
659
  contentDiv.innerHTML = formHtml;
660
 
661
  tg.MainButton.setText(itemToEdit ? 'Save Changes' : 'Post');
662
  tg.MainButton.show();
663
- tg.MainButton.onClick(() => handleSubmit(type, itemToEdit ? itemToEdit.id : null));
664
  }
665
 
666
  function handleSubmit(type, itemId = null) {
@@ -697,142 +608,173 @@ MAIN_APP_TEMPLATE = '''
697
  }
698
 
699
  if (!isValid) {
700
- document.getElementById('formError').textContent = 'Please fill in all required fields (marked with *).';
701
  tg.HapticFeedback.notificationOccurred('error');
702
  return;
703
  }
704
 
705
- tg.MainButton.showProgress();
706
-
707
  const method = itemId ? 'PUT' : 'POST';
708
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
709
 
710
  apiCall(endpoint, method, payload)
711
  .then(response => {
712
  tg.HapticFeedback.notificationOccurred('success');
713
- tg.MainButton.hideProgress();
714
  loadView(type);
715
- tg.showAlert(itemId ? 'Successfully updated!' : 'Successfully posted!');
716
  })
717
  .catch(err => {
718
- tg.HapticFeedback.notificationOccurred('error');
719
- tg.MainButton.hideProgress();
720
- document.getElementById('formError').textContent = err.message || 'Failed to submit. Please try again.';
721
  });
722
  }
723
 
724
- function loadView(tabName, noHaptic = false) {
725
  currentView = tabName;
726
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
727
- const activeTabButton = document.querySelector(`.tab-button[data-tab="${tabName}"]`);
728
- if (activeTabButton) activeTabButton.classList.add('active');
729
-
730
- if (!noHaptic) tg.HapticFeedback.selectionChanged();
731
 
732
  const contentDiv = document.getElementById('mainContent');
733
- contentDiv.innerHTML = `<div class="loading">Loading ${tabName}...</div>`;
734
- contentDiv.scrollTop = 0;
735
  tg.BackButton.hide();
736
  tg.MainButton.hide();
737
- document.getElementById('fabButton').style.display = 'block';
738
- document.getElementById('fabButton').innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>';
739
-
740
-
741
- apiCall(`/api/${tabName}`)
742
- .then(data => renderList(data, tabName))
743
- .catch(err => {
744
- document.getElementById('mainContent').innerHTML = `<div class="empty-state">Error loading ${tabName}. Please check your connection and try again.</div>`;
745
- });
746
- }
747
-
748
- let touchStartX = 0;
749
- let touchEndX = 0;
750
- const swipeThreshold = 50;
751
-
752
- function handleTouchStart(event) {
753
- if (event.touches.length === 1) {
754
- touchStartX = event.touches[0].clientX;
755
  }
756
  }
757
 
758
- function handleTouchMove(event) {
759
- if (event.touches.length === 1) {
760
- touchEndX = event.touches[0].clientX;
 
 
761
  }
762
- }
 
 
 
 
 
 
 
 
 
763
 
764
- function handleTouchEnd() {
765
- if (touchStartX === 0 || touchEndX === 0) return;
766
-
767
- const diffX = touchStartX - touchEndX;
768
- const mainContent = document.getElementById('mainContent');
769
- const isFormOrDetailActive = mainContent.querySelector('.form-container') || mainContent.querySelector('.detail-view');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
 
771
- if (isFormOrDetailActive) { // Disable swipe on form/detail for now
772
- touchStartX = 0;
773
- touchEndX = 0;
774
- return;
775
  }
 
 
 
 
776
 
 
 
777
 
778
- if (Math.abs(diffX) > swipeThreshold) {
779
- const currentIndex = tabOrder.indexOf(currentView);
780
- if (diffX > 0) { // Swiped left
781
- if (currentIndex < tabOrder.length - 1) {
782
- loadView(tabOrder[currentIndex + 1]);
783
- }
784
- } else { // Swiped right
785
- if (currentIndex > 0) {
786
- loadView(tabOrder[currentIndex - 1]);
787
- }
 
 
788
  }
789
  }
790
- touchStartX = 0;
791
- touchEndX = 0;
792
  }
793
 
794
  async function init() {
795
  tg.ready();
796
  applyThemeParams();
797
- tg.onEvent('themeChanged', applyThemeParams);
798
  tg.expand();
799
  tg.enableClosingConfirmation();
800
-
801
- const userInfoDiv = document.getElementById('userInfo');
802
- const defaultUserText = `Welcome, ${tg.initDataUnsafe.user?.first_name || 'User'}! (@${tg.initDataUnsafe.user?.username || 'anonymous'})`;
803
- let avatarHtml = tg.initDataUnsafe.user?.photo_url ? `<img src="${tg.initDataUnsafe.user.photo_url}" alt="avatar">` : '<div style="width:36px; height:36px; border-radius:50%; background-color:var(--tg-theme-hint-color); margin-right:10px; display:flex; align-items:center; justify-content:center; font-size:18px; color:var(--tg-theme-button-text-color);">${(tg.initDataUnsafe.user?.first_name || "U").charAt(0)}</div>';
804
- userInfoDiv.innerHTML = `${avatarHtml}<span>${defaultUserText}</span>`;
805
 
 
 
 
 
806
  try {
807
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
808
  currentUser = authResponse.user;
809
  if (currentUser) {
810
- let userDisplayName = currentUser.first_name;
811
- if (currentUser.last_name) userDisplayName += ` ${currentUser.last_name}`;
812
- const avatar = currentUser.photo_url ? `<img src="${currentUser.photo_url}" alt="avatar">` : `<div style="width:36px; height:36px; border-radius:50%; background-color:var(--tg-theme-hint-color); margin-right:10px; display:flex; align-items:center; justify-content:center; font-size:18px; color:var(--tg-theme-button-text-color);">${(currentUser.first_name || "U").charAt(0)}</div>`;
813
- userInfoDiv.innerHTML = `${avatar}<span>${userDisplayName} (@${currentUser.username || 'id'+currentUser.id})</span>`;
 
 
 
 
 
814
  }
815
  } catch (error) {
816
  console.error("Auth error:", error);
817
- userInfoDiv.innerHTML = `${avatarHtml}<span>Auth failed. Limited functionality.</span>`;
818
  tg.showAlert("Authentication with the server failed. Some features might not work correctly.");
819
  }
820
 
821
 
822
  document.querySelectorAll('.tab-button').forEach(button => {
823
- button.addEventListener('click', () => loadView(button.dataset.tab));
824
- });
825
- document.getElementById('fabButton').addEventListener('click', () => {
826
- showForm(currentView);
827
- tg.HapticFeedback.impactOccurred('medium');
828
  });
 
829
 
830
- const mainContentEl = document.getElementById('mainContent');
831
- mainContentEl.addEventListener('touchstart', handleTouchStart, { passive: true });
832
- mainContentEl.addEventListener('touchmove', handleTouchMove, { passive: true });
833
- mainContentEl.addEventListener('touchend', handleTouchEnd, { passive: true });
834
 
835
- loadView('resumes', true); // Initial load without haptic
836
  }
837
 
838
  init();
@@ -966,31 +908,29 @@ def auth_user():
966
  else:
967
  return jsonify({"error": "Authentication data not provided"}), 401
968
 
969
- is_valid, user_data_from_tg = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
970
 
971
- if not is_valid or not user_data_from_tg:
972
  return jsonify({"error": "Invalid authentication data"}), 403
973
 
974
  data = load_data()
975
  users = data.get('users', {})
976
- user_id_str = str(user_data_from_tg.get('id'))
977
 
978
  if user_id_str not in users:
979
  users[user_id_str] = {
980
- 'id': user_data_from_tg.get('id'),
981
- 'first_name': user_data_from_tg.get('first_name'),
982
- 'last_name': user_data_from_tg.get('last_name'),
983
- 'username': user_data_from_tg.get('username'),
984
- 'language_code': user_data_from_tg.get('language_code'),
985
- 'photo_url': user_data_from_tg.get('photo_url'),
986
  'first_seen': datetime.now().isoformat()
987
  }
988
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
989
- users[user_id_str]['first_name'] = user_data_from_tg.get('first_name', users[user_id_str].get('first_name'))
990
- users[user_id_str]['last_name'] = user_data_from_tg.get('last_name', users[user_id_str].get('last_name'))
991
- users[user_id_str]['username'] = user_data_from_tg.get('username', users[user_id_str].get('username'))
992
- users[user_id_str]['photo_url'] = user_data_from_tg.get('photo_url', users[user_id_str].get('photo_url'))
993
-
994
  data['users'] = users
995
  save_data(data)
996
 
@@ -1000,9 +940,9 @@ def get_authenticated_user(request_headers):
1000
  auth_data_str = request_headers.get('X-Telegram-Auth')
1001
  if not auth_data_str:
1002
  return None
1003
- is_valid, user_data = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
1004
- if is_valid and user_data:
1005
- return user_data
1006
  return None
1007
 
1008
  @app.route('/api/<item_type>', methods=['GET'])
@@ -1107,9 +1047,6 @@ def update_item(item_type, item_id):
1107
  updated_item['updated_timestamp'] = datetime.now().isoformat()
1108
 
1109
  if item_type == 'resumes':
1110
- required_fields = ['name', 'title']
1111
- for field in required_fields:
1112
- if not req_data.get(field): return jsonify({"error": f"Missing field: {field} in update"}), 400
1113
  updated_item.update({
1114
  "name": req_data.get('name', original_item.get('name')),
1115
  "title": req_data.get('title', original_item.get('title')),
@@ -1120,9 +1057,6 @@ def update_item(item_type, item_id):
1120
  "portfolio_link": req_data.get('portfolio_link', original_item.get('portfolio_link'))
1121
  })
1122
  elif item_type == 'vacancies':
1123
- required_fields = ['company_name', 'title']
1124
- for field in required_fields:
1125
- if not req_data.get(field): return jsonify({"error": f"Missing field: {field} in update"}), 400
1126
  updated_item.update({
1127
  "company_name": req_data.get('company_name', original_item.get('company_name')),
1128
  "title": req_data.get('title', original_item.get('title')),
@@ -1133,10 +1067,7 @@ def update_item(item_type, item_id):
1133
  "contact": req_data.get('contact', original_item.get('contact'))
1134
  })
1135
  elif item_type == 'freelance_offers':
1136
- required_fields = ['title']
1137
- for field in required_fields:
1138
- if not req_data.get(field): return jsonify({"error": f"Missing field: {field} in update"}), 400
1139
- updated_item.update({
1140
  "title": req_data.get('title', original_item.get('title')),
1141
  "description": req_data.get('description', original_item.get('description')),
1142
  "budget": req_data.get('budget', original_item.get('budget')),
 
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
 
202
  <html lang="en">
203
  <head>
204
  <meta charset="UTF-8">
205
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
206
  <title>TonTalent</title>
207
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
208
  <style>
 
229
  overscroll-behavior-y: none;
230
  -webkit-font-smoothing: antialiased;
231
  -moz-osx-font-smoothing: grayscale;
232
+ display: flex;
233
+ flex-direction: column;
234
+ min-height: 100vh;
235
  }
236
+ .app-container { display: flex; flex-direction: column; flex-grow: 1; }
237
  .header {
238
  background-color: var(--tg-theme-header-bg-color);
239
+ padding: 10px 15px;
240
  text-align: center;
241
  font-weight: 600;
242
  font-size: 17px;
243
+ border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color);
244
  position: sticky;
245
  top: 0;
246
  z-index: 100;
247
  }
248
+ .user-info {
249
+ padding: 8px 15px;
250
+ background-color: var(--tg-theme-bg-color);
251
+ font-size: 13px;
252
+ text-align: left;
253
+ color: var(--tg-theme-text-color);
254
+ border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color);
255
  display: flex;
256
  align-items: center;
 
257
  }
258
+ .user-info img { width: 28px; height: 28px; border-radius: 50%; vertical-align: middle; margin-right: 10px; }
 
259
 
260
+ .tabs { display: flex; background-color: var(--tg-theme-header-bg-color); padding: 0px 5px; border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color); }
261
  .tab-button {
262
  flex: 1;
263
  padding: 12px 5px;
 
268
  color: var(--tg-theme-hint-color);
269
  font-size: 15px;
270
  font-weight: 500;
271
+ border-bottom: 2.5px solid transparent;
272
+ transition: color 0.2s ease, border-bottom-color 0.2s ease;
273
+ position: relative;
 
 
 
 
274
  }
275
+ .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); font-weight: 600;}
276
+ .content { flex-grow: 1; padding: 10px; overflow-y: auto; }
 
277
  .list-item {
278
  background-color: var(--tg-theme-section-bg-color);
279
  border-radius: 10px;
280
+ padding: 12px 15px;
281
+ margin-bottom: 10px;
282
+ box-shadow: 0 2px 6px rgba(0,0,0,0.08);
283
  cursor: pointer;
284
+ transition: background-color 0.15s ease, transform 0.15s ease;
285
+ animation: fadeIn 0.4s ease-out;
286
+ }
287
+ @keyframes fadeIn {
288
+ from { opacity: 0; transform: translateY(8px); }
289
+ to { opacity: 1; transform: translateY(0); }
290
  }
291
+ .list-item:active { background-color: var(--tg-theme-secondary-bg-color); transform: scale(0.98); }
292
  .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
293
  .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); line-height: 1.4; }
294
+ .list-item .meta { font-size: 12px; color: var(--tg-theme-hint-color); margin-top: 8px; }
295
 
296
+ .form-container { padding: 15px; background-color: var(--tg-theme-section-bg-color); }
 
 
 
 
 
297
  .form-group { margin-bottom: 18px; }
298
  .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500; }
299
  .form-group input, .form-group textarea {
 
305
  background-color: var(--tg-theme-bg-color);
306
  color: var(--tg-theme-text-color);
307
  box-sizing: border-box;
308
+ transition: border-color 0.2s ease;
309
  }
310
  .form-group input:focus, .form-group textarea:focus {
311
  border-color: var(--tg-theme-link-color);
 
312
  outline: none;
313
  }
314
  .form-group textarea { min-height: 100px; resize: vertical; }
 
315
  .fab {
316
  position: fixed;
317
+ bottom: 20px;
318
  right: 20px;
319
  width: 56px;
320
  height: 56px;
 
324
  display: flex;
325
  align-items: center;
326
  justify-content: center;
 
327
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
328
  cursor: pointer;
329
  z-index: 1000;
330
  border: none;
331
+ transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
332
  }
333
+ .fab:active { transform: scale(0.92); }
334
+ .fab svg { width: 28px; height: 28px; fill: currentColor; }
335
+
336
+ .detail-view { padding: 15px; background-color: var(--tg-theme-section-bg-color); animation: fadeIn 0.3s ease-out; }
337
+ .detail-view h2 { margin-top: 0; margin-bottom: 15px; font-size: 22px; font-weight: 600; color: var(--tg-theme-text-color); }
338
+ .detail-view p { margin-bottom: 10px; line-height: 1.6; font-size: 16px; color: var(--tg-theme-text-color); }
339
+ .detail-view p strong { font-weight: 500; color: var(--tg-theme-section-header-text-color); display: block; margin-bottom: 3px; font-size: 14px;}
340
+ .detail-view .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 20px; }
341
+ .detail-view a { color: var(--tg-theme-link-color); text-decoration: none; }
342
+ .detail-view a:hover { text-decoration: underline; }
343
+ .delete-post-button {
 
 
 
 
344
  background-color: var(--tg-theme-destructive-text-color);
345
+ color: var(--tg-theme-button-text-color);
346
+ padding: 12px 15px; border: none; border-radius: 8px; cursor: pointer;
347
+ margin-top: 20px; width: 100%; font-size: 16px; font-weight: 500;
348
+ transition: background-color 0.2s ease;
 
 
 
 
349
  }
350
+ .delete-post-button:active { background-color: color-mix(in srgb, var(--tg-theme-destructive-text-color) 80%, black); }
351
+
352
+
353
+ .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
354
+ .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 8px; }
355
+
356
+ .profile-header { text-align: center; margin-bottom: 25px; }
357
+ .profile-header img { width: 80px; height: 80px; border-radius: 50%; margin-bottom: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
358
+ .profile-header h2 { margin: 5px 0; font-size: 20px; font-weight: 600; }
359
+ .profile-header p { margin: 0; font-size: 15px; color: var(--tg-theme-hint-color); }
360
+ .profile-section h3 { font-size: 18px; font-weight: 600; color: var(--tg-theme-text-color); margin-top: 25px; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid var(--tg-theme-secondary-bg-color); }
361
+ .profile-section .list-item h4 {font-size: 16px; margin: 0 0 5px 0; font-weight: 500;}
362
+
363
  </style>
364
  </head>
365
  <body>
366
  <div class="app-container">
367
  <div class="header">TonTalent</div>
368
+ <div class="user-info" id="userInfo">Loading user...</div>
369
  <div class="tabs">
370
+ <button class="tab-button active" data-tab="resumes">Resumes</button>
371
+ <button class="tab-button" data-tab="vacancies">Vacancies</button>
372
+ <button class="tab-button" data-tab="freelance_offers">Freelance</button>
373
+ <button class="tab-button" data-tab="my_activity">My Activity</button>
 
 
 
 
 
 
 
 
374
  </div>
375
  <div class="content" id="mainContent">
376
  <div class="loading">Loading content...</div>
377
  </div>
378
  <button class="fab" id="fabButton" title="Add New Item">
379
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
380
  </button>
381
  </div>
382
 
 
385
  let currentUser = null;
386
  let currentView = 'resumes';
387
  let currentItem = null;
388
+ const TABS_ORDER = ['resumes', 'vacancies', 'freelance_offers', 'my_activity'];
389
+
390
+ let touchstartX = 0;
391
+ let touchendX = 0;
392
+ const SWIPE_THRESHOLD = 75;
393
 
394
  function applyThemeParams() {
395
+ const style = document.documentElement.style;
396
+ style.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
397
+ style.setProperty('--tg-theme-text-color', tg.themeParams.text_color || '#000000');
398
+ style.setProperty('--tg-theme-hint-color', tg.themeParams.hint_color || '#999999');
399
+ style.setProperty('--tg-theme-link-color', tg.themeParams.link_color || '#007aff');
400
+ style.setProperty('--tg-theme-button-color', tg.themeParams.button_color || '#007aff');
401
+ style.setProperty('--tg-theme-button-text-color', tg.themeParams.button_text_color || '#ffffff');
402
+ style.setProperty('--tg-theme-secondary-bg-color', tg.themeParams.secondary_bg_color || '#f0f0f0');
403
+ style.setProperty('--tg-theme-header-bg-color', tg.themeParams.header_bg_color || tg.themeParams.secondary_bg_color || '#efeff4');
404
+ style.setProperty('--tg-theme-section-bg-color', tg.themeParams.section_bg_color || tg.themeParams.bg_color || '#ffffff');
405
+ style.setProperty('--tg-theme-section-header-text-color', tg.themeParams.section_header_text_color || tg.themeParams.hint_color || '#8e8e93');
406
+ style.setProperty('--tg-theme-destructive-text-color', tg.themeParams.destructive_text_color || '#ff3b30');
407
+ style.setProperty('--tg-theme-accent-text-color', tg.themeParams.accent_text_color || tg.themeParams.link_color || '#007aff');
 
 
 
 
 
408
  }
409
 
410
  async function apiCall(endpoint, method = 'GET', body = null) {
411
  const headers = { 'Content-Type': 'application/json' };
412
+ if (tg.initData) headers['X-Telegram-Auth'] = tg.initData;
 
 
413
  const options = { method, headers };
414
  if (body) options.body = JSON.stringify(body);
415
 
416
+ tg.MainButton.showProgress();
417
  try {
418
  const response = await fetch(endpoint, options);
419
  if (!response.ok) {
420
+ const errorData = await response.json().catch(() => ({ error: 'Request failed without JSON body' }));
421
  throw new Error(errorData.error || `HTTP error ${response.status}`);
422
  }
423
  return response.json();
 
433
 
434
  function renderList(items, type) {
435
  const contentDiv = document.getElementById('mainContent');
 
436
  if (!items || items.length === 0) {
437
+ contentDiv.innerHTML = `<div class="empty-state">No ${type.replace('_', ' ')} found. Be the first to add one!</div>`;
438
  return;
439
  }
440
  contentDiv.innerHTML = items.map(item => `
441
+ <div class="list-item" onclick="showDetailView('${type}', '${item.id}'); tg.HapticFeedback.selectionChanged();">
442
  <h3>${item.title || item.name || 'Untitled'}</h3>
443
  ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
444
  ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
 
448
  }
449
 
450
  function showDetailView(type, id) {
 
451
  tg.BackButton.show();
452
+ tg.BackButton.onClick(() => { loadView(type); tg.HapticFeedback.impactOccurred('light'); });
 
 
 
453
  tg.MainButton.hide();
454
  document.getElementById('fabButton').style.display = 'none';
455
 
 
457
  .then(item => {
458
  currentItem = item;
459
  const contentDiv = document.getElementById('mainContent');
 
460
  let detailsHtml = `<div class="detail-view"><h2>${item.title || item.name}</h2>`;
461
  if (type === 'resumes') {
462
  detailsHtml += `
463
  <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
464
+ <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
465
+ <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
466
+ <p><strong>Contact:</strong> ${item.contact || ('@' + item.user_telegram_username)}</p>
467
+ ${item.portfolio_link ? `<p><strong>Portfolio:</strong> <a href="${item.portfolio_link}" target="_blank" rel="noopener noreferrer">${item.portfolio_link}</a></p>` : ''}
468
  `;
469
  } else if (type === 'vacancies') {
470
  detailsHtml += `
471
  <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
472
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
473
+ <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
474
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
475
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
476
+ <p><strong>Contact/Apply:</strong> ${item.contact || ('@' + item.user_telegram_username)}</p>
477
  `;
478
  } else if (type === 'freelance_offers') {
479
  detailsHtml += `
480
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n|\n/g, '<br>') : 'N/A'}</p>
481
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
482
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
483
  <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
484
+ <p><strong>Contact:</strong> ${item.contact || ('@' + item.user_telegram_username)}</p>
 
 
 
 
 
 
 
 
 
485
  `;
 
 
 
 
 
 
486
  }
487
+ detailsHtml += `<p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p></div>`;
488
  contentDiv.innerHTML = detailsHtml;
489
 
490
+ if (currentUser && String(item.user_id) === String(currentUser.id)) {
491
+ tg.MainButton.setText('Edit My Post');
492
+ tg.MainButton.onClick(() => { showForm(type, item); tg.HapticFeedback.impactOccurred('medium'); });
493
+ tg.MainButton.show();
494
+
495
+ const detailViewDiv = contentDiv.querySelector('.detail-view');
496
+ if (detailViewDiv) {
497
+ const deleteButtonHtml = \`<button id="deletePostButton" class="delete-post-button">Delete Post</button>\`;
498
+ detailViewDiv.insertAdjacentHTML('beforeend', deleteButtonHtml);
499
+ document.getElementById('deletePostButton').addEventListener('click', () => {
500
+ tg.HapticFeedback.impactOccurred('medium');
501
+ handleDeletePost(type, item.id);
502
+ });
503
+ }
504
  }
505
  })
506
  .catch(err => {
507
+ document.getElementById('mainContent').innerHTML = \`<div class="empty-state">Error loading details. Check connection.</div>\`;
508
  });
509
  }
510
+
511
+ async function handleDeletePost(type, itemId) {
512
+ tg.showConfirm(\`Are you sure you want to delete this ${type.slice(0,-1)}?\`, async (confirmed) => {
 
513
  if (confirmed) {
514
+ try {
515
+ await apiCall(\`/api/${type}/${itemId}\`, 'DELETE');
516
+ tg.HapticFeedback.notificationOccurred('success');
517
+ tg.showAlert(\`${type.slice(0,-1)} deleted successfully.\`);
518
+ loadView(currentView);
519
+ } catch (error) {
520
+ tg.HapticFeedback.notificationOccurred('error');
521
+ }
 
 
 
 
 
522
  }
523
  });
524
  }
525
+
526
  function showForm(type, itemToEdit = null) {
 
527
  currentItem = itemToEdit;
528
  tg.BackButton.show();
529
  tg.BackButton.onClick(() => {
 
530
  if (itemToEdit) showDetailView(type, itemToEdit.id);
531
  else loadView(type);
532
+ tg.HapticFeedback.impactOccurred('light');
533
  });
534
  document.getElementById('fabButton').style.display = 'none';
535
 
536
  const contentDiv = document.getElementById('mainContent');
537
+ let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1).replace('_', ' ')}</h2>`;
 
538
 
539
  if (type === 'resumes') {
540
+ formHtml += \`
541
+ <div class="form-group"><label for="name">Full Name</label><input type="text" id="name" value="\${itemToEdit?.name || ''}" required></div>
542
+ <div class="form-group"><label for="title">Job Title / Desired Position</label><input type="text" id="title" value="\${itemToEdit?.title || ''}" required></div>
543
+ <div class="form-group"><label for="skills">Skills (comma separated)</label><textarea id="skills">\${itemToEdit?.skills || ''}</textarea></div>
544
+ <div class="form-group"><label for="experience">Experience</label><textarea id="experience">\${itemToEdit?.experience || ''}</textarea></div>
545
+ <div class="form-group"><label for="education">Education</label><textarea id="education">\${itemToEdit?.education || ''}</textarea></div>
546
+ <div class="form-group"><label for="contact">Contact Info (e.g., email, or blank for Telegram)</label><input type="text" id="contact" value="\${itemToEdit?.contact || ''}"></div>
547
+ <div class="form-group"><label for="portfolio_link">Portfolio Link (optional)</label><input type="url" id="portfolio_link" value="\${itemToEdit?.portfolio_link || ''}"></div>
548
+ \`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  } else if (type === 'vacancies') {
550
+ formHtml += \`
551
+ <div class="form-group"><label for="company_name">Company Name</label><input type="text" id="company_name" value="\${itemToEdit?.company_name || ''}" required></div>
552
+ <div class="form-group"><label for="title">Job Title</label><input type="text" id="title" value="\${itemToEdit?.title || ''}" required></div>
553
+ <div class="form-group"><label for="description">Description</label><textarea id="description">\${itemToEdit?.description || ''}</textarea></div>
554
+ <div class="form-group"><label for="requirements">Requirements</label><textarea id="requirements">\${itemToEdit?.requirements || ''}</textarea></div>
555
+ <div class="form-group"><label for="salary">Salary/Compensation</label><input type="text" id="salary" value="\${itemToEdit?.salary || ''}"></div>
556
+ <div class="form-group"><label for="location">Location (e.g., Remote, City)</label><input type="text" id="location" value="\${itemToEdit?.location || ''}"></div>
557
+ <div class="form-group"><label for="contact">Contact Info / How to Apply</label><textarea id="contact">\${itemToEdit?.contact || ''}</textarea></div>
558
+ \`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  } else if (type === 'freelance_offers') {
560
+ formHtml += \`
561
+ <div class="form-group"><label for="title">Project Title</label><input type="text" id="title" value="\${itemToEdit?.title || ''}" required></div>
562
+ <div class="form-group"><label for="description">Description of Work</label><textarea id="description">\${itemToEdit?.description || ''}</textarea></div>
563
+ <div class="form-group"><label for="budget">Budget</label><input type="text" id="budget" value="\${itemToEdit?.budget || ''}"></div>
564
+ <div class="form-group"><label for="deadline">Expected Deadline</label><input type="text" id="deadline" value="\${itemToEdit?.deadline || ''}"></div>
565
+ <div class="form-group"><label for="skills_needed">Skills Needed (comma separated)</label><textarea id="skills_needed">\${itemToEdit?.skills_needed || ''}</textarea></div>
566
+ <div class="form-group"><label for="contact">Contact Info (or blank for Telegram)</label><input type="text" id="contact" value="\${itemToEdit?.contact || ''}"></div>
567
+ \`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  }
569
+ formHtml += \`<div id="formError" class="error-message"></div></div>\`;
570
  contentDiv.innerHTML = formHtml;
571
 
572
  tg.MainButton.setText(itemToEdit ? 'Save Changes' : 'Post');
573
  tg.MainButton.show();
574
+ tg.MainButton.onClick(() => { handleSubmit(type, itemToEdit ? itemToEdit.id : null); tg.HapticFeedback.impactOccurred('medium'); });
575
  }
576
 
577
  function handleSubmit(type, itemId = null) {
 
608
  }
609
 
610
  if (!isValid) {
611
+ document.getElementById('formError').textContent = 'Please fill in all required fields.';
612
  tg.HapticFeedback.notificationOccurred('error');
613
  return;
614
  }
615
 
 
 
616
  const method = itemId ? 'PUT' : 'POST';
617
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
618
 
619
  apiCall(endpoint, method, payload)
620
  .then(response => {
621
  tg.HapticFeedback.notificationOccurred('success');
 
622
  loadView(type);
 
623
  })
624
  .catch(err => {
625
+ document.getElementById('formError').textContent = err.message || 'Failed to submit.';
 
 
626
  });
627
  }
628
 
629
+ function loadView(tabName) {
630
  currentView = tabName;
631
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
632
+ document.querySelector(\`.tab-button[data-tab="\${tabName}"]\`).classList.add('active');
 
 
 
633
 
634
  const contentDiv = document.getElementById('mainContent');
635
+ contentDiv.innerHTML = \`<div class="loading">Loading \${tabName.replace('_', ' ')}...</div>\`;
 
636
  tg.BackButton.hide();
637
  tg.MainButton.hide();
638
+
639
+ if (tabName === 'my_activity') {
640
+ renderMyActivityPage();
641
+ document.getElementById('fabButton').style.display = 'none';
642
+ } else {
643
+ document.getElementById('fabButton').style.display = 'flex';
644
+ apiCall(`/api/${tabName}`)
645
+ .then(data => renderList(data, tabName))
646
+ .catch(err => {
647
+ contentDiv.innerHTML = \`<div class="empty-state">Error loading \${tabName.replace('_', ' ')}. Please check your connection.</div>\`;
648
+ });
 
 
 
 
 
 
 
649
  }
650
  }
651
 
652
+ async function renderMyActivityPage() {
653
+ const contentDiv = document.getElementById('mainContent');
654
+ if (!currentUser) {
655
+ contentDiv.innerHTML = \`<div class="empty-state">Please wait, authenticating... Or reload if this persists.</div>\`;
656
+ return;
657
  }
658
+ contentDiv.innerHTML = \`<div class="loading">Loading your activity...</div>\`;
659
+
660
+ let activityHtml = \`<div class="detail-view profile-section">\`;
661
+ activityHtml += \`
662
+ <div class="profile-header">
663
+ \${currentUser.photo_url ? \`<img src="\${currentUser.photo_url}" alt="avatar">\` : '<div style="width:80px;height:80px;border-radius:50%;background-color:var(--tg-theme-secondary-bg-color);display:flex;align-items:center;justify-content:center;margin:0 auto 10px;"><span style="font-size:30px;">\${(currentUser.first_name || 'U')[0]}</span></div>'}
664
+ <h2>\${currentUser.first_name || ''} \${currentUser.last_name || ''}</h2>
665
+ <p>@\${currentUser.username || 'anonymous'}</p>
666
+ </div>
667
+ \`;
668
 
669
+ try {
670
+ const [resumes, vacancies, offers] = await Promise.all([
671
+ apiCall('/api/resumes').catch(() => []),
672
+ apiCall('/api/vacancies').catch(() => []),
673
+ apiCall('/api/freelance_offers').catch(() => [])
674
+ ]);
675
+
676
+ const myResumes = resumes.filter(r => String(r.user_id) === String(currentUser.id));
677
+ const myVacancies = vacancies.filter(v => String(v.user_id) === String(currentUser.id));
678
+ const myOffers = offers.filter(f => String(f.user_id) === String(currentUser.id));
679
+
680
+ activityHtml += \`<h3>My Resumes (\${myResumes.length})</h3>\`;
681
+ if (myResumes.length > 0) {
682
+ activityHtml += myResumes.map(item => \`
683
+ <div class="list-item" onclick="showDetailView('resumes', '\${item.id}'); tg.HapticFeedback.selectionChanged();">
684
+ <h4>\${item.title || item.name}</h4>
685
+ <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
686
+ </div>\`).join('');
687
+ } else { activityHtml += \`<p>You haven't posted any resumes.</p>\`; }
688
+
689
+ activityHtml += \`<h3>My Vacancies (\${myVacancies.length})</h3>\`;
690
+ if (myVacancies.length > 0) {
691
+ activityHtml += myVacancies.map(item => \`
692
+ <div class="list-item" onclick="showDetailView('vacancies', '\${item.id}'); tg.HapticFeedback.selectionChanged();">
693
+ <h4>\${item.title} - \${item.company_name}</h4>
694
+ <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
695
+ </div>\`).join('');
696
+ } else { activityHtml += \`<p>You haven't posted any vacancies.</p>\`; }
697
+
698
+ activityHtml += \`<h3>My Freelance Offers (\${myOffers.length})</h3>\`;
699
+ if (myOffers.length > 0) {
700
+ activityHtml += myOffers.map(item => \`
701
+ <div class="list-item" onclick="showDetailView('freelance_offers', '\${item.id}'); tg.HapticFeedback.selectionChanged();">
702
+ <h4>\${item.title}</h4>
703
+ <p class="meta">Posted on \${new Date(item.timestamp).toLocaleDateString()}</p>
704
+ </div>\`).join('');
705
+ } else { activityHtml += \`<p>You haven't posted any freelance offers.</p>\`; }
706
+ activityHtml += \`</div>\`;
707
+ contentDiv.innerHTML = activityHtml;
708
 
709
+ } catch (error) {
710
+ contentDiv.innerHTML = \`<div class="empty-state">Error loading your activity.</div>\`;
 
 
711
  }
712
+ }
713
+
714
+ function handleSwipeGesture() {
715
+ if (tg.BackButton.isVisible) return;
716
 
717
+ const activeTabButton = document.querySelector('.tab-button.active');
718
+ if (!activeTabButton) return;
719
 
720
+ const currentIndex = TABS_ORDER.indexOf(activeTabButton.dataset.tab);
721
+
722
+ if (touchendX < touchstartX - SWIPE_THRESHOLD) {
723
+ if (currentIndex < TABS_ORDER.length - 1) {
724
+ loadView(TABS_ORDER[currentIndex + 1]);
725
+ tg.HapticFeedback.selectionChanged();
726
+ }
727
+ }
728
+ if (touchendX > touchstartX + SWIPE_THRESHOLD) {
729
+ if (currentIndex > 0) {
730
+ loadView(TABS_ORDER[currentIndex - 1]);
731
+ tg.HapticFeedback.selectionChanged();
732
  }
733
  }
 
 
734
  }
735
 
736
  async function init() {
737
  tg.ready();
738
  applyThemeParams();
 
739
  tg.expand();
740
  tg.enableClosingConfirmation();
 
 
 
 
 
741
 
742
+ const userInfoDiv = document.getElementById('userInfo');
743
+ userInfoDiv.innerHTML = \`<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="avatar" style="opacity:0;"> Authenticating...\`;
744
+
745
+
746
  try {
747
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
748
  currentUser = authResponse.user;
749
  if (currentUser) {
750
+ let avatarHtml = '';
751
+ if (currentUser.photo_url) {
752
+ avatarHtml = \`<img src="\${currentUser.photo_url}" alt="avatar">\`;
753
+ } else {
754
+ avatarHtml = \`<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="avatar" style="display:none;">\`;
755
+ }
756
+ userInfoDiv.innerHTML = \`\${avatarHtml} \${currentUser.first_name || ''} (@\${currentUser.username || 'anonymous'})\`;
757
+ } else {
758
+ userInfoDiv.textContent = \`Welcome, \${tg.initDataUnsafe.user?.first_name || 'User'}! (@\${tg.initDataUnsafe.user?.username || 'anonymous'})\`;
759
  }
760
  } catch (error) {
761
  console.error("Auth error:", error);
762
+ userInfoDiv.textContent = \`Auth failed. \${tg.initDataUnsafe.user?.first_name || 'User'}\`;
763
  tg.showAlert("Authentication with the server failed. Some features might not work correctly.");
764
  }
765
 
766
 
767
  document.querySelectorAll('.tab-button').forEach(button => {
768
+ button.addEventListener('click', () => { loadView(button.dataset.tab); tg.HapticFeedback.impactOccurred('light'); });
 
 
 
 
769
  });
770
+ document.getElementById('fabButton').addEventListener('click', () => { showForm(currentView); tg.HapticFeedback.impactOccurred('medium'); });
771
 
772
+ document.body.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; }, { passive: true });
773
+ document.body.addEventListener('touchend', e => { touchendX = e.changedTouches[0].screenX; handleSwipeGesture(); }, { passive: true });
774
+
775
+ tg.onEvent('themeChanged', applyThemeParams);
776
 
777
+ loadView('resumes');
778
  }
779
 
780
  init();
 
908
  else:
909
  return jsonify({"error": "Authentication data not provided"}), 401
910
 
911
+ is_valid, user_data_dict = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
912
 
913
+ if not is_valid or not user_data_dict:
914
  return jsonify({"error": "Invalid authentication data"}), 403
915
 
916
  data = load_data()
917
  users = data.get('users', {})
918
+ user_id_str = str(user_data_dict.get('id'))
919
 
920
  if user_id_str not in users:
921
  users[user_id_str] = {
922
+ 'id': user_data_dict.get('id'),
923
+ 'first_name': user_data_dict.get('first_name'),
924
+ 'last_name': user_data_dict.get('last_name'),
925
+ 'username': user_data_dict.get('username'),
926
+ 'language_code': user_data_dict.get('language_code'),
927
+ 'photo_url': user_data_dict.get('photo_url'),
928
  'first_seen': datetime.now().isoformat()
929
  }
930
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
931
+ if 'photo_url' in user_data_dict: # Ensure photo_url is updated if changed
932
+ users[user_id_str]['photo_url'] = user_data_dict.get('photo_url')
933
+
 
 
934
  data['users'] = users
935
  save_data(data)
936
 
 
940
  auth_data_str = request_headers.get('X-Telegram-Auth')
941
  if not auth_data_str:
942
  return None
943
+ is_valid, user_data_dict = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
944
+ if is_valid and user_data_dict:
945
+ return user_data_dict
946
  return None
947
 
948
  @app.route('/api/<item_type>', methods=['GET'])
 
1047
  updated_item['updated_timestamp'] = datetime.now().isoformat()
1048
 
1049
  if item_type == 'resumes':
 
 
 
1050
  updated_item.update({
1051
  "name": req_data.get('name', original_item.get('name')),
1052
  "title": req_data.get('title', original_item.get('title')),
 
1057
  "portfolio_link": req_data.get('portfolio_link', original_item.get('portfolio_link'))
1058
  })
1059
  elif item_type == 'vacancies':
 
 
 
1060
  updated_item.update({
1061
  "company_name": req_data.get('company_name', original_item.get('company_name')),
1062
  "title": req_data.get('title', original_item.get('title')),
 
1067
  "contact": req_data.get('contact', original_item.get('contact'))
1068
  })
1069
  elif item_type == 'freelance_offers':
1070
+ updated_item.update({
 
 
 
1071
  "title": req_data.get('title', original_item.get('title')),
1072
  "description": req_data.get('description', original_item.get('description')),
1073
  "budget": req_data.get('budget', original_item.get('budget')),