1/* Reyting moduli â brend qo'shish oqimi va taklif berish. 2 3 Taklif berish `window.prompt()` o'rniga Bootstrap modali orqali ketadi: 4 foydalanuvchi summani o'zi hisoblamaydi, tez tanlash tugmalari va balans 5 qoldig'i ko'rsatiladi. Modal shabloni: ranking/blocks/outbid_modal.html. 6*/ 7(function ($) { 8 'use strict'; 9 10 // ------------------------------------------------------------------ 11 // Yordamchilar 12 // ------------------------------------------------------------------ 13 14 function csrfToken() { 15 return $('[name=csrfmiddlewaretoken]').val() || 16 document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1'); 17 } 18 19 /* Foydalanuvchi kiritgan summadan ajratgichlarni olib tashlaydi. 20 Bo'shliq (oddiy va uzilmas), vergul va apostrof â barchasi minglik 21 ajratgich sifatida kelishi mumkin. */ 22 function cleanAmount(value) { 23 return String(value === null || value === undefined ? '' : value) 24 .replace(/[\s ,'`]/g, ''); 25 } 26 27 function toNumber(value) { 28 var parsed = parseFloat(cleanAmount(value)); 29 return isFinite(parsed) ? parsed : 0; 30 } 31 32 function formatNumber(value) { 33 var cleaned = cleanAmount(value); 34 if (!cleaned) { return ''; } 35 // Kasr qismi bo'lsa tashlanadi â takliflar butun so'mda. 36 return String(cleaned).split('.')[0] 37 .replace(/\B(?=(\d{3})+(?!\d))/g, ' '); 38 } 39 40 /* Tarjimalarni HTML dan o'qiydi â JS da qattiq yozilgan uzbekcha matn 41 ru/en sahifalarda ham chiqib qolmasligi uchun. */ 42 function t(key, fallback) { 43 var node = document.getElementById('rankingI18n'); 44 if (node && node.dataset && node.dataset[key]) { return node.dataset[key]; } 45 return fallback; 46 } 47 48 function errorDetail(xhr, fallback) { 49 var data = xhr && xhr.responseJSON; 50 if (!data) { return fallback; } 51 if (data.detail) { return data.detail; } 52 // DRF maydon xatolari: {"amount": ["..."], "source": ["..."]} 53 var keys = ['amount', 'source', 'display_name', 'category', 54 'non_field_errors']; 55 for (var i = 0; i < keys.length; i++) { 56 var value = data[keys[i]]; 57 if (value) { return $.isArray(value) ? value[0] : value; } 58 } 59 return fallback; 60 } 61 62 // ------------------------------------------------------------------ 63 // Taklif berish modali 64 // ------------------------------------------------------------------ 65 66 var OutbidModal = (function () { 67 var $modal = $('#rankingOutbidModal'); 68 if (!$modal.length) { return null; } 69 70 var instance = null; 71 var state = { listingId: null, step: 0, totalBid: 0, rank: null }; 72 73 var $amount = $('#rankingOutbidAmount'); 74 var $quick = $('#rankingOutbidQuick'); 75 var $submit = $('#rankingOutbidSubmit'); 76 var $error = $('#rankingOutbidError'); 77 var $balance = $('#rankingOutbidBalance'); 78 var $remainder = $('#rankingOutbidRemainder'); 79 var $lowBalance = $('#rankingOutbidLowBalance'); 80 81 var hasBalance = $balance.length > 0; 82 83 function balanceValue() { 84 return hasBalance ? toNumber($balance.data('balance')) : null; 85 } 86 87 function showError(message) { 88 $error.text(message).removeClass('d-none'); 89 } 90 91 function hideError() { 92 $error.addClass('d-none').text(''); 93 } 94 95 /* Tez tanlash tugmalari. `suggested` â backend hisoblab bergan 96 "shu qatorni ortda qoldirish" summasi; qolganlari qadamning 97 karralari. Backend 1-o'rin narxini bermaydi, shuning uchun 98 bunday tugma qo'yilmaydi â mavjud ma'lumot bilan aldamaymiz. */ 99 function renderQuick(suggested, step) { 100 $quick.empty(); 101 var options = []; 102 103 if (step > 0) { 104 options.push({ label: '+' + formatNumber(step), value: step }); 105 options.push({ label: '+' + formatNumber(step * 4), value: step * 4 }); 106 } 107 if (suggested > 0) { 108 options.push({ 109 label: t('outbidLabel', "Ortda qoldirish"), 110 value: suggested, 111 primary: true 112 }); 113 } 114 115 $.each(options, function (index, option) { 116 $('<button/>', { 117 type: 'button', 118 'class': 'ranking-quick-btn' + (option.primary ? ' active' : ''), 119 text: option.label, 120 'data-amount': option.value 121 }).appendTo($quick); 122 }); 123 } 124 125 function syncQuickState() { 126 var current = toNumber($amount.val()); 127 $quick.find('.ranking-quick-btn').each(function () { 128 var $button = $(this); 129 $button.toggleClass('active', toNumber($button.data('amount')) === current); 130 }); 131 } 132 133 function recalc() { 134 var amount = toNumber($amount.val()); 135 var balance = balanceValue(); 136 137 if (hasBalance) { 138 var remainder = balance - amount; 139 $remainder.text(formatNumber(remainder < 0 ? 0 : remainder) + ' ' + 140 t('currency', "so'm")); 141 var notEnough = amount > balance; 142 $lowBalance.toggleClass('d-none', !notEnough); 143 $submit.prop('disabled', notEnough || amount <= 0); 144 } else { 145 $submit.prop('disabled', amount <= 0); 146 } 147 148 syncQuickState(); 149 } 150 151 function open(data) { 152 hideError(); 153 154 state.listingId = data.listingId;
155 state.step = data.step > 0 ? data.step : 0; 156 state.totalBid = data.totalBid; 157 state.rank = data.rank; 158 159 $('#rankingOutbidSubtitle').text( 160 data.name 161 ? (data.rank ? data.name + ' · ' + data.rank + '-' + t('place', "o'rin") 162 : data.name) 163 : '' 164 ); 165 166 // Shu qatordagi hozirgi summa â foydalanuvchiniki emas, e'lonniki. 167 if (data.totalBid > 0) { 168 $('#rankingOutbidCurrent') 169 .text(formatNumber(data.totalBid) + ' ' + t('currency', "so'm")) 170 .closest('.ranking-summary-row').removeClass('d-none'); 171 } else { 172 $('#rankingOutbidCurrent').closest('.ranking-summary-row').addClass('d-none'); 173 } 174 175 // Tavsiya etilgan qo'shimcha summa. `suggested` â API `amount` 176 // maydoni kutadigan QO'SHIMCHA qiymat (balansdan shuncha yechiladi), 177 // jami summa emas â shuning uchun "+" belgisi o'rinli. 178 if (data.suggested > 0) { 179 $('#rankingOutbidNeedLabel').text(t('needLabel', "Tavsiya etiladi")); 180 $('#rankingOutbidNeed').text('+ ' + formatNumber(data.suggested) + ' ' + 181 t('currency', "so'm")); 182 $('#rankingOutbidNeedRow').removeClass('d-none'); 183 } else { 184 $('#rankingOutbidNeedRow').addClass('d-none'); 185 } 186 187 if (hasBalance) { 188 $balance.text(formatNumber(balanceValue()) + ' ' + t('currency', "so'm")); 189 } 190 191 var initial = data.suggested > 0 ? data.suggested : state.step; 192 $amount.val(formatNumber(initial)); 193 renderQuick(data.suggested, state.step); 194 recalc(); 195 196 if (!instance) { 197 instance = new window.bootstrap.Modal($modal[0]); 198 } 199 instance.show(); 200 // Modal ochilgach fokus summaga tushadi (mobil klaviatura ochilmasligi 201 // uchun `select` emas, oddiy `focus`). 202 $modal.one('shown.bs.modal', function () { $amount.trigger('focus'); }); 203 } 204 205 $quick.on('click', '.ranking-quick-btn', function () { 206 $amount.val(formatNumber($(this).data('amount'))); 207 recalc(); 208 }); 209 210 $amount.on('input', function () { 211 // Kursor sakramasligi uchun kiritish vaqtida formatlamaymiz â 212 // faqat hisob-kitobni yangilaymiz. Formatlash `blur` da. 213 recalc(); 214 }); 215 216 $amount.on('blur', function () { 217 var formatted = formatNumber($(this).val()); 218 if (formatted) { $(this).val(formatted); } 219 recalc(); 220 }); 221 222 $submit.on('click', function () { 223 var amount = cleanAmount($amount.val()); 224 if (!amount || toNumber(amount) <= 0) { 225 showError(t('emptyAmount', "Summani kiriting.")); 226 return; 227 } 228 229 hideError(); 230 var $button = $(this).prop('disabled', true); 231 232 $.ajax({ 233 url: '/api/ranking/listings/' + state.listingId + '/bid/', 234 method: 'POST', 235 contentType: 'application/json', 236 headers: { 'X-CSRFToken': csrfToken() }, 237 data: JSON.stringify({ amount: amount }) 238 }).done(function () { 239 window.location.reload(); 240 }).fail(function (xhr) { 241 showError(errorDetail(xhr, t('bidFailed', "Taklif berilmadi."))); 242 $button.prop('disabled', false); 243 }); 244 }); 245 246 return { open: open }; 247 }()); 248 249 // ------------------------------------------------------------------ 250 // "Ortda qoldirish" / "Ko'tarish" tugmalari 251 // ------------------------------------------------------------------ 252 253 $(document).on('click', '.ranking-outbid-btn', function () { 254 var $button = $(this); 255 var data = { 256 listingId: $button.data('listing-id'), 257 name: $button.data('listing-name') || '', 258 rank: $button.data('listing-rank') || null, 259 totalBid: toNumber($button.data('total-bid')), 260 suggested: toNumber($button.data('suggested-amount')), 261 // Qadam ikki manbadan: brend qo'shish sahifasida `#rankingMinHint`, 262 // qolgan sahifalarda modalning o'z `data-bid-step` atributi. 263 // Ilgari faqat birinchisi o'qilardi, shu sabab reyting sahifasida 264 // tez tanlash tugmalari chizilmay qolardi. 265 step: toNumber($('#rankingMinHint').data('bid-step')) || 266 toNumber($('#rankingOutbidModal').data('bid-step')) 267 }; 268 269 if (OutbidModal) { 270 OutbidModal.open(data); 271 return; 272 } 273 274 // Modal shabloni sahifada bo'lmasa â eski oqim (zaxira yo'l). 275 var amount = window.prompt( 276 "Qo'shimcha taklif miqdorini kiriting (so'm):" + (data.name ? '\n' + data.name : ''), 277 data.suggested || '' 278 ); 279 if (!amount) { return; } 280 281 $button.prop('disabled', true); 282 $.ajax({ 283 url: '/api/ranking/listings/' + data.listingId + '/bid/', 284 method: 'POST', 285 contentType: 'application/json', 286 headers: { 'X-CSRFToken': csrfToken() }, 287 data: JSON.stringify({ amount: cleanAmount(amount) }) 288 }).done(function () { 289 window.location.reload(); 290 }).fail(function (xhr) { 291 window.alert(errorDetail(xhr, "Taklif berilmadi.")); 292 $button.prop('disabled', false); 293 }); 294 }); 295 296 // ------------------------------------------------------------------ 297 // Brend qo'shish oqimi 298 // ------------------------------------------------------------------ 299 300 var $form = $('#rankingAddForm'); 301 302 if ($form.length) { 303 var $error = $('#rankingError');
304 var $amount = $('#rankingAmount'); 305 var $quick = $('#rankingAddQuick'); 306 var $submit = $('#rankingSubmitBtn'); 307 308 function showError(message) { 309 $error.text(message).removeClass('d-none'); 310 $('html, body').animate({ scrollTop: $error.offset().top - 100 }, 200); 311 } 312 313 function hideError() { 314 $error.addClass('d-none').text(''); 315 } 316 317 /* Qadam indikatorini yangilaydi va faol kartochkani ajratadi. */ 318 function activateStep(step) { 319 $('#rankingSteps .ranking-step-track').each(function () { 320 var $track = $(this); 321 $track.toggleClass('active', parseInt($track.data('step'), 10) <= step); 322 }); 323 324 $('#ranking-step-1, #ranking-step-2, #ranking-step-3') 325 .removeClass('ranking-card--active'); 326 $('#ranking-step-' + step).addClass('ranking-card--active'); 327 328 // Bajarilgan qadamlar raqami belgiga aylanadi. 329 for (var i = 1; i < step; i++) { 330 $('#rankingStep' + i + 'Num') 331 .addClass('ranking-card-num--done') 332 .html('<svg width="16" height="16" viewBox="0 0 24 24" fill="none" ' + 333 'stroke="currentColor" stroke-width="2.6" stroke-linecap="round" ' + 334 'stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>'); 335 } 336 } 337 338 function balanceValue() { 339 return toNumber($('#rankingBalance').data('balance')); 340 } 341 342 function minBid() { 343 return toNumber($('#rankingMinHint').data('min-bid')); 344 } 345 346 function bidStep() { 347 return toNumber($('#rankingMinHint').data('bid-step')); 348 } 349 350 function renderQuick() { 351 $quick.empty(); 352 var base = minBid(); 353 var step = bidStep(); 354 if (base <= 0) { return; } 355 356 var options = [base]; 357 if (step > 0) { 358 options.push(base + step * 2); 359 options.push(base + step * 10); 360 } 361 362 $.each(options, function (index, value) { 363 $('<button/>', { 364 type: 'button', 365 'class': 'ranking-quick-btn', 366 text: formatNumber(value), 367 'data-amount': value 368 }).appendTo($quick); 369 }); 370 } 371 372 function syncQuickState() { 373 var current = toNumber($amount.val()); 374 $quick.find('.ranking-quick-btn').each(function () { 375 var $button = $(this); 376 $button.toggleClass('active', toNumber($button.data('amount')) === current); 377 }); 378 } 379 380 function recalc() { 381 var amount = toNumber($amount.val()); 382 var balance = balanceValue(); 383 var remainder = balance - amount; 384 385 $('#rankingRemainder').text( 386 formatNumber(remainder < 0 ? 0 : remainder) + ' ' + t('currency', "so'm")); 387 388 var notEnough = amount > balance; 389 $('#rankingLowBalance').toggleClass('d-none', !notEnough); 390 $submit.prop('disabled', notEnough || amount <= 0); 391 392 syncQuickState(); 393 } 394 395 $quick.on('click', '.ranking-quick-btn', function () { 396 $amount.val(formatNumber($(this).data('amount'))); 397 recalc(); 398 }); 399 400 $amount.on('input', recalc); 401 $amount.on('blur', function () { 402 var formatted = formatNumber($(this).val()); 403 if (formatted) { $(this).val(formatted); } 404 recalc(); 405 }); 406 407 // --- 1-qadam: manbani tekshirish --- 408 409 $('#rankingPreviewBtn').on('click', function () { 410 var source = $('#rankingSource').val(); 411 if (!source) { 412 showError(t('emptySource', "Manbani kiriting.")); 413 return; 414 } 415 416 hideError(); 417 var $button = $(this).prop('disabled', true); 418 419 $.ajax({ 420 url: $form.data('preview-url'), 421 method: 'GET', 422 data: { source: source }, 423 dataType: 'json' 424 }).done(function (data) { 425 var name = data.display_name || ''; 426 var description = data.description || ''; 427 428 $('#rankingName').val(name); 429 $('#rankingDescription').val(description); 430 431 // Ko'rinish kartochkasi 432 $('#rankingPreviewName').text(name); 433 $('#rankingPreviewDescription').text(description); 434 var $avatar = $('#rankingPreviewAvatar').empty(); 435 if (data.avatar) { 436 $('<img/>', { src: data.avatar, alt: name, loading: 'lazy' }) 437 .on('error', function () { $(this).remove(); $avatar.text(name.charAt(0).toUpperCase()); }) 438 .appendTo($avatar); 439 } else { 440 $avatar.text(name ? name.charAt(0).toUpperCase() : ''); 441 } 442 443 // Manba turi nishoni â foydalanuvchi Instagram profil kiritgan 444 // bo'lsa, kartochkada shuni ko'rishi kerak (Telegram bilan 445 // adashmaslik uchun). Shablon to'rtala nishonni yashirin beradi, 446 // API dan kelgan `source_type` ga mos kelganini ochamiz. 447 var $source = $('#rankingPreviewSource'); 448 var $sourceMatch = $source.find( 449 '[data-source-type="' + String(data.source_type || '') + '"]'); 450 $source.find('[data-source-type]').prop('hidden', true); 451 $sourceMatch.prop('hidden', false); 452 $source.toggleClass('d-none', $sourceMatch.length === 0); 453 454 var minimumAmount = cleanAmount(data.minimum_amount || '0').split('.')[0]; 455 456 var $notice = $('#rankingExistingNotice'); 457 if (data.existing_listing_id) { 458 $notice.text(t('existingListing', 459 "Bu brend allaqachon reytingda. Kiritilgan summa mavjud taklifga qo'shiladi.")) 460 .removeClass('d-none'); 461 } else { 462 $notice.addClass('d-none'); 463 } 464 465 // Minimal summa manbaga qarab o'zgaradi (mavjud e'lonni ko'tarish 466 // uchun boshqa, yangi e'lon uchun boshqa) â data-min-bid ni ham 467 // yangilaymiz, aks holda tez tanlash tugmalari eski qiymatga 468 // tayanib qolardi. 469 var $hint = $('#rankingMinHint'); 470 $hint.data('min-bid', minimumAmount); 471 $hint.text(t('minPrefix', 'Minimal') + ': ' + formatNumber(minimumAmount) + 472 ' ' + t('currency', "so'm")); 473 474 $amount.val(formatNumber(minimumAmount)); 475 renderQuick(); 476 477 $('#ranking-step-2').removeClass('d-none'); 478 activateStep(2); 479 }).fail(function (xhr) { 480 showError(errorDetail(xhr, 481 t('previewFailed', "Havolani tekshirib, qayta urinib ko'ring."))); 482 }).always(function () { 483 $button.prop('disabled', false); 484 }); 485 }); 486 487 // --- 2-qadam: ma'lumotlarni tasdiqlash --- 488 489 $('#rankingToPaymentBtn').on('click', function () { 490 if (!$('#rankingName').val()) { 491 showError(t('emptyName', "Brend nomini kiriting.")); 492 return; 493 } 494 hideError(); 495 $('#ranking-step-3').removeClass('d-none'); 496 activateStep(3); 497 recalc(); 498 $('html, body').animate( 499 { scrollTop: $('#ranking-step-3').offset().top - 100 }, 250); 500 }); 501 502 // Nom/tavsif tahrirlansa ko'rinish kartochkasi ham yangilanadi. 503 $('#rankingName').on('input', function () { 504 $('#rankingPreviewName').text($(this).val()); 505 }); 506 $('#rankingDescription').on('input', function () { 507 $('#rankingPreviewDescription').text($(this).val()); 508 }); 509 510 // --- 3-qadam: yuborish --- 511 512 $form.on('submit', function (event) { 513 event.preventDefault(); 514 hideError(); 515 516 var $button = $submit.prop('disabled', true); 517 518 $.ajax({ 519 url: $form.data('create-url'), 520 method: 'POST', 521 contentType: 'application/json', 522 headers: { 'X-CSRFToken': csrfToken() }, 523 data: JSON.stringify({ 524 source: $('#rankingSource').val(), 525 category: $('#rankingCategory').val(), 526 amount: cleanAmount($amount.val()), 527 display_name: $('#rankingName').val(), 528 description: $('#rankingDescription').val() 529 }) 530 }).done(function () { 531 window.location.href = $form.data('success-url'); 532 }).fail(function (xhr) { 533 showError(errorDetail(xhr, 534 t('createFailed', "Taklif berilmadi. Qayta urinib ko'ring."))); 535 $button.prop('disabled', false); 536 }); 537 }); 538 539 renderQuick(); 540 } 541 542})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.