PageSourceSearch

https://orginfo.uz/static/js/ranking.js

js orginfo.uz collected 2026-10-02 17:10:01 UTC 18,373 bytes, 542 lines download raw bytes

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.