templates/seller/apply.html.twig line 1

Open in your IDE?
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Become a Seller β€” Julico</title>
  7. <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.css"/>
  9. <link rel="stylesheet" href="/css/julico-home.css">
  10. <style>
  11. *, *::before, *::after { box-sizing:border-box; }
  12. body { font-family:'Plus Jakarta Sans',-apple-system,sans-serif; background:var(--gray-50); margin:0; min-height:100vh; }
  13. .s-nav { background:var(--white); border-bottom:1px solid var(--gray-200); padding:0 24px; height:64px; display:flex; align-items:center; justify-content:space-between; }
  14. .s-link { font-size:14px; font-weight:700; color:var(--brand); text-decoration:none; }
  15. .s-wrap { max-width:680px; margin:0 auto; padding:32px 18px 60px; }
  16. .s-hero { text-align:center; margin-bottom:26px; }
  17. .s-hero h1 { font-size:28px; font-weight:800; color:var(--gray-900); margin:0 0 8px; letter-spacing:-0.02em; }
  18. .s-hero p { font-size:14px; color:var(--gray-500); margin:0; line-height:1.6; }
  19. .s-card { background:var(--white); border:1.5px solid var(--gray-100); border-radius:var(--rlg); padding:26px; box-shadow:var(--shadow-sm); }
  20. .s-row { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
  21. .fld { margin-bottom:15px; }
  22. .fld label { display:block; font-size:12px; font-weight:800; color:var(--gray-700); margin-bottom:6px; text-transform:uppercase; letter-spacing:.03em; }
  23. .fld .opt { color:var(--gray-400); font-weight:600; text-transform:none; letter-spacing:0; }
  24. .fld input, .fld textarea, .fld select { width:100%; font-family:inherit; font-size:14px; padding:11px 13px; border:1.5px solid var(--gray-200); border-radius:var(--rsm); color:var(--gray-900); background:#fff; }
  25. .fld input:focus, .fld textarea:focus, .fld select:focus { outline:none; border-color:var(--brand); }
  26. .fld textarea { min-height:96px; resize:vertical; }
  27. .fld input[type=file] { padding:9px 11px; background:var(--gray-50); cursor:pointer; }
  28. .fld-hint { font-size:11px; color:var(--gray-400); margin-top:4px; line-height:1.5; }
  29. .logo-preview { margin-top:8px; }
  30. .logo-preview img { width:64px; height:64px; border-radius:12px; object-fit:cover; border:1.5px solid var(--gray-200); display:none; }
  31. .req { color:#dc2626; }
  32. .sec-label { font-size:12px; font-weight:800; color:var(--brand-dark); text-transform:uppercase; letter-spacing:.04em; margin:22px 0 10px; padding-top:18px; border-top:1.5px solid var(--gray-100); }
  33. .loc-search-row { display:flex; gap:8px; margin-bottom:10px; }
  34. .loc-search-wrap { position:relative; flex:1; }
  35. .loc-search-wrap input { width:100%; font-family:inherit; font-size:14px; padding:11px 13px; border:1.5px solid var(--gray-200); border-radius:var(--rsm); color:var(--gray-900); }
  36. .loc-search-wrap input:focus { outline:none; border-color:var(--brand); }
  37. .loc-suggest { position:absolute; top:calc(100% + 4px); left:0; right:0; background:#fff; border:1.5px solid var(--gray-200); border-radius:var(--rsm); box-shadow:0 8px 24px rgba(0,0,0,0.12); z-index:1000; max-height:240px; overflow-y:auto; display:none; }
  38. .loc-suggest.open { display:block; }
  39. .loc-suggest-item { padding:10px 12px; font-size:13px; color:var(--gray-800); cursor:pointer; border-bottom:1px solid var(--gray-100); line-height:1.4; }
  40. .loc-suggest-item:last-child { border-bottom:none; }
  41. .loc-suggest-item:hover, .loc-suggest-item.active { background:var(--brand-light); }
  42. .loc-search-btn { position:static !important; top:auto !important; right:auto !important; left:auto !important; bottom:auto !important; font-size:13px; font-weight:700; color:#fff; background:var(--brand); border:none; border-radius:var(--rsm); padding:0 16px; cursor:pointer; white-space:nowrap; }
  43. .loc-search-btn:hover { background:var(--brand-dark); }
  44. .locate-btn { font-size:13px; font-weight:700; color:var(--brand-dark); background:var(--brand-light); border:1.5px solid var(--brand-mid); border-radius:var(--rsm); padding:9px 14px; cursor:pointer; margin-bottom:10px; }
  45. .locate-btn:hover { background:var(--brand); color:#fff; }
  46. #map { height:240px; border-radius:var(--rsm); border:1.5px solid var(--gray-200); margin-bottom:8px; }
  47. .map-status { font-size:12px; font-weight:600; min-height:18px; margin-bottom:10px; }
  48. .map-status.loading { color:#92400e; }
  49. .map-status.success { color:#047857; }
  50. .map-status.error { color:#b91c1c; }
  51. .map-filled { border-color:var(--brand) !important; background:#f0fdfa; }
  52. .map-hint { font-size:11px; color:var(--gray-400); margin-bottom:10px; }
  53. .agree-fld { margin:20px 0 6px; padding-top:16px; border-top:1.5px solid var(--gray-100); }
  54. .agree-check { display:flex; align-items:flex-start; gap:10px; font-size:13px; font-weight:600; color:var(--gray-700); line-height:1.55; }
  55. .agree-check input { width:18px; height:18px; margin-top:1px; flex-shrink:0; accent-color:var(--brand); cursor:pointer; }
  56. .agree-check a { color:var(--brand); font-weight:700; text-decoration:underline; cursor:pointer; }
  57. .s-btn { width:100%; font-size:15px; font-weight:800; color:#fff; background:var(--brand); border:none; border-radius:var(--rsm); padding:14px; cursor:pointer; margin-top:6px; }
  58. .s-btn:hover { background:var(--brand-dark); }
  59. .s-errors { background:#fef2f2; border:1.5px solid #fecaca; color:#b91c1c; border-radius:var(--rsm); padding:12px 14px; font-size:13px; font-weight:600; margin-bottom:14px; }
  60. .s-errors ul { margin:6px 0 0; padding-left:18px; }
  61. .s-suggest { background:#f0fdfa; border:1.5px solid var(--brand-mid); border-radius:var(--rsm); padding:12px 14px; margin-bottom:18px; }
  62. .s-suggest-h { font-size:12px; font-weight:800; color:var(--brand-dark); margin-bottom:8px; }
  63. .s-suggest-chips { display:flex; flex-wrap:wrap; gap:8px; }
  64. .s-chip { font-family:inherit; font-size:13px; font-weight:700; color:var(--brand-dark); background:#fff; border:1.5px solid var(--brand-mid); border-radius:999px; padding:7px 13px; cursor:pointer; }
  65. .s-chip:hover { background:var(--brand); color:#fff; border-color:var(--brand); }
  66. .s-ok { background:var(--white); border:1.5px solid var(--gray-100); border-radius:var(--rlg); padding:40px 26px; box-shadow:var(--shadow-sm); text-align:center; }
  67. .s-ok .ic { font-size:46px; }
  68. .s-ok h2 { font-size:22px; font-weight:800; color:var(--gray-900); margin:14px 0 8px; }
  69. .s-ok p { color:var(--gray-500); font-size:14px; line-height:1.6; margin:0 0 20px; }
  70. .legal-modal { display:none; position:fixed; inset:0; z-index:3000; align-items:center; justify-content:center; padding:3vh 3vw; }
  71. .legal-modal.open { display:flex; }
  72. .legal-modal-backdrop { position:absolute; inset:0; background:rgba(15,23,42,0.55); }
  73. .legal-modal-card { position:relative; width:min(780px,96vw); height:92vh; background:#fff; border-radius:14px; overflow:hidden; display:flex; flex-direction:column; box-shadow:0 24px 70px rgba(0,0,0,0.35); }
  74. .legal-modal-bar { display:flex; align-items:center; justify-content:space-between; padding:12px 16px; border-bottom:1.5px solid var(--gray-100); flex-shrink:0; }
  75. .legal-modal-title { font-size:14px; font-weight:800; color:var(--gray-900); }
  76. .legal-modal-close { font-size:15px; font-weight:800; color:var(--gray-500); background:var(--gray-50); border:1.5px solid var(--gray-200); border-radius:var(--rsm); width:34px; height:34px; cursor:pointer; }
  77. .legal-modal-close:hover { background:var(--gray-100); color:var(--gray-800); }
  78. .legal-modal-frame { flex:1; width:100%; border:0; }
  79. @media (max-width:560px){ .s-row{ grid-template-columns:1fr; } }
  80. </style>
  81. </head>
  82. <body>
  83. <nav class="s-nav">
  84.   <a href="{{ path('app_home') }}"><img src="/images/julico-logo.png" alt="Julico" style="height:42px;object-fit:contain"></a>
  85.   <a href="{{ path('app_home') }}" class="s-link">← Back to store</a>
  86. </nav>
  87. <div class="s-wrap">
  88.   {% if sent %}
  89.     <div class="s-ok">
  90.       <div class="ic">πŸŽ‰</div>
  91.       <h2>Application received!</h2>
  92.       <p>Thanks for your interest in selling on Julico. Our team will review your application and get in touch. Approved sellers get their own shop on a flat monthly plan β€” no commission, and you handle your own delivery.</p>
  93.       <a href="{{ path('app_home') }}" class="s-link">← Back to store</a>
  94.     </div>
  95.   {% else %}
  96.     <div class="s-hero">
  97.       <h1>Sell on Julico</h1>
  98.       <p>Reach wholesale buyers across Lebanon. Flat monthly subscription, zero commission β€” you keep 100% of every sale.</p>
  99.     </div>
  100.     <div class="s-card">
  101.       {% if errors|length > 0 %}
  102.         <div class="s-errors">Please fix the following:<ul>{% for e in errors %}<li>{{ e }}</li>{% endfor %}</ul></div>
  103.       {% endif %}
  104.       {% if suggestions is defined and suggestions|length > 0 %}
  105.         <div class="s-suggest">
  106.           <div class="s-suggest-h">✨ These names are available β€” tap to use one:</div>
  107.           <div class="s-suggest-chips">
  108.             {% for name in suggestions %}
  109.               <button type="button" class="s-chip" data-name="{{ name }}">{{ name }}</button>
  110.             {% endfor %}
  111.           </div>
  112.         </div>
  113.       {% endif %}
  114.       <form method="post" action="{{ path('seller_apply') }}" enctype="multipart/form-data">
  115.         <div class="fld">
  116.           <label>Business / shop name <span class="req">*</span></label>
  117.           <input type="text" id="field-business" name="business_name" value="{{ old.business_name ?? '' }}" required>
  118.         </div>
  119.         <div class="fld">
  120.           <label>Shop logo <span class="opt">(optional)</span></label>
  121.           <input type="file" name="logo" id="logo-input" accept="image/png,image/jpeg,image/webp">
  122.           <div class="logo-preview"><img id="logo-preview-img" alt="Logo preview"></div>
  123.           <div class="fld-hint">A square image works best β€” it becomes your shop's icon across Julico. PNG, JPG or WEBP, up to 4&nbsp;MB. You can also add or change it later from your dashboard.</div>
  124.         </div>
  125.         <div class="s-row">
  126.           <div class="fld">
  127.             <label>Your name <span class="req">*</span></label>
  128.             <input type="text" name="contact_name" value="{{ old.contact_name ?? prefill.contact_name }}" required>
  129.           </div>
  130.           <div class="fld">
  131.             <label>Phone <span class="req">*</span></label>
  132.             <input type="text" name="phone" value="{{ old.phone ?? '' }}" required>
  133.           </div>
  134.         </div>
  135.         <div class="s-row">
  136.           <div class="fld">
  137.             <label>Email <span class="opt">(optional)</span></label>
  138.             <input type="email" name="email" value="{{ old.email ?? prefill.email }}">
  139.             <div class="fld-hint">No email? Leave it blank β€” we'll set up your login when your shop is approved.</div>
  140.           </div>
  141.           <div class="fld">
  142.             <label>What do you sell?</label>
  143.             <select name="category">
  144.               <option value="">β€” Select a category β€”</option>
  145.               {% for c in categories ?? [] %}
  146.                 <option value="{{ c.nom }}"{{ (old.category ?? '') == c.nom ? ' selected' }}>{{ c.nom }}</option>
  147.               {% endfor %}
  148.             </select>
  149.           </div>
  150.         </div>
  151.         <div class="sec-label">πŸ“ Your location</div>
  152.         <div class="loc-search-row">
  153.           <div class="loc-search-wrap">
  154.             <input type="text" id="loc-search" autocomplete="off" placeholder="Search a place β€” e.g. Tripoli, Lebanon">
  155.             <div id="loc-suggest" class="loc-suggest"></div>
  156.           </div>
  157.           <button type="button" id="search-btn" class="loc-search-btn">πŸ” Search</button>
  158.         </div>
  159.         <button type="button" id="locate-btn" class="locate-btn">πŸ“‘ Use my location</button>
  160.         <div id="map"></div>
  161.         <div id="map-status" class="map-status"></div>
  162.         <div class="map-hint">Start typing a place and pick a suggestion, use your location, or drop a pin on the map. Country, region and city fill in automatically β€” you can edit them after.</div>
  163.         <div class="fld">
  164.           <label>Country</label>
  165.           <input type="text" id="field-country" name="country" value="{{ old.country ?? '' }}">
  166.         </div>
  167.         <div class="s-row">
  168.           <div class="fld">
  169.             <label>Region</label>
  170.             <input type="text" id="field-region" name="region" value="{{ old.region ?? '' }}" placeholder="e.g. Beirut, South">
  171.           </div>
  172.           <div class="fld">
  173.             <label>City</label>
  174.             <input type="text" id="field-city" name="city" value="{{ old.city ?? '' }}">
  175.           </div>
  176.         </div>
  177.         <input type="hidden" id="field-lat" name="latitude" value="{{ old.latitude ?? '' }}">
  178.         <input type="hidden" id="field-lng" name="longitude" value="{{ old.longitude ?? '' }}">
  179.         <div class="fld" style="margin-top:6px">
  180.           <label>Tell us about your business</label>
  181.           <textarea name="description" placeholder="Product range, number of SKUs, current sales channels β€” anything that helps us review.">{{ old.description ?? '' }}</textarea>
  182.         </div>
  183.         <div class="sec-label">πŸͺͺ Identity verification</div>
  184.         <div class="fld">
  185.           <label>Owner's government ID <span class="req">*</span></label>
  186.           <input type="file" name="id_document" accept=".jpg,.jpeg,.png,.webp,.pdf,image/*,application/pdf" required>
  187.           <div class="fld-hint">A clear photo or scan of the owner's valid government-issued ID β€” National ID, Iqama, or Passport. JPG, PNG, WEBP or PDF, up to 8&nbsp;MB. By uploading your ID you confirm it is genuine and belongs to you, and you reconfirm your agreement to Julico's Seller Agreement and Privacy Policy.</div>
  188.         </div>
  189.         <div class="agree-fld">
  190.           <div class="agree-check">
  191.             <input type="checkbox" name="agree" value="1"{{ (old.agree ?? '') ? ' checked' }} required>
  192.             <span>I have read and agree to the
  193.               <a href="{{ path('legal_seller_agreement') }}" class="legal-open" data-title="Seller Agreement">Seller Agreement</a>
  194.               and the
  195.               <a href="{{ path('legal_privacy') }}" class="legal-open" data-title="Privacy Policy">Privacy Policy</a>.</span>
  196.           </div>
  197.         </div>
  198.         <button type="submit" class="s-btn">Submit application</button>
  199.       </form>
  200.     </div>
  201.   {% endif %}
  202. </div>
  203. <div id="legal-modal" class="legal-modal" aria-hidden="true">
  204.   <div class="legal-modal-backdrop" data-close></div>
  205.   <div class="legal-modal-card">
  206.     <div class="legal-modal-bar">
  207.       <span class="legal-modal-title">Document</span>
  208.       <button type="button" class="legal-modal-close" data-close aria-label="Close">βœ•</button>
  209.     </div>
  210.     <iframe class="legal-modal-frame" title="Document"></iframe>
  211.   </div>
  212. </div>
  213. <script>
  214. document.addEventListener('DOMContentLoaded',function(){
  215.   var biz=document.getElementById('field-business');
  216.   document.querySelectorAll('.s-chip').forEach(function(ch){
  217.     ch.addEventListener('click',function(){
  218.       if(biz){ biz.value=ch.getAttribute('data-name'); biz.focus(); window.scrollTo({top:0,behavior:'smooth'}); }
  219.     });
  220.   });
  221.   // Live logo preview before upload
  222.   var logoInput = document.getElementById('logo-input');
  223.   var logoImg   = document.getElementById('logo-preview-img');
  224.   if (logoInput && logoImg) {
  225.     logoInput.addEventListener('change', function(){
  226.       var f = this.files && this.files[0];
  227.       if (f) { logoImg.src = URL.createObjectURL(f); logoImg.style.display = 'block'; }
  228.       else { logoImg.style.display = 'none'; }
  229.     });
  230.   }
  231.   // Open legal documents in a pop-up over the form, so nothing typed is lost.
  232.   var modal = document.getElementById('legal-modal');
  233.   var frame = modal.querySelector('.legal-modal-frame');
  234.   var titleEl = modal.querySelector('.legal-modal-title');
  235.   frame.addEventListener('load', function(){
  236.     try {
  237.       var d = frame.contentDocument;
  238.       if (d) {
  239.         var nav = d.querySelector('.l-nav, .s-nav');
  240.         if (nav) nav.style.display = 'none';
  241.       }
  242.     } catch(e){}
  243.   });
  244.   function openDoc(src, title){
  245.     titleEl.textContent = title || 'Document';
  246.     frame.src = src;
  247.     modal.classList.add('open');
  248.     modal.setAttribute('aria-hidden','false');
  249.     document.body.style.overflow = 'hidden';
  250.   }
  251.   function closeDoc(){
  252.     modal.classList.remove('open');
  253.     modal.setAttribute('aria-hidden','true');
  254.     document.body.style.overflow = '';
  255.     setTimeout(function(){ frame.src = 'about:blank'; }, 200);
  256.   }
  257.   document.querySelectorAll('.legal-open').forEach(function(a){
  258.     a.addEventListener('click', function(e){
  259.       e.preventDefault();
  260.       openDoc(a.getAttribute('href'), a.getAttribute('data-title'));
  261.     });
  262.   });
  263.   modal.querySelectorAll('[data-close]').forEach(function(el){
  264.     el.addEventListener('click', closeDoc);
  265.   });
  266.   document.addEventListener('keydown', function(e){ if(e.key==='Escape' && modal.classList.contains('open')) closeDoc(); });
  267. });
  268. </script>
  269. <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.min.js"></script>
  270. <script>
  271. (function(){
  272.   var DEFAULT=[33.8938,35.5018];
  273.   var map, marker;
  274.   var suggestTimer=null, suggestXhr=null, suggestions=[];
  275.   function makeIcon(){
  276.     var c=document.createElement('canvas');
  277.     c.width=30; c.height=38;
  278.     var ctx=c.getContext('2d');
  279.     ctx.fillStyle='#00a7b5';
  280.     ctx.beginPath(); ctx.arc(15,15,13,0,Math.PI*2); ctx.fill();
  281.     ctx.strokeStyle='white'; ctx.lineWidth=3; ctx.stroke();
  282.     ctx.beginPath(); ctx.moveTo(9,24); ctx.lineTo(15,38); ctx.lineTo(21,24); ctx.fill();
  283.     return L.icon({iconUrl:c.toDataURL(),iconSize:[30,38],iconAnchor:[15,38]});
  284.   }
  285.   function setLatLng(lat,lng){
  286.     var la=document.getElementById('field-lat'), lo=document.getElementById('field-lng');
  287.     if(la) la.value=Number(lat).toFixed(7);
  288.     if(lo) lo.value=Number(lng).toFixed(7);
  289.   }
  290.   function setField(id,val){
  291.     var el=document.getElementById(id);
  292.     if(el&&val){ el.value=val; el.classList.add('map-filled'); }
  293.   }
  294.   function fillAddress(a){
  295.     if(!a) return;
  296.     setField('field-country', a.country||'');
  297.     setField('field-region', a.state||a.region||a.county||a.state_district||'');
  298.     setField('field-city', a.city||a.town||a.village||a.municipality||a.suburb||'');
  299.   }
  300.   function showStatus(type,msg){
  301.     var el=document.getElementById('map-status');
  302.     if(el){ el.className='map-status '+type; el.textContent=msg; }
  303.   }
  304.   function geocode(lat,lng){
  305.     showStatus('loading','πŸ” Looking up address…');
  306.     var xhr=new XMLHttpRequest();
  307.     xhr.open('GET','https://nominatim.openstreetmap.org/reverse?lat='+lat+'&lon='+lng+'&format=json&addressdetails=1');
  308.     xhr.setRequestHeader('Accept-Language','en');
  309.     xhr.onload=function(){
  310.       if(xhr.status!==200){showStatus('error','❌ Could not find address');return;}
  311.       var d=JSON.parse(xhr.responseText);
  312.       if(!d||!d.address){showStatus('error','❌ No address found');return;}
  313.       fillAddress(d.address);
  314.       showStatus('success','βœ… Location set β€” review the fields below.');
  315.     };
  316.     xhr.onerror=function(){showStatus('error','❌ Network error');};
  317.     xhr.send();
  318.   }
  319.   function hideSuggest(){
  320.     var d=document.getElementById('loc-suggest');
  321.     if(d){ d.classList.remove('open'); d.innerHTML=''; }
  322.     suggestions=[];
  323.   }
  324.   function chooseSuggestion(s){
  325.     var lat=parseFloat(s.lat), lng=parseFloat(s.lon);
  326.     map.setView([lat,lng],14);
  327.     marker.setLatLng([lat,lng]);
  328.     setLatLng(lat,lng);
  329.     if(s.address){ fillAddress(s.address); }
  330.     else { geocode(lat,lng); }
  331.     var si=document.getElementById('loc-search');
  332.     if(si){ si.value=s.display_name||''; }
  333.     hideSuggest();
  334.     showStatus('success','βœ… Location set β€” review the fields below.');
  335.   }
  336.   function renderSuggest(list){
  337.     var d=document.getElementById('loc-suggest');
  338.     if(!d) return;
  339.     suggestions=list||[];
  340.     if(!suggestions.length){ hideSuggest(); return; }
  341.     d.innerHTML='';
  342.     suggestions.forEach(function(s){
  343.       var it=document.createElement('div');
  344.       it.className='loc-suggest-item';
  345.       it.textContent=s.display_name;
  346.       it.addEventListener('mousedown',function(e){ e.preventDefault(); chooseSuggestion(s); });
  347.       d.appendChild(it);
  348.     });
  349.     d.classList.add('open');
  350.   }
  351.   function fetchSuggest(q){
  352.     if(suggestXhr){ try{ suggestXhr.abort(); }catch(e){} }
  353.     suggestXhr=new XMLHttpRequest();
  354.     suggestXhr.open('GET','https://nominatim.openstreetmap.org/search?q='+encodeURIComponent(q)+'&format=json&addressdetails=1&limit=5');
  355.     suggestXhr.setRequestHeader('Accept-Language','en');
  356.     suggestXhr.onload=function(){
  357.       if(suggestXhr.status!==200){ return; }
  358.       var r;
  359.       try { r=JSON.parse(suggestXhr.responseText||'[]'); } catch(e){ r=[]; }
  360.       renderSuggest(Array.isArray(r)? r : []);
  361.     };
  362.     suggestXhr.onerror=function(){};
  363.     suggestXhr.send();
  364.   }
  365.   function searchLocation(){
  366.     var q=(document.getElementById('loc-search').value||'').trim();
  367.     if(!q){ showStatus('error','Type a place to search.'); return; }
  368.     showStatus('loading','πŸ” Searching…');
  369.     var xhr=new XMLHttpRequest();
  370.     xhr.open('GET','https://nominatim.openstreetmap.org/search?q='+encodeURIComponent(q)+'&format=json&addressdetails=1&limit=1');
  371.     xhr.setRequestHeader('Accept-Language','en');
  372.     xhr.onload=function(){
  373.       if(xhr.status!==200){showStatus('error','❌ Search failed');return;}
  374.       var r=JSON.parse(xhr.responseText);
  375.       if(!r||!r.length){showStatus('error','❌ No place found β€” try a different search');return;}
  376.       chooseSuggestion(r[0]);
  377.     };
  378.     xhr.onerror=function(){showStatus('error','❌ Network error');};
  379.     xhr.send();
  380.   }
  381.   function initMap(lat,lng){
  382.     map=L.map('map').setView([lat,lng],11);
  383.     L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'Β© OpenStreetMap',maxZoom:19}).addTo(map);
  384.     marker=L.marker([lat,lng],{draggable:true,icon:makeIcon()}).addTo(map);
  385.     setLatLng(lat,lng);
  386.     marker.on('dragend',function(e){var p=e.target.getLatLng();setLatLng(p.lat,p.lng);geocode(p.lat,p.lng);});
  387.     map.on('click',function(e){marker.setLatLng(e.latlng);setLatLng(e.latlng.lat,e.latlng.lng);geocode(e.latlng.lat,e.latlng.lng);});
  388.   }
  389.   function locate(){
  390.     var btn=document.getElementById('locate-btn');
  391.     if(!navigator.geolocation){showStatus('error','❌ Not supported by your browser');return;}
  392.     btn.disabled=true; btn.textContent='⏳ Locating…';
  393.     showStatus('loading','πŸ“‘ Getting your location…');
  394.     navigator.geolocation.getCurrentPosition(
  395.       function(p){
  396.         map.setView([p.coords.latitude,p.coords.longitude],16);
  397.         marker.setLatLng([p.coords.latitude,p.coords.longitude]);
  398.         setLatLng(p.coords.latitude,p.coords.longitude);
  399.         geocode(p.coords.latitude,p.coords.longitude);
  400.         btn.disabled=false; btn.textContent='πŸ“‘ Use my location';
  401.       },
  402.       function(){ btn.disabled=false; btn.textContent='πŸ“‘ Use my location'; showStatus('error','❌ Could not get your location'); }
  403.     );
  404.   }
  405.   document.addEventListener('DOMContentLoaded',function(){
  406.     if(!document.getElementById('map')||!window.L){return;}
  407.     initMap(DEFAULT[0],DEFAULT[1]);
  408.     var lb=document.getElementById('locate-btn');
  409.     if(lb) lb.addEventListener('click',locate);
  410.     var sb=document.getElementById('search-btn');
  411.     if(sb) sb.addEventListener('click',function(){ hideSuggest(); searchLocation(); });
  412.     var si=document.getElementById('loc-search');
  413.     if(si){
  414.       si.addEventListener('input',function(){
  415.         var q=(si.value||'').trim();
  416.         if(suggestTimer){ clearTimeout(suggestTimer); }
  417.         if(q.length<3){ hideSuggest(); return; }
  418.         suggestTimer=setTimeout(function(){ fetchSuggest(q); },350);
  419.       });
  420.       si.addEventListener('keydown',function(e){
  421.         if(e.key==='Enter'){
  422.           e.preventDefault();
  423.           if(suggestions.length){ chooseSuggestion(suggestions[0]); }
  424.           else { searchLocation(); }
  425.         } else if(e.key==='Escape'){
  426.           hideSuggest();
  427.         }
  428.       });
  429.       si.addEventListener('blur',function(){ setTimeout(hideSuggest,150); });
  430.     }
  431.     var hasLoc = (document.getElementById('field-country').value
  432.                || document.getElementById('field-region').value
  433.                || document.getElementById('field-city').value);
  434.     if(!hasLoc && navigator.geolocation){
  435.       navigator.geolocation.getCurrentPosition(
  436.         function(p){
  437.           map.setView([p.coords.latitude,p.coords.longitude],16);
  438.           marker.setLatLng([p.coords.latitude,p.coords.longitude]);
  439.           setLatLng(p.coords.latitude,p.coords.longitude);
  440.           geocode(p.coords.latitude,p.coords.longitude);
  441.         },
  442.         function(){}
  443.       );
  444.     }
  445.   });
  446. }());
  447. </script>
  448. </body>
  449. </html>