Shveiauto commited on
Commit
f34e78a
·
verified ·
1 Parent(s): 86408a7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +346 -405
app.py CHANGED
@@ -1,5 +1,3 @@
1
- # --- START OF FILE app (20).py ---
2
-
3
  from flask import Flask, render_template_string, request, redirect, url_for, jsonify, flash, send_from_directory
4
  import json
5
  import os
@@ -40,7 +38,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
40
 
41
  if not os.path.exists(app.config['UPLOADS_DIR']):
42
  os.makedirs(app.config['UPLOADS_DIR'], exist_ok=True)
43
- for item_type_subdir in ['resumes', 'vacancies', 'freelance_offers', 'profile_pictures']:
44
  path = os.path.join(app.config['UPLOADS_DIR'], item_type_subdir)
45
  os.makedirs(path, exist_ok=True)
46
 
@@ -52,13 +50,11 @@ def delete_existing_image(image_url):
52
  if not image_url:
53
  return
54
  try:
55
- parsed_url = urllib.parse.urlparse(image_url)
56
- path_parts = parsed_url.path.strip('/').split('/')
57
-
58
- if len(path_parts) == 3 and path_parts[0] == UPLOADS_DIR_NAME:
59
- item_type_subdir = path_parts[1]
60
- img_filename = path_parts[2]
61
- local_image_path = os.path.join(app.config['UPLOADS_DIR'], item_type_subdir, secure_filename(img_filename))
62
  if os.path.exists(local_image_path):
63
  os.remove(local_image_path)
64
  logging.info(f"Deleted old image: {local_image_path}")
@@ -100,8 +96,8 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
100
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
101
  except Exception as create_e:
102
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
103
- success = True
104
- break
105
  else:
106
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
107
  except Exception as e:
@@ -253,6 +249,10 @@ MAIN_APP_TEMPLATE = '''
253
  --tg-theme-section-header-text-color: #8e8e93;
254
  --tg-theme-destructive-text-color: #ff3b30;
255
  --tg-theme-accent-text-color: #007aff;
 
 
 
 
256
  }
257
  body {
258
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
@@ -264,54 +264,45 @@ MAIN_APP_TEMPLATE = '''
264
  -webkit-font-smoothing: antialiased;
265
  -moz-osx-font-smoothing: grayscale;
266
  transition: background-color 0.3s, color 0.3s;
267
- display: flex;
268
- flex-direction: column;
269
  min-height: 100vh;
270
- min-height: 100dvh;
 
271
  }
272
- .app-container { display: flex; flex-direction: column; flex-grow: 1; }
273
  .header {
274
  background-color: var(--tg-theme-header-bg-color);
275
- padding: 10px 15px;
276
  text-align: center;
277
  font-weight: 600;
278
- font-size: 18px;
279
  border-bottom: 1px solid var(--tg-theme-secondary-bg-color);
280
  position: sticky;
281
  top: 0;
282
  z-index: 100;
283
  transition: background-color 0.3s, border-bottom-color 0.3s;
284
- backdrop-filter: blur(10px);
285
- -webkit-backdrop-filter: blur(10px);
286
  }
287
  .user-info {
288
- padding: 12px 15px;
289
  background-color: var(--tg-theme-secondary-bg-color);
290
- font-size: 14px;
291
- text-align: center;
292
  color: var(--tg-theme-hint-color);
293
- display: flex;
294
- align-items: center;
295
- justify-content: center;
296
- gap: 8px;
297
- border-bottom: 1px solid var(--tg-theme-bg-color);
298
  transition: background-color 0.3s, color 0.3s;
 
299
  }
300
- .user-info img { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; }
301
-
302
  .tabs {
303
  display: flex;
304
- background-color: var(--tg-theme-header-bg-color);
305
- padding: 5px;
306
- gap: 5px;
307
- transition: background-color 0.3s;
308
  position: sticky;
309
- top: 48px; /* Adjust if header height changes */
310
  z-index: 99;
311
  }
312
  .tab-button {
313
  flex: 1;
314
- padding: 12px 8px;
315
  text-align: center;
316
  cursor: pointer;
317
  background: none;
@@ -320,60 +311,65 @@ MAIN_APP_TEMPLATE = '''
320
  font-size: 15px;
321
  font-weight: 500;
322
  border-bottom: 3px solid transparent;
323
- transition: color 0.25s ease, border-bottom-color 0.25s ease;
324
- border-radius: 6px 6px 0 0;
325
  }
326
- .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); font-weight: 600; }
327
- .content { flex-grow: 1; padding: 15px; transition: opacity 0.3s ease-in-out; }
328
 
329
- .view-transition { opacity: 0; transform: translateY(15px); transition: opacity 0.3s ease-out, transform 0.3s ease-out; }
330
- .view-transition.visible { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  .list-item {
333
  background-color: var(--tg-theme-section-bg-color);
334
- border-radius: 12px;
335
- padding: 15px;
336
  margin-bottom: 12px;
337
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
338
  cursor: pointer;
339
- transition: background-color 0.2s, transform 0.15s ease-out;
 
340
  }
341
- .list-item:active { background-color: var(--tg-theme-secondary-bg-color); transform: scale(0.97); }
342
- .list-item-content { display: flex; align-items: flex-start; gap: 15px; }
343
- .list-item-image {
344
- width: 60px; height: 60px;
345
- border-radius: 10px;
346
- object-fit: cover;
347
- background-color: var(--tg-theme-secondary-bg-color);
348
- flex-shrink: 0;
349
- display: flex; align-items: center; justify-content: center; font-size: 28px; color: var(--tg-theme-hint-color);
350
- }
351
- .list-item-text h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); line-height: 1.3; }
352
- .list-item-text p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); line-height: 1.4; }
353
- .list-item-text .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 6px; }
354
- .list-item-actions { margin-top: 10px; display: flex; gap: 10px; }
355
- .list-item-actions button {
356
- padding: 6px 12px; font-size: 13px; border-radius: 6px; border: none; cursor: pointer;
357
- background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color);
358
- }
359
- .list-item-actions .delete-btn { background-color: var(--tg-theme-destructive-text-color); }
360
-
361
-
362
- .form-container { padding: 20px; background-color: var(--tg-theme-section-bg-color); border-radius: 12px; margin-bottom: 15px;}
363
- .form-group { margin-bottom: 18px; }
364
- .form-group label { display: block; font-size: 15px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500;}
365
  .form-group input, .form-group textarea, .form-group input[type="file"] {
366
  width: 100%;
367
  padding: 12px;
368
  border: 1px solid var(--tg-theme-secondary-bg-color);
369
- border-radius: 10px;
370
  font-size: 16px;
371
  background-color: var(--tg-theme-bg-color);
372
  color: var(--tg-theme-text-color);
373
  box-sizing: border-box;
374
- transition: border-color 0.2s;
 
 
 
 
 
375
  }
376
- .form-group input:focus, .form-group textarea:focus { border-color: var(--tg-theme-link-color); outline: none; }
377
  .form-group input[type="file"] { padding: 8px; }
378
  .form-group textarea { min-height: 100px; resize: vertical; }
379
 
@@ -381,39 +377,70 @@ MAIN_APP_TEMPLATE = '''
381
  position: fixed;
382
  bottom: calc(20px + env(safe-area-inset-bottom));
383
  right: 20px;
384
- width: 60px;
385
- height: 60px;
386
  background-color: var(--tg-theme-button-color);
387
  color: var(--tg-theme-button-text-color);
388
  border-radius: 50%;
389
  display: flex;
390
  align-items: center;
391
  justify-content: center;
392
- font-size: 32px;
393
- line-height: 32px;
394
- box-shadow: 0 6px 16px rgba(0,0,0,0.2);
395
  cursor: pointer;
396
  z-index: 1000;
397
  border: none;
398
- transition: background-color 0.3s, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
399
  }
400
- .fab:active { transform: scale(0.92); }
 
401
 
402
- .detail-view { padding: 20px; background-color: var(--tg-theme-section-bg-color); border-radius: 12px; }
403
- .detail-view h2 { margin-top: 0; font-size: 24px; font-weight: 700; color: var(--tg-theme-text-color); margin-bottom: 15px; }
404
  .detail-view p { margin-bottom: 12px; line-height: 1.65; font-size: 16px; }
405
- .detail-view strong { font-weight: 600; color: var(--tg-theme-section-header-text-color); }
406
- .detail-image { width: 100%; max-height: 300px; border-radius: 10px; margin-bottom: 20px; background-color: var(--tg-theme-secondary-bg-color); object-fit: cover; }
407
- .detail-meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 20px; }
408
- .button-destructive {
409
- background-color: var(--tg-theme-destructive-text-color) !important;
410
- color: var(--tg-theme-button-text-color) !important;
411
- padding: 12px 15px; border: none; border-radius: 8px; font-size: 16px; width: 100%;
412
- cursor: pointer; margin-top: 15px;
 
 
 
 
 
413
  }
 
 
 
414
 
415
- .loading, .empty-state { text-align: center; padding: 50px 20px; color: var(--tg-theme-hint-color); font-size: 17px; }
 
416
  .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  </style>
418
  </head>
419
  <body>
@@ -427,10 +454,12 @@ MAIN_APP_TEMPLATE = '''
427
  <button class="tab-button active" data-tab="resumes">Resumes</button>
428
  <button class="tab-button" data-tab="vacancies">Vacancies</button>
429
  <button class="tab-button" data-tab="freelance_offers">Freelance</button>
430
- <button class="tab-button" data-tab="profile">Profile</button>
431
  </div>
432
- <div class="content view-transition" id="mainContent">
433
- <div class="loading">Loading content...</div>
 
 
434
  </div>
435
  <button class="fab" id="fabButton" title="Add New Item">+</button>
436
  </div>
@@ -441,26 +470,37 @@ MAIN_APP_TEMPLATE = '''
441
  let currentView = 'resumes';
442
  let currentItem = null;
443
  const tabOrder = ['resumes', 'vacancies', 'freelance_offers', 'profile'];
444
- const mainContentEl = document.getElementById('mainContent');
 
445
 
446
  function applyThemeParams() {
447
  const root = document.documentElement;
448
- const theme = tg.themeParams;
449
- root.style.setProperty('--tg-theme-bg-color', theme.bg_color || '#ffffff');
450
- root.style.setProperty('--tg-theme-text-color', theme.text_color || '#000000');
451
- root.style.setProperty('--tg-theme-hint-color', theme.hint_color || '#999999');
452
- root.style.setProperty('--tg-theme-link-color', theme.link_color || '#007aff');
453
- root.style.setProperty('--tg-theme-button-color', theme.button_color || '#007aff');
454
- root.style.setProperty('--tg-theme-button-text-color', theme.button_text_color || '#ffffff');
455
- root.style.setProperty('--tg-theme-secondary-bg-color', theme.secondary_bg_color || '#f0f0f0');
456
- root.style.setProperty('--tg-theme-header-bg-color', theme.header_bg_color || theme.secondary_bg_color || '#efeff4');
457
- root.style.setProperty('--tg-theme-section-bg-color', theme.section_bg_color || theme.bg_color || '#ffffff');
458
- root.style.setProperty('--tg-theme-section-header-text-color', theme.section_header_text_color || theme.hint_color || '#8e8e93');
459
- root.style.setProperty('--tg-theme-destructive-text-color', theme.destructive_text_color || '#ff3b30');
460
- root.style.setProperty('--tg-theme-accent-text-color', theme.accent_text_color || theme.link_color || '#007aff');
461
 
462
- if (tg.colorScheme) {
463
- document.body.setAttribute('data-color-scheme', tg.colorScheme);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  }
465
  }
466
 
@@ -481,9 +521,10 @@ MAIN_APP_TEMPLATE = '''
481
  try {
482
  const response = await fetch(endpoint, options);
483
  if (!response.ok) {
484
- const errorData = await response.json().catch(() => ({ error: 'Request failed: ' + response.status }));
485
  throw new Error(errorData.error || `HTTP error ${response.status}`);
486
  }
 
487
  return response.json();
488
  } catch (error) {
489
  console.error('API Call Error:', error);
@@ -491,64 +532,44 @@ MAIN_APP_TEMPLATE = '''
491
  throw error;
492
  }
493
  }
 
 
 
 
494
 
495
- function renderList(items, type) {
496
- mainContentEl.classList.remove('visible');
497
  setTimeout(() => {
498
- if (!items || items.length === 0) {
499
- mainContentEl.innerHTML = `<div class="empty-state">No ${type.replace('_', ' ')} found. Be the first to add one!</div>`;
500
- } else {
501
- mainContentEl.innerHTML = items.map(item => `
502
- <div class="list-item" onclick="showDetailView('${type}', '${item.id}')">
503
- <div class="list-item-content">
504
- <div class="list-item-image">${item.image_url ? `<img src="${item.image_url}" alt="Image" style="width:100%;height:100%;object-fit:cover;">` : '🖼️'}</div>
505
- <div class="list-item-text">
506
- <h3>${item.title || item.name || 'Untitled'}</h3>
507
- ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
508
- ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
509
- <p class="meta">By: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>
510
- </div>
511
- </div>
512
- </div>
513
- `).join('');
514
- }
515
- mainContentEl.classList.add('visible');
516
- }, 150);
517
  }
518
-
519
- function renderProfileList(items) {
520
- mainContentEl.classList.remove('visible');
521
- setTimeout(() => {
522
- if (!items || items.length === 0) {
523
- mainContentEl.innerHTML = `<div class="empty-state">You haven't posted anything yet.</div>`;
524
- } else {
525
- mainContentEl.innerHTML = items.map(item => `
526
- <div class="list-item">
527
- <div class="list-item-content" onclick="showDetailView('${item.original_type}', '${item.id}')">
528
- <div class="list-item-image">${item.image_url ? `<img src="${item.image_url}" alt="Image" style="width:100%;height:100%;object-fit:cover;">` : '📝'}</div>
529
- <div class="list-item-text">
530
- <h3>${item.title || item.name || 'Untitled'} (${item.original_type.slice(0,-1).replace('_', ' ')})</h3>
531
- <p class="meta">Posted on ${new Date(item.timestamp).toLocaleDateString()}</p>
532
- </div>
533
- </div>
534
- <div class="list-item-actions">
535
- <button onclick="event.stopPropagation(); showForm('${item.original_type}', '${item.id}')">Edit</button>
536
- <button class="delete-btn" onclick="event.stopPropagation(); handleDeleteItem('${item.original_type}', '${item.id}', true)">Delete</button>
537
  </div>
538
  </div>
539
- `).join('');
540
- }
541
- mainContentEl.classList.add('visible');
542
- }, 150);
543
  }
544
 
545
-
546
  function showDetailView(type, id) {
547
  tg.BackButton.show();
548
- tg.BackButton.onClick(() => { loadView(currentView === 'profile' ? 'profile' : type); tg.HapticFeedback.impactOccurred('light'); });
549
  tg.MainButton.hide();
550
  document.getElementById('fabButton').style.display = 'none';
551
- mainContentEl.classList.remove('visible');
552
 
553
  apiCall(`/api/${type}/${id}`)
554
  .then(item => {
@@ -559,81 +580,61 @@ MAIN_APP_TEMPLATE = '''
559
  }
560
  detailsHtml += `<h2>${item.title || item.name}</h2>`;
561
 
562
- const nl2br = (str) => str ? str.replace(/\\n/g, '<br>').replace(/\n/g, '<br>') : 'N/A';
563
-
564
  if (type === 'resumes') {
565
  detailsHtml += `
566
  <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
567
- <p><strong>Experience:</strong><br>${nl2br(item.experience)}</p>
568
- <p><strong>Education:</strong><br>${nl2br(item.education)}</p>
569
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
570
  ${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>` : ''}
571
  `;
572
  } else if (type === 'vacancies') {
573
  detailsHtml += `
574
  <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
575
- <p><strong>Description:</strong><br>${nl2br(item.description)}</p>
576
- <p><strong>Requirements:</strong><br>${nl2br(item.requirements)}</p>
577
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
578
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
579
- <p><strong>Contact/Apply:</strong> ${nl2br(item.contact) || `@${item.user_telegram_username}`}</p>
580
  `;
581
  } else if (type === 'freelance_offers') {
582
  detailsHtml += `
583
- <p><strong>Description:</strong><br>${nl2br(item.description)}</p>
584
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
585
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
586
  <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
587
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
588
  `;
589
  }
590
- detailsHtml += `<p class="detail-meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
591
-
592
  if (currentUser && item.user_id === currentUser.id) {
593
- detailsHtml += `<button class="button-destructive" onclick="handleDeleteItem('${type}', '${item.id}')">Delete This Post</button>`;
594
- tg.MainButton.setText('Edit My Post');
595
- tg.MainButton.onClick(() => { showForm(type, item.id); tg.HapticFeedback.impactOccurred('light'); });
596
- tg.MainButton.show();
 
 
597
  }
598
  detailsHtml += `</div>`;
599
-
600
- setTimeout(() => {
601
- mainContentEl.innerHTML = detailsHtml;
602
- mainContentEl.classList.add('visible');
603
- },150);
604
  })
605
  .catch(err => {
606
- setTimeout(() => {
607
- mainContentEl.innerHTML = `<div class="empty-state">Error loading details. Please try again.</div>`;
608
- mainContentEl.classList.add('visible');
609
- }, 150);
610
  });
611
  }
612
 
613
- async function showForm(type, itemIdToEdit = null) {
614
- let itemToEdit = null;
615
- if(itemIdToEdit){
616
- try {
617
- itemToEdit = await apiCall(`/api/${type}/${itemIdToEdit}`);
618
- } catch(e) {
619
- tg.showAlert("Failed to load item data for editing.");
620
- loadView(currentView); // Go back to list
621
- return;
622
- }
623
- }
624
  currentItem = itemToEdit;
625
-
626
  tg.BackButton.show();
627
  tg.BackButton.onClick(() => {
628
  if (itemToEdit) showDetailView(type, itemToEdit.id);
629
- else loadView(currentView); // currentView should be the tab from which form was opened
630
  tg.HapticFeedback.impactOccurred('light');
631
  });
632
  document.getElementById('fabButton').style.display = 'none';
633
- mainContentEl.classList.remove('visible');
634
-
635
- let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1).replace('_',' ')}</h2>`;
636
 
 
637
  const commonFields = `
638
  <div class="form-group">
639
  <label for="image">Image (Optional)</label>
@@ -644,103 +645,39 @@ MAIN_APP_TEMPLATE = '''
644
 
645
  if (type === 'resumes') {
646
  formHtml += `
647
- <div class="form-group">
648
- <label for="name">Full Name*</label>
649
- <input type="text" id="name" value="${itemToEdit?.name || ''}" required>
650
- </div>
651
- <div class="form-group">
652
- <label for="title">Job Title / Desired Position*</label>
653
- <input type="text" id="title" value="${itemToEdit?.title || ''}" required>
654
- </div>
655
  ${commonFields}
656
- <div class="form-group">
657
- <label for="skills">Skills (comma separated)</label>
658
- <textarea id="skills">${itemToEdit?.skills || ''}</textarea>
659
- </div>
660
- <div class="form-group">
661
- <label for="experience">Experience</label>
662
- <textarea id="experience">${itemToEdit?.experience || ''}</textarea>
663
- </div>
664
- <div class="form-group">
665
- <label for="education">Education</label>
666
- <textarea id="education">${itemToEdit?.education || ''}</textarea>
667
- </div>
668
- <div class="form-group">
669
- <label for="contact">Contact Info (e.g., email, or blank for Telegram)</label>
670
- <input type="text" id="contact" value="${itemToEdit?.contact || ''}">
671
- </div>
672
- <div class="form-group">
673
- <label for="portfolio_link">Portfolio Link (optional)</label>
674
- <input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}">
675
- </div>
676
  `;
677
  } else if (type === 'vacancies') {
678
  formHtml += `
679
- <div class="form-group">
680
- <label for="company_name">Company Name*</label>
681
- <input type="text" id="company_name" value="${itemToEdit?.company_name || ''}" required>
682
- </div>
683
- <div class="form-group">
684
- <label for="title">Job Title*</label>
685
- <input type="text" id="title" value="${itemToEdit?.title || ''}" required>
686
- </div>
687
  ${commonFields}
688
- <div class="form-group">
689
- <label for="description">Description</label>
690
- <textarea id="description">${itemToEdit?.description || ''}</textarea>
691
- </div>
692
- <div class="form-group">
693
- <label for="requirements">Requirements</label>
694
- <textarea id="requirements">${itemToEdit?.requirements || ''}</textarea>
695
- </div>
696
- <div class="form-group">
697
- <label for="salary">Salary/Compensation</label>
698
- <input type="text" id="salary" value="${itemToEdit?.salary || ''}">
699
- </div>
700
- <div class="form-group">
701
- <label for="location">Location (e.g., Remote, City)</label>
702
- <input type="text" id="location" value="${itemToEdit?.location || ''}">
703
- </div>
704
- <div class="form-group">
705
- <label for="contact">Contact Info / How to Apply</label>
706
- <textarea id="contact">${itemToEdit?.contact || ''}</textarea>
707
- </div>
708
  `;
709
  } else if (type === 'freelance_offers') {
710
  formHtml += `
711
- <div class="form-group">
712
- <label for="title">Project Title*</label>
713
- <input type="text" id="title" value="${itemToEdit?.title || ''}" required>
714
- </div>
715
  ${commonFields}
716
- <div class="form-group">
717
- <label for="description">Description of Work</label>
718
- <textarea id="description">${itemToEdit?.description || ''}</textarea>
719
- </div>
720
- <div class="form-group">
721
- <label for="budget">Budget</label>
722
- <input type="text" id="budget" value="${itemToEdit?.budget || ''}">
723
- </div>
724
- <div class="form-group">
725
- <label for="deadline">Expected Deadline</label>
726
- <input type="text" id="deadline" value="${itemToEdit?.deadline || ''}">
727
- </div>
728
- <div class="form-group">
729
- <label for="skills_needed">Skills Needed (comma separated)</label>
730
- <textarea id="skills_needed">${itemToEdit?.skills_needed || ''}</textarea>
731
- </div>
732
- <div class="form-group">
733
- <label for="contact">Contact Info (or blank for Telegram)</label>
734
- <input type="text" id="contact" value="${itemToEdit?.contact || ''}">
735
- </div>
736
  `;
737
  }
738
  formHtml += `<div id="formError" class="error-message"></div></div>`;
739
-
740
- setTimeout(() => {
741
- mainContentEl.innerHTML = formHtml;
742
- mainContentEl.classList.add('visible');
743
- }, 150);
744
 
745
  tg.MainButton.setText(itemToEdit ? 'Save Changes' : 'Post');
746
  tg.MainButton.show();
@@ -756,8 +693,7 @@ MAIN_APP_TEMPLATE = '''
756
  const name = document.getElementById('name').value.trim();
757
  const title = document.getElementById('title').value.trim();
758
  if (!name || !title) isValid = false;
759
- formData.append('name', name);
760
- formData.append('title', title);
761
  formData.append('skills', document.getElementById('skills').value.trim());
762
  formData.append('experience', document.getElementById('experience').value.trim());
763
  formData.append('education', document.getElementById('education').value.trim());
@@ -767,8 +703,7 @@ MAIN_APP_TEMPLATE = '''
767
  const company_name = document.getElementById('company_name').value.trim();
768
  const title = document.getElementById('title').value.trim();
769
  if (!company_name || !title) isValid = false;
770
- formData.append('company_name', company_name);
771
- formData.append('title', title);
772
  formData.append('description', document.getElementById('description').value.trim());
773
  formData.append('requirements', document.getElementById('requirements').value.trim());
774
  formData.append('salary', document.getElementById('salary').value.trim());
@@ -786,18 +721,15 @@ MAIN_APP_TEMPLATE = '''
786
  }
787
 
788
  const imageInput = document.getElementById('image');
789
- if (imageInput && imageInput.files[0]) {
790
- formData.append('image', imageInput.files[0]);
791
- }
792
 
793
  if (!isValid) {
794
- document.getElementById('formError').textContent = 'Please fill in all required fields (marked with *).';
795
  tg.HapticFeedback.notificationOccurred('error');
796
  return;
797
  }
798
 
799
  tg.MainButton.showProgress();
800
-
801
  const method = itemId ? 'PUT' : 'POST';
802
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
803
 
@@ -810,94 +742,122 @@ MAIN_APP_TEMPLATE = '''
810
  .catch(err => {
811
  tg.HapticFeedback.notificationOccurred('error');
812
  tg.MainButton.hideProgress();
813
- document.getElementById('formError').textContent = err.message || 'Failed to submit. Please try again.';
814
  });
815
  }
816
-
817
- function handleDeleteItem(type, id, fromProfile = false) {
818
- tg.showConfirm("Are you sure you want to delete this post? This action cannot be undone.", (confirmed) => {
819
  if (confirmed) {
820
- tg.MainButton.showProgress();
821
- apiCall(`/api/${type}/${id}`, 'DELETE')
 
 
 
 
822
  .then(() => {
823
  tg.HapticFeedback.notificationOccurred('success');
824
- tg.MainButton.hideProgress();
825
- tg.showAlert("Post deleted successfully.");
826
- if(fromProfile || currentView === 'profile'){
827
- loadView('profile');
828
  } else {
829
- loadView(type);
830
  }
831
  })
832
  .catch(err => {
833
  tg.HapticFeedback.notificationOccurred('error');
834
- tg.MainButton.hideProgress();
835
- tg.showAlert(err.message || "Failed to delete post.");
836
  });
837
  }
838
  });
839
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
 
841
  function loadView(tabName) {
842
  currentView = tabName;
843
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
844
  document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active');
845
 
846
- mainContentEl.classList.remove('visible');
847
- mainContentEl.innerHTML = `<div class="loading">Loading ${tabName.replace('_',' ')}...</div>`;
848
-
849
  tg.BackButton.hide();
850
  tg.MainButton.hide();
851
 
852
  if (tabName === 'profile') {
853
  document.getElementById('fabButton').style.display = 'none';
854
- apiCall(`/api/user/items`)
855
- .then(data => renderProfileList(data.items))
856
- .catch(err => {
857
- setTimeout(() => {
858
- mainContentEl.innerHTML = `<div class="empty-state">Error loading your items.</div>`;
859
- mainContentEl.classList.add('visible');
860
- }, 150);
861
- });
862
  } else {
863
  document.getElementById('fabButton').style.display = 'flex';
 
864
  apiCall(`/api/${tabName}`)
865
  .then(data => renderList(data, tabName))
866
- .catch(err => {
867
- setTimeout(() => {
868
- mainContentEl.innerHTML = `<div class="empty-state">Error loading ${tabName.replace('_',' ')}.</div>`;
869
- mainContentEl.classList.add('visible');
870
- }, 150);
871
- });
872
  }
873
  }
874
 
875
  function updateUserDisplay(user) {
876
  const userAvatar = document.getElementById('userAvatar');
877
  const userText = document.getElementById('userText');
878
- const defaultAvatar = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
879
-
880
- let photoUrlToUse = defaultAvatar;
881
- let displayName = "User";
882
- let username = "anonymous";
883
-
884
  if (user && user.photo_url) {
885
- photoUrlToUse = user.photo_url;
 
886
  } else if (tg.initDataUnsafe.user?.photo_url) {
887
- photoUrlToUse = tg.initDataUnsafe.user.photo_url;
 
 
 
888
  }
889
 
890
  if (user) {
891
- displayName = user.first_name || "User";
892
- username = user.username || "unknown_user";
893
  } else if (tg.initDataUnsafe.user) {
894
- displayName = tg.initDataUnsafe.user.first_name || "User";
895
- username = tg.initDataUnsafe.user.username || "anonymous";
 
896
  }
897
-
898
- userAvatar.src = photoUrlToUse;
899
- userAvatar.style.display = (photoUrlToUse !== defaultAvatar) ? 'inline-block' : 'none';
900
- userText.textContent = `${displayName} (@${username})`;
901
  }
902
 
903
  async function init() {
@@ -914,10 +874,14 @@ MAIN_APP_TEMPLATE = '''
914
  currentUser = authResponse.user;
915
  if (currentUser) {
916
  updateUserDisplay(currentUser);
 
 
 
917
  }
918
  } catch (error) {
919
  console.error("Auth error:", error);
920
- document.getElementById('userText').textContent = `Welcome! (Limited access)`;
 
921
  }
922
 
923
  document.querySelectorAll('.tab-button').forEach(button => {
@@ -927,7 +891,7 @@ MAIN_APP_TEMPLATE = '''
927
  });
928
  });
929
  document.getElementById('fabButton').addEventListener('click', () => {
930
- if(currentView === 'profile') return;
931
  showForm(currentView);
932
  tg.HapticFeedback.impactOccurred('medium');
933
  });
@@ -941,52 +905,34 @@ MAIN_APP_TEMPLATE = '''
941
  let touchendX = 0;
942
  const swipeThreshold = 75;
943
 
944
- mainContentEl.addEventListener('touchstart', e => {
945
- if (e.touches.length === 1) { // Single touch
 
946
  touchstartX = e.changedTouches[0].screenX;
 
 
947
  }
948
  }, {passive: true});
949
 
950
- mainContentEl.addEventListener('touchend', e => {
951
- if (e.changedTouches.length === 1) { // Single touch
952
- touchendX = e.changedTouches[0].screenX;
953
- handleSwipe();
954
- }
955
  });
956
 
957
  function handleSwipe() {
958
- if (document.querySelector('.detail-view') || document.querySelector('.form-container')) {
959
- return; // Disable swipe in detail or form views
960
- }
961
-
962
  const currentTabIndex = tabOrder.indexOf(currentView);
963
- let newView = null;
964
-
965
  if (touchendX < touchstartX - swipeThreshold) {
966
  if (currentTabIndex < tabOrder.length - 1) {
967
- newView = tabOrder[currentTabIndex + 1];
 
968
  }
969
  }
970
  if (touchendX > touchstartX + swipeThreshold) {
971
  if (currentTabIndex > 0) {
972
- newView = tabOrder[currentTabIndex - 1];
973
- }
974
- }
975
-
976
- if(newView){
977
- mainContentEl.style.transition = 'opacity 0.2s ease-out, transform 0.2s ease-out';
978
- mainContentEl.style.transform = `translateX(${touchendX < touchstartX ? '-' : ''}30px)`;
979
- mainContentEl.style.opacity = '0';
980
- setTimeout(() => {
981
- loadView(newView);
982
  tg.HapticFeedback.impactOccurred('light');
983
- mainContentEl.style.transform = `translateX(${touchendX < touchstartX ? '' : '-'}30px)`; // From opposite direction
984
- setTimeout(() => { // Stagger for smooth transition in
985
- mainContentEl.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
986
- mainContentEl.style.transform = 'translateX(0px)';
987
- mainContentEl.style.opacity = '1';
988
- }, 20);
989
- }, 200);
990
  }
991
  }
992
  }
@@ -1117,7 +1063,7 @@ def main_app_view():
1117
 
1118
  @app.route('/uploads/<item_type_subdir>/<filename>')
1119
  def uploaded_file_typed(item_type_subdir, filename):
1120
- if item_type_subdir not in ['resumes', 'vacancies', 'freelance_offers', 'profile_pictures']:
1121
  return jsonify({"error": "Invalid category"}), 404
1122
 
1123
  directory = os.path.join(app.config['UPLOADS_DIR'], item_type_subdir)
@@ -1157,11 +1103,8 @@ def auth_user():
1157
  'first_seen': datetime.now().isoformat()
1158
  }
1159
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
1160
- if user_data_dict.get('photo_url'):
1161
  users[user_id_str]['photo_url'] = user_data_dict.get('photo_url')
1162
- else: # If TG doesn't provide it, make sure it's None or empty in our DB
1163
- users[user_id_str]['photo_url'] = users[user_id_str].get('photo_url') # Keep existing if TG sends empty now
1164
-
1165
  data['users'] = users
1166
  save_data(data)
1167
 
@@ -1184,27 +1127,6 @@ def get_items(item_type):
1184
  items = sorted(data.get(item_type, []), key=lambda x: x.get('timestamp', ''), reverse=True)
1185
  return jsonify(items), 200
1186
 
1187
- @app.route('/api/user/items', methods=['GET'])
1188
- def get_user_items():
1189
- user = get_authenticated_user(request.headers)
1190
- if not user:
1191
- return jsonify({"error": "Authentication required"}), 401
1192
-
1193
- user_id_str = str(user.get('id'))
1194
- data = load_data()
1195
- user_items = []
1196
-
1197
- for item_type in ['resumes', 'vacancies', 'freelance_offers']:
1198
- for item in data.get(item_type, []):
1199
- if str(item.get('user_id')) == user_id_str:
1200
- item_copy = item.copy()
1201
- item_copy['original_type'] = item_type
1202
- user_items.append(item_copy)
1203
-
1204
- user_items_sorted = sorted(user_items, key=lambda x: x.get('timestamp', ''), reverse=True)
1205
- return jsonify({"items": user_items_sorted}), 200
1206
-
1207
-
1208
  @app.route('/api/<item_type>/<item_id>', methods=['GET'])
1209
  def get_item(item_type, item_id):
1210
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
@@ -1215,6 +1137,28 @@ def get_item(item_type, item_id):
1215
  return jsonify(item), 200
1216
  return jsonify({"error": "Item not found"}), 404
1217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1218
  @app.route('/api/<item_type>', methods=['POST'])
1219
  def create_item(item_type):
1220
  user = get_authenticated_user(request.headers)
@@ -1240,12 +1184,11 @@ def create_item(item_type):
1240
  image_file = request.files.get('image')
1241
  if image_file and allowed_file(image_file.filename):
1242
  try:
1243
- file_ext = os.path.splitext(secure_filename(image_file.filename))[1].lower()
1244
  image_filename = f"{new_item_id}{file_ext}"
1245
  image_save_path = os.path.join(app.config['UPLOADS_DIR'], item_type, image_filename)
1246
- os.makedirs(os.path.join(app.config['UPLOADS_DIR'], item_type), exist_ok=True)
1247
  image_file.save(image_save_path)
1248
- new_item['image_url'] = url_for('uploaded_file_typed', item_type_subdir=item_type, filename=image_filename, _external=True)
1249
  except Exception as e:
1250
  logging.error(f"Error saving image for new item: {e}")
1251
 
@@ -1317,12 +1260,11 @@ def update_item(item_type, item_id):
1317
  if image_file and allowed_file(image_file.filename):
1318
  delete_existing_image(original_item.get('image_url'))
1319
  try:
1320
- file_ext = os.path.splitext(secure_filename(image_file.filename))[1].lower()
1321
- image_filename = f"{item_id}{file_ext}" # Use item_id for consistency on update
1322
  image_save_path = os.path.join(app.config['UPLOADS_DIR'], item_type, image_filename)
1323
- os.makedirs(os.path.join(app.config['UPLOADS_DIR'], item_type), exist_ok=True)
1324
  image_file.save(image_save_path)
1325
- updated_item['image_url'] = url_for('uploaded_file_typed', item_type_subdir=item_type, filename=image_filename, _external=True)
1326
  except Exception as e:
1327
  logging.error(f"Error saving image for updated item {item_id}: {e}")
1328
 
@@ -1384,7 +1326,7 @@ def delete_item(item_type, item_id):
1384
 
1385
  if len(data[item_type]) < original_length:
1386
  save_data(data)
1387
- return jsonify({"message": "Item deleted successfully"}), 200
1388
  return jsonify({"error": "Item not found or deletion failed"}), 404
1389
 
1390
 
@@ -1466,5 +1408,4 @@ if __name__ == '__main__':
1466
 
1467
  port = int(os.environ.get('PORT', 7860))
1468
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1469
- app.run(debug=False, host='0.0.0.0', port=port)
1470
- # --- END OF FILE app (20).py ---
 
 
 
1
  from flask import Flask, render_template_string, request, redirect, url_for, jsonify, flash, send_from_directory
2
  import json
3
  import os
 
38
 
39
  if not os.path.exists(app.config['UPLOADS_DIR']):
40
  os.makedirs(app.config['UPLOADS_DIR'], exist_ok=True)
41
+ for item_type_subdir in ['resumes', 'vacancies', 'freelance_offers']:
42
  path = os.path.join(app.config['UPLOADS_DIR'], item_type_subdir)
43
  os.makedirs(path, exist_ok=True)
44
 
 
50
  if not image_url:
51
  return
52
  try:
53
+ image_path_parts = image_url.split('/')
54
+ if len(image_path_parts) == 4 and image_path_parts[1] == UPLOADS_DIR_NAME:
55
+ item_type_subdir = image_path_parts[2]
56
+ img_filename = image_path_parts[3]
57
+ local_image_path = os.path.join(app.config['UPLOADS_DIR'], item_type_subdir, img_filename)
 
 
58
  if os.path.exists(local_image_path):
59
  os.remove(local_image_path)
60
  logging.info(f"Deleted old image: {local_image_path}")
 
96
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
97
  except Exception as create_e:
98
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
99
+ success = False
100
+ break
101
  else:
102
  logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
103
  except Exception as e:
 
249
  --tg-theme-section-header-text-color: #8e8e93;
250
  --tg-theme-destructive-text-color: #ff3b30;
251
  --tg-theme-accent-text-color: #007aff;
252
+ --border-radius-s: 8px;
253
+ --border-radius-m: 12px;
254
+ --padding-s: 10px;
255
+ --padding-m: 15px;
256
  }
257
  body {
258
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
 
264
  -webkit-font-smoothing: antialiased;
265
  -moz-osx-font-smoothing: grayscale;
266
  transition: background-color 0.3s, color 0.3s;
 
 
267
  min-height: 100vh;
268
+ min-height: calc(100vh - env(safe-area-inset-bottom));
269
+ padding-bottom: env(safe-area-inset-bottom);
270
  }
271
+ .app-container { display: flex; flex-direction: column; min-height: 100vh; }
272
  .header {
273
  background-color: var(--tg-theme-header-bg-color);
274
+ padding: var(--padding-s) var(--padding-m);
275
  text-align: center;
276
  font-weight: 600;
277
+ font-size: 17px;
278
  border-bottom: 1px solid var(--tg-theme-secondary-bg-color);
279
  position: sticky;
280
  top: 0;
281
  z-index: 100;
282
  transition: background-color 0.3s, border-bottom-color 0.3s;
 
 
283
  }
284
  .user-info {
285
+ padding: var(--padding-s) var(--padding-m);
286
  background-color: var(--tg-theme-secondary-bg-color);
287
+ font-size: 14px; text-align: center;
 
288
  color: var(--tg-theme-hint-color);
289
+ display: flex; align-items: center; justify-content: center;
 
 
 
 
290
  transition: background-color 0.3s, color 0.3s;
291
+ border-bottom: 1px solid var(--tg-theme-secondary-bg-color);
292
  }
293
+ .user-info img { width: 28px; height: 28px; border-radius: 50%; vertical-align: middle; margin-right: 10px; object-fit: cover; }
 
294
  .tabs {
295
  display: flex;
296
+ background-color: var(--tg-theme-secondary-bg-color);
297
+ padding: 5px;
298
+ transition: background-color 0.3s;
 
299
  position: sticky;
300
+ top: calc(env(safe-area-inset-top) + 47px); /* Approx header height */
301
  z-index: 99;
302
  }
303
  .tab-button {
304
  flex: 1;
305
+ padding: var(--padding-s);
306
  text-align: center;
307
  cursor: pointer;
308
  background: none;
 
311
  font-size: 15px;
312
  font-weight: 500;
313
  border-bottom: 3px solid transparent;
314
+ transition: color 0.2s, border-bottom-color 0.2s, background-color 0.2s;
315
+ border-radius: var(--border-radius-s) var(--border-radius-s) 0 0;
316
  }
317
+ .tab-button:hover { background-color: rgba(0,0,0,0.05); }
318
+ .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); font-weight: 600;}
319
 
320
+ .content {
321
+ flex-grow: 1;
322
+ padding: var(--padding-m);
323
+ }
324
+ .content-inner {
325
+ opacity: 0;
326
+ transform: translateY(15px);
327
+ transition: opacity 0.3s ease-out, transform 0.3s ease-out;
328
+ }
329
+ .content-inner.visible {
330
+ opacity: 1;
331
+ transform: translateY(0);
332
+ }
333
 
334
  .list-item {
335
  background-color: var(--tg-theme-section-bg-color);
336
+ border-radius: var(--border-radius-m);
337
+ padding: 12px var(--padding-m);
338
  margin-bottom: 12px;
339
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
340
  cursor: pointer;
341
+ transition: background-color 0.2s, transform 0.15s ease-out, box-shadow 0.2s;
342
+ overflow: hidden;
343
  }
344
+ .list-item:hover { box-shadow: 0 6px 16px rgba(0,0,0,0.12); transform: translateY(-2px); }
345
+ .list-item:active { background-color: var(--tg-theme-secondary-bg-color); transform: scale(0.98) translateY(0); }
346
+ .list-item h3 { margin: 0 0 6px 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); line-height: 1.3; }
347
+ .list-item p { margin: 0 0 4px 0; font-size: 14px; color: var(--tg-theme-hint-color); line-height: 1.4; }
348
+ .list-item .meta { font-size: 13px; color: var(--tg-theme-hint-color); margin-top: 8px; }
349
+ .list-item-image { width: 60px; height: 60px; border-radius: var(--border-radius-s); object-fit: cover; margin-right: 12px; background-color: var(--tg-theme-secondary-bg-color); flex-shrink: 0; }
350
+ .list-item-content { display: flex; align-items: center; }
351
+ .list-item-text-content { flex-grow: 1; min-width: 0; /* Fix for text overflow */ }
352
+
353
+
354
+ .form-container { padding: var(--padding-m); background-color: var(--tg-theme-section-bg-color); border-radius: var(--border-radius-m); margin-bottom: var(--padding-m);}
355
+ .form-group { margin-bottom: var(--padding-m); }
356
+ .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 6px; font-weight: 500;}
 
 
 
 
 
 
 
 
 
 
 
357
  .form-group input, .form-group textarea, .form-group input[type="file"] {
358
  width: 100%;
359
  padding: 12px;
360
  border: 1px solid var(--tg-theme-secondary-bg-color);
361
+ border-radius: var(--border-radius-s);
362
  font-size: 16px;
363
  background-color: var(--tg-theme-bg-color);
364
  color: var(--tg-theme-text-color);
365
  box-sizing: border-box;
366
+ transition: border-color 0.2s, box-shadow 0.2s;
367
+ }
368
+ .form-group input:focus, .form-group textarea:focus {
369
+ border-color: var(--tg-theme-link-color);
370
+ box-shadow: 0 0 0 2px var(--tg-theme-link-color_alpha_0_3, rgba(0,122,255,0.3)); /* Use alpha if available */
371
+ outline: none;
372
  }
 
373
  .form-group input[type="file"] { padding: 8px; }
374
  .form-group textarea { min-height: 100px; resize: vertical; }
375
 
 
377
  position: fixed;
378
  bottom: calc(20px + env(safe-area-inset-bottom));
379
  right: 20px;
380
+ width: 56px;
381
+ height: 56px;
382
  background-color: var(--tg-theme-button-color);
383
  color: var(--tg-theme-button-text-color);
384
  border-radius: 50%;
385
  display: flex;
386
  align-items: center;
387
  justify-content: center;
388
+ font-size: 28px;
389
+ line-height: 28px;
390
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
391
  cursor: pointer;
392
  z-index: 1000;
393
  border: none;
394
+ transition: background-color 0.3s, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s;
395
  }
396
+ .fab:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.25); transform: translateY(-2px); }
397
+ .fab:active { transform: scale(0.9); box-shadow: 0 2px 10px rgba(0,0,0,0.2); }
398
 
399
+ .detail-view { padding: var(--padding-m); background-color: var(--tg-theme-section-bg-color); border-radius: var(--border-radius-m); margin-bottom: var(--padding-m); }
400
+ .detail-view h2 { margin-top: 0; font-size: 24px; font-weight: 600; color: var(--tg-theme-text-color); margin-bottom: 15px; }
401
  .detail-view p { margin-bottom: 12px; line-height: 1.65; font-size: 16px; }
402
+ .detail-view strong { font-weight: 500; color: var(--tg-theme-section-header-text-color); }
403
+ .detail-image { width: 100%; max-height: 300px; object-fit: cover; border-radius: var(--border-radius-s); margin-bottom: var(--padding-m); background-color: var(--tg-theme-secondary-bg-color); }
404
+ .detail-actions { margin-top: 20px; display: flex; gap: 10px; }
405
+ .action-button {
406
+ flex: 1;
407
+ padding: 12px 15px;
408
+ border-radius: var(--border-radius-s);
409
+ font-size: 16px;
410
+ font-weight: 500;
411
+ cursor: pointer;
412
+ text-align: center;
413
+ transition: background-color 0.2s, transform 0.1s;
414
+ border: none;
415
  }
416
+ .action-button-edit { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
417
+ .action-button-delete { background-color: var(--tg-theme-destructive-text-color); color: var(--tg-theme-button-text-color); }
418
+ .action-button:active { transform: scale(0.98); }
419
 
420
+ .loading, .empty-state { text-align: center; padding: 50px var(--padding-m); color: var(--tg-theme-hint-color); font-size: 17px; }
421
+
422
  .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 8px; }
423
+ .profile-item {
424
+ background-color: var(--tg-theme-section-bg-color);
425
+ border-radius: var(--border-radius-m);
426
+ padding: var(--padding-m);
427
+ margin-bottom: 12px;
428
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
429
+ }
430
+ .profile-item h4 { margin: 0 0 8px 0; font-size: 16px; font-weight: 600; }
431
+ .profile-item p { margin: 0 0 10px 0; font-size: 14px; color: var(--tg-theme-hint-color); }
432
+ .profile-item-actions { display: flex; gap: 10px; margin-top: 10px; }
433
+ .profile-item-actions button {
434
+ padding: 8px 12px;
435
+ border-radius: var(--border-radius-s);
436
+ font-size: 14px;
437
+ cursor: pointer;
438
+ border: none;
439
+ transition: background-color 0.2s;
440
+ }
441
+ .profile-button-view { background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); }
442
+ .profile-button-delete { background-color: var(--tg-theme-destructive-text-color); color: var(--tg-theme-button-text-color); }
443
+
444
  </style>
445
  </head>
446
  <body>
 
454
  <button class="tab-button active" data-tab="resumes">Resumes</button>
455
  <button class="tab-button" data-tab="vacancies">Vacancies</button>
456
  <button class="tab-button" data-tab="freelance_offers">Freelance</button>
457
+ <button class="tab-button" data-tab="profile">My Profile</button>
458
  </div>
459
+ <div class="content" id="mainContentWrapper">
460
+ <div id="mainContent" class="content-inner">
461
+ <div class="loading">Loading content...</div>
462
+ </div>
463
  </div>
464
  <button class="fab" id="fabButton" title="Add New Item">+</button>
465
  </div>
 
470
  let currentView = 'resumes';
471
  let currentItem = null;
472
  const tabOrder = ['resumes', 'vacancies', 'freelance_offers', 'profile'];
473
+ const mainContentWrapper = document.getElementById('mainContentWrapper');
474
+ const mainContent = document.getElementById('mainContent');
475
 
476
  function applyThemeParams() {
477
  const root = document.documentElement;
478
+ root.style.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
479
+ root.style.setProperty('--tg-theme-text-color', tg.themeParams.text_color || '#000000');
480
+ root.style.setProperty('--tg-theme-hint-color', tg.themeParams.hint_color || '#999999');
481
+ root.style.setProperty('--tg-theme-link-color', tg.themeParams.link_color || '#007aff');
482
+ root.style.setProperty('--tg-theme-button-color', tg.themeParams.button_color || '#007aff');
483
+ root.style.setProperty('--tg-theme-button-text-color', tg.themeParams.button_text_color || '#ffffff');
484
+ root.style.setProperty('--tg-theme-secondary-bg-color', tg.themeParams.secondary_bg_color || '#f0f0f0');
 
 
 
 
 
 
485
 
486
+ const headerBg = tg.themeParams.header_bg_color || tg.themeParams.secondary_bg_color || '#efeff4';
487
+ root.style.setProperty('--tg-theme-header-bg-color', headerBg);
488
+
489
+ const sectionBg = tg.themeParams.section_bg_color || tg.themeParams.bg_color || '#ffffff';
490
+ root.style.setProperty('--tg-theme-section-bg-color', sectionBg);
491
+
492
+ root.style.setProperty('--tg-theme-section-header-text-color', tg.themeParams.section_header_text_color || tg.themeParams.hint_color || '#8e8e93');
493
+ root.style.setProperty('--tg-theme-destructive-text-color', tg.themeParams.destructive_text_color || '#ff3b30');
494
+ root.style.setProperty('--tg-theme-accent-text-color', tg.themeParams.accent_text_color || tg.themeParams.link_color || '#007aff');
495
+
496
+ if (tg.themeParams.link_color) {
497
+ // For input focus box-shadow
498
+ try {
499
+ let r = parseInt(tg.themeParams.link_color.slice(1,3), 16);
500
+ let g = parseInt(tg.themeParams.link_color.slice(3,5), 16);
501
+ let b = parseInt(tg.themeParams.link_color.slice(5,7), 16);
502
+ root.style.setProperty('--tg-theme-link-color_alpha_0_3', `rgba(${r},${g},${b},0.3)`);
503
+ } catch(e) { /*ignore*/ }
504
  }
505
  }
506
 
 
521
  try {
522
  const response = await fetch(endpoint, options);
523
  if (!response.ok) {
524
+ const errorData = await response.json().catch(() => ({ error: 'Request failed with status ' + response.status }));
525
  throw new Error(errorData.error || `HTTP error ${response.status}`);
526
  }
527
+ if (response.status === 204) return null; // No content for DELETE
528
  return response.json();
529
  } catch (error) {
530
  console.error('API Call Error:', error);
 
532
  throw error;
533
  }
534
  }
535
+
536
+ function setContent(htmlContent, isLoading = false) {
537
+ mainContent.classList.remove('visible');
538
+ mainContentWrapper.scrollTop = 0; // Scroll to top on content change
539
 
 
 
540
  setTimeout(() => {
541
+ mainContent.innerHTML = isLoading ? `<div class="loading">${htmlContent}</div>` : htmlContent;
542
+ mainContent.classList.add('visible');
543
+ }, 100); // Short delay for transition effect
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  }
545
+
546
+ function renderList(items, type) {
547
+ if (!items || items.length === 0) {
548
+ setContent(`<div class="empty-state">No ${type.replace('_', ' ')} found. Be the first to add one!</div>`);
549
+ } else {
550
+ const listHtml = items.map(item => `
551
+ <div class="list-item" onclick="showDetailView('${type}', '${item.id}')">
552
+ <div class="list-item-content">
553
+ ${item.image_url ? `<img src="${item.image_url}" alt="Image" class="list-item-image">` : `<div class="list-item-image" style="display:flex;align-items:center;justify-content:center;font-size:24px;color:var(--tg-theme-hint-color);">🖼️</div>`}
554
+ <div class="list-item-text-content">
555
+ <h3>${item.title || item.name || 'Untitled'}</h3>
556
+ ${type === 'vacancies' && item.company_name ? `<p><strong>Company:</strong> ${item.company_name}</p>` : ''}
557
+ ${type === 'freelance_offers' && item.budget ? `<p><strong>Budget:</strong> ${item.budget}</p>` : ''}
558
+ <p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>
 
 
 
 
 
559
  </div>
560
  </div>
561
+ </div>
562
+ `).join('');
563
+ setContent(listHtml);
564
+ }
565
  }
566
 
 
567
  function showDetailView(type, id) {
568
  tg.BackButton.show();
569
+ tg.BackButton.onClick(() => { loadView(currentView); tg.HapticFeedback.impactOccurred('light'); });
570
  tg.MainButton.hide();
571
  document.getElementById('fabButton').style.display = 'none';
572
+ setContent(`Loading details...`, true);
573
 
574
  apiCall(`/api/${type}/${id}`)
575
  .then(item => {
 
580
  }
581
  detailsHtml += `<h2>${item.title || item.name}</h2>`;
582
 
 
 
583
  if (type === 'resumes') {
584
  detailsHtml += `
585
  <p><strong>Skills:</strong> ${item.skills || 'N/A'}</p>
586
+ <p><strong>Experience:</strong><br>${item.experience ? item.experience.replace(/\\n/g, '<br>') : 'N/A'}</p>
587
+ <p><strong>Education:</strong><br>${item.education ? item.education.replace(/\\n/g, '<br>') : 'N/A'}</p>
588
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
589
  ${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>` : ''}
590
  `;
591
  } else if (type === 'vacancies') {
592
  detailsHtml += `
593
  <p><strong>Company:</strong> ${item.company_name || 'N/A'}</p>
594
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n/g, '<br>') : 'N/A'}</p>
595
+ <p><strong>Requirements:</strong><br>${item.requirements ? item.requirements.replace(/\\n/g, '<br>') : 'N/A'}</p>
596
  <p><strong>Salary:</strong> ${item.salary || 'N/A'}</p>
597
  <p><strong>Location:</strong> ${item.location || 'N/A'}</p>
598
+ <p><strong>Contact/Apply:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
599
  `;
600
  } else if (type === 'freelance_offers') {
601
  detailsHtml += `
602
+ <p><strong>Description:</strong><br>${item.description ? item.description.replace(/\\n/g, '<br>') : 'N/A'}</p>
603
  <p><strong>Budget:</strong> ${item.budget || 'N/A'}</p>
604
  <p><strong>Deadline:</strong> ${item.deadline || 'N/A'}</p>
605
  <p><strong>Skills Needed:</strong> ${item.skills_needed || 'N/A'}</p>
606
  <p><strong>Contact:</strong> ${item.contact || `@${item.user_telegram_username}`}</p>
607
  `;
608
  }
609
+ detailsHtml += `<p class="meta">Posted by: @${item.user_telegram_username || 'anonymous'} on ${new Date(item.timestamp).toLocaleDateString()}</p>`;
610
+
611
  if (currentUser && item.user_id === currentUser.id) {
612
+ detailsHtml += `
613
+ <div class="detail-actions">
614
+ <button class="action-button action-button-edit" onclick="showForm('${type}', currentItem)">Edit Post</button>
615
+ <button class="action-button action-button-delete" onclick="handleDeleteItem('${type}', '${item.id}')">Delete Post</button>
616
+ </div>
617
+ `;
618
  }
619
  detailsHtml += `</div>`;
620
+ setContent(detailsHtml);
 
 
 
 
621
  })
622
  .catch(err => {
623
+ setContent(`<div class="empty-state">Error loading details.</div>`);
 
 
 
624
  });
625
  }
626
 
627
+ function showForm(type, itemToEdit = null) {
 
 
 
 
 
 
 
 
 
 
628
  currentItem = itemToEdit;
 
629
  tg.BackButton.show();
630
  tg.BackButton.onClick(() => {
631
  if (itemToEdit) showDetailView(type, itemToEdit.id);
632
+ else loadView(type);
633
  tg.HapticFeedback.impactOccurred('light');
634
  });
635
  document.getElementById('fabButton').style.display = 'none';
 
 
 
636
 
637
+ let formHtml = `<div class="form-container"><h2>${itemToEdit ? 'Edit' : 'New'} ${type.slice(0, -1).replace('_',' ')}</h2>`;
638
  const commonFields = `
639
  <div class="form-group">
640
  <label for="image">Image (Optional)</label>
 
645
 
646
  if (type === 'resumes') {
647
  formHtml += `
648
+ <div class="form-group"> <label for="name">Full Name</label> <input type="text" id="name" value="${itemToEdit?.name || ''}" required> </div>
649
+ <div class="form-group"> <label for="title">Job Title / Desired Position</label> <input type="text" id="title" value="${itemToEdit?.title || ''}" required> </div>
 
 
 
 
 
 
650
  ${commonFields}
651
+ <div class="form-group"> <label for="skills">Skills (comma separated)</label> <textarea id="skills">${itemToEdit?.skills || ''}</textarea> </div>
652
+ <div class="form-group"> <label for="experience">Experience</label> <textarea id="experience">${itemToEdit?.experience || ''}</textarea> </div>
653
+ <div class="form-group"> <label for="education">Education</label> <textarea id="education">${itemToEdit?.education || ''}</textarea> </div>
654
+ <div class="form-group"> <label for="contact">Contact Info (e.g., email, or leave blank to use Telegram)</label> <input type="text" id="contact" value="${itemToEdit?.contact || ''}"> </div>
655
+ <div class="form-group"> <label for="portfolio_link">Portfolio Link (optional)</label> <input type="url" id="portfolio_link" value="${itemToEdit?.portfolio_link || ''}"> </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  `;
657
  } else if (type === 'vacancies') {
658
  formHtml += `
659
+ <div class="form-group"> <label for="company_name">Company Name</label> <input type="text" id="company_name" value="${itemToEdit?.company_name || ''}" required> </div>
660
+ <div class="form-group"> <label for="title">Job Title</label> <input type="text" id="title" value="${itemToEdit?.title || ''}" required> </div>
 
 
 
 
 
 
661
  ${commonFields}
662
+ <div class="form-group"> <label for="description">Description</label> <textarea id="description">${itemToEdit?.description || ''}</textarea> </div>
663
+ <div class="form-group"> <label for="requirements">Requirements</label> <textarea id="requirements">${itemToEdit?.requirements || ''}</textarea> </div>
664
+ <div class="form-group"> <label for="salary">Salary/Compensation</label> <input type="text" id="salary" value="${itemToEdit?.salary || ''}"> </div>
665
+ <div class="form-group"> <label for="location">Location (e.g., Remote, City)</label> <input type="text" id="location" value="${itemToEdit?.location || ''}"> </div>
666
+ <div class="form-group"> <label for="contact">Contact Info / How to Apply</label> <textarea id="contact">${itemToEdit?.contact || ''}</textarea> </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  `;
668
  } else if (type === 'freelance_offers') {
669
  formHtml += `
670
+ <div class="form-group"> <label for="title">Project Title</label> <input type="text" id="title" value="${itemToEdit?.title || ''}" required> </div>
 
 
 
671
  ${commonFields}
672
+ <div class="form-group"> <label for="description">Description of Work</label> <textarea id="description">${itemToEdit?.description || ''}</textarea> </div>
673
+ <div class="form-group"> <label for="budget">Budget</label> <input type="text" id="budget" value="${itemToEdit?.budget || ''}"> </div>
674
+ <div class="form-group"> <label for="deadline">Expected Deadline</label> <input type="text" id="deadline" value="${itemToEdit?.deadline || ''}"> </div>
675
+ <div class="form-group"> <label for="skills_needed">Skills Needed (comma separated)</label> <textarea id="skills_needed">${itemToEdit?.skills_needed || ''}</textarea> </div>
676
+ <div class="form-group"> <label for="contact">Contact Info (or leave blank to use Telegram)</label> <input type="text" id="contact" value="${itemToEdit?.contact || ''}"> </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  `;
678
  }
679
  formHtml += `<div id="formError" class="error-message"></div></div>`;
680
+ setContent(formHtml);
 
 
 
 
681
 
682
  tg.MainButton.setText(itemToEdit ? 'Save Changes' : 'Post');
683
  tg.MainButton.show();
 
693
  const name = document.getElementById('name').value.trim();
694
  const title = document.getElementById('title').value.trim();
695
  if (!name || !title) isValid = false;
696
+ formData.append('name', name); formData.append('title', title);
 
697
  formData.append('skills', document.getElementById('skills').value.trim());
698
  formData.append('experience', document.getElementById('experience').value.trim());
699
  formData.append('education', document.getElementById('education').value.trim());
 
703
  const company_name = document.getElementById('company_name').value.trim();
704
  const title = document.getElementById('title').value.trim();
705
  if (!company_name || !title) isValid = false;
706
+ formData.append('company_name', company_name); formData.append('title', title);
 
707
  formData.append('description', document.getElementById('description').value.trim());
708
  formData.append('requirements', document.getElementById('requirements').value.trim());
709
  formData.append('salary', document.getElementById('salary').value.trim());
 
721
  }
722
 
723
  const imageInput = document.getElementById('image');
724
+ if (imageInput && imageInput.files[0]) { formData.append('image', imageInput.files[0]); }
 
 
725
 
726
  if (!isValid) {
727
+ document.getElementById('formError').textContent = 'Please fill in all required fields.';
728
  tg.HapticFeedback.notificationOccurred('error');
729
  return;
730
  }
731
 
732
  tg.MainButton.showProgress();
 
733
  const method = itemId ? 'PUT' : 'POST';
734
  const endpoint = itemId ? `/api/${type}/${itemId}` : `/api/${type}`;
735
 
 
742
  .catch(err => {
743
  tg.HapticFeedback.notificationOccurred('error');
744
  tg.MainButton.hideProgress();
745
+ document.getElementById('formError').textContent = err.message || 'Failed to submit.';
746
  });
747
  }
748
+
749
+ function handleDeleteItem(itemType, itemId) {
750
+ tg.showConfirm("Are you sure you want to delete this item? This action cannot be undone.", (confirmed) => {
751
  if (confirmed) {
752
+ tg.showPopup({
753
+ title: "Deleting...",
754
+ message: "Please wait while the item is being deleted.",
755
+ buttons: []
756
+ });
757
+ apiCall(`/api/${itemType}/${itemId}`, 'DELETE')
758
  .then(() => {
759
  tg.HapticFeedback.notificationOccurred('success');
760
+ tg.closePopup();
761
+ tg.showAlert("Item deleted successfully!");
762
+ if (currentView === 'profile') {
763
+ loadMyProfileView(); // Refresh profile view
764
  } else {
765
+ loadView(itemType); // Refresh the list view if deleting from detail
766
  }
767
  })
768
  .catch(err => {
769
  tg.HapticFeedback.notificationOccurred('error');
770
+ tg.closePopup();
771
+ tg.showAlert(err.message || "Failed to delete item.");
772
  });
773
  }
774
  });
775
  }
776
+
777
+ function loadMyProfileView() {
778
+ setContent("Loading your profile...", true);
779
+ tg.BackButton.hide();
780
+ tg.MainButton.hide();
781
+ document.getElementById('fabButton').style.display = 'none'; // Hide FAB on profile
782
+
783
+ if (!currentUser) {
784
+ setContent('<div class="empty-state">Please log in to see your profile. Authentication might have failed. Try reloading.</div>');
785
+ return;
786
+ }
787
+
788
+ apiCall('/api/my_items')
789
+ .then(data => {
790
+ let profileHtml = '<h2>My Publications</h2>';
791
+ let itemCount = 0;
792
+ ['resumes', 'vacancies', 'freelance_offers'].forEach(type => {
793
+ if (data[type] && data[type].length > 0) {
794
+ profileHtml += `<h3>My ${type.replace('_', ' ')}</h3>`;
795
+ data[type].forEach(item => {
796
+ itemCount++;
797
+ profileHtml += `
798
+ <div class="profile-item">
799
+ <h4>${item.title || item.name}</h4>
800
+ <p>Posted: ${new Date(item.timestamp).toLocaleDateString()}</p>
801
+ <div class="profile-item-actions">
802
+ <button class="profile-button-view" onclick="showDetailView('${type}', '${item.id}')">View</button>
803
+ <button class="profile-button-delete" onclick="handleDeleteItem('${type}', '${item.id}')">Delete</button>
804
+ </div>
805
+ </div>
806
+ `;
807
+ });
808
+ }
809
+ });
810
+ if (itemCount === 0) {
811
+ profileHtml += '<div class="empty-state">You have not posted anything yet.</div>';
812
+ }
813
+ setContent(profileHtml);
814
+ })
815
+ .catch(err => {
816
+ setContent('<div class="empty-state">Error loading your profile.</div>');
817
+ });
818
+ }
819
+
820
 
821
  function loadView(tabName) {
822
  currentView = tabName;
823
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
824
  document.querySelector(`.tab-button[data-tab="${tabName}"]`).classList.add('active');
825
 
 
 
 
826
  tg.BackButton.hide();
827
  tg.MainButton.hide();
828
 
829
  if (tabName === 'profile') {
830
  document.getElementById('fabButton').style.display = 'none';
831
+ loadMyProfileView();
 
 
 
 
 
 
 
832
  } else {
833
  document.getElementById('fabButton').style.display = 'flex';
834
+ setContent(`Loading ${tabName}...`, true);
835
  apiCall(`/api/${tabName}`)
836
  .then(data => renderList(data, tabName))
837
+ .catch(err => setContent(`<div class="empty-state">Error loading ${tabName}.</div>`));
 
 
 
 
 
838
  }
839
  }
840
 
841
  function updateUserDisplay(user) {
842
  const userAvatar = document.getElementById('userAvatar');
843
  const userText = document.getElementById('userText');
 
 
 
 
 
 
844
  if (user && user.photo_url) {
845
+ userAvatar.src = user.photo_url;
846
+ userAvatar.style.display = 'inline-block';
847
  } else if (tg.initDataUnsafe.user?.photo_url) {
848
+ userAvatar.src = tg.initDataUnsafe.user.photo_url;
849
+ userAvatar.style.display = 'inline-block';
850
+ } else {
851
+ userAvatar.style.display = 'none';
852
  }
853
 
854
  if (user) {
855
+ userText.textContent = `Logged in as: ${user.first_name} (@${user.username})`;
 
856
  } else if (tg.initDataUnsafe.user) {
857
+ userText.textContent = `Welcome, ${tg.initDataUnsafe.user?.first_name || 'User'}! (@${tg.initDataUnsafe.user?.username || 'anonymous'})`;
858
+ } else {
859
+ userText.textContent = `User not identified.`;
860
  }
 
 
 
 
861
  }
862
 
863
  async function init() {
 
874
  currentUser = authResponse.user;
875
  if (currentUser) {
876
  updateUserDisplay(currentUser);
877
+ } else {
878
+ document.getElementById('userText').textContent = `Auth failed. Limited functionality.`;
879
+ tg.showAlert("Authentication with the server failed. Some features might not work correctly.");
880
  }
881
  } catch (error) {
882
  console.error("Auth error:", error);
883
+ document.getElementById('userText').textContent = `Auth failed. Limited functionality.`;
884
+ tg.showAlert("Authentication with the server failed. Some features might not work correctly.");
885
  }
886
 
887
  document.querySelectorAll('.tab-button').forEach(button => {
 
891
  });
892
  });
893
  document.getElementById('fabButton').addEventListener('click', () => {
894
+ if (currentView === 'profile') return; // Should not happen as FAB is hidden
895
  showForm(currentView);
896
  tg.HapticFeedback.impactOccurred('medium');
897
  });
 
905
  let touchendX = 0;
906
  const swipeThreshold = 75;
907
 
908
+ mainContentWrapper.addEventListener('touchstart', e => {
909
+ // Only listen for swipes if not scrolling vertically
910
+ if (mainContentWrapper.scrollHeight <= mainContentWrapper.clientHeight) {
911
  touchstartX = e.changedTouches[0].screenX;
912
+ } else {
913
+ touchstartX = 0; // Disable swipe if content is scrollable
914
  }
915
  }, {passive: true});
916
 
917
+ mainContentWrapper.addEventListener('touchend', e => {
918
+ if (touchstartX === 0) return; // Swipe disabled
919
+ touchendX = e.changedTouches[0].screenX;
920
+ handleSwipe();
 
921
  });
922
 
923
  function handleSwipe() {
 
 
 
 
924
  const currentTabIndex = tabOrder.indexOf(currentView);
 
 
925
  if (touchendX < touchstartX - swipeThreshold) {
926
  if (currentTabIndex < tabOrder.length - 1) {
927
+ loadView(tabOrder[currentTabIndex + 1]);
928
+ tg.HapticFeedback.impactOccurred('light');
929
  }
930
  }
931
  if (touchendX > touchstartX + swipeThreshold) {
932
  if (currentTabIndex > 0) {
933
+ loadView(tabOrder[currentTabIndex - 1]);
 
 
 
 
 
 
 
 
 
934
  tg.HapticFeedback.impactOccurred('light');
935
+ }
 
 
 
 
 
 
936
  }
937
  }
938
  }
 
1063
 
1064
  @app.route('/uploads/<item_type_subdir>/<filename>')
1065
  def uploaded_file_typed(item_type_subdir, filename):
1066
+ if item_type_subdir not in ['resumes', 'vacancies', 'freelance_offers']:
1067
  return jsonify({"error": "Invalid category"}), 404
1068
 
1069
  directory = os.path.join(app.config['UPLOADS_DIR'], item_type_subdir)
 
1103
  'first_seen': datetime.now().isoformat()
1104
  }
1105
  users[user_id_str]['last_seen'] = datetime.now().isoformat()
1106
+ if user_data_dict.get('photo_url'):
1107
  users[user_id_str]['photo_url'] = user_data_dict.get('photo_url')
 
 
 
1108
  data['users'] = users
1109
  save_data(data)
1110
 
 
1127
  items = sorted(data.get(item_type, []), key=lambda x: x.get('timestamp', ''), reverse=True)
1128
  return jsonify(items), 200
1129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1130
  @app.route('/api/<item_type>/<item_id>', methods=['GET'])
1131
  def get_item(item_type, item_id):
1132
  if item_type not in ['resumes', 'vacancies', 'freelance_offers']:
 
1137
  return jsonify(item), 200
1138
  return jsonify({"error": "Item not found"}), 404
1139
 
1140
+ @app.route('/api/my_items', methods=['GET'])
1141
+ def get_my_items():
1142
+ user = get_authenticated_user(request.headers)
1143
+ if not user:
1144
+ return jsonify({"error": "Authentication required"}), 401
1145
+
1146
+ user_id = str(user.get('id'))
1147
+ data = load_data()
1148
+
1149
+ my_items = {
1150
+ "resumes": [],
1151
+ "vacancies": [],
1152
+ "freelance_offers": []
1153
+ }
1154
+
1155
+ for item_type in ['resumes', 'vacancies', 'freelance_offers']:
1156
+ items_list = data.get(item_type, [])
1157
+ user_specific_items = [item for item in items_list if str(item.get('user_id')) == user_id]
1158
+ my_items[item_type] = sorted(user_specific_items, key=lambda x: x.get('timestamp', ''), reverse=True)
1159
+
1160
+ return jsonify(my_items), 200
1161
+
1162
  @app.route('/api/<item_type>', methods=['POST'])
1163
  def create_item(item_type):
1164
  user = get_authenticated_user(request.headers)
 
1184
  image_file = request.files.get('image')
1185
  if image_file and allowed_file(image_file.filename):
1186
  try:
1187
+ file_ext = os.path.splitext(image_file.filename)[1].lower()
1188
  image_filename = f"{new_item_id}{file_ext}"
1189
  image_save_path = os.path.join(app.config['UPLOADS_DIR'], item_type, image_filename)
 
1190
  image_file.save(image_save_path)
1191
+ new_item['image_url'] = f"/{UPLOADS_DIR_NAME}/{item_type}/{image_filename}"
1192
  except Exception as e:
1193
  logging.error(f"Error saving image for new item: {e}")
1194
 
 
1260
  if image_file and allowed_file(image_file.filename):
1261
  delete_existing_image(original_item.get('image_url'))
1262
  try:
1263
+ file_ext = os.path.splitext(image_file.filename)[1].lower()
1264
+ image_filename = f"{item_id}{file_ext}"
1265
  image_save_path = os.path.join(app.config['UPLOADS_DIR'], item_type, image_filename)
 
1266
  image_file.save(image_save_path)
1267
+ updated_item['image_url'] = f"/{UPLOADS_DIR_NAME}/{item_type}/{image_filename}"
1268
  except Exception as e:
1269
  logging.error(f"Error saving image for updated item {item_id}: {e}")
1270
 
 
1326
 
1327
  if len(data[item_type]) < original_length:
1328
  save_data(data)
1329
+ return jsonify({"message": "Item deleted successfully"}), 200
1330
  return jsonify({"error": "Item not found or deletion failed"}), 404
1331
 
1332
 
 
1408
 
1409
  port = int(os.environ.get('PORT', 7860))
1410
  logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
1411
+ app.run(debug=False, host='0.0.0.0', port=port)