Български | Català | Deutsche | Hrvatski | Čeština | Dansk | Nederlandse | English | Eesti keel | Français | Ελληνικά | Magyar | Italiano | Latviski | Norsk | Polski | Português | Română | Русский | Српски | Slovenský | Slovenščina | Español | Svenska | Türkçe | 汉语 | 日本語 |
N

news.nes.ru

Регистрован
Снимак екрана news.nes.ru
Снимак екрана веб странице

Руска економска школа је место где се рађају идеје и људиВести са сајта вести. нес. ру је као прозор у свет где се наука сусреће са стварношћу, где професори расправљају о конкуренцији у К...

Датум ажурирања анализе странице: 2026/03/30 19:33:25
Датум последњег вхоис ажурирања: 2026/06/06 21:02:14
Статус домена
Регистрован
Плаћено до
08.06.2027
Доступно од
10.07.2027

Вебсите Десцриптион

📝

<х2>Руска економска школа је место где се рађају идеје и људи<п>Вести са сајта вести. нес. ру је као прозор у свет где се наука сусреће са стварношћу, где професори расправљају о конкуренцији у Кини, а студенти тихо граде каријеру на излету. Да, ово је сајт НСЗ – Руске економске школе, али не само образовне институције, већ праве академске заједнице. <п>Дух се одмах осећа на главној страници: овде је све наглашено - од горњег менија до најновијих вести. Постоје одељци који могу изгледати као обичан скуп веза, али ако боље погледате, сваки од њих је део већег система. О школи, мисији, људима, оценама. Ово није само информација, ово је историја. Такође има пуно детаља о кампусу, фонду задужбина и контактима. Па скоро све осим телефона - нема. Али има много других ствари. <х3>Шта се овде дешава?<п>Сајт живи свој живот. Чланци се објављују са портала ГУРУ, где аутори постављају питања попут: Може ли конкуренција штетити привреди? И да, испоставило се да може. Поготово када се власт превише активно меша. Пример - Кина. Чини се да се цела земља развија, али у исто време постоје парадокси. Да ли је могуће да систем који се чинио идеалним не функционише како би требало?<п>Има и других тема. Подкасти, на пример. Економија по слуху. Гости разговарају о староверцима, њиховом поштењу, речи, поверењу. Можете слушати сатима. Истина, понекад се чини да говоре о послу, али у стварности говоре о моралу. Занимљиво, зар не? Уосталом, зар се капитализам без етике не претвара у хаос?<п>Или ево још једног примера – чланак Буди човек!. Овде говоримо о стереотипима. Мушкарци теже да буду снажни, преморени, бацају се на посао, али зашто? Да докажу себи да су заиста мушкарци? Али ово такође утиче на здравље, каријеру, породицу. Зашто смо изграђени на овај начин? Можда је цела поента у томе што нас друштво тера на ово?<х3>Образовање, истраживање, живот<п>НСЗ не учи само. Овде постоје програми: бачелор, мастер, радионице и постдипломске студије. Постоје чак и онлајн програми. Мини-МИФ, на пример. Или програм управљања богатством. Ово није само наставни план и програм – то је пут за оне који желе не само да стекну диплому, већ да разумеју свет. <п>Овде такође одржавају догађаје. Пикник, на пример. Већ другу годину за редом. Људи долазе, шетају, причају. Неформално. Нема презентација. Али то је жива заједница. Све је како треба. Мада. можда не у потпуности. Понекад се чини да су сви превише озбиљни. Па, или обрнуто - превише смешно, да будем искрен. <п>Иначе, у септембру је била трибина на којој је говорио ректор Антон Суворов. О новим решењима за бизнис и владу у условима несташице.Занимљиво, зар не? На крају крајева, свако од нас се суочава са овим - где пронаћи кадрове, капитал, идеје? А ево, испоставља се, већ покушавају да то реше. <х3>Какав је портал ГУРУ?<п>То је посебна прича. Постоје чланци, колумне, спискови за лектиру. На пример, листа за читање: Закони функционишу као порези и цене. Звучи чудно, али да. Замислите да је закон роба. Продаје се, цена му зависи од потражње, казне су новчане, а правда резултат равнотеже подстицаја. Не, није тако једноставно. Али занимљиво је, зар не? Али људи тамо студирају, чак и ако не у НСЗ. <х3>Резултат<п>Вести о вестима. нес. ру није само информација. То је део академске културе. Овде нема места празним заглавцима. Ниједан домен није истекао, сајт је привремено недоступан - све ради. Он само живи. Сваки дан се појављује нешто ново - или чланак о такмичењу, или подкаст о староверцима, или извештај о резултатима пријема. И то је у реду. Тако би требало да буде. <див цласс="фак-аццордион"><х2>Честа питања<детаилс><суммари>Шта су вести. нес. ру?<п>Ово је сајт Руске школе економије, где се објављују вести, истраживања, мишљења стручњака, материјали са портала ГУРУ и информације о програмима обуке. <детаилс><суммари>Који програми постоје на НСЗ?<п>Бацхелор (БА), мастер (МАМСц), мастер програми (ДПО), постдипломске школе, онлајн програми као што су Мини-МИФ и Мастер оф Финанце. <детаилс><суммари>Шта је ГУРУ портал?<п>Ово је платформа за објављивање научних чланака, аналитике, колумни и материјала о економији, друштву и култури, коју креира НСЗ. <детаилс><суммари>Где можете сазнати о пријемној кампањи?<п>На веб страници вести. нес. ру објављени су резултати пријемне кампање 2025. године са подацима о упису студената на основне и мастер студије. <детаилс><суммари>Да ли НСЗ има догађаје?<п>Да. На пример, традиционални пикник на почетку школске године, учешће ректора на Источном економском форуму, говори на конференцијама и семинарима. .

СЕО Сцоре
61.64%
90
Скор постигнут
146
Максимални резултат

Маин Информатион

ℹ️
Наслов: --> <title>Научные статьи, исследования, экспертное мнение | РЭШ
Опис: Новости и события
Кључне речи: Научные статьи, исследования, экспертное мнение, экономика, финансы, выпускники, студенты, абитуриенты
Паге Енцодинг: utf-8
Величина датотеке странице: 158 KB

Информације о серверу

🖥️
IP: 217.16.17.177
Локација: Russia,RU,,,55.7386,37.6068,Europe/Moscow
ХТТП сервер: nginx
Енцодинг: utf-8

Листа мета ознака

🏷️

Интерне везе

🔗

Ектернал Линкс

🌐

Вхоис Информатион

📄
domain_name: nes.ru
taxpayer_id: 7727016888
update_date: on
update_time:
creation_date: 2000-06-09T11:41:42Z
creation_time: 960550902

Вхоис необрађени подаци

📋
            domain:        NES.RU
nserver: ns3-l2.nic.ru.
nserver: ns4-cloud.nic.ru.
nserver: ns4-l2.nic.ru.
nserver: ns8-cloud.nic.ru.
nserver: ns8-l2.nic.ru.
state: REGISTERED, DELEGATED, VERIFIED
org: New Economic School, Non-Govemmental Education Institution of Higher Education
taxpayer-id: 7727016888
registrar: RU-CENTER-RU
admin-contact: https://www.nic.ru/whois
created: 2000-06-09T11:41:42Z
paid-till: 2027-06-08T21:00:00Z
free-date: 2027-07-10
source: TCI
Last updated on 2026-06-06T20:58:01Z

Robots.txt

🤖
			User-agent: Yandex 
Disallow: /*?
Disallow: /*?id=
Disallow: /assets/components/
Disallow: /core/
Disallow: /mgr/
Disallow: /*connectors/
Disallow: /*manager/
Disallow: /index.php
Disallow: /404
Disallow: /404.html
Disallow: /log/
Disallow: /maillog/
Disallow: /cg-bin/
Disallow: /templstes/
Disallow: /revolution/
Disallow: /scriptsl/
Disallow: /permit-covid
Disallow: /permit-covid.html1
Disallow: /permit-covid-staff
Disallow: /permit-covid-staff.html
Disallow: /permit-covid-MIF
Disallow: /permit-covid-MIF.html
Disallow: /permit-covid-MAE
Disallow: /permit-covid-MAE.html
Disallow: /permit-covid-BAE
Disallow: /permit-covid-BAE.html
Disallow: /permit-covid-MAF
Disallow: /permit-covid-MAF.html
Disallow: /permit-covid-MAEE
Disallow: /permit-covid-MAEE.html
Disallow: /permit-covid-MSEE
Disallow: /permit-covid-MSEE.html
Disallow: /permit-covid-EADA
Disallow: /permit-covid-EADA.html
Disallow: /permit-covid-MSF
Disallow: /permit-covid-MSF.html
Disallow: /rectorsearch
Disallow: /rectorsearch.html
Disallow: /rectorsearch-ad
Disallow: /rectorsearch-ad.html
Disallow: *?
Crawl-Delay: 5

User-agent: Googlebot
Disallow: /*?
Disallow: /*?id=
Disallow: /assets/components/
Disallow: /core/
Disallow: /mgr/
Disallow: /*connectors/
Disallow: /*manager/
Disallow: /index.php
Disallow: /404
Disallow: /404.html
Disallow: /log/
Disallow: /maillog/
Disallow: /cg-bin/
Disallow: /templstes/
Disallow: /revolution/
Disallow: /scriptsl/
Disallow: /permit-covid
Disallow: /permit-covid.html
Disallow: /permit-covid-staff
Disallow: /permit-covid-staff.html
Disallow: /permit-covid-MIF
Disallow: /permit-covid-MIF.html
Disallow: /permit-covid-MAE
Disallow: /permit-covid-MAE.html
Disallow: /permit-covid-BAE
Disallow: /permit-covid-BAE.html
Disallow: /permit-covid-MAF
Disallow: /permit-covid-MAF.html
Disallow: /permit-covid-MAEE
Disallow: /permit-covid-MAEE.html
Disallow: /permit-covid-MSEE
Disallow: /permit-covid-MSEE.html
Disallow: /permit-covid-EADA
Disallow: /permit-covid-EADA.html
Disallow: /permit-covid-MSF
Disallow: /permit-covid-MSF.html
Disallow: /rectorsearch
Disallow: /rectorsearch.html
Disallow: /rectorsearch-ad
Disallow: /rectorsearch-ad.html
Disallow: *?
Crawl-Delay: 5

User-agent: Googlebot-Image 
Disallow: /*?
Disallow: /*?id=
Disallow: /assets/components/
Disallow: /core/
Disallow: /mgr/
Disallow: /*connectors/
Disallow: /*manager/
Disallow: /index.php
Disallow: /404
Disallow: /404.html
Disallow: /log/
Disallow: /maillog/
Disallow: /cg-bin/
Disallow: /templstes/
Disallow: /revolution/
Disallow: /scriptsl/
Disallow: /permit-covid
Disallow: /permit-covid.html
Disallow: /permit-covid-staff
Disallow: /permit-covid-staff.html
Disallow: /permit-covid-MIF
Disallow: /permit-covid-MIF.html
Disallow: /permit-covid-MAE
Disallow: /permit-covid-MAE.html
Disallow: /permit-covid-BAE
Disallow: /permit-covid-BAE.html
Disallow: /permit-covid-MAF
Disallow: /permit-covid-MAF.html
Disallow: /permit-covid-MAEE
Disallow: /permit-covid-MAEE.html
Disallow: /permit-covid-MSEE
Disallow: /permit-covid-MSEE.html
Disallow: /permit-covid-EADA
Disallow: /permit-covid-EADA.html
Disallow: /permit-covid-MSF
Disallow: /permit-covid-MSF.html
Disallow: /rectorsearch
Disallow: /rectorsearch.html
Disallow: /rectorsearch-ad
Disallow: /rectorsearch-ad.html
Disallow: *?
Crawl-Delay: 5

User-agent: *
Disallow: /

Sitemap: https://www.nes.ru/sitemap.xml
Host: https://www.nes.ru/
        

СЕО Аудит

🔍

Тецхницал СЕО

Респонсе Цоде
200
Статус 200 ОК - страница се учитава исправно.
Кодирање знакова
Page: utf-8, Header: utf-8
Кодирање знакова конзистентно између ХТМЛ-а и заглавља.
Величина странице
162717 bytes
Величина странице прихватљива за брзо учитавање.
!
Ресурси
62 total
Превише ресурса. Оптимизујте и комбинујте датотеке за боље перформансе.
Хрефланг Тагс
0 hreflang tags
Додајте ознаке хрефланг ако имате вишејезичан садржај.
Robots.txt
Exists
Пронађена датотека Роботс.ткт.
Sitemap
Declared in robots.txt
Мапа сајта је декларисана у роботс.ткт.
HTTPS
Yes
Сигурна ХТТПС веза је омогућена.
Компресија
gzip
Гзип или Зстд компресија је омогућена за брже учитавање.
Кеширање
no-store, no-cache, must-revalidate
Заглавља контроле кеша исправно постављена.
Брзина странице
1.70 ms
Одлична брзина утовара.

СЕО на страници

!
Наслов
--> Научные статьи, исследования, экспертное мнение | РЭШ</div><div class="audit-recommendation">Наслов предугачак. Смањите на 30-60 знакова да бисте избегли скраћивање.</div></div></div><div class="audit-item"><div class="audit-status status-fail">!</div><div class="audit-info"><div class="audit-name">Мета Десцриптион</div><div class="audit-value">Новости и события Lenght:17</div><div class="audit-recommendation">Мета опис је прекратак. Проширите на 100-160 знакова.</div></div></div><div class="audit-item"><div class="audit-status status-pass">✓</div><div class="audit-info"><div class="audit-name">Х1 Наслов</div><div class="audit-value">1 found - "Новости"</div><div class="audit-recommendation">Добро – пронађен је један Х1 наслов.</div></div></div><div class="audit-item"><div class="audit-status status-fail">!</div><div class="audit-info"><div class="audit-name">Број речи</div><div class="audit-value">383</div><div class="audit-recommendation">Садржај донекле кратак. Проширите на 500+ речи за оптималан СЕО.</div></div></div><div class="audit-item"><div class="audit-status status-fail">!</div><div class="audit-info"><div class="audit-name">Цаноницал Таг</div><div class="audit-value"></div><div class="audit-recommendation">Додајте канонску ознаку да бисте спречили проблеме са дуплим садржајем.</div></div></div><div class="audit-item"><div class="audit-status status-fail">!</div><div class="audit-info"><div class="audit-name">Дупликат Мета</div><div class="audit-value">{"yandex-verification":2,"format-detection":2}</div><div class="audit-recommendation">Уклоните дупле мета ознаке да бисте избегли сукобе.</div></div></div><div class="audit-item"><div class="audit-status status-pass">✓</div><div class="audit-info"><div class="audit-name">Кључне речи</div><div class="audit-value">Научные статьи, исследования, экспертное мнение, экономика, финансы, выпускники, студенты, абитуриенты</div><div class="audit-recommendation">Постављене мета кључне речи (напомена: не користе их главни претраживачи).</div></div></div><h3 class="category-title" style="margin-top: 1.5rem;">Садржај и кориснички доживљај</h3><div class="audit-item"><div class="audit-status status-fail">!</div><div class="audit-info"><div class="audit-name">Језик</div><div class="audit-value"></div><div class="audit-recommendation">Додајте ланг атрибут у <хтмл> ознаку за приступачност и СЕО.</div></div></div><div class="audit-item"><div class="audit-status status-fail">!</div><div class="audit-info"><div class="audit-name">Слике</div><div class="audit-value">18 total, 5 missing ALT</div><div class="audit-recommendation">Додајте АЛТ текст сликама за приступачност и СЕО.</div></div></div><div class="audit-item"><div class="audit-status status-pass">✓</div><div class="audit-info"><div class="audit-name">Виевпорт</div><div class="audit-value">width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no</div><div class="audit-recommendation">Метаознака оквира приказа правилно подешена за мобилне уређаје.</div></div></div><div class="audit-item"><div class="audit-status status-pass">✓</div><div class="audit-info"><div class="audit-name">Отворите Графикон</div><div class="audit-value">All OG tags present.</div><div class="audit-recommendation">Присутне су све неопходне ОпенГрапх ознаке.</div></div></div><div class="audit-item"><div class="audit-status status-pass">✓</div><div class="audit-info"><div class="audit-name">Структурирани подаци</div><div class="audit-value">1 JSON-LD scripts</div><div class="audit-recommendation">Пронађени су структурирани подаци (ЈСОН-ЛД).</div></div></div></div></div> <!-- Load Google Charts at the top of the search engine section --> <script> // Load Google Charts google.charts.load('current', {'packages':['corechart', 'line']}); </script> <!-- Google Positions --> <div class="chart-container"> <h2 class="chart-title">Позиције у Google</h2> <div id="chart_div_google" style="width: 100%; height: 400px;"></div> </div> <!-- Google Search Phrases --> <div class="card"> <div class="card-header"> <h2 class="card-title">Тражи фразе - Google</h2> <div class="card-icon">🔍</div> </div> <div class="card-content"> <div class="filters"> <div class="filter-group"> <label class="filter-label" for="googlePhraseFilter">Фраза:</label> <input type="text" id="googlePhraseFilter" class="filter-input" placeholder="Филтрирајте по фрази"> </div> <div class="filter-group"> <label class="filter-label" for="googlePageFilter">Страница:</label> <input type="text" id="googlePageFilter" class="filter-input" placeholder="Филтрирајте по страници"> </div> <div class="filter-group"> <label class="filter-label"> </label> <button class="cta-button" onclick="applyFilters('google')">Примени филтер</button> </div> </div> <div class="table-responsive"> <table class="data-table" id="googlePhrasesTable"> <thead> <tr> <th>Положај</th> <th>Фраза</th> <th>Страница</th> <th>Сниппет</th> </tr> </thead> <tbody id="googlePhrasesBody"> <tr><td><span class="badge badge-success">1</span><span class="position-change positive">(+1)</span></td><td><noindex><a href="/sr/phrase/%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83-%D0%B6%D0%B5%D0%BD%D1%89%D0%B8%D0%BD-%D0%BC%D0%B0%D0%BB%D0%BE/" rel="nofollow">почему женщин мало</a></noindex></td><td>/news/Pamela-Campa-w...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/Pamela-Campa-women-in-politics-lecture/" rel="nofollow" target="_blank">Почему женщины мало представлены в политике?</a></noindex><br>19 нояб. 2019 г. — Глубинная причина такой расстановки приоритетов – недостаток поддержки со стороны родителей и учителей , маленький опыт конкурентной борьбы и, ...</div></td></tr><tr><td><span class="badge badge-success">1</span><span class="position-change positive">(+3)</span></td><td><noindex><a href="/sr/phrase/%D0%BF%D0%BE%D0%BB%D0%B8%D1%82%D0%B8%D0%BA%D0%B8-%D0%BC%D0%B5%D0%BD%D0%B5%D0%B5/" rel="nofollow">политики менее</a></noindex></td><td>/news/Pamela-Campa-w...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/Pamela-Campa-women-in-politics-lecture/" rel="nofollow" target="_blank">Почему женщины мало представлены в политике?</a></noindex><br>19 нояб. 2019 г. — Глубинная причина такой расстановки приоритетов – недостаток поддержки со стороны родителей и учителей, маленький опыт конкурентной борьбы и, ...</div></td></tr><tr><td><span class="badge badge-success">1</span></td><td><noindex><a href="/sr/phrase/%D0%BF%D1%80%D0%B8%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C-%D0%B8%D1%82%D0%BE%D0%B3/" rel="nofollow">приходить итог</a></noindex></td><td>/news/kogda-ii-prixo...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/kogda-ii-prixodit-v-sud-itogi-pervogo-masshtabnogo-eksperimenta/" rel="nofollow" target="_blank">Когда ИИ приходит в суд: итоги первого масштабного ...</a></noindex><br>10 июл. 2026 г. — Что получится, если дополнить судью роботом? Чем языковая модель может помочь правосудию? И готовы ли судьи доверить чат-ботам право ...</div></td></tr><tr><td><span class="badge badge-success">1</span></td><td><noindex><a href="/sr/phrase/%D1%80%D0%B5%D1%88%D0%B8%D1%82%D0%B5-%D1%8D%D0%BA%D0%BE%D0%BD%D0%BE%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D1%83%D1%8E/" rel="nofollow">решите экономическую</a></noindex></td><td>/news/ne-tolko-vsero...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/ne-tolko-vseros.-podrobnyij-gajd-resh-po-ekonomicheskim-olimpiadam/" rel="nofollow" target="_blank">Не только Всерос. Подробный гайд ...</a></noindex><br>30 нояб. 2021 г. — Большинство поступающих на Совместный бакалавриат РЭШ и ВШЭ – победители и призеры олимпиад по экономике. Как выбрать, в каких участвовать?</div></td></tr><tr><td><span class="badge badge-success">2</span></td><td><noindex><a href="/sr/phrase/%D1%82%D0%B5%D0%BF%D0%BB%D1%83%D1%85%D0%B8%D0%BD-%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0-%D0%B4%D0%B8%D0%B0%D0%BB%D0%BE%D0%B3/" rel="nofollow">теплухин тройка диалог</a></noindex></td><td>/news/pavel-tepluxin...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/pavel-tepluxin-v-resh/&lang=ru" rel="nofollow" target="_blank">Павел Теплухин, один из основателей «Тройка Диалог</a></noindex><br>21 февр. 2018 г. — Президент Matrix Capital Павел Теплухин рассказал студентам и выпускникам РЭШ о взгляде изнутри на фондовые рынки России.</div></td></tr><tr><td><span class="badge badge-success">2</span></td><td><noindex><a href="/sr/phrase/%D0%B2%D1%8B%D0%BF%D1%83%D1%81%D0%BA%D0%BD%D0%B8%D0%BA%D0%B8-%D0%BC%D0%B0%D0%B3%D0%B8%D1%81%D1%82%D1%80%D0%B0%D1%82%D1%83%D1%80%D1%8B/" rel="nofollow">выпускники магистратуры</a></noindex></td><td>/news/gde-rabotayut-...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/gde-rabotayut-i-skolko-zarabatyivayut-vyipuskniki-magistraturyi-resh/" rel="nofollow" target="_blank">Где работают и сколько зарабатывают выпускники ...</a></noindex><br>7 февр. 2025 г. — Узнайте, на какие позиции и в какие компании трудоустраиваются выпускники магистратуры РЭШ сразу после выпуска. Ключевики: карьера после РЭШ ...</div></td></tr><tr><td><span class="badge badge-success">2</span></td><td><noindex><a href="/sr/phrase/%D0%B6%D0%B5%D0%BD%D1%89%D0%B8%D0%BD%D1%8B-%D0%BC%D0%B0%D0%BB%D0%BE/" rel="nofollow">женщины мало</a></noindex></td><td>/news/Pamela-Campa-w...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/Pamela-Campa-women-in-politics-lecture/" rel="nofollow" target="_blank">Почему женщины мало представлены в политике?</a></noindex><br>19 нояб. 2019 г. — Глубинная причина такой расстановки приоритетов – недостаток поддержки со стороны родителей и учителей, маленький опыт конкурентной борьбы и, ...</div></td></tr><tr><td><span class="badge badge-success">2</span></td><td><noindex><a href="/sr/phrase/%D0%BC%D0%B0%D0%BB%D0%BE-%D0%BE%D1%82%D0%BB%D0%B8%D1%87%D0%B0%D0%B5%D1%82%D1%81%D1%8F/" rel="nofollow">мало отличается</a></noindex></td><td>/news/bolshe-biznesa...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/bolshe-biznesa-menshe-sporta-chem-chm-po-futbolu-otlichaetsya-ot-drugix-podkast-ekonomika-na-slux/" rel="nofollow" target="_blank">Больше бизнеса, меньше спорта. Чем ЧМ по футболу ...</a></noindex><br>30 июн. 2026 г. — «Экономика на слух» не могла остаться в стороне от чемпионата мира по футболу 2026 г. На этом турнире число команд выросло с 32 до 48, появилось ...</div></td></tr><tr><td><span class="badge badge-success">2</span></td><td><noindex><a href="/sr/phrase/%D0%BF%D0%BE%D0%BC%D0%BE%D0%B3%D0%B8%D1%82%D0%B5-16/" rel="nofollow">помогите 16</a></noindex></td><td>/news/na-16-m-semina...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/na-16-m-seminare-po-ekonomicheskim-issledovaniyam-ekspertyi-obsudili-kak-bolshie-i-alternativnyie-dannyie-vliyayut-na-ekonomiku-i-finansovyij-sektor/" rel="nofollow" target="_blank">На 16-м семинаре по экономическим исследованиям ...</a></noindex><br>2 дня назад — На 16 -м семинаре по экономическим исследованиям эксперты обсудили, как большие и альтернативные данные влияют на экономику и финансовый сектор ...</div></td></tr><tr><td><span class="badge badge-success">2</span></td><td><noindex><a href="/sr/phrase/%D0%B3%D0%B4%D0%B5-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%8E%D1%82-%D0%BF%D0%BE%D1%81%D0%BB%D0%B5/" rel="nofollow">где работают после</a></noindex></td><td>/news/kariera-posle-...</td><td><button class="show-snippet-btn" onclick="toggleSnippet(this)">Прикажи исечак</button><div class="snippet" style="display: none;"><noindex><a href="https://news.nes.ru//news/kariera-posle-resh-gde-rabotayut-i-skolko-zarabatyivayut-vyipuskniki-magistraturyi-2025/" rel="nofollow" target="_blank">Где работают и сколько зарабатывают выпускники ...</a></noindex><br>3 февр. 2026 г. — Такой подход обеспечивает выпускникам широкий спектр карьерных возможностей в экономике, финансовом секторе, международных компаниях, ...</div></td></tr> </tbody> </table> </div> <div class="pagination" id="googlePagination"> <button disabled onclick="loadSearchPhrases('google', 0)">«</button> <button class="active" onclick="loadSearchPhrases('google', 1)">1</button> <button class="" onclick="loadSearchPhrases('google', 2)">2</button> <button class="" onclick="loadSearchPhrases('google', 3)">3</button> <button disabled="">...</button> <button class="" onclick="loadSearchPhrases('google', 12)">12</button> <button class="" onclick="loadSearchPhrases('google', 13)">13</button> <button onclick="loadSearchPhrases('google', 2)">»</button> </div> </div> </div> <script> // Google Charts for google google.charts.setOnLoadCallback(drawChart_google); function drawChart_google() { var data = new google.visualization.DataTable(); data.addColumn('date', 'Дан'); data.addColumn("number", "почему женщин мало");data.addColumn("number", "политики менее");data.addColumn("number", "приходить итог");data.addColumn("number", "решите экономическую");data.addColumn("number", "теплухин тройка диалог");data.addColumn("number", "выпускники магистратуры");data.addColumn("number", "женщины мало");data.addColumn("number", "мало отличается");data.addColumn("number", "помогите 16");data.addColumn("number", "где работают после"); data.addRows([ [new Date(2025, 9, 28),null,null,null,null,2,null,null,null,null,null,],[new Date(2025, 9, 30),null,null,null,null,null,null,2,null,null,null,],[new Date(2025, 10, 26),null,null,null,null,null,null,null,null,2,null,],[new Date(2025, 6, 1),2,null,null,null,null,null,null,null,null,null,],[new Date(2025, 6, 13),null,4,null,null,null,null,null,null,null,null,],[new Date(2025, 7, 10),null,null,null,null,null,2,null,null,null,null,],[new Date(2026, 6, 19),null,null,null,1,null,null,null,null,null,null,],[new Date(2026, 7, 13),null,null,null,null,null,null,null,2,null,null,],[new Date(2026, 7, 14),1,null,null,null,null,null,null,null,null,null,],[new Date(2026, 7, 17),null,null,null,null,null,null,null,null,null,2,],[new Date(2026, 7, 3),null,1,1,null,null,null,null,null,null,null,], ]); var options = { chart: { title: 'Фразе за претрагу за које је домен на врху google претраживач: "news.nes.ru"', subtitle: 'Историја позиције фразе' }, width: '100%', height: 400, curveType: 'function', legend: { position: 'bottom' }, pointSize: 5, pointsVisible: true, interpolateNulls: true, selectionMode: 'multiple', vAxis: { direction: '-1', title: 'Положај', }, hAxis: { title: 'Датум', }, }; var chart = new google.visualization.LineChart(document.getElementById("chart_div_google")); chart.draw(data, options); } </script> <!-- Additional Services --> <div class="card" id="domain-additional-services"> <div class="card-header"> <h2 class="card-title">Додатне услуге</h2> <div class="card-icon">💎</div> </div> <div class="card-content"> <div class="info-grid"> <div class="info-item"> <span class="popup-trigger" alt="Потпуна СЕО ревизија са детаљним препорукама"> <button class="cta-button" data-modal="paymentModal" data-payment-type="domain_seo_pay" data-payment-item-id="8754252" data-title="Потпуна СЕО ревизија news.nes.ru" data-text="Потпуна СЕО ревизија са детаљним препорукама за побољшање позиција претраживача." data-price="99.99"> Потпуна СЕО ревизија </button> <span class="popup-text"></span> </span> </div> <div class="info-item"> <span class="popup-trigger" alt="Месечно праћење позиције за 100 кључних фраза"> <button class="cta-button" data-modal="paymentModal" data-payment-type="domain_position_pay" data-payment-item-id="8754252" data-title="Поситион Мониторинг news.nes.ru" data-text="Месечно праћење позиције за 100 кључних фраза у Гоогле-у и Иандек-у." data-price="49.99"> Поситион Мониторинг </button> <span class="popup-text"></span> </span> </div> <div class="info-item"> <span class="popup-trigger" alt="Анализа конкурената и идентификација њихових стратегија промоције"> <button class="cta-button" data-modal="paymentModal" data-payment-type="domain_analiz_pay" data-payment-item-id="8754252" data-title="Анализа конкуренције news.nes.ru" data-text="Анализа конкурената и идентификација њихових стратегија промоције." data-price="79.99"> Анализа конкуренције </button> <span class="popup-text"></span> </span> </div> </div> </div> </div> <!-- Search Engine Positions --> <!-- Load Google Charts --> <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> <script> // Load Google Charts google.charts.load('current', {'packages':['corechart', 'line']}); </script> <script> // Google Charts loader (already in your HTML) google.charts.load('current', {'packages':['corechart', 'line']}); // Global variable to store chart instances let chartInstances = {}; // AJAX function for loading search phrases with pagination/filtering function loadSearchPhrases(engine, page) { const phraseFilterEl = document.getElementById(`${engine}PhraseFilter`); const pageFilterEl = document.getElementById(`${engine}PageFilter`); const phraseFilter = phraseFilterEl ? phraseFilterEl.value : ''; const pageFilter = pageFilterEl ? pageFilterEl.value : ''; const domain = document.getElementById('domain_name').value; const data = { type: 'search_phrases', engine: engine, page: page, domain: domain, phrase_filter: phraseFilter, page_filter: pageFilter }; // Show loading state for table const tbody = document.getElementById(`${engine}PhrasesBody`); if (tbody) { tbody.innerHTML = ` <tr> <td colspan="4" style="text-align: center; padding: 2rem;"> <div class="loading-spinner"></div> <p>Loading data...</p> </td> </tr> `; } // Show loading state for chart const chartContainer = document.getElementById(`chart_div_${engine}`); if (chartContainer) { chartContainer.innerHTML = ` <div style="text-align: center; padding: 4rem;"> <div class="loading-spinner"></div> <p>Updating chart...</p> </div> `; } sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { updateTableContent(engine, response.data); updatePagination(engine, response.pagination); // Generate and update chart with new data if (response.data && response.data.length > 0) { const chartData = generateChartData(response.data, engine); updateChart(engine, chartData); } else { // Hide chart if no data const chartContainerDiv = document.getElementById(`chart_container_${engine}`); if (chartContainerDiv) { chartContainerDiv.style.display = 'none'; } } } else { showErrorInTable(engine, response.error); } }); } // Generate chart data from API response function generateChartData(phrasesData, engine) { const chartData = { js_addColumn_str: '', js_dataRows_str: '' }; if (!phrasesData || phrasesData.length === 0) { return chartData; } // Parse all phrases and their history const allPhrases = []; const allHistory = []; phrasesData.forEach(item => { const pages = parsePageData(item.page_data); const titles = parseContentData(item.title_data); const snippets = parseContentData(item.snippet_data); const history = parsePositionHistory(item.position_history, pages, titles, snippets); if (history.length > 0) { allPhrases.push(item.phrase); allHistory.push({ phrase: item.phrase, history: history }); } }); if (allHistory.length === 0) { return chartData; } // Collect all unique days const allDays = new Set(); allHistory.forEach(item => { item.history.forEach(h => { allDays.add(h.day); }); }); // Sort days in ascending order const sortedDays = Array.from(allDays).sort((a, b) => a - b); // Generate columns chartData.js_addColumn_str = 'data.addColumn("date", "Day")\n'; allPhrases.forEach(phrase => { const label = phrase.length > 30 ? phrase.substring(0, 27) + '...' : phrase; chartData.js_addColumn_str += `data.addColumn("number", "${label}")\n`; }); // Count how many columns we should have const expectedColumnCount = 1 + allPhrases.length; // Generate data rows const dataRows = []; sortedDays.forEach(day => { // Convert day number to Date object const date = new Date(day * 86400 * 1000); // Create row array const row = [date]; // Add position for EACH phrase on this day allHistory.forEach(item => { const history = item.history; const entry = history.find(h => h.day === day); row.push(entry ? entry.position : null); }); // Adjust row length if needed if (row.length !== expectedColumnCount) { while (row.length < expectedColumnCount) { row.push(null); } while (row.length > expectedColumnCount) { row.pop(); } } dataRows.push(row); }); // Format rows for Google Charts if (dataRows.length > 0) { const rowStrings = dataRows.map(row => { const formattedRow = row.map((value, index) => { if (index === 0 && value instanceof Date) { const year = value.getFullYear(); const month = value.getMonth(); const day = value.getDate(); return `new Date(${year}, ${month}, ${day})`; } return value; }); return JSON.stringify(formattedRow); }); chartData.js_dataRows_str = rowStrings.join(',\n'); chartData.js_dataRows_str = chartData.js_dataRows_str.replace(/"new Date\((\d+), (\d+), (\d+)\)"/g, 'new Date($1, $2, $3)'); } return chartData; } // Update chart with new data function updateChart(engine, chartData) { if (!chartData || !chartData.js_dataRows_str || !chartData.js_addColumn_str) { console.warn(`No chart data for ${engine}`); const chartContainerDiv = document.getElementById(`chart_container_${engine}`); if (chartContainerDiv) { chartContainerDiv.style.display = 'none'; } return; } // Show chart container const chartContainerDiv = document.getElementById(`chart_container_${engine}`); if (chartContainerDiv) { chartContainerDiv.style.display = 'block'; } // Check if Google Charts is loaded if (typeof google === 'undefined' || typeof google.visualization === 'undefined') { console.warn('Google Charts not loaded yet'); google.charts.load('current', {'packages':['corechart', 'line']}); google.charts.setOnLoadCallback(function() { setTimeout(() => { drawChart(engine, chartData); }, 100); }); } else { setTimeout(() => { drawChart(engine, chartData); }, 100); } } // Draw chart function function drawChart(engine, chartData) { try { const chartContainer = document.getElementById(`chart_div_${engine}`); // Check if chart container exists if (!chartContainer) { console.warn(`Chart container for ${engine} not found`); return; } var data = new google.visualization.DataTable(); // Add date column data.addColumn('date', 'Day'); // Parse and add data columns const columns = chartData.js_addColumn_str.split('\n').filter(line => line.trim() !== ''); columns.forEach(col => { const match = col.match(/data\.addColumn\("(\w+)",\s*"([^"]+)"\)/); if (match) { const type = match[1]; const label = match[2]; if (type === 'date' && label === 'Day') { return; } data.addColumn(type, label); } }); // Parse data rows const rowsStr = chartData.js_dataRows_str.trim(); if (rowsStr && rowsStr !== '[]') { try { const rows = eval(`[${rowsStr}]`); // Adjust rows if needed const adjustedRows = rows.map(row => { if (!Array.isArray(row)) { return new Array(data.getNumberOfColumns()).fill(null); } if (row.length === data.getNumberOfColumns()) { return row; } const adjusted = new Array(data.getNumberOfColumns()).fill(null); // Copy available values const copyCount = Math.min(row.length, data.getNumberOfColumns()); for (let i = 0; i < copyCount; i++) { adjusted[i] = row[i]; } return adjusted; }); data.addRows(adjustedRows); } catch (e) { console.error(`Error parsing data rows:`, e); return; } } const domainName = document.getElementById('domain_name').value; const engineName = engine.charAt(0).toUpperCase() + engine.slice(1); var options = { chart: { title: `Search phrases for which the domain is at the top of the ${engine} search engine: "${domainName}"`, subtitle: 'Phrase position history' }, width: '100%', height: 400, curveType: 'function', legend: { position: 'bottom', maxLines: 3 }, pointSize: 3, pointsVisible: true, interpolateNulls: true, selectionMode: 'multiple', vAxis: { direction: -1, title: 'Position', viewWindow: { min: 1, max: 100 }, ticks: [ {v: 1, f: '1'}, {v: 10, f: '10'}, {v: 20, f: '20'}, {v: 30, f: '30'}, {v: 50, f: '50'}, {v: 100, f: '100'} ] }, hAxis: { title: 'Date', format: 'MMM yyyy' }, }; // Store chart instance for potential future updates const chart = new google.visualization.LineChart(chartContainer); chartInstances[engine] = chart; chart.draw(data, options); } catch (error) { console.error(`Error drawing chart for ${engine}:`, error); } } // Parse functions (same as before) function parsePageData(pageDataStr) { const pages = {}; if (!pageDataStr || pageDataStr.trim() === '') return pages; try { const pageItems = pageDataStr.split(';'); pageItems.forEach(item => { if (item.trim()) { const parts = item.split(':'); if (parts.length >= 2) { const id = parts[0].trim(); const page = parts.slice(1).join(':'); if (id) { pages[id] = decodeURIComponent(page).replace(/\+/g, ' '); } } } }); } catch (e) { console.error('Error parsing page data:', e); } return pages; } function parseContentData(contentDataStr) { const content = {}; if (!contentDataStr || contentDataStr.trim() === '') return content; try { const items = contentDataStr.split(';'); items.forEach(item => { if (item.trim()) { const parts = item.split(':'); if (parts.length >= 2) { const id = parts[0].trim(); const value = parts.slice(1).join(':'); if (id) { let decodedValue; try { decodedValue = decodeURIComponent(value); decodedValue = decodedValue.replace(/\\x([0-9A-Fa-f]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)) ); } catch (e) { decodedValue = value; } content[id] = decodedValue; } } } }); } catch (e) { console.error('Error parsing content data:', e); } return content; } function parsePositionHistory(historyStr, pages, titles, snippets) { const history = []; if (!historyStr || historyStr.trim() === '') return history; try { const entries = historyStr.split(';'); entries.forEach(entry => { if (entry.trim()) { const parts = entry.split(':'); if (parts.length >= 3) { const day = parseInt(parts[0]); const position = parseInt(parts[1]); const pageId = parts[2]; const titleId = parts[3] || ''; const snippetId = parts[4] || ''; if (!isNaN(day) && !isNaN(position)) { history.push({ day: day, position: position, page: pages[pageId] || '', title: titles[titleId] || '', snippet: snippets[snippetId] || '' }); } } } }); history.sort((a, b) => a.day - b.day); } catch (e) { console.error('Error parsing position history:', e); } return history; } // Update table content with AJAX response function updateTableContent(engine, data) { const tbody = document.getElementById(`${engine}PhrasesBody`); if (!tbody) return; let tableRows = ''; const domain = document.getElementById('domain_name').value; data.forEach(item => { const pages = parsePageData(item.page_data); const titles = parseContentData(item.title_data); const snippets = parseContentData(item.snippet_data); const history = parsePositionHistory(item.position_history, pages, titles, snippets); const latestEntry = history.length > 0 ? history[history.length - 1] : null; const position = latestEntry ? latestEntry.position : null; const page = latestEntry ? latestEntry.page : ''; const title = latestEntry ? latestEntry.title : ''; const snippet = latestEntry ? latestEntry.snippet : ''; let change = null; if (history.length >= 2) { const previousEntry = history[history.length - 2]; if (previousEntry && position !== null && previousEntry.position !== null) { change = previousEntry.position - position; } } const badgeClass = getPositionBadgeClass(position); const positionHtml = position ? `<span class="badge ${badgeClass}">${position}</span>` : '<span class="badge">N/A</span>'; const changeHtml = change !== null && change !== 0 ? `<span class="position-change ${change > 0 ? 'positive' : 'negative'}">(${change > 0 ? '+' : ''}${change})</span>` : ''; const hasTitle = title && title.trim() !== ''; const hasSnippet = snippet && snippet.trim() !== ''; const showSnippet = hasTitle || hasSnippet; let snippetHtml = ''; if (showSnippet) { snippetHtml = ` <button class="show-snippet-btn" onclick="toggleSnippet(this)">Show snippet</button> <div class="snippet" style="display: none;"> <noindex><a href="https://${domain}${escapeHtml(page)}" rel="nofollow" target="_blank"> ${hasTitle ? `<strong>${escapeHtml(title)}</strong><br>` : ''} </a></noindex> ${hasSnippet ? escapeHtml(snippet) : ''} </div> `; } else { snippetHtml = '<span style="color: var(--gray);">-</span>'; } tableRows += ` <tr> <td>${positionHtml}${changeHtml}</td> <td><noindex><a href="/en/phrase/${encodeURIComponent(item.phrase)}/" rel="nofollow">${escapeHtml(item.phrase)}</a></noindex></td> <td>${escapeHtml(page)}</td> <td>${snippetHtml}</td> </tr> `; }); tbody.innerHTML = tableRows; } // Update pagination with AJAX response function updatePagination(engine, pagination) { const paginationContainer = document.getElementById(`${engine}Pagination`); if (!paginationContainer) return; const { current_page, total_pages, has_previous, has_next } = pagination; let paginationButtons = ''; if (total_pages > 1) { // Previous button paginationButtons += `<button ${!has_previous ? 'disabled' : ''} onclick="loadSearchPhrases('${engine}', ${current_page - 1})">«</button>`; // First page paginationButtons += `<button class="${current_page === 1 ? 'active' : ''}" ${current_page === 1 ? 'disabled' : ''} onclick="loadSearchPhrases('${engine}', 1)">1</button>`; // Calculate range let startPage = Math.max(2, current_page - 2); let endPage = Math.min(total_pages - 1, current_page + 2); if (current_page <= 3) { endPage = Math.min(total_pages - 1, 5); } if (current_page >= total_pages - 2) { startPage = Math.max(2, total_pages - 4); } if (startPage > 2) { paginationButtons += `<button disabled>...</button>`; } for (let i = startPage; i <= endPage; i++) { const activeClass = i === current_page ? 'active' : ''; const disabledAttr = i === current_page ? 'disabled' : ''; paginationButtons += `<button class="${activeClass}" ${disabledAttr} onclick="loadSearchPhrases('${engine}', ${i})">${i}</button>`; } if (endPage < total_pages - 1) { paginationButtons += `<button disabled>...</button>`; } if (total_pages > 1) { paginationButtons += `<button class="${current_page === total_pages ? 'active' : ''}" ${current_page === total_pages ? 'disabled' : ''} onclick="loadSearchPhrases('${engine}', ${total_pages})">${total_pages}</button>`; } // Next button paginationButtons += `<button ${!has_next ? 'disabled' : ''} onclick="loadSearchPhrases('${engine}', ${current_page + 1})">»</button>`; } paginationContainer.innerHTML = paginationButtons; } // Show error in table function showErrorInTable(engine, errorMessage) { const tbody = document.getElementById(`${engine}PhrasesBody`); if (tbody) { tbody.innerHTML = ` <tr> <td colspan="4" style="text-align: center; padding: 2rem; color: var(--danger);"> <div style="margin-bottom: 1rem;"> <span style="font-size: 2rem;">❌</span> </div> <p><strong>Error loading data:</strong></p> <p>${escapeHtml(errorMessage)}</p> <button class="toggle-btn" onclick="loadSearchPhrases('${engine}', 1)" style="margin-top: 1rem;"> Retry </button> </td> </tr> `; } } // Helper functions function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function getPositionBadgeClass(position) { if (!position) return 'badge'; if (position <= 3) return 'badge-success'; if (position <= 10) return 'badge-warning'; return 'badge-danger'; } function toggleSnippet(button) { const snippet = button.nextElementSibling; const isVisible = snippet.style.display === 'block'; snippet.style.display = isVisible ? 'none' : 'block'; button.textContent = isVisible ? 'Show snippet' : 'Hide snippet'; } // Filter functions function applyFilters(engine) { loadSearchPhrases(engine, 1); } function clearFilters(engine) { const phraseFilterEl = document.getElementById(`${engine}PhraseFilter`); const pageFilterEl = document.getElementById(`${engine}PageFilter`); if (phraseFilterEl) phraseFilterEl.value = ''; if (pageFilterEl) pageFilterEl.value = ''; loadSearchPhrases(engine, 1); } // Initialize filter events on page load document.addEventListener('DOMContentLoaded', function() { // Initialize enter key for filter inputs ['google', 'yandex'].forEach(engine => { const phraseFilter = document.getElementById(`${engine}PhraseFilter`); const pageFilter = document.getElementById(`${engine}PageFilter`); if (phraseFilter) { phraseFilter.addEventListener('keypress', function(e) { if (e.key === 'Enter') { applyFilters(engine); } }); } if (pageFilter) { pageFilter.addEventListener('keypress', function(e) { if (e.key === 'Enter') { applyFilters(engine); } }); } }); // Load Google Charts google.charts.load('current', {'packages':['corechart', 'line']}); // Initial chart drawing for server-side data google.charts.setOnLoadCallback(function() { // You might want to extract chart data from initial server-side render // or make an initial AJAX call to get chart data for page 1 }); }); // AJAX request function function sendRequest(url, data, callback) { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); callback(response); } catch (e) { console.error("JSON parse error:", e); callback({status: 'error', error: 'Invalid response from server'}); } } else { callback({status: 'error', error: 'Server error: ' + xhr.status}); } } }; xhr.send(JSON.stringify(data)); } </script> <script> (function() { 'use strict'; // --- Configuration --- const STORAGE_KEY = 'serpuls_domain_sections_state'; const SECTIONS_TO_COLLAPSE = [ { id: 'domain-main-info', title: 'Маин Информатион', btnText: 'Инфо' }, { id: 'domain-server-info', title: 'Информације о серверу', btnText: 'Сервер' }, { id: 'domain-meta-tags', title: 'Листа мета ознака', btnText: 'Мета' }, { id: 'domain-internal-links', title: 'Интерне везе', btnText: 'Унутрашње' }, { id: 'domain-external-links', title: 'Ектернал Линкс', btnText: 'Екстерни' }, { id: 'domain-whois-info', title: 'Вхоис Информатион', btnText: 'Вхоис' }, { id: 'domain-seo-audit', title: 'СЕО Аудит', btnText: 'СЕО Аудит' } ]; // --- Helper Functions --- function saveSectionState(sectionId, isCollapsed) { const savedState = localStorage.getItem(STORAGE_KEY); const state = savedState ? JSON.parse(savedState) : {}; state[sectionId] = isCollapsed; localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function loadSectionState(sectionId) { const savedState = localStorage.getItem(STORAGE_KEY); if (!savedState) return false; const state = JSON.parse(savedState); return state[sectionId] || false; } // --- Create Floating TOC --- function createFloatingTOC() { // Find all major sections const sections = []; // Add main sections from the page const sectionSelectors = [ { id: 'domain-header', title: 'Информације о домену', selector: '.domain-header' }, { id: 'domain-description', title: 'Вебсите Десцриптион', selector: '.card:has(.card-title:contains("Вебсите Десцриптион"))' }, { id: 'domain-backup', title: 'Резервна копија домена', selector: '.card:has(.card-title:contains("Резервна копија домена"))' }, { id: 'domain-seo-score', title: 'СЕО Сцоре', selector: '.seo-score' }, { id: 'domain-main-info', title: 'Маин Информатион', selector: '.card:has(.card-title:contains("Маин Информатион"))' }, { id: 'domain-server-info', title: 'Информације о серверу', selector: '.card:has(.card-title:contains("Информације о серверу"))' }, { id: 'domain-meta-tags', title: 'Листа мета ознака', selector: '.card:has(.card-title:contains("Листа мета ознака"))' }, { id: 'domain-internal-links', title: 'Интерне везе', selector: '.card:has(.card-title:contains("Интерне везе"))' }, { id: 'domain-external-links', title: 'Ектернал Линкс', selector: '.card:has(.card-title:contains("Ектернал Линкс"))' }, { id: 'domain-whois-info', title: 'Вхоис Информатион', selector: '.card:has(.card-title:contains("Вхоис Информатион"))' }, { id: 'domain-whois-raw', title: 'Вхоис необрађени подаци', selector: '.card:has(.card-title:contains("Вхоис необрађени подаци"))' }, { id: 'domain-robots', title: 'Robots.txt', selector: '.card:has(.card-title:contains("Robots.txt"))' }, { id: 'domain-seo-audit', title: 'СЕО Аудит', selector: '.card:has(.card-title:contains("СЕО Аудит"))' }, { id: 'domain-google-positions', title: 'Позиције у Google', selector: '.chart-container:has(h2:contains("Google"))' }, { id: 'domain-google-phrases', title: 'Тражи фразе - Google', selector: '.card:has(.card-title:contains("Google"))' }, { id: 'domain-yandex-positions', title: 'Позиције у Yandex', selector: '.chart-container:has(h2:contains("Yandex"))' }, { id: 'domain-yandex-phrases', title: 'Тражи фразе - Yandex', selector: '.card:has(.card-title:contains("Yandex"))' }, { id: 'domain-additional-services', title: 'Додатне услуге', selector: '.card:has(.card-title:contains("Додатне услуге"))' } ]; sectionSelectors.forEach(section => { let element = null; // Try to find by selector if (section.selector) { if (section.selector.includes(':contains')) { // Simple contains selector const containsText = section.selector.match(/:contains\("([^"]+)"\)/); if (containsText) { const searchText = containsText[1]; const cards = document.querySelectorAll('.card, .chart-container, .seo-score, .domain-header'); for (const card of cards) { if (card.textContent.includes(searchText) && !card.id) { element = card; break; } } } } else { element = document.querySelector(section.selector); } } if (element && !element.id) { element.id = section.id; sections.push({ id: section.id, title: section.title, element: element }); } else if (element && element.id) { sections.push({ id: element.id, title: section.title, element: element }); } }); // Also add any card with an ID that wasn't captured document.querySelectorAll('.card[id], .chart-container[id], .seo-score[id]').forEach(el => { if (!sections.find(s => s.id === el.id)) { const titleEl = el.querySelector('.card-title, h2'); if (titleEl) { sections.push({ id: el.id, title: titleEl.innerText.trim(), element: el }); } } }); if (sections.length < 2) return; // Create TOC container const tocContainer = document.createElement('div'); tocContainer.id = 'floating-toc'; tocContainer.className = 'floating-toc'; tocContainer.innerHTML = ` <div class="floating-toc-header"> <span>📑 Садржај</span> <button id="toc-toggle-btn" class="toc-toggle-btn" aria-label="Скупи садржај">−</button> </div> <div id="toc-links-container" class="toc-links-container"> ${sections.map(section => `<a href="#${section.id}" class="toc-link" data-id="${section.id}">${escapeHtml(section.title)}</a>`).join('')} </div> `; document.body.appendChild(tocContainer); // Toggle TOC visibility const toggleBtn = document.getElementById('toc-toggle-btn'); const linksContainer = document.getElementById('toc-links-container'); let isTocExpanded = true; toggleBtn.addEventListener('click', () => { if (isTocExpanded) { linksContainer.style.display = 'none'; toggleBtn.textContent = '+'; tocContainer.classList.add('collapsed'); } else { linksContainer.style.display = 'block'; toggleBtn.textContent = '−'; tocContainer.classList.remove('collapsed'); } isTocExpanded = !isTocExpanded; }); // Highlight active section on scroll const tocLinks = document.querySelectorAll('.toc-link'); const sectionElements = sections.map(s => s.element); function updateActiveLink() { let current = ''; const scrollPosition = window.scrollY + 100; for (const section of sectionElements) { if (!section) continue; const sectionTop = section.offsetTop; const sectionBottom = sectionTop + section.offsetHeight; if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) { current = section.id; break; } } tocLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('data-id') === current) { link.classList.add('active'); } }); } window.addEventListener('scroll', updateActiveLink); updateActiveLink(); // Smooth scroll tocLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('data-id'); const targetElement = document.getElementById(targetId); if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); } // --- Add Collapsible Buttons to Sections --- function addCollapsibleButtons() { SECTIONS_TO_COLLAPSE.forEach(section => { // Find the card by ID or by title let targetCard = document.getElementById(section.id); if (!targetCard) { const cards = document.querySelectorAll('.card'); for (const card of cards) { const titleEl = card.querySelector('.card-title'); if (titleEl && titleEl.innerText.trim() === section.title) { targetCard = card; break; } } } if (!targetCard) return; // Set ID if not exists if (!targetCard.id) targetCard.id = section.id; // Create button const button = document.createElement('button'); button.className = 'collapse-section-btn'; button.setAttribute('data-section-id', section.id); button.setAttribute('aria-label', 'Скупи/прошири одељак'); // Get initial state const isCollapsed = loadSectionState(section.id); if (isCollapsed) { button.innerHTML = '➕'; targetCard.classList.add('section-collapsed'); const content = targetCard.querySelector('.card-content'); if (content) content.style.display = 'none'; } else { button.innerHTML = '➖'; targetCard.classList.remove('section-collapsed'); const content = targetCard.querySelector('.card-content'); if (content) content.style.display = ''; } // Insert button into card-header const cardHeader = targetCard.querySelector('.card-header'); if (cardHeader) { const iconDiv = cardHeader.querySelector('.card-icon'); if (iconDiv) { cardHeader.insertBefore(button, iconDiv); } else { cardHeader.appendChild(button); } // Add click handler button.addEventListener('click', (e) => { e.stopPropagation(); const sectionId = button.getAttribute('data-section-id'); const card = document.getElementById(sectionId); if (!card) return; const content = card.querySelector('.card-content'); if (!content) return; if (card.classList.contains('section-collapsed')) { content.style.display = ''; card.classList.remove('section-collapsed'); button.innerHTML = '➖'; saveSectionState(sectionId, false); } else { content.style.display = 'none'; card.classList.add('section-collapsed'); button.innerHTML = '➕'; saveSectionState(sectionId, true); } }); } }); } // --- Create Back to Top Button --- function createBackToTopButton() { const btn = document.createElement('button'); btn.id = 'back-to-top'; btn.innerHTML = '↑'; btn.setAttribute('aria-label', 'Назад на врх'); btn.className = 'back-to-top-btn'; document.body.appendChild(btn); window.addEventListener('scroll', () => { if (window.scrollY > 500) { btn.classList.add('visible'); } else { btn.classList.remove('visible'); } }); btn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } // --- Improve Table Responsiveness --- function improveTables() { const tables = document.querySelectorAll('.data-table, .tasks-table'); tables.forEach(table => { const container = table.closest('.table-responsive') || table.parentElement; if (container && !container.querySelector('.scroll-hint')) { const hint = document.createElement('div'); hint.className = 'scroll-hint'; hint.innerHTML = '← → Превуците да бисте видели више'; hint.style.cssText = ` display: none; text-align: center; font-size: 0.7rem; color: var(--gray); margin-top: 0.5rem; padding: 0.25rem; background: rgba(67, 97, 238, 0.05); border-radius: 12px; `; container.style.position = 'relative'; container.appendChild(hint); function checkScroll() { if (container.scrollWidth > container.clientWidth) { hint.style.display = 'block'; } else { hint.style.display = 'none'; } } checkScroll(); window.addEventListener('resize', checkScroll); container.addEventListener('scroll', () => { hint.style.opacity = '0.5'; setTimeout(() => { hint.style.opacity = '1'; }, 300); }); } }); } // --- Escape HTML helper --- function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // --- Add custom styles --- function addCustomStyles() { const style = document.createElement('style'); style.textContent = ` .floating-toc { position: fixed; top: 80px; right: 20px; z-index: 1000; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); border: 1px solid #e9ecef; width: 260px; max-width: calc(100% - 40px); transition: all 0.3s ease; backdrop-filter: blur(8px); background: rgba(255,255,255,0.95); } .floating-toc-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; border-radius: 12px 12px 0 0; font-weight: 600; font-size: 0.9rem; cursor: pointer; } .toc-toggle-btn { background: none; border: none; color: white; font-size: 1.2rem; cursor: pointer; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background 0.2s; } .toc-toggle-btn:hover { background: rgba(255,255,255,0.2); } .floating-toc.collapsed { width: auto; } .floating-toc.collapsed .floating-toc-header { border-radius: 12px; } .toc-links-container { max-height: 400px; overflow-y: auto; padding: 8px 0; } .toc-link { display: block; padding: 8px 15px; color: var(--dark); text-decoration: none; font-size: 0.85rem; transition: all 0.2s; border-left: 3px solid transparent; } .toc-link:hover { background: rgba(67, 97, 238, 0.05); color: var(--primary); padding-left: 20px; } .toc-link.active { background: rgba(67, 97, 238, 0.1); border-left-color: var(--primary); color: var(--primary); font-weight: 500; } .collapse-section-btn { background: none; border: none; font-size: 1.1rem; cursor: pointer; width: 28px; height: 28px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; transition: all 0.2s; color: var(--gray); margin-right: 8px; } .collapse-section-btn:hover { background: rgba(67, 97, 238, 0.1); color: var(--primary); } .card.section-collapsed .card-header { border-bottom: none; } .back-to-top-btn { position: fixed; bottom: 30px; right: 30px; width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; border: none; cursor: pointer; font-size: 1.5rem; display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: all 0.3s ease; z-index: 999; box-shadow: 0 4px 12px rgba(67, 97, 238, 0.4); } .back-to-top-btn.visible { opacity: 1; visibility: visible; } .back-to-top-btn:hover { transform: translateY(-3px); box-shadow: 0 6px 16px rgba(67, 97, 238, 0.5); } @media (max-width: 768px) { .floating-toc { top: auto; bottom: 20px; right: 20px; left: auto; width: 48px; overflow: hidden; } .floating-toc .floating-toc-header span { display: none; } .floating-toc .floating-toc-header { justify-content: center; padding: 12px; } .floating-toc .toc-toggle-btn { margin: 0; } .floating-toc:not(.collapsed) { width: 260px; left: auto; right: 20px; bottom: 20px; top: auto; } .floating-toc:not(.collapsed) .floating-toc-header span { display: inline; } .floating-toc:not(.collapsed) .floating-toc-header { justify-content: space-between; } .back-to-top-btn { bottom: 80px; right: 20px; width: 40px; height: 40px; font-size: 1.2rem; } .scroll-hint { display: block !important; font-size: 0.65rem; } } html { scroll-behavior: smooth; } .card .card-content { transition: display 0.2s ease; } [dir="rtl"] .floating-toc { right: auto; left: 20px; } [dir="rtl"] .back-to-top-btn { right: auto; left: 30px; } [dir="rtl"] .collapse-section-btn { margin-right: 0; margin-left: 8px; } `; document.head.appendChild(style); } // --- Initialize --- function init() { addCustomStyles(); createFloatingTOC(); addCollapsibleButtons(); createBackToTopButton(); improveTables(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script></div> <!-- Footer --> <footer> <div class="footer-container"> <div class="footer-content"> <div class="footer-section"> <h3>About serpuls.com</h3> <div class="footer-info"> <p>Професионални алат за анализу претраживача, праћење позиције и СЕО оптимизацију.</p> <p style="margin-top: 1rem;"><strong>Skynet360 LP.</strong></p> <p><strong>Предузеће бр:</strong> SL027564</p> <p><strong>Адреса:</strong> Suite 1 4 Queen Street, Edinburgh, Scotland, EH2 1JE</p> </div> </div> <div class="footer-section"> <h3>Брзе везе</h3> <ul class="footer-links"> <li><a href="/sr/">Хоме</a></li> <li><a href="/sr/profile/">Профил</a></li> <li><a href="/sr/tasks/">Задаци праћења</a></li> <li><a href="/sr/api_docs/">АПИ документација</a></li> <li><a href="/sr/team/">Наш тим</a></li> </ul> </div> <div class="footer-section"> <h3>Ресурси</h3> <ul class="footer-links"> <li><a href="/sr/blogs/">Блог</a></li> <li><a href="/sr/faq/">ФАК</a></li> <li><a href="/sr/support/">Подршка</a></li> </ul> </div> <div class="footer-section"> <h3>Контакти</h3> <div class="footer-info"> <p>Емаил: <a href="mailto:info@serpuls.com">info@serpuls.com</a></p> <p>Телеграм: <a href="https://t.me/serpulse_support">@serpulse_support</a></p> <div class="social-links"> <a href="#" class="social-link" title="Телеграм">📱</a> <a href="https://github.com/SkyNetJose/serpuls" class="social-link" title="ГитХуб">💻</a> <a href="#" class="social-link" title="Твиттер">🐦</a> </div> </div> </div> </div> <div class="footer-bottom"> <p class="copyright">© 2026 serpuls.com. Сва права задржана. Skynet360 LP.</p> <div class="footer-bottom-links"> <a href="/sr/privacy/">Политика приватности</a> <a href="/sr/terms/">Услови коришћења</a> <a href="/sr/agreement/">Кориснички уговор</a> <a href="/sr/cookie/">Политика колачића</a> </div> </div> </div> </footer> <!-- Updated Payment Modal HTML --> <div class="modal" id="paymentModal"> <div class="modal-content"> <span class="close-btn">×</span> <div class="modal-success" id="paymentSuccess"> <div class="success-icon">✓</div> <h3>Плаћање успешно!</h3> <p>Хвала вам на наруџбини. Ускоро ћемо вас контактирати.</p> <button class="cta-button" onclick="closeModal('paymentModal')">Затвори</button> </div> <form id="paymentForm" onsubmit="return processPayment(event)"> <h2 class="modal-title" id="modalTitle">Плаћање услуге</h2> <p class="modal-text" id="modalText">Опис услуге</p> <!-- Hidden fields for payment type and ID --> <input type="hidden" id="paymentType" name="payment_type" value=""> <input type="hidden" id="paymentItemId" name="payment_item_id" value=""> <!-- Payment Options --> <div class="payment-options" id="paymentOptions"> <!-- Options will be dynamically generated here --> </div> <!-- Manual Price Input (Optional) - Now dynamically shown based on config --> <div class="form-group" id="manualPriceGroup" style="display: none;"> <label for="manualPrice">Унесите износ (УСД):</label> <input type="number" id="manualPrice" class="form-control" placeholder="Унесите износ" min="1" max="10000" step="0.01" oninput="updateManualPrice(this.value)"> <div class="error-message" id="manualPriceError"></div> </div> <!-- Custom Price Option Toggle - Now dynamically shown --> <div class="form-group" id="customPriceToggle" style="display: none; margin-bottom: 1rem;"> <label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;"> <input type="radio" id="customPriceRadio" name="paymentOption" value="custom" onchange="toggleCustomPriceSelection(this.checked)"> Унесите прилагођени износ </label> </div> <div class="form-group"> <label for="modalName">име:</label> <input type="text" id="modalName" class="form-control" placeholder="Унесите своје име" required> <div class="error-message" id="modalNameError"></div> </div> <div class="form-group"> <label for="modalEmail">Емаил:</label> <input type="email" id="modalEmail" class="form-control" placeholder="Унесите своју е-пошту" required> <div class="error-message" id="modalEmailError"></div> </div> <p class="modal-price" id="modalPrice">Цена: $0.00</p> <button type="submit" class="cta-button" id="payButton">Плати $0.00</button> </form> </div> </div> <!-- Redirect Modal (for iframe) --> <div class="modal" id="redirectModal"> <div class="modal-content modal-large"> <span class="close-btn">×</span> <h2 class="modal-title">Плаћање</h2> <iframe id="redirectIframe" class="modal-iframe" src=""></iframe> </div> </div> <!-- Auth Modal --> <div class="modal" id="authModal"> <div class="modal-content"> <span class="close-btn">×</span> <div class="auth-tabs"> <div class="auth-tab active" data-tab="login">Логин</div> <div class="auth-tab" data-tab="register">Региструјте се</div> </div> <div class="auth-form active" id="loginForm"> <h2 class="modal-title">Пријавите се на свој налог</h2> <!-- Regular Login --> <div class="form-group"> <label for="loginEmail">Емаил:</label> <input type="email" id="loginEmail" class="form-control" placeholder="Унесите своју е-пошту"> <div class="error-message" id="loginEmailError"></div> </div> <div class="form-group"> <label for="loginPassword">Лозинка:</label> <input type="password" id="loginPassword" class="form-control" placeholder="Унесите своју лозинку"> <div class="error-message" id="loginPasswordError"></div> </div> <button class="cta-button" style="width: 100%;" onclick="processLogin()">Логин</button> <!-- Telegram Login Option --> <div style="text-align: center; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #e9ecef;"> <p style="margin-bottom: 1rem; color: var(--gray);">Или се пријавите преко Телеграма</p> <button class="cta-button" style="background: #0088cc; width: 100%;" onclick="loginWithTelegram()"> <span style="margin-right: 0.5rem;">📱</span> Пријавите се преко Телеграма </button> </div> </div> <div class="auth-form" id="registerForm"> <h2 class="modal-title">Региструјте се преко Телеграма</h2> <!-- Registration Form --> <div class="form-group"> <label for="registerName">име:</label> <input type="text" id="registerName" class="form-control" placeholder="Унесите своје име" required> <div class="error-message" id="registerNameError"></div> </div> <div class="form-group"> <label for="registerEmail">Емаил:</label> <input type="email" id="registerEmail" class="form-control" placeholder="Унесите своју е-пошту" required> <div class="error-message" id="registerEmailError"></div> </div> <button class="cta-button" style="background: #0088cc; width: 100%; padding: 1rem; font-size: 1.1rem;" onclick="startTelegramRegistration()"> <span style="margin-right: 0.5rem;">🚀</span> Наставите са регистрацијом у Телеграму </button> <div class="how-it-works"> <h4 class="how-it-works-title">Како то ради:</h4> <ol class="how-it-works-list"> <li>Унесите име и имејл изнад</li> <li>Кликните на дугме да отворите Телеграм</li> <li>Заврши регистрацију у боту (потребан је број телефона)</li> <li>Примите акредитиве за пријаву путем е-поште</li> </ol> </div> </div> </div> </div> <!-- Telegram Code Input Modal --> <div class="modal" id="telegramCodeModal"> <div class="modal-content"> <span class="close-btn" onclick="closeModal('telegramCodeModal')">×</span> <div class="centered-content"> <div class="success-icon">🔐</div> <h2 class="modal-title">Верификациони код</h2> <p class="modal-text">Код је послат Телеграму. Унесите га испод:</p> <div class="form-group"> <input type="text" id="telegramLoginCode" class="form-control verify-code-input" placeholder="0000000" maxlength="7" style="font-size: 1.5rem; letter-spacing: 0.5rem;"> <div class="error-message" id="telegramLoginCodeError"></div> </div> <button class="cta-button" style="width: 100%; margin-top: 1rem;" onclick="verifyTelegramLoginCode()"> Потврдите пријаву </button> <div class="timer-container" id="loginCodeTimer"> Код важи за: <span id="loginTimer">05:00</span> </div> </div> </div> </div> <!-- Updated Telegram Verification Modal --> <div class="modal" id="telegramVerifyModal"> <div class="modal-content"> <span class="close-btn" onclick="closeTelegramVerifyModal()">×</span> <div class="centered-content"> <div class="success-icon">📱</div> <h2 class="modal-title">Завршите регистрацију у Телеграму</h2> <p class="modal-text" id="telegramModalText"> Телеграм је отворен у новој картици.<br> Ако се бот није аутоматски отворио, користите дугме испод или скенирајте КР код. </p> <!-- QR Code Container --> <div id="qrCodeContainer" style="margin: 1.5rem 0; text-align: center;"> <div id="qrcode" style="display: inline-block;"></div> <p style="font-size: 0.9rem; color: var(--gray); margin-top: 0.5rem;"> Скенирајте КР код да бисте отворили бот </p> </div> <!-- Timer and Status --> <div id="telegramTimer" class="timer-container"> Чека се потврда... <span id="timer">05:00</span> </div> <!-- Action Buttons --> <div style="display: flex; gap: 1rem; margin-top: 1.5rem; flex-wrap: wrap; justify-content: center;"> <button class="cta-button" style="background: #0088cc;" onclick="openTelegramBot()" id="openTelegramBtn"> 📱 Поново отворите Телеграм </button> <button class="cta-button secondary" onclick="refreshTelegramQR()" id="refreshQRBtn"> 🔄 Освежите КР код </button> </div> <!-- Status Info --> <div class="info-box" style="margin-top: 1.5rem;"> <p style="margin: 0; font-size: 0.9rem;"> <strong>Како то ради:</strong><br> 1. Кликните на Отвори Телеграм или скенирајте КР код<br> 2. У боту кликните на СТАРТ и поделите свој број телефона<br> 3. Статус потврде ће се аутоматски ажурирати </p> </div> </div> </div> </div> <!-- Email Verification Modal --> <div class="modal" id="emailVerifyModal"> <div class="modal-content"> <span class="close-btn" onclick="closeEmailVerifyModal()">×</span> <div class="centered-content"> <div class="success-icon">📧</div> <h2 class="modal-title">Верификација е-поште</h2> <p class="modal-text">Послали смо верификациони емаил на вашу адресу</p> <!-- Email Verification Input --> <div class="form-group"> <label for="emailVerifyToken">Верификациони код:</label> <input type="text" id="emailVerifyToken" class="form-control" placeholder="Унесите код из е-поште" style="text-align: center; font-size: 1.1rem;"> <div class="error-message" id="emailVerifyTokenError"></div> </div> <button class="cta-button" style="width: 100%; margin-bottom: 1rem;" onclick="verifyEmailToken()"> Потврдите е-пошту </button> <button id="resendEmailBtn" class="cta-button secondary" style="width: 100%;" onclick="resendEmailVerification()" disabled> Поново пошаљи код (<span id="resendTimer">300</span>) </button> <!-- Auto-check status --> <div id="emailCheckStatus" style="margin-top: 1rem; display: none;"> <p style="color: var(--gray); font-size: 0.9rem;"> 🔍 Провера статуса верификације... </p> </div> </div> </div> </div> <!-- Image reusable Modal --> <div class="modal" id="imgModal"> <div class="modal-content"> <span class="close-btn">×</span> <img id="modalImg" src="" style="max-width:90%; max-height:90%; margin-top:40px; border-radius:8px;"> </div> </div> <!--Loading overlay --> <div class="loading-overlay" id="loadingOverlay"> <div class="loading-spinner"></div> </div> <div class="toast" id="toast"></div> <script> // Get language from URL path like /en/about or /ru/products function getLangFromUrl() { const path = window.location.pathname; const match = path.match(/^\/([a-z]{2})(\/|$)/i); return match ? match[1] : 'en'; // default to 'en' if no language code found } // Store in global variable window.currentLang = getLangFromUrl(); // Show loading overlay function showLoading() { document.getElementById('loadingOverlay').classList.add('active'); } // Hide loading overlay function hideLoading() { document.getElementById('loadingOverlay').classList.remove('active'); } // Show toast notification function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = 'toast ' + type; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); }, 3000); } // Check user login status on page load document.addEventListener('DOMContentLoaded', function() { checkUserLoginStatus(); }); // Function to check if user is logged in function checkUserLoginStatus() { const userInfoElement = document.getElementById('userInfo'); const guestButtonsElement = document.getElementById('guestButtons'); const userNameElement = document.getElementById('userName'); // Show loading state or keep defaults while checking if (guestButtonsElement) { guestButtonsElement.style.display = 'flex'; } if (userInfoElement) { userInfoElement.style.display = 'none'; } const data = { type: 'check_user_logined' }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { if (response.user_logined === 1) { // User is logged in if (userInfoElement) { userInfoElement.style.display = 'block'; } if (guestButtonsElement) { guestButtonsElement.style.display = 'none'; } if (userNameElement) { // Use logined_user_name if available, otherwise show default userNameElement.textContent = response.logined_user_name || 'Профил'; } // Optionally store minimal info in localStorage for quick access // but don't rely on it for auth state if (response.logined_user_id) { localStorage.setItem('user_id', response.logined_user_id); } if (response.logined_user_name) { localStorage.setItem('user_name', response.logined_user_name); } } else { // User is not logged in if (userInfoElement) { userInfoElement.style.display = 'none'; } if (guestButtonsElement) { guestButtonsElement.style.display = 'flex'; } // Clear any previously stored user data localStorage.removeItem('user_id'); localStorage.removeItem('user_name'); localStorage.removeItem('user_info'); } } else { console.error('Грешка при провери статуса пријаве:', response.error); // On error, default to guest view if (userInfoElement) { userInfoElement.style.display = 'none'; } if (guestButtonsElement) { guestButtonsElement.style.display = 'flex'; } } }, false); // Don't show loading overlay for this quick check } // Logout function function logout() { // Send logout request to server const data = { type: 'logout' }; sendRequest('/ajax/', data, function(response) { // Clear all user data from localStorage regardless of response localStorage.removeItem('user_info'); localStorage.removeItem('user_id'); localStorage.removeItem('user_name'); localStorage.removeItem('current_user_token'); // Update UI checkUserLoginStatus(); // Redirect to main page window.location.href = '/'; }); } </script> <script> // Mobile menu toggle - Combined approach document.getElementById('hamburger').addEventListener('click', function() { const mobileMenu = document.getElementById('mobileMenu'); mobileMenu.classList.toggle('active'); }); // Close mobile menu when clicking on links or buttons document.querySelectorAll('.nav-links a, .auth-buttons button').forEach(element => { element.addEventListener('click', function() { document.getElementById('mobileMenu').classList.remove('active'); }); }); // Close mobile menu when clicking outside document.addEventListener('click', function(event) { const mobileMenu = document.getElementById('mobileMenu'); const hamburger = document.getElementById('hamburger'); const navContainer = document.querySelector('.nav-container'); if (!navContainer.contains(event.target) && mobileMenu.classList.contains('active')) { mobileMenu.classList.remove('active'); } }); // Simple animation on scroll document.addEventListener('DOMContentLoaded', function() { const cards = document.querySelectorAll('.card, .domain-header, .chart-container, .lemma-card'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; } }); }, { threshold: 0.0001, // Optional: keep small percentage rootMargin: '350px 0px 350px 0px' // Trigger 100px before element enters }); cards.forEach(card => { card.style.opacity = '0'; card.style.transform = 'translateY(20px)'; card.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; observer.observe(card); }); }); // Toggle links visibility function toggleLinks(className, btn, max_count = 5) { const list = document.getElementById(className); const items = list.querySelectorAll('li'); const hiddenItems = list.querySelectorAll('li.hidden'); if (hiddenItems.length > 0) { // Show all list.querySelectorAll('li.hidden').forEach(el => el.classList.remove('hidden')); btn.textContent = 'Сакриј се'; } else { // Hide all beyond the first 5 items.forEach((el, index) => { if (index >= max_count) el.classList.add('hidden'); }); btn.textContent = 'Покажи више'; } } // Toggle raw data visibility function toggleRawData(elementId, btn) { const element = document.getElementById(elementId); if (element.style.maxHeight === '200px') { element.style.maxHeight = 'none'; btn.textContent = 'Сакриј пуне податке'; } else { element.style.maxHeight = '200px'; btn.textContent = 'Прикажи пуне податке'; } } </script> <script> // Payment Modal // Global variable to store payment config let paymentConfig = { allowCustomAmount: false, minAmount: 1, maxAmount: 1000 }; // Payment options functionality document.addEventListener('DOMContentLoaded', function() { // Replace the entire payment options event listener with this: document.addEventListener('change', function(e) { if (e.target && e.target.name === 'paymentOption') { // Update selected state for regular options document.querySelectorAll('.payment-option').forEach(opt => { opt.classList.remove('selected'); }); // Handle custom price radio separately if (e.target.id === 'customPriceRadio') { // For custom price, we don't have a .payment-option wrapper // Just update the custom price toggle UI document.querySelectorAll('.payment-option').forEach(opt => { opt.classList.remove('selected'); }); // You might want to add visual feedback for custom price selection const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = 'rgba(67, 97, 238, 0.1)'; customPriceToggle.style.borderLeft = '3px solid var(--primary)'; customPriceToggle.style.padding = '0.5rem'; customPriceToggle.style.borderRadius = 'var(--border-radius)'; } } else { // For regular options, highlight the .payment-option container const paymentOption = e.target.closest('.payment-option'); if (paymentOption) { paymentOption.classList.add('selected'); // Reset custom price toggle styling if it was highlighted const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = ''; customPriceToggle.style.borderLeft = ''; customPriceToggle.style.padding = ''; } } } // Update price (this should work for all options) updatePaymentPrice(e.target.value); } }); }); // Update the updatePaymentPrice function to handle custom price: function updatePaymentPrice(price) { const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); const customPriceRadio = document.getElementById('customPriceRadio'); // Uncheck custom price radio if regular option is selected if (customPriceRadio && customPriceRadio.checked === false) { document.getElementById('manualPriceGroup').style.display = 'none'; // Reset custom price toggle styling const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = ''; customPriceToggle.style.borderLeft = ''; customPriceToggle.style.padding = ''; } } modalPrice.textContent = 'Цена: $' + parseFloat(price).toFixed(2); payButton.textContent = 'Плати $' + parseFloat(price).toFixed(2); payButton.disabled = false; payButton.style.opacity = '1'; } // Generate payment options based on config function generatePaymentOptions(optionsData, paymentType = 'balance', paymentItemId = '', config = {}) { const paymentOptionsContainer = document.getElementById('paymentOptions'); const paymentTypeInput = document.getElementById('paymentType'); const paymentItemIdInput = document.getElementById('paymentItemId'); const customPriceToggle = document.getElementById('customPriceToggle'); const manualPriceGroup = document.getElementById('manualPriceGroup'); // Set hidden fields paymentTypeInput.value = paymentType; paymentItemIdInput.value = paymentItemId; // Store config globally paymentConfig = { allowCustomAmount: config.allowCustomAmount || false, minAmount: config.minAmount || 1, maxAmount: config.maxAmount || 1000 }; // Clear container paymentOptionsContainer.innerHTML = ''; // Generate regular options if (optionsData && optionsData.length > 0) { optionsData.forEach((option, index) => { const optionElement = document.createElement('div'); optionElement.className = 'payment-option'; optionElement.innerHTML = ` <input type="radio" id="option${index}" name="paymentOption" value="${option.price}" data-label="${option.label}" ${option.default ? 'checked' : ''} onchange="updatePaymentPrice('${option.price}')"> <label for="option${index}">${option.label} - $${option.price}</label> `; paymentOptionsContainer.appendChild(optionElement); }); paymentOptionsContainer.style.display = 'block'; } else { paymentOptionsContainer.style.display = 'none'; } // Show/hide custom amount option based on config if (paymentConfig.allowCustomAmount) { customPriceToggle.style.display = 'block'; // Update manual price input constraints document.getElementById('manualPrice').min = paymentConfig.minAmount; document.getElementById('manualPrice').max = paymentConfig.maxAmount; document.getElementById('manualPrice').placeholder = 'Од $${paymentConfig.minAmount} да $${paymentConfig.maxAmount}'; } else { customPriceToggle.style.display = 'none'; manualPriceGroup.style.display = 'none'; } // Select first option by default if none is marked as default if (optionsData && optionsData.length > 0) { const defaultOption = paymentOptionsContainer.querySelector('input[type="radio"][checked]') || paymentOptionsContainer.querySelector('input[type="radio"]'); if (defaultOption) { defaultOption.checked = true; defaultOption.closest('.payment-option').classList.add('selected'); updatePaymentPrice(defaultOption.value); } } else if (paymentConfig.allowCustomAmount) { // If no options but custom amount is allowed, show custom by default document.getElementById('customPriceRadio').checked = true; toggleCustomPriceSelection(true); } } // Also update the toggleCustomPriceSelection function: function toggleCustomPriceSelection(isSelected) { const manualPriceGroup = document.getElementById('manualPriceGroup'); const paymentOptions = document.getElementById('paymentOptions'); const customPriceRadio = document.getElementById('customPriceRadio'); if (isSelected) { manualPriceGroup.style.display = 'block'; // Deselect all regular options document.querySelectorAll('input[name="paymentOption"]').forEach(radio => { if (radio.id !== 'customPriceRadio') { radio.checked = false; const paymentOption = radio.closest('.payment-option'); if (paymentOption) { paymentOption.classList.remove('selected'); } } }); // Visual feedback for custom price selection const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = 'rgba(67, 97, 238, 0.1)'; customPriceToggle.style.borderLeft = '3px solid var(--primary)'; customPriceToggle.style.padding = '0.5rem'; customPriceToggle.style.borderRadius = 'var(--border-radius)'; customPriceToggle.style.transition = 'var(--transition)'; } // Focus on manual price input setTimeout(() => { const manualInput = document.getElementById('manualPrice'); manualInput.focus(); // Update price if there's already a value if (manualInput.value) { updateManualPrice(manualInput.value); } else { // Set default to min amount updateManualPrice(paymentConfig.minAmount); manualInput.value = paymentConfig.minAmount; } }, 100); } else { manualPriceGroup.style.display = 'none'; // Reset custom price toggle styling const customPriceToggle = document.getElementById('customPriceToggle'); if (customPriceToggle) { customPriceToggle.style.backgroundColor = ''; customPriceToggle.style.borderLeft = ''; customPriceToggle.style.padding = ''; } } } // Update price from manual input in real-time function updateManualPrice(price) { if (!price || price < paymentConfig.minAmount || price > paymentConfig.maxAmount) { const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); if (price && (price < paymentConfig.minAmount || price > paymentConfig.maxAmount)) { modalPrice.textContent = 'Цена: из $${paymentConfig.minAmount} да $${paymentConfig.maxAmount}'; payButton.textContent = 'Неважећи износ'; payButton.disabled = true; payButton.style.opacity = '0.6'; } else { modalPrice.textContent = 'Цена: $0.00'; payButton.textContent = 'Плати $0.00'; payButton.disabled = true; payButton.style.opacity = '0.6'; } return; } const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); modalPrice.textContent = 'Цена: $' + parseFloat(price).toFixed(2); payButton.textContent = 'Плати $' + parseFloat(price).toFixed(2); payButton.disabled = false; payButton.style.opacity = '1'; } const paymentModal = document.getElementById('paymentModal'); const paymentCloseBtn = paymentModal.querySelector('.close-btn'); // Elements inside modal const modalTitle = document.getElementById('modalTitle'); const modalText = document.getElementById('modalText'); const modalPrice = document.getElementById('modalPrice'); const payButton = document.getElementById('payButton'); const paymentForm = document.getElementById('paymentForm'); const paymentSuccess = document.getElementById('paymentSuccess'); // Updated modal opening handler document.querySelectorAll('[data-modal="paymentModal"]').forEach(trigger => { trigger.addEventListener('click', function(e) { e.preventDefault(); // Get data attributes const title = this.getAttribute('data-title'); const text = this.getAttribute('data-text'); const price = this.getAttribute('data-price'); const paymentType = this.getAttribute('data-payment-type') || 'balance'; const paymentItemId = this.getAttribute('data-payment-item-id') || ''; // Get payment config from data attributes const allowCustomAmount = this.getAttribute('data-allow-custom') === 'true'; const minAmount = parseFloat(this.getAttribute('data-min-amount')) || 1; const maxAmount = parseFloat(this.getAttribute('data-max-amount')) || 1000; // Get payment options from data attribute const optionsData = this.getAttribute('data-options'); let paymentOptions = []; if (optionsData) { try { paymentOptions = JSON.parse(optionsData); } catch (e) { console.error('Грешка при рашчлањивању опција плаћања:', e); } } // Fill modal content document.getElementById('modalTitle').textContent = title; document.getElementById('modalText').textContent = text; // Generate payment options with config const config = { allowCustomAmount: allowCustomAmount, minAmount: minAmount, maxAmount: maxAmount }; generatePaymentOptions(paymentOptions, paymentType, paymentItemId, config); // If no options and custom amount not allowed, use default price if (paymentOptions.length === 0 && !allowCustomAmount) { updatePaymentPrice(price); } // Reset form resetForm('paymentModal'); document.getElementById('paymentForm').style.display = 'block'; document.getElementById('paymentSuccess').style.display = 'none'; // Show modal document.getElementById('paymentModal').style.display = 'flex'; }); }); // Helper function to reset form function resetPaymentForm() { document.getElementById('modalName').value = ''; document.getElementById('modalEmail').value = ''; document.getElementById('manualPrice').value = ''; if (document.getElementById('customPriceRadio')) { document.getElementById('customPriceRadio').checked = false; } clearErrors(['nameError', 'emailError', 'manualPriceError']); } // Close payment modal paymentCloseBtn.addEventListener('click', function() { paymentModal.style.display = 'none'; }); // Close on outside click window.addEventListener('click', function(e) { if (e.target === paymentModal) { paymentModal.style.display = 'none'; } }); // Fill tooltip with alt text automatically document.querySelectorAll('.popup-trigger').forEach(trigger => { const altText = trigger.getAttribute('alt'); const popupText = trigger.querySelector('.popup-text'); if (popupText && altText) { popupText.textContent = altText; } }); </script> <script> // Fill tooltip with alt text automatically document.querySelectorAll('.popup-trigger').forEach(trigger => { const altText = trigger.getAttribute('alt'); const popupText = trigger.querySelector('.popup-text'); if (popupText && altText) { popupText.textContent = altText; } }); // Auth Modal const authModal = document.getElementById('authModal'); const authCloseBtn = authModal.querySelector('.close-btn'); const loginBtn = document.getElementById('loginBtn'); const registerBtn = document.getElementById('registerBtn'); const authTabs = document.querySelectorAll('.auth-tab'); const authForms = document.querySelectorAll('.auth-form'); // Open auth modal loginBtn.addEventListener('click', function() { authModal.style.display = 'flex'; switchAuthTab('login'); resetForm('authModal'); }); registerBtn.addEventListener('click', function() { authModal.style.display = 'flex'; switchAuthTab('register'); resetForm('authModal'); }); // Close auth modal authCloseBtn.addEventListener('click', function() { authModal.style.display = 'none'; }); // Close on outside click window.addEventListener('click', function(e) { if (e.target === authModal) { authModal.style.display = 'none'; } }); // Switch auth tabs function switchAuthTab(tabName) { authTabs.forEach(tab => { if (tab.getAttribute('data-tab') === tabName) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); authForms.forEach(form => { if (form.id === tabName + 'Form') { form.classList.add('active'); } else { form.classList.remove('active'); } }); } // Add click events to auth tabs authTabs.forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); switchAuthTab(tabName); resetForm('authModal'); }); }); // Reset form and errors function resetForm(modalType) { if (modalType === 'paymentModal') { document.getElementById('modalName').value = ''; document.getElementById('modalEmail').value = ''; clearErrors(['nameError', 'emailError']); } else if (modalType === 'authModal') { document.getElementById('loginEmail').value = ''; document.getElementById('loginPassword').value = ''; document.getElementById('registerName').value = ''; document.getElementById('registerEmail').value = ''; if(document.getElementById('registerPassword'))document.getElementById('registerPassword').value = ''; if(document.getElementById('registerConfirmPassword'))document.getElementById('registerConfirmPassword').value = ''; clearErrors(['loginEmailError', 'loginPasswordError', 'registerNameError', 'registerEmailError', 'registerPasswordError', 'registerConfirmPasswordError']); } } // Clear error messages function clearErrors(errorIds) { errorIds.forEach(id => { const errorElement = document.getElementById(id); if (errorElement) { errorElement.textContent = ''; errorElement.classList.remove('show'); const inputElement = document.getElementById(id.replace('Error', '')); if (inputElement) { inputElement.classList.remove('error'); } } }); } // Show error message function showError(fieldId, message) { const errorElement = document.getElementById(fieldId + 'Error'); const inputElement = document.getElementById(fieldId); if (errorElement) { errorElement.textContent = message; errorElement.classList.add('show'); } if (inputElement) { inputElement.classList.add('error'); } } // Enhanced close modal function for general use function closeModal(modalId) { // Clear timers based on which modal is being closed switch(modalId) { case 'telegramVerifyModal': clearTelegramTimers(); break; case 'emailVerifyModal': clearEmailTimers(); break; case 'authModal': // Clear any potential timers in auth modal break; } document.getElementById(modalId).style.display = 'none'; } // Enhanced processPayment function function processPayment(event) { if (event) event.preventDefault(); const name = document.getElementById('modalName').value; const email = document.getElementById('modalEmail').value; const paymentType = document.getElementById('paymentType').value; const paymentItemId = document.getElementById('paymentItemId').value; // Get price - check if using custom amount or selected option let price; let optionLabel = ''; const customPriceRadio = document.getElementById('customPriceRadio'); if (customPriceRadio && customPriceRadio.checked) { // Custom amount price = document.getElementById('manualPrice').value; optionLabel = 'custom_amount'; // Validate custom amount if (!price || price < paymentConfig.minAmount || price > paymentConfig.maxAmount) { showError('manualPrice', 'Износ мора бити између $${paymentConfig.minAmount} и $${paymentConfig.maxAmount}'); return false; } } else { // Regular option const selectedOption = document.querySelector('input[name="paymentOption"]:checked'); if (selectedOption) { price = selectedOption.value; optionLabel = selectedOption.getAttribute('data-label') || 'selected_option'; } else { // Fallback to default price price = document.getElementById('modalPrice').textContent.match(/\d+\.\d+/)?.[0] || '0'; optionLabel = 'default'; } } const service = document.getElementById('modalTitle').textContent; // Clear previous errors clearErrors(['nameError', 'emailError', 'manualPriceError']); // Validate form let hasErrors = false; if (!name) { showError('modalName', 'Унесите своје име'); hasErrors = true; } if (!email) { showError('modalEmail', 'Унесите своју адресу е-поште'); hasErrors = true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('modalEmail', 'Унесите важећу адресу е-поште'); hasErrors = true; } if (hasErrors) return false; // Prepare data for API request const data = { type: 'payment', name: name, email: email, price: parseFloat(price).toFixed(2), service: service, payment_type: paymentType, payment_item_id: paymentItemId, option: optionLabel, is_custom_amount: (customPriceRadio && customPriceRadio.checked) ? 1 : 0 }; // Send request to API sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { if (response.redirect_url) { handlePaymentRedirect(response.redirect_url, response.redirect_type); } else { // Show success message document.getElementById('paymentForm').style.display = 'none'; document.getElementById('paymentSuccess').style.display = 'block'; } } else { // Show error message alert('Грешка: ' + response.error); } }); return false; } // Handle payment redirect function handlePaymentRedirect(redirectUrl, redirectType) { switch (redirectType) { case 'new_window': window.open(redirectUrl, '_blank'); closeModal('paymentModal'); break; case 'iframe': document.getElementById('redirectIframe').src = redirectUrl; closeModal('paymentModal'); document.getElementById('redirectModal').style.display = 'flex'; break; case 'default': default: window.location.href = redirectUrl; break; } } // Process login function processLogin() { const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; // Clear previous errors clearErrors(['loginEmail', 'loginPassword']); // Validate form let hasErrors = false; if (!email) { showError('loginEmail', 'Унесите своју е-пошту'); hasErrors = true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('loginEmail', 'Унесите важећу е-пошту'); hasErrors = true; } if (!password) { showError('loginPassword', 'Унесите лозинку'); hasErrors = true; } if (hasErrors) return; // Prepare data for API request const data = { type: 'login', email: email, password: password }; // Send request to API sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Close modal and redirect or show success closeModal('authModal'); alert('Пријава је успела!'); // Redirect to profile window.location.href = '/' + window.currentLang + '/profile/'; } else { // Show error message alert('Грешка: ' + response.error); } }); } // Process registration function processRegistration() { const name = document.getElementById('registerName').value; const email = document.getElementById('registerEmail').value; const password = document.getElementById('registerPassword').value; const confirmPassword = document.getElementById('registerConfirmPassword').value; // Clear previous errors clearErrors(['registerName', 'registerEmail', 'registerPassword', 'registerConfirmPassword']); // Validate form let hasErrors = false; if (!name) { showError('registerName', 'Унесите своје име'); hasErrors = true; } if (!email) { showError('registerEmail', 'Унесите своју е-пошту'); hasErrors = true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('registerEmail', 'Унесите важећу е-пошту'); hasErrors = true; } if (!password) { showError('registerPassword', 'Направите лозинку'); hasErrors = true; } else if (password.length < 6) { showError('registerPassword', 'Лозинка мора имати најмање 6 знакова'); hasErrors = true; } if (!confirmPassword) { showError('registerConfirmPassword', 'Молимо потврдите своју лозинку'); hasErrors = true; } else if (password !== confirmPassword) { showError('registerConfirmPassword', 'Лозинке се не поклапају'); hasErrors = true; } if (hasErrors) return; // Prepare data for API request const data = { type: 'registration', name: name, email: email, password: password }; // Send request to API sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Close modal and redirect or show success closeModal('authModal'); alert('Регистрација успела!'); // Here you would typically redirect or update UI } else { // Show error message alert('Грешка: ' + response.error); } }); } // Send AJAX request function sendRequest(url, data, callback,need_loading_overlay=true) { // Show loading overlay when request starts if(need_loading_overlay)showLoading(); //console.log("XMLHttpRequest started"); const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function() { //console.log("XMLHttpRequest onready fired, readyState:", xhr.readyState); if (xhr.readyState === 4) { // Hide loading overlay when request completes if(need_loading_overlay)hideLoading(); //console.log("Request complete, status:", xhr.status); //console.log("responseText:", xhr.responseText); if (xhr.status === 200) { let is_good = true; let response; // Declare response variable outside try block try { response = JSON.parse(xhr.responseText); } catch (e) { is_good=false; console.error("JSON parse error:", e); callback({status: 'error', error: 'Неважећи одговор са сервера'}); } if(is_good) callback(response); } else { callback({status: 'error', error: 'Грешка сервера: ' + xhr.status}); } } }; xhr.send(JSON.stringify(data)); } // Close redirect modal document.getElementById('redirectModal').querySelector('.close-btn').addEventListener('click', function() { document.getElementById('redirectModal').style.display = 'none'; }); // Mock chart data (in a real implementation, you would use Google Charts) function initCharts() { // This is a placeholder for actual chart implementation //console.log('Charts would be initialized here with Google Charts API'); } // Image modal functionality document.addEventListener('DOMContentLoaded', function() { // Image modal const imgModal = document.getElementById('imgModal'); const modalImg = document.getElementById('modalImg'); const imgCloseBtn = imgModal.querySelector('.close-btn'); // Open image modal document.querySelectorAll('[data-modal="imgModal"]').forEach(img => { img.addEventListener('click', function() { modalImg.src = this.src; imgModal.style.display = 'flex'; }); }); // Close image modal imgCloseBtn.addEventListener('click', function() { imgModal.style.display = 'none'; }); // Close on outside click window.addEventListener('click', function(e) { if (e.target === imgModal) { imgModal.style.display = 'none'; } }); }); </script> <script> //console.log("herei"); // Analysis functionality document.addEventListener('DOMContentLoaded', function() { //console.log("DOMContentLoaded"); const analyzeBtn = document.getElementById('analyzeNowBtn'); const analysisStatus = document.getElementById('analysisStatus'); if (analyzeBtn) { analyzeBtn.addEventListener('click', function() { //console.log("click"); // Show captcha modal showCaptchaModal(); }); } }); function showCaptchaModal() { //console.log("showCaptchaModal"); // Create modal for captcha const modal = document.createElement('div'); modal.className = 'modal'; modal.id = 'captchaModal'; modal.style.display = 'flex'; modal.innerHTML = ` <div class="modal-content"> <span class="close-btn" onclick="closeCaptchaModal()">×</span> <h2 class="modal-title">Потврдите да нисте робот</h2> <p class="modal-text">Унесите текст са слике:</p> <div class="captcha-container"> <img src="/captcha.php?t=` + new Date().getTime() + `" alt="CAPTCHA" class="captcha-image" id="captchaImage"> <button type="button" class="refresh-captcha" onclick="refreshCaptcha()">Освежите код</button> <input type="text" class="captcha-input" id="captchaInput" placeholder="Унесите код" maxlength="6"> </div> <button class="cta-button" onclick="submitAnalysis()" style="margin-top: 1rem;">Пошаљите на анализу</button> </div> `; document.body.appendChild(modal); } function closeCaptchaModal() { const modal = document.getElementById('captchaModal'); if (modal) { modal.remove(); } } function refreshCaptcha() { const captchaImage = document.getElementById('captchaImage'); if (captchaImage) { // Add timestamp to prevent caching captchaImage.src = '/captcha.php?t=' + new Date().getTime(); } } function submitAnalysis() { const captchaInput = document.getElementById('captchaInput'); const captchaCode = captchaInput ? captchaInput.value.trim() : ''; const analysisStatus = document.getElementById('analysisStatus'); if (!captchaCode) { alert('Унесите код са слике'); return; } // Show loading state analysisStatus.textContent = 'Слање захтева...'; analysisStatus.className = 'analysis-status'; analysisStatus.style.display = 'block'; // Prepare data for API request type = "none"; if(document.getElementById('domain_name')){ type = "domain_analysis"; sendData = document.getElementById('domain_name').value; } if(document.getElementById('phrase')){ type = "phrase_analysis"; sendData = document.getElementById('phrase').value; } if(type=="none"){ // Show error message closeCaptchaModal(); analysisStatus.textContent = 'Грешка: тип није никакав'; analysisStatus.className = 'analysis-status error'; analysisStatus.style.display = 'block'; return false; } const data = { type: type, data: sendData, captcha_code: captchaCode }; // Send request to API sendRequest('/ajax/', data, function(response) { // Close captcha modal closeCaptchaModal(); if (response.status === 'ok') { // Show success message analysisStatus.textContent = 'Ваш захтев је послат, сачекајте верификацију домена'; analysisStatus.className = 'analysis-status success'; } else { // Show error message analysisStatus.textContent = 'Грешка: ' + response.error; analysisStatus.className = 'analysis-status error'; // Refresh captcha on error if (response.refresh_captcha) { setTimeout(refreshCaptcha, 500); } } analysisStatus.style.display = 'block'; // Clear captcha input if (captchaInput) { captchaInput.value = ''; } }); } </script> <script> // Full screenshot functionality function openFullScreenshot(src) { const modal = document.getElementById('fullScreenshotModal'); const img = document.getElementById('fullScreenshotImg'); img.src = src; modal.style.display = 'flex'; // Prevent body scroll when modal is open document.body.style.overflow = 'hidden'; } function closeFullScreenshot() { const modal = document.getElementById('fullScreenshotModal'); modal.style.display = 'none'; // Restore body scroll document.body.style.overflow = 'auto'; } function downloadScreenshot() { const img = document.getElementById('fullScreenshotImg'); const link = document.createElement('a'); link.download = `screenshot-${document.getElementById('domain_name').value}-${new Date().getTime()}.jpg`; link.href = img.src; link.click(); } // Close modal on outside click document.addEventListener('click', function(e) { const modal = document.getElementById('fullScreenshotModal'); if (e.target === modal) { closeFullScreenshot(); } }); // Close modal on Escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeFullScreenshot(); } }); </script> <script> // Global variables to track intervals let telegramStatusInterval = null; let telegramTimerInterval = null; let resendTimerInterval = null; let emailCheckInterval = null; // Global variable to store QR code instance let qrcodeInstance = null; function startTelegramRegistration(name=false,email=false) { if(!name)name = document.getElementById('registerName').value.trim(); if(!email)email = document.getElementById('registerEmail').value.trim(); // Clear previous errors clearErrors(['registerName', 'registerEmail']); // Validate form let hasErrors = false; if (!name) { showError('registerName', 'Унесите своје име'); hasErrors = true; } if (!email) { showError('registerEmail', 'Унесите своју е-пошту'); hasErrors = true; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('registerEmail', 'Унесите важећу адресу е-поште'); hasErrors = true; } if (hasErrors) return; // Prepare data for API request const data = { type: 'telegram_registration_start', name: name, email: email }; //console.log("telegram_registration_start request"); // Send request to API sendRequest('/ajax/', data, function(response) { //console.log("telegram_registration_start callback"); if (response.status === 'ok') { // Store user token localStorage.setItem('current_user_token', response.user_token); // Immediately open Telegram bot in new tab if (response.deep_link) { window.open(response.deep_link, '_blank'); } // Show Telegram verification modal with deep link and QR code showTelegramVerifyModal(response.bot_username, response.telegram_token, response.deep_link,response.telegram_token_expires_remaining); // Clear form clearRegistrationForm(); // Close auth modal closeModal('authModal'); //YM reach goal send ym(109200701,'reachGoal','registration_start'); } else if (response.status === 'continue_email_verification') { //!! // User exists, Telegram verified, but email not verified localStorage.setItem('current_user_token', response.user_token); // Auto-resend email and show verification modal autoResendEmailAndShowModal(response.user_token); // Clear form clearRegistrationForm(); // Close auth modal closeModal('authModal'); } else { alert('Грешка1: ' + response.error); } }); } // Auto-resend email and show verification modal function autoResendEmailAndShowModal(userToken) { // First, try to resend the email const data = { type: 'resend_email_verification', user_token: userToken }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Email sent successfully, show verification modal with full timer //showEmailVerifyModal(); safeShowEmailVerifyModal(); } else { // Handle resend error - use cooldown_remaining from response if (response.cooldown_remaining !== undefined) { // Use the exact remaining seconds from the response const remainingSeconds = Math.max(1, Math.floor(response.cooldown_remaining)); // Show verification modal with synchronized timer showEmailVerifyModalWithCooldown(remainingSeconds); } else { // Other error - show modal with default timer //showEmailVerifyModal(); safeShowEmailVerifyModal(); alert('Грешка: ' + response.error); } } }); } // Enhanced resend email verification function function resendEmailVerification() { const userToken = localStorage.getItem('current_user_token'); const resendBtn = document.getElementById('resendEmailBtn'); if (!resendBtn) { alert('Грешка: Дугме није пронађено. Освежите страницу.'); return; } // Prevent multiple clicks if (resendBtn.disabled) { return; } // Immediately disable button and show loading state resendBtn.disabled = true; resendBtn.style.opacity = '0.7'; resendBtn.innerHTML = 'Слање...'; const data = { type: 'resend_email_verification', user_token: userToken }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Success - restart timer AND restart email check interval setupResendButtonForTimer(); startResendTimer(300); // CRITICAL FIX: Restart the email check interval restartEmailCheckInterval(); // Update status message const statusElement = document.getElementById('emailCheckStatus'); if (statusElement) { statusElement.innerHTML = '<p style="color: var(--primary);">🔍 Нови код је послан. Провера верификације...</p>'; statusElement.className = 'email-check-status status-checking'; statusElement.style.display = 'block'; } alert('Нови верификациони код је послат на вашу е-пошту'); } else { // Handle different error cases if (response.cooldown_remaining !== undefined) { const remainingSeconds = Math.max(1, Math.floor(response.cooldown_remaining)); setupResendButtonForTimer(); startResendTimer(remainingSeconds); // CRITICAL FIX: Even on cooldown, restart the email check interval restartEmailCheckInterval(); const remainingMinutes = Math.ceil(remainingSeconds / 60); // Update status message const statusElement = document.getElementById('emailCheckStatus'); if (statusElement) { statusElement.innerHTML = `<p style="color: #856404;">⏰ Нови код се може поново послати ${remainingMinutes} минута. Проверава се претходна верификација кода...</p>`; statusElement.style.display = 'block'; } alert('Нови код се може поново послати ' + remainingMinutes + ' минута.'); } else { alert('Грешка: ' + response.error); // Re-enable button on other errors resendBtn.disabled = false; resendBtn.style.opacity = '1'; resendBtn.innerHTML = 'Поново пошаљи код'; } } }); } function restartEmailCheckInterval() { const userToken = localStorage.getItem('current_user_token'); let attempts = 0; const maxAttempts = 30; // Clear existing interval if any if (emailCheckInterval) { clearInterval(emailCheckInterval); emailCheckInterval = null; } // Update status to show checking updateEmailCheckStatus('checking', 'Провера статуса верификације...'); // Start new interval emailCheckInterval = setInterval(function() { if (attempts >= maxAttempts) { if (emailCheckInterval) { clearInterval(emailCheckInterval); emailCheckInterval = null; } updateEmailCheckStatus('timeout', 'Проверите временско ограничење. Освежите страницу.'); return; } checkEmailVerificationStatus(userToken); attempts++; }, 10000); } // Setup resend button for timer display function setupResendButtonForTimer() { const resendBtn = document.getElementById('resendEmailBtn'); if (!resendBtn) return; resendBtn.disabled = true; resendBtn.style.opacity = '0.7'; // Ensure timer element exists let timerElement = document.getElementById('resendTimer'); if (!timerElement) { timerElement = document.createElement('span'); timerElement.id = 'resendTimer'; resendBtn.appendChild(timerElement); } // Set initial timer display resendBtn.innerHTML = 'Поново пошаљи код (<span id="resendTimer">5:00</span>)'; } function showEmailVerifyModalWithCooldown(remainingSeconds) { // Clear previous input const emailVerifyToken = document.getElementById('emailVerifyToken'); if (emailVerifyToken) { emailVerifyToken.value = ''; } clearErrors(['emailVerifyToken']); // Setup button for timer setupResendButtonForTimer(); // Start timer with remaining time startResendTimer(remainingSeconds); // Start email check interval startEmailCheckInterval(); // Show modal document.getElementById('emailVerifyModal').style.display = 'flex'; // Show info message const statusElement = document.getElementById('emailCheckStatus'); if (statusElement) { const remainingMinutes = Math.ceil(remainingSeconds / 60); statusElement.innerHTML = `<p style="color: #856404;">⏰ Нови код се може поново послати ${remainingMinutes} минута</p>`; statusElement.className = 'email-check-status status-checking'; statusElement.style.display = 'block'; } } // Safe function to show email verification modal function safeShowEmailVerifyModal() { // Clear previous input const emailVerifyToken = document.getElementById('emailVerifyToken'); if (emailVerifyToken) { emailVerifyToken.value = ''; } clearErrors(['emailVerifyToken']); // Setup button for timer setupResendButtonForTimer(); // Start fresh timer startResendTimer(300); // Start email check interval startEmailCheckInterval(); // Show modal document.getElementById('emailVerifyModal').style.display = 'flex'; } // Safe function to clear registration form function clearRegistrationForm() { const nameField = document.getElementById('registerName'); const emailField = document.getElementById('registerEmail'); if (nameField) nameField.value = ''; if (emailField) emailField.value = ''; // Only clear password fields if they exist (for traditional registration) const passwordField = document.getElementById('registerPassword'); const confirmPasswordField = document.getElementById('registerConfirmPassword'); if (passwordField) passwordField.value = ''; if (confirmPasswordField) confirmPasswordField.value = ''; // Clear any status messages const statusElement = document.getElementById('registrationStatus'); if (statusElement) { statusElement.innerHTML = ''; statusElement.style.display = 'none'; } } // Enhanced showTelegramVerifyModal function function showTelegramVerifyModal(botUsername, telegramToken, deepLink,telegram_token_expires_remaining) { // Set Telegram bot deep link const telegramUrl = deepLink || `https://t.me/${botUsername}?start=${telegramToken}`; window.telegramBotUrl = telegramUrl; // Generate QR code generateQRCode(telegramUrl); // Update the "Open Telegram" button const openTelegramBtn = document.getElementById('openTelegramBtn'); if (openTelegramBtn) { openTelegramBtn.onclick = function() { window.open(telegramUrl, '_blank'); }; } // Update modal text const modalText = document.getElementById('telegramModalText'); if (modalText) { modalText.innerHTML = ` Телеграм је отворен у новој картици.<br> Ако се бот није аутоматски отворио, користите дугме испод или скенирајте КР код. `; } // Start timer //console.log("start timer:"+telegram_token_expires_remaining); startTelegramTimer(telegram_token_expires_remaining); // Start status checking startTelegramStatusCheck(); // Show modal document.getElementById('telegramVerifyModal').style.display = 'flex'; } // Generate QR code using qrcodejs library function generateQRCode(url) { const qrcodeElement = document.getElementById('qrcode'); // Clear previous QR code if exists if (qrcodeInstance) { qrcodeInstance.clear(); qrcodeElement.innerHTML = ''; } try { // Create new QR code instance qrcodeInstance = new QRCode(qrcodeElement, { text: url, width: 200, height: 200, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.H }); //console.log('QR code generated successfully'); } catch (error) { console.error('Генерисање КР кода није успело:', error); // Fallback: show message to use the button instead qrcodeElement.innerHTML = ` <div style="color: var(--warning); font-size: 0.9rem; padding: 1rem;"> <p>КР код није доступан. Користите дугме Отвори Телеграм.</p> </div> `; } } // Refresh QR code function refreshTelegramQR() { if (window.telegramBotUrl) { const refreshBtn = document.getElementById('refreshQRBtn'); const originalText = refreshBtn.innerHTML; refreshBtn.innerHTML = '🔄 Освежавање...'; refreshBtn.disabled = true; // Regenerate QR code generateQRCode(window.telegramBotUrl); setTimeout(() => { refreshBtn.innerHTML = originalText; refreshBtn.disabled = false; }, 500); } } // Open Telegram bot in new tab function openTelegramBot() { if (window.telegramBotUrl) { window.open(window.telegramBotUrl, '_blank'); } } function startTelegramStatusCheck() { const userToken = localStorage.getItem('current_user_token'); // Clear existing interval if any if (telegramStatusInterval) { clearInterval(telegramStatusInterval); } // Check status every 5 seconds telegramStatusInterval = setInterval(function() { checkTelegramVerificationStatus(userToken); }, 5000); } // Update the checkTelegramVerificationStatus to clear timers on success function checkTelegramVerificationStatus(userToken) { const data = { type: 'check_telegram_verification', user_token: userToken }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { if (response.telegram_verified) { // Telegram verified successfully clearTelegramTimers(); // Show success message const timerContainer = document.getElementById('telegramTimer'); timerContainer.innerHTML = '✅ Телеграм је успешно верификован!'; timerContainer.className = 'timer-container'; timerContainer.style.background = '#d4edda'; timerContainer.style.color = '#155724'; //YM reach goal send ym(109200701,'reachGoal','registration_tg_success'); // Auto-proceed to email verification after 2 seconds setTimeout(function() { closeTelegramVerifyModal(); safeShowEmailVerifyModal(); }, 2000); } } },false); } // Enhanced timer functions that store interval IDs function startTelegramTimer(duration) { let timerContainer = document.getElementById('telegramTimer'); let timerElement = document.getElementById('timer'); // Check if timer was expired and reset if needed if (timerContainer && timerContainer.classList.contains('timer-expired')) { //console.log('Timer has expired - resetting UI'); timerContainer.classList.remove('timer-expired'); timerContainer.innerHTML = 'Чека се потврда... <span id="timer">05:00</span>'; // Update timerElement reference after resetting HTML timerElement = document.getElementById('timer'); } let timer = duration; // Clear existing timer if any if (telegramTimerInterval) { clearInterval(telegramTimerInterval); } telegramTimerInterval = setInterval(function() { const minutes = parseInt(timer / 60, 10); const seconds = parseInt(timer % 60, 10); if (timerElement) { timerElement.textContent = minutes + ":" + (seconds < 10 ? "0" + seconds : seconds); } if (--timer < 0) { clearTelegramTimers(); if (timerElement) { timerElement.textContent = "00:00"; } if (timerContainer) { timerContainer.classList.add('timer-expired'); timerContainer.innerHTML = 'Време је истекло. Молим вас почните испочетка.'; } // Auto-close after 3 seconds setTimeout(function() { closeTelegramVerifyModal(); }, 3000); } }, 1000); } // Enhanced close function to clean up QR code function closeTelegramVerifyModal() { // Clear QR code instance if (qrcodeInstance) { qrcodeInstance.clear(); qrcodeInstance = null; } // Clear container const qrcodeElement = document.getElementById('qrcode'); if (qrcodeElement) { qrcodeElement.innerHTML = ''; } // Clear timers clearTelegramTimers(); // Hide modal document.getElementById('telegramVerifyModal').style.display = 'none'; } // Verify Telegram code function verifyTelegramCode() { const code = document.getElementById('telegramVerifyCode').value.trim(); const userToken = localStorage.getItem('current_user_token'); // Clear previous errors clearErrors(['telegramVerifyCode']); if (!code) { showError('telegramVerifyCode', 'Унесите верификациони код'); return; } if (code.length !== 7) { showError('telegramVerifyCode', 'Шифра мора да садржи 7 цифара'); return; } // Prepare data for API request const data = { type: 'verify_telegram_code', user_token: userToken, verify_code: code }; // Send request to API sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Stop timer clearInterval(telegramTimer); // Close Telegram modal closeTelegramVerifyModal(); // Show email verification modal //showEmailVerifyModal(); safeShowEmailVerifyModal(); } else { showError('telegramVerifyCode', response.error); } }); } // Update the modal opening calls function showEmailVerifyModal() { safeShowEmailVerifyModal(); } // Enhanced start resend timer function function startResendTimer(duration) { let timer = duration; // Clear existing timer if any if (resendTimerInterval) { clearInterval(resendTimerInterval); } // Update timer display immediately updateResendTimerDisplay(timer); resendTimerInterval = setInterval(function() { timer--; updateResendTimerDisplay(timer); if (timer <= 0) { if (resendTimerInterval) { clearInterval(resendTimerInterval); resendTimerInterval = null; } enableResendButton(); } }, 1000); } // Safe function to update timer display function updateResendTimerDisplay(timer) { const timerElement = document.getElementById('resendTimer'); const resendBtn = document.getElementById('resendEmailBtn'); if (!timerElement || !resendBtn) { console.warn('Елементи тајмера нису пронађени током ажурирања'); return; } const minutes = Math.floor(timer / 60); const seconds = timer % 60; const timeString = minutes + ":" + (seconds < 10 ? "0" + seconds : seconds); timerElement.textContent = timeString; // Update button text while preserving the timer element resendBtn.innerHTML = 'Поново пошаљи код (<span id="resendTimer">' + timeString + '</span>)'; } // Helper function to update timer display function updateTimerDisplay(timerElement, timer) { if (!timerElement) return; const minutes = Math.floor(timer / 60); const seconds = timer % 60; timerElement.textContent = minutes + ":" + (seconds < 10 ? "0" + seconds : seconds); } // Enable resend button when timer expires function enableResendButton() { const resendBtn = document.getElementById('resendEmailBtn'); if (!resendBtn) { console.error('Дугме за поновно слање није пронађено'); return; } resendBtn.disabled = false; resendBtn.style.opacity = '1'; // Set final state without timer resendBtn.innerHTML = 'Поново пошаљи код'; // Re-create the timer element for future use const timerSpan = document.createElement('span'); timerSpan.id = 'resendTimer'; timerSpan.style.display = 'none'; // Hide when not active resendBtn.appendChild(timerSpan); } // Enhanced email check interval function startEmailCheckInterval() { restartEmailCheckInterval(); // Just call the new function } // Update the checkEmailVerificationStatus to clear interval on success function checkEmailVerificationStatus(userToken) { const data = { type: 'check_email_verification', user_token: userToken }; sendRequest('/ajax/', data, function(response) { const statusElement = document.getElementById('emailCheckStatus'); if (response.status === 'ok') { if (response.email_verified) { // Email verified successfully if (emailCheckInterval) { clearInterval(emailCheckInterval); emailCheckInterval = null; } if (statusElement) { statusElement.innerHTML = '<p style="color: #155724;">✅ Имејл је успешно верификован!</p>'; statusElement.className = 'email-check-status status-success'; } //YM reach goal send ym(109200701,'reachGoal','registration_email_success'); // Auto-redirect after 2 seconds setTimeout(function() { completeRegistration(); }, 2000); } else { if (statusElement) { statusElement.style.display = 'block'; statusElement.innerHTML = '<p style="color: var(--primary);">🔍 Провера статуса верификације...</p>'; statusElement.className = 'email-check-status status-checking'; } } } else { // Handle error response if (statusElement) { statusElement.innerHTML = `<p style="color: #721c24;">❌ Грешка у провери верификације: ${response.error}</p>`; statusElement.className = 'email-check-status status-error'; } } },false); } // Verify email token manually function verifyEmailToken() { const token = document.getElementById('emailVerifyToken').value.trim(); const userToken = localStorage.getItem('current_user_token'); // Clear previous errors clearErrors(['emailVerifyToken']); if (!token) { showError('emailVerifyToken', 'Унесите верификациони код'); return; } const data = { type: 'verify_email_token', user_token: userToken, email_token: token }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { completeRegistration(); } else { showError('emailVerifyToken', response.error); } }); } // Complete registration and login function completeRegistration() { const userToken = localStorage.getItem('current_user_token'); const data = { type: 'complete_registration', user_token: userToken }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Store user info localStorage.setItem('user_info', JSON.stringify(response.user)); // Close modal closeEmailVerifyModal(); //YM send reachGoal ym(109200701,'reachGoal','registration_complete'); // Redirect to profile window.location.href = '/' + window.currentLang + '/profile/'; } else { alert('Грешка: ' + response.error); } }); } function closeEmailVerifyModal() { // Clear all email-related intervals if (resendTimerInterval) { clearInterval(resendTimerInterval); resendTimerInterval = null; } if (emailCheckInterval) { clearInterval(emailCheckInterval); emailCheckInterval = null; } document.getElementById('emailVerifyModal').style.display = 'none'; } // Add this to check registration status when email is entered document.getElementById('registerEmail').addEventListener('blur', function() { const email = this.value.trim(); if(statusElement = document.getElementById('registrationStatus')){ statusElement.innerHTML = ''; } if (email && /\S+@\S+\.\S+/.test(email)) { checkRegistrationStatus(email); } }); function checkRegistrationStatus(email) { const data = { type: 'check_registration_status', email: email }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { const status = response.registration_status; let statusElement = document.getElementById('registrationStatus'); // Remove 'const' declaration if (!statusElement) { // Create status element if it doesn't exist statusElement = document.createElement('div'); // Now we're assigning to the existing variable statusElement.id = 'registrationStatus'; statusElement.style.marginTop = '1rem'; statusElement.style.padding = '0.75rem'; statusElement.style.borderRadius = 'var(--border-radius)'; statusElement.style.fontSize = '0.9rem'; const registerForm = document.getElementById('registerForm'); const submitButton = registerForm.querySelector('button[onclick="startTelegramRegistration()"]'); registerForm.insertBefore(statusElement, submitButton.nextSibling); } // Remove the duplicate declaration of statusElement here // const statusElement = document.getElementById('registrationStatus'); // ← This line is removed switch(status) { case 'completed': statusElement.innerHTML = '✅ Овај емаил је већ регистрован. <a href="#" onclick="switchAuthTab(\'login\')">Логин</a>'; statusElement.style.background = '#d4edda'; statusElement.style.color = '#155724'; break; case 'email_pending': statusElement.innerHTML = '📧 Регистрација је скоро завршена! Потребна је верификација е-поште.'; statusElement.style.background = '#fff3cd'; statusElement.style.color = '#856404'; break; case 'telegram_pending': statusElement.innerHTML = '📱 Наставите са регистрацијом у Телеграму.'; statusElement.style.background = '#cce5ff'; statusElement.style.color = '#004085'; break; case 'incomplete': statusElement.innerHTML = '🔄 Наставите са регистрацијом.'; statusElement.style.background = '#e2e3e5'; statusElement.style.color = '#383d41'; break; default: statusElement.innerHTML = ''; } } },false); } // Enhanced email check status function function updateEmailCheckStatus(type, message) { const statusElement = document.getElementById('emailCheckStatus'); if (!statusElement) return; statusElement.style.display = 'block'; switch(type) { case 'checking': statusElement.innerHTML = '<p style="color: var(--primary);">🔍 Провера статуса верификације...</p>'; statusElement.className = 'email-check-status status-checking'; break; case 'success': statusElement.innerHTML = '<p style="color: #155724;">✅ ' + message + '</p>'; statusElement.className = 'email-check-status status-success'; break; case 'error': statusElement.innerHTML = '<p style="color: #721c24;">❌ ' + message + '</p>'; statusElement.className = 'email-check-status status-error'; break; case 'timeout': statusElement.innerHTML = '<p style="color: #856404;">⏰ ' + message + '</p>'; statusElement.className = 'email-check-status status-error'; break; } } // Helper function to clear all Telegram timers function clearTelegramTimers() { if (telegramTimerInterval) { clearInterval(telegramTimerInterval); telegramTimerInterval = null; } if (telegramStatusInterval) { clearInterval(telegramStatusInterval); telegramStatusInterval = null; } } // Helper function to clear all email timers function clearEmailTimers() { if (resendTimerInterval) { clearInterval(resendTimerInterval); resendTimerInterval = null; } if (emailCheckInterval) { clearInterval(emailCheckInterval); emailCheckInterval = null; } } </script> <script> // Login with Telegram function function loginWithTelegram() { const email = document.getElementById('loginEmail').value.trim(); // Clear previous errors clearErrors(['loginEmail']); // Validate email if (!email) { showError('loginEmail', 'Унесите своју е-пошту'); return; } else if (!/\S+@\S+\.\S+/.test(email)) { showError('loginEmail', 'Унесите важећу адресу е-поште'); return; } // Check if user exists and is verified checkUserForTelegramLogin(email); } // Check if user exists and is verified for Telegram login function checkUserForTelegramLogin(email) { const data = { type: 'check_user_for_telegram_login', email: email }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { if(response.user_exists && response.telegram_verified && response.email_verified){ // User exists and Telegram verified and email verifed- generate login link generateTelegramLoginLink(email); }else if (response.user_exists && response.telegram_verified) { // User exists and Telegram verified //alert('Для входа через Telegram необходимо сначала завершить регистрацию подтвердите email'); startTelegramRegistration("noneedname",email); } else if (response.user_exists && !response.telegram_verified) { // User exists but Telegram not verified //alert('Для входа через Telegram необходимо сначала завершить регистрацию в Telegram'); startTelegramRegistration("noneedname",email); } else { // User doesn't exist alert('Корисник са овом е-поштом није пронађен. Молимо вас да се региструјете.'); } } else { alert('Грешка: ' + response.error); } }); } // Generate Telegram login code function generateTelegramLoginLink(email) { const data = { type: 'generate_telegram_login_link', email: email }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Store user token localStorage.setItem('current_user_token', response.user_token); // Show Telegram verification modal for login showTelegramLoginModal(response.bot_username, response.deep_link, response.telegram_token_expires_remaining); // Close auth modal closeModal('authModal'); } else { alert('Грешка: ' + response.error); } }); } // Show Telegram login modal function showTelegramLoginModal(botUsername, deepLink, expiresRemaining) { // Set Telegram bot deep link const telegramUrl = deepLink || `https://t.me/${botUsername}?start=login_${Date.now()}`; window.telegramBotUrl = telegramUrl; //open forced window.open(telegramUrl, '_blank'); // Generate QR code generateQRCode(telegramUrl); // Update modal text for login const modalText = document.getElementById('telegramModalText'); if (modalText) { modalText.innerHTML = ` Телеграм је отворен за пријаву.<br> Ако се бот није аутоматски отворио, користите дугме испод или скенирајте КР код. `; } // Update modal title const modalTitle = document.querySelector('#telegramVerifyModal .modal-title'); if (modalTitle) { modalTitle.textContent = 'Пријавите се преко Телеграма'; } // Start timer startTelegramTimer(expiresRemaining || 300); // Start status checking for login startTelegramLoginStatusCheck(); // Show modal document.getElementById('telegramVerifyModal').style.display = 'flex'; } // Start Telegram login status check function startTelegramLoginStatusCheck() { const userToken = localStorage.getItem('current_user_token'); // Clear existing interval if any if (telegramStatusInterval) { clearInterval(telegramStatusInterval); } // Check status every 5 seconds telegramStatusInterval = setInterval(function() { checkTelegramLoginStatus(userToken); }, 5000); } // Check Telegram login status function checkTelegramLoginStatus(userToken) { const data = { type: 'check_telegram_login_status', user_token: userToken }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { if (response.login_code_sended ) { // Login code received - show code input modal clearTelegramTimers(); showTelegramCodeInputModal(response.verify_code); } else if (response.login_completed) { // Login completed successfully clearTelegramTimers(); completeTelegramLogin(); } } }); } // Show Telegram code input modal function showTelegramCodeInputModal(verifyCode) { // Close Telegram verify modal closeTelegramVerifyModal(); // Create or show code input modal let codeModal = document.getElementById('telegramCodeModal'); // Start code timer startLoginCodeTimer(300); // Show modal codeModal.style.display = 'flex'; } // Start login code timer function startLoginCodeTimer(duration) { let timer = duration; const timerElement = document.getElementById('loginTimer'); if (!timerElement) return; // Clear existing timer if any if (window.loginCodeTimer) { clearInterval(window.loginCodeTimer); } window.loginCodeTimer = setInterval(function() { const minutes = parseInt(timer / 60, 10); const seconds = parseInt(timer % 60, 10); timerElement.textContent = minutes + ":" + (seconds < 10 ? "0" + seconds : seconds); if (--timer < 0) { clearInterval(window.loginCodeTimer); timerElement.textContent = "00:00"; // Show expired message const timerContainer = document.getElementById('loginCodeTimer'); if (timerContainer) { timerContainer.classList.add('timer-expired'); timerContainer.innerHTML = 'Код је истекао. Молим вас почните испочетка.'; } } }, 1000); } // Verify Telegram login code function verifyTelegramLoginCode() { const userToken = localStorage.getItem('current_user_token'); const codeInput = document.getElementById('telegramLoginCode'); const code = codeInput ? codeInput.value.trim() : ''; // Clear previous errors clearErrors(['telegramLoginCode']); if (!code) { showError('telegramLoginCode', 'Унесите верификациони код'); return; } if (code.length !== 7) { showError('telegramLoginCode', 'Шифра мора да садржи 7 цифара'); return; } const data = { type: 'verify_telegram_login_code', user_token: userToken, verify_code: code }; sendRequest('/ajax/', data, function(response) { if (response.status === 'ok') { // Clear timer if (window.loginCodeTimer) { clearInterval(window.loginCodeTimer); } localStorage.setItem('user_info', JSON.stringify(response.user)); // Close any open modals closeModal('telegramCodeModal'); closeModal('telegramVerifyModal'); // Show success message and redirect alert('Пријава је успела!'); window.location.href = '/' + window.currentLang + '/profile/'; } else { showError('telegramLoginCode', response.error); } }); } </script> <!-- Add this JavaScript before closing body tag --> <script> // Animation on scroll document.addEventListener('DOMContentLoaded', function() { const fadeElements = document.querySelectorAll('.fade-in'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } }); }, { threshold: 0.1 }); fadeElements.forEach(element => { observer.observe(element); }); // Add click handlers for project CTAs document.querySelectorAll('.project-cta').forEach(button => { button.addEventListener('click', function() { const projectTitle = this.closest('.project-card').querySelector('.project-title').textContent; // alert(`Exploring: ${projectTitle}`); // In production, this would navigate to the appropriate page }); }); }); </script> <script> // Cookie consent management (function() { // Configuration const COOKIE_CONSENT_KEY = 'serpuls_cookie_consent'; const COOKIE_EXPIRY_DAYS = 365; // Create cookie consent banner if not already accepted function initCookieConsent() { // Check if user has already made a choice const consent = getCookieConsent(); if (consent !== null) { // User already made a choice, apply preferences applyCookiePreferences(consent); return; } // Create banner createCookieBanner(); } // Create the cookie banner HTML function createCookieBanner() { const banner = document.createElement('div'); banner.id = 'cookie-consent-banner'; banner.style.cssText = ` position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -4px 20px rgba(0,0,0,0.15); padding: 1.5rem; z-index: 9999; border-top: 3px solid var(--primary, #4361ee); animation: slideUp 0.5s ease; `; // Add animation style if not exists if (!document.getElementById('cookie-animation-style')) { const style = document.createElement('style'); style.id = 'cookie-animation-style'; style.textContent = ` @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } } `; document.head.appendChild(style); } banner.innerHTML = ` <div style="max-width: 1200px; margin: 0 auto; display: flex; flex-wrap: wrap; gap: 2rem; align-items: center; justify-content: space-between;"> <div style="flex: 2; min-width: 280px;"> <h3 style="margin: 0 0 0.5rem 0; color: var(--dark, #212529); font-size: 1.25rem;">🍪 Сагласност за колачиће</h3> <p style="margin: 0; color: var(--gray, #6c757d); line-height: 1.5;"> Користимо колачиће да побољшамо ваше искуство прегледања, послужимо персонализовани садржај и анализирамо наш саобраћај. Кликом на Прихвати све, пристајете на нашу употребу колачића. Прочитајте наше <a href="/en/cookie/" style="color: var(--primary, #4361ee); text-decoration: underline;">Политика колачића</a> и <a href="/en/privacy/" style="color: var(--primary, #4361ee); text-decoration: underline;">Политика приватности</a>. </p> </div> <div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center;"> <button id="cookie-accept-all" style="padding: 0.75rem 1.5rem; background: linear-gradient(135deg, var(--primary, #4361ee), var(--secondary, #7209b7)); color: white; border: none; border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 6px rgba(67,97,238,0.3);"> Прихвати све </button> <button id="cookie-accept-essential" style="padding: 0.75rem 1.5rem; background: white; color: var(--dark, #212529); border: 1px solid var(--gray, #6c757d); border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer; transition: all 0.3s ease;"> Ессентиал Онли </button> <button id="cookie-settings" style="padding: 0.75rem 1.5rem; background: transparent; color: var(--primary, #4361ee); border: none; font-weight: 600; cursor: pointer; text-decoration: underline;"> Прилагодите </button> </div> </div> `; document.body.appendChild(banner); // Add event listeners document.getElementById('cookie-accept-all').addEventListener('click', function() { acceptAllCookies(); }); document.getElementById('cookie-accept-essential').addEventListener('click', function() { acceptEssentialCookies(); }); document.getElementById('cookie-settings').addEventListener('click', function() { showCookieSettings(); }); } // Show cookie settings modal function showCookieSettings() { // Remove existing modal if any const existingModal = document.getElementById('cookie-settings-modal'); if (existingModal) existingModal.remove(); const modal = document.createElement('div'); modal.id = 'cookie-settings-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; animation: fadeIn 0.3s ease; `; modal.innerHTML = ` <div style="background: white; border-radius: var(--border-radius, 8px); max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 10px 25px rgba(0,0,0,0.2);"> <div style="padding: 1.5rem; border-bottom: 1px solid #e9ecef;"> <h3 style="margin: 0; color: var(--dark, #212529);">Подешавања колачића</h3> </div> <div style="padding: 1.5rem;"> <p style="color: var(--gray, #6c757d); margin-bottom: 1.5rem;">Одаберите које колачиће дозвољавате. Ова подешавања можете променити у било ком тренутку.</p> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: not-allowed;"> <input type="checkbox" checked disabled style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">Ессентиал Цоокиес</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">Неопходно да би веб локација функционисала. Не може се онемогућити.</p> </div> </label> </div> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;"> <input type="checkbox" id="cookie-pref-analytics" checked style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">Колачићи аналитике</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">Помозите нам да разумемо како посетиоци комуницирају са нашом веб локацијом.</p> </div> </label> </div> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;"> <input type="checkbox" id="cookie-pref-functional" checked style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">Функционални колачићи</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">Запамтите своје жељене поставке и подешавања.</p> </div> </label> </div> <div style="margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: var(--border-radius, 8px);"> <label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer;"> <input type="checkbox" id="cookie-pref-targeting" checked style="width: 18px; height: 18px;"> <div> <strong style="color: var(--dark, #212529);">Колачићи за циљање</strong> <p style="margin: 0.25rem 0 0; font-size: 0.9rem; color: var(--gray, #6c757d);">Користи се за испоруку релевантног садржаја и огласа.</p> </div> </label> </div> </div> <div style="padding: 1.5rem; border-top: 1px solid #e9ecef; display: flex; gap: 1rem; justify-content: flex-end;"> <button id="cookie-save-preferences" style="padding: 0.75rem 1.5rem; background: linear-gradient(135deg, var(--primary, #4361ee), var(--secondary, #7209b7)); color: white; border: none; border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer;"> Сачувај подешавања </button> <button id="cookie-close-modal" style="padding: 0.75rem 1.5rem; background: white; color: var(--dark, #212529); border: 1px solid var(--gray, #6c757d); border-radius: var(--border-radius, 8px); font-weight: 600; cursor: pointer;"> Откажи </button> </div> </div> `; document.body.appendChild(modal); // Close modal handlers document.getElementById('cookie-close-modal').addEventListener('click', function() { modal.remove(); }); document.getElementById('cookie-save-preferences').addEventListener('click', function() { const preferences = { essential: true, // Always true analytics: document.getElementById('cookie-pref-analytics').checked, functional: document.getElementById('cookie-pref-functional').checked, targeting: document.getElementById('cookie-pref-targeting').checked }; saveCookiePreferences(preferences); modal.remove(); removeCookieBanner(); }); // Click outside to close modal.addEventListener('click', function(e) { if (e.target === modal) { modal.remove(); } }); } // Accept all cookies function acceptAllCookies() { const preferences = { essential: true, analytics: true, functional: true, targeting: true }; saveCookiePreferences(preferences); removeCookieBanner(); } // Accept only essential cookies function acceptEssentialCookies() { const preferences = { essential: true, analytics: false, functional: false, targeting: false }; saveCookiePreferences(preferences); removeCookieBanner(); } // Save cookie preferences function saveCookiePreferences(preferences) { // Save to cookie const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + COOKIE_EXPIRY_DAYS); const cookieValue = JSON.stringify(preferences); document.cookie = `${COOKIE_CONSENT_KEY}=${encodeURIComponent(cookieValue)}; expires=${expiryDate.toUTCString()}; path=/; SameSite=Lax`; // Apply preferences applyCookiePreferences(preferences); // Store in localStorage for quick access localStorage.setItem(COOKIE_CONSENT_KEY, cookieValue); // Optional: send event to analytics if (typeof window.gtag !== 'undefined' && preferences.analytics) { window.gtag('consent', 'update', { 'analytics_storage': 'granted' }); } } // Get cookie consent from cookie or localStorage function getCookieConsent() { // Try to get from cookie first const match = document.cookie.match(new RegExp('(^| )' + COOKIE_CONSENT_KEY + '=([^;]+)')); if (match) { try { return JSON.parse(decodeURIComponent(match[2])); } catch (e) { console.error('Грешка при рашчлањивању сагласности за колачиће:', e); } } // Try localStorage const stored = localStorage.getItem(COOKIE_CONSENT_KEY); if (stored) { try { return JSON.parse(stored); } catch (e) { console.error('Грешка при рашчлањивању сагласности за лоцалСтораге:', e); } } return null; } // Apply cookie preferences (enable/disable features) function applyCookiePreferences(preferences) { if (!preferences) return; // Example: Disable analytics if not consented if (!preferences.analytics) { // Disable Google Analytics if present if (typeof window.gtag !== 'undefined') { window.gtag('consent', 'default', { 'analytics_storage': 'denied' }); } // Optionally remove existing analytics cookies // This is handled by the analytics script itself usually } // Disable targeting cookies if (!preferences.targeting) { // Disable ad-related tracking // Remove Facebook pixel, etc. } // Store preferences in data attribute for other scripts to check document.documentElement.setAttribute('data-cookie-consent', JSON.stringify(preferences)); // Dispatch event for other scripts to react window.dispatchEvent(new CustomEvent('cookieConsentChanged', { detail: preferences })); } // Remove cookie banner function removeCookieBanner() { const banner = document.getElementById('cookie-consent-banner'); if (banner) { banner.style.animation = 'slideDown 0.5s ease forwards'; setTimeout(() => { if (banner.parentNode) banner.remove(); }, 500); // Add slideDown animation if not exists if (!document.getElementById('cookie-animation-slidedown')) { const style = document.createElement('style'); style.id = 'cookie-animation-slidedown'; style.textContent = ` @keyframes slideDown { from { transform: translateY(0); } to { transform: translateY(100%); } } `; document.head.appendChild(style); } } } // Add a floating button to reopen cookie settings (optional) function addCookieSettingsButton() { const btn = document.createElement('button'); btn.id = 'cookie-settings-button'; btn.innerHTML = '🍪'; btn.setAttribute('aria-label', 'Подешавања колачића'); btn.style.cssText = ` position: fixed; bottom: 20px; left: 20px; width: 48px; height: 48px; border-radius: 50%; background: white; border: 1px solid var(--gray, #6c757d); box-shadow: var(--box-shadow, 0 4px 6px rgba(0,0,0,0.1)); cursor: pointer; font-size: 1.2rem; display: flex; align-items: center; justify-content: center; z-index: 9998; transition: var(--transition, all 0.3s ease); opacity: 0.8; `; btn.addEventListener('mouseenter', function() { this.style.opacity = '1'; this.style.transform = 'scale(1.1)'; }); btn.addEventListener('mouseleave', function() { this.style.opacity = '0.8'; this.style.transform = 'scale(1)'; }); btn.addEventListener('click', function() { showCookieSettings(); }); document.body.appendChild(btn); } // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCookieConsent); } else { initCookieConsent(); } // Optional: Add settings button after consent is given window.addEventListener('cookieConsentChanged', function() { // Only show settings button if not already present if (!document.getElementById('cookie-settings-button')) { //addCookieSettingsButton(); } }); // If consent already exists, add settings button const existingConsent = getCookieConsent(); if (existingConsent && !document.getElementById('cookie-settings-button')) { document.addEventListener('DOMContentLoaded', function() { //addCookieSettingsButton(); }); } })(); </script> </body> </html>