Crisis V6: panel expand + klub matching + ne-klub filter + samo_klubovi default
DB: - pgz_sport.potpore_nositelji.je_klub flag (false za RSS programs/savezi) - Re-match klub_id case-insensitive trim normalize Endpoint: - /api/dashboard/top-primatelji: samo_klubovi=True default Frontend: - sport2.html #panel/#dpanel: 70vw / 1100px max-width za HNS karijera - mobile responsive za panel
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"SONKEI Respect in Sport, Respect in Life","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"QUdarRg0eZ\"><a href=\"https:\/\/rss.hr\/sonkei-respect-in-sport-respect-in-life\/\">SONKEI Respect in Sport, Respect in Life<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/sonkei-respect-in-sport-respect-in-life\/embed\/#?secret=QUdarRg0eZ\" width=\"600\" height=\"338\" title=\"“SONKEI Respect in Sport, Respect in Life” — Rijecki sportski savez\" data-secret=\"QUdarRg0eZ\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
@@ -48,7 +48,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
</style>
|
</style>
|
||||||
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_091119' media='all' />
|
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_114357' media='all' />
|
||||||
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
||||||
@@ -404,8 +404,8 @@ var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"","is_scroll_to_t
|
|||||||
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
||||||
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_091119" id="event-micromodal-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_114357" id="event-micromodal-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_091119" id="event-custom-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_114357" id="event-custom-js"></script>
|
||||||
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
||||||
<script id="starter-templates-zip-preview-js-extra">
|
<script id="starter-templates-zip-preview-js-extra">
|
||||||
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
||||||
@@ -449,4 +449,4 @@ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 09:11:19 -->
|
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 11:43:57 -->
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -48,7 +48,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
</style>
|
</style>
|
||||||
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_090659' media='all' />
|
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_114354' media='all' />
|
||||||
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
||||||
@@ -96,7 +96,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
<meta name="msapplication-TileImage" content="https://rss.hr/wp-content/uploads/2021/04/cropped-logo-mali-270x270.png" />
|
<meta name="msapplication-TileImage" content="https://rss.hr/wp-content/uploads/2021/04/cropped-logo-mali-270x270.png" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body itemtype='https://schema.org/WebPage' itemscope='itemscope' class="wp-singular page-template-default page page-id-3701 wp-custom-logo wp-embed-responsive wp-theme-astra ast-header-break-point ast-page-builder-template ast-no-sidebar astra-4.13.1 group-blog ast-single-post ast-mobile-inherit-site-logo ast-replace-site-logo-transparent ast-inherit-site-logo-transparent ast-theme-transparent-header ast-hfb-header elementor-default elementor-kit-38 elementor-page elementor-page-3701">
|
<body itemtype='https://schema.org/WebPage' itemscope='itemscope' class="wp-singular page-template-default page page-id-3701 wp-custom-logo wp-embed-responsive wp-theme-astra ast-desktop ast-page-builder-template ast-no-sidebar astra-4.13.1 group-blog ast-single-post ast-mobile-inherit-site-logo ast-replace-site-logo-transparent ast-inherit-site-logo-transparent ast-theme-transparent-header ast-hfb-header elementor-default elementor-kit-38 elementor-page elementor-page-3701">
|
||||||
<script>
|
<script>
|
||||||
(function(){var w=document.documentElement.clientWidth;if(w>0&&w<=921){document.body.classList.add('ast-header-break-point');document.body.classList.remove('ast-desktop');}})();
|
(function(){var w=document.documentElement.clientWidth;if(w>0&&w<=921){document.body.classList.add('ast-header-break-point');document.body.classList.remove('ast-desktop');}})();
|
||||||
</script>
|
</script>
|
||||||
@@ -500,8 +500,8 @@ var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"","is_scroll_to_t
|
|||||||
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
||||||
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_090659" id="event-micromodal-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_114354" id="event-micromodal-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_090659" id="event-custom-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_114354" id="event-custom-js"></script>
|
||||||
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
||||||
<script id="starter-templates-zip-preview-js-extra">
|
<script id="starter-templates-zip-preview-js-extra">
|
||||||
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
||||||
@@ -545,4 +545,4 @@ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 09:06:59 -->
|
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 11:43:54 -->
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
.events-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event {
|
||||||
|
border-bottom: 1px solid #5e5e5e;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date {
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day,
|
||||||
|
.events-list .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day {
|
||||||
|
font-size: 2.1875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .month {}
|
||||||
|
|
||||||
|
.events-list .event .title {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .description {
|
||||||
|
margin: .5rem auto;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.events-list .event .images {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .event-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.events-grid .event {
|
||||||
|
float: left;
|
||||||
|
width: 31.33%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
border: none !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:after {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images .date {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: red;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .day {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content .title {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content p {
|
||||||
|
font-size: .875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .buttons .btn-cta {
|
||||||
|
background-color: red;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px) {
|
||||||
|
.events-grid .event {
|
||||||
|
width: 48.5%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.events-grid .event {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Basic Modal Styles
|
||||||
|
\**************************/
|
||||||
|
|
||||||
|
.modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
width: 900px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #000000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__close {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header .modal__close:before {
|
||||||
|
content: "\2715";
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn {
|
||||||
|
font-size: .875rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: .5rem;
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
border-radius: .25rem;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
text-transform: none;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
will-change: transform;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
transition: -webkit-transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out, -webkit-transform .25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn:focus,
|
||||||
|
.modal__btn:hover {
|
||||||
|
-webkit-transform: scale(1.05);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn-primary {
|
||||||
|
background-color: #00449e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Demo Animation Style
|
||||||
|
\**************************/
|
||||||
|
@keyframes mmfadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmfadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideOut {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__overlay {
|
||||||
|
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__container {
|
||||||
|
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__overlay {
|
||||||
|
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__container {
|
||||||
|
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .modal__container,
|
||||||
|
.micromodal-slide .modal__overlay {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .title {
|
||||||
|
font-size: 1.5625rem;
|
||||||
|
color: #111111;
|
||||||
|
border: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images {
|
||||||
|
margin: .5rem auto 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 350px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
-moz-border-radius: 10px;
|
||||||
|
-ms-border-radius: 10px;
|
||||||
|
-o-border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h2.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h4.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .short_description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
.micromodal-slide .long_description {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period {
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td strong {
|
||||||
|
display: block;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td {
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder td {
|
||||||
|
padding: 0 .5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag {
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag.canceled {
|
||||||
|
background-color: #FF0000;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.free-entry {
|
||||||
|
background-color: #28A745;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.limited {
|
||||||
|
background-color: #1668B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .tags {
|
||||||
|
margin-right: 1rem;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #111111;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
.events-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event {
|
||||||
|
border-bottom: 1px solid #5e5e5e;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date {
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day,
|
||||||
|
.events-list .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day {
|
||||||
|
font-size: 2.1875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .month {}
|
||||||
|
|
||||||
|
.events-list .event .title {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .description {
|
||||||
|
margin: .5rem auto;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.events-list .event .images {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .event-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.events-grid .event {
|
||||||
|
float: left;
|
||||||
|
width: 31.33%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
border: none !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:after {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images .date {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: red;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .day {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content .title {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content p {
|
||||||
|
font-size: .875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .buttons .btn-cta {
|
||||||
|
background-color: red;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px) {
|
||||||
|
.events-grid .event {
|
||||||
|
width: 48.5%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.events-grid .event {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Basic Modal Styles
|
||||||
|
\**************************/
|
||||||
|
|
||||||
|
.modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
width: 900px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #000000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__close {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header .modal__close:before {
|
||||||
|
content: "\2715";
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn {
|
||||||
|
font-size: .875rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: .5rem;
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
border-radius: .25rem;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
text-transform: none;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
will-change: transform;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
transition: -webkit-transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out, -webkit-transform .25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn:focus,
|
||||||
|
.modal__btn:hover {
|
||||||
|
-webkit-transform: scale(1.05);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn-primary {
|
||||||
|
background-color: #00449e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Demo Animation Style
|
||||||
|
\**************************/
|
||||||
|
@keyframes mmfadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmfadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideOut {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__overlay {
|
||||||
|
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__container {
|
||||||
|
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__overlay {
|
||||||
|
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__container {
|
||||||
|
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .modal__container,
|
||||||
|
.micromodal-slide .modal__overlay {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .title {
|
||||||
|
font-size: 1.5625rem;
|
||||||
|
color: #111111;
|
||||||
|
border: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images {
|
||||||
|
margin: .5rem auto 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 350px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
-moz-border-radius: 10px;
|
||||||
|
-ms-border-radius: 10px;
|
||||||
|
-o-border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h2.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h4.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .short_description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
.micromodal-slide .long_description {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period {
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td strong {
|
||||||
|
display: block;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td {
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder td {
|
||||||
|
padding: 0 .5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag {
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag.canceled {
|
||||||
|
background-color: #FF0000;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.free-entry {
|
||||||
|
background-color: #28A745;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.limited {
|
||||||
|
background-color: #1668B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .tags {
|
||||||
|
margin-right: 1rem;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #111111;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Javne potrebe","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"YNBJELuXbe\"><a href=\"https:\/\/rss.hr\/javne-potrebe\/\">Javne potrebe<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/javne-potrebe\/embed\/#?secret=YNBJELuXbe\" width=\"600\" height=\"338\" title=\"“Javne potrebe” — Rijecki sportski savez\" data-secret=\"YNBJELuXbe\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Javne potrebe","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"neg877dRM6\"><a href=\"https:\/\/rss.hr\/javne-potrebe\/\">Javne potrebe<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/javne-potrebe\/embed\/#?secret=neg877dRM6\" width=\"600\" height=\"338\" title=\"“Javne potrebe” — Rijecki sportski savez\" data-secret=\"neg877dRM6\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
File diff suppressed because one or more lines are too long
@@ -47,7 +47,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
</style>
|
</style>
|
||||||
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_091555' media='all' />
|
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_115154' media='all' />
|
||||||
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
||||||
@@ -462,8 +462,8 @@ var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"","is_scroll_to_t
|
|||||||
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
||||||
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_091555" id="event-micromodal-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_115154" id="event-micromodal-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_091555" id="event-custom-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_115154" id="event-custom-js"></script>
|
||||||
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
||||||
<script id="starter-templates-zip-preview-js-extra">
|
<script id="starter-templates-zip-preview-js-extra">
|
||||||
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
||||||
@@ -507,4 +507,4 @@ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 09:15:55 -->
|
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 11:51:54 -->
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Sve\u010dano obilje\u017eeno 100 godina ko\u0161arke u Rijeci i predstavljena monografija Ivice \u0160ubata","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"LvjIgpp3LL\"><a href=\"https:\/\/rss.hr\/svecano-obiljezeno-100-godina-kosarke-u-rijeci-i-predstavljena-monografija-ivice-subata\/\">Sve\u010dano obilje\u017eeno 100 godina ko\u0161arke u Rijeci i predstavljena monografija Ivice \u0160ubata<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/svecano-obiljezeno-100-godina-kosarke-u-rijeci-i-predstavljena-monografija-ivice-subata\/embed\/#?secret=LvjIgpp3LL\" width=\"600\" height=\"338\" title=\"“Sve\u010dano obilje\u017eeno 100 godina ko\u0161arke u Rijeci i predstavljena monografija Ivice \u0160ubata” — Rijecki sportski savez\" data-secret=\"LvjIgpp3LL\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n","thumbnail_url":"https:\/\/rss.hr\/wp-content\/uploads\/2026\/04\/stoljece-rijecke-kosarke-768x461.jpg","thumbnail_width":600,"thumbnail_height":360}
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Sve\u010dano obilje\u017eeno 100 godina ko\u0161arke u Rijeci i predstavljena monografija Ivice \u0160ubata","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"xOQ7SFoOnK\"><a href=\"https:\/\/rss.hr\/svecano-obiljezeno-100-godina-kosarke-u-rijeci-i-predstavljena-monografija-ivice-subata\/\">Sve\u010dano obilje\u017eeno 100 godina ko\u0161arke u Rijeci i predstavljena monografija Ivice \u0160ubata<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/svecano-obiljezeno-100-godina-kosarke-u-rijeci-i-predstavljena-monografija-ivice-subata\/embed\/#?secret=xOQ7SFoOnK\" width=\"600\" height=\"338\" title=\"“Sve\u010dano obilje\u017eeno 100 godina ko\u0161arke u Rijeci i predstavljena monografija Ivice \u0160ubata” — Rijecki sportski savez\" data-secret=\"xOQ7SFoOnK\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n","thumbnail_url":"https:\/\/rss.hr\/wp-content\/uploads\/2026\/04\/stoljece-rijecke-kosarke-768x461.jpg","thumbnail_width":600,"thumbnail_height":360}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,434 @@
|
|||||||
|
.events-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event {
|
||||||
|
border-bottom: 1px solid #5e5e5e;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date {
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day,
|
||||||
|
.events-list .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day {
|
||||||
|
font-size: 2.1875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .month {}
|
||||||
|
|
||||||
|
.events-list .event .title {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .description {
|
||||||
|
margin: .5rem auto;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.events-list .event .images {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .event-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.events-grid .event {
|
||||||
|
float: left;
|
||||||
|
width: 31.33%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
border: none !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:after {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images .date {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: red;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .day {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content .title {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content p {
|
||||||
|
font-size: .875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .buttons .btn-cta {
|
||||||
|
background-color: red;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px) {
|
||||||
|
.events-grid .event {
|
||||||
|
width: 48.5%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.events-grid .event {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Basic Modal Styles
|
||||||
|
\**************************/
|
||||||
|
|
||||||
|
.modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
width: 900px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #000000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__close {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header .modal__close:before {
|
||||||
|
content: "\2715";
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn {
|
||||||
|
font-size: .875rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: .5rem;
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
border-radius: .25rem;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
text-transform: none;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
will-change: transform;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
transition: -webkit-transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out, -webkit-transform .25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn:focus,
|
||||||
|
.modal__btn:hover {
|
||||||
|
-webkit-transform: scale(1.05);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn-primary {
|
||||||
|
background-color: #00449e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Demo Animation Style
|
||||||
|
\**************************/
|
||||||
|
@keyframes mmfadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmfadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideOut {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__overlay {
|
||||||
|
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__container {
|
||||||
|
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__overlay {
|
||||||
|
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__container {
|
||||||
|
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .modal__container,
|
||||||
|
.micromodal-slide .modal__overlay {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .title {
|
||||||
|
font-size: 1.5625rem;
|
||||||
|
color: #111111;
|
||||||
|
border: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images {
|
||||||
|
margin: .5rem auto 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 350px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
-moz-border-radius: 10px;
|
||||||
|
-ms-border-radius: 10px;
|
||||||
|
-o-border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h2.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h4.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .short_description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
.micromodal-slide .long_description {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period {
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td strong {
|
||||||
|
display: block;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td {
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder td {
|
||||||
|
padding: 0 .5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag {
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag.canceled {
|
||||||
|
background-color: #FF0000;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.free-entry {
|
||||||
|
background-color: #28A745;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.limited {
|
||||||
|
background-color: #1668B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .tags {
|
||||||
|
margin-right: 1rem;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #111111;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"Iris Spanjol","author_url":"https:\/\/rss.hr\/author\/iris-spanjol\/","title":"BESPLATAN online te\u010daj – DATA projekt","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"0lyJ8fVKAW\"><a href=\"https:\/\/rss.hr\/besplatan-online-tecaj-data-projekt\/\">BESPLATAN online te\u010daj – DATA projekt<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/besplatan-online-tecaj-data-projekt\/embed\/#?secret=0lyJ8fVKAW\" width=\"600\" height=\"338\" title=\"“BESPLATAN online te\u010daj – DATA projekt” — Rijecki sportski savez\" data-secret=\"0lyJ8fVKAW\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n","thumbnail_url":"https:\/\/rss.hr\/wp-content\/uploads\/2026\/02\/Data-768x431.png","thumbnail_width":600,"thumbnail_height":337}
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"Iris Spanjol","author_url":"https:\/\/rss.hr\/author\/iris-spanjol\/","title":"BESPLATAN online te\u010daj – DATA projekt","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"KGt7ZdCj7p\"><a href=\"https:\/\/rss.hr\/besplatan-online-tecaj-data-projekt\/\">BESPLATAN online te\u010daj – DATA projekt<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/besplatan-online-tecaj-data-projekt\/embed\/#?secret=KGt7ZdCj7p\" width=\"600\" height=\"338\" title=\"“BESPLATAN online te\u010daj – DATA projekt” — Rijecki sportski savez\" data-secret=\"KGt7ZdCj7p\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n","thumbnail_url":"https:\/\/rss.hr\/wp-content\/uploads\/2026\/02\/Data-768x431.png","thumbnail_width":600,"thumbnail_height":337}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.elementor-6870 .elementor-element.elementor-element-5a7d978d:not(.elementor-motion-effects-element-type-background), .elementor-6870 .elementor-element.elementor-element-5a7d978d > .elementor-motion-effects-container > .elementor-motion-effects-layer{background-color:#FFFFFF;background-image:url("https://rss.hr/wp-content/uploads/2022/01/flag-eu-europe-1463476.jpg");background-position:center center;background-size:cover;}.elementor-6870 .elementor-element.elementor-element-5a7d978d > .elementor-background-overlay{background-color:transparent;background-image:linear-gradient(180deg, #153243 0%, #192D31 100%);opacity:0.7;transition:background 0.3s, border-radius 0.3s, opacity 0.3s;}.elementor-6870 .elementor-element.elementor-element-5a7d978d > .elementor-container{min-height:300px;}.elementor-6870 .elementor-element.elementor-element-5a7d978d{transition:background 0.3s, border 0.3s, border-radius 0.3s, box-shadow 0.3s;padding:200px 0px 100px 0px;}.elementor-6870 .elementor-element.elementor-element-36e3182{text-align:center;}.elementor-6870 .elementor-element.elementor-element-36e3182 .elementor-heading-title{letter-spacing:2.5px;color:#FFFFFF;}.elementor-6870 .elementor-element.elementor-element-37512b3{text-align:center;}.elementor-6870 .elementor-element.elementor-element-37512b3 img{width:100%;max-width:100%;height:192px;object-fit:contain;object-position:center center;}.elementor-6870 .elementor-element.elementor-element-f9184cf{text-align:start;}.elementor-6870 .elementor-element.elementor-element-6f65952{margin-top:0px;margin-bottom:0px;padding:0px 0px 0px 0px;}.elementor-6870 .elementor-element.elementor-element-968aaf0 > .elementor-widget-container{margin:0px 0px 0px 0px;padding:0px 0px 0px 0px;}.elementor-6870 .elementor-element.elementor-element-968aaf0{text-align:center;}.elementor-6870 .elementor-element.elementor-element-968aaf0 img{width:40%;max-width:40%;height:155px;object-fit:contain;object-position:center center;border-radius:0px 0px 0px 0px;}.elementor-6870 .elementor-element.elementor-element-23957eb{--spacer-size:10px;}body.elementor-page-6870:not(.elementor-motion-effects-element-type-background), body.elementor-page-6870 > .elementor-motion-effects-container > .elementor-motion-effects-layer{background-color:#ffffff;}@media(min-width:1025px){.elementor-6870 .elementor-element.elementor-element-5a7d978d:not(.elementor-motion-effects-element-type-background), .elementor-6870 .elementor-element.elementor-element-5a7d978d > .elementor-motion-effects-container > .elementor-motion-effects-layer{background-attachment:fixed;}}@media(max-width:1024px){.elementor-6870 .elementor-element.elementor-element-5a7d978d{padding:140px 80px 80px 80px;}.elementor-6870 .elementor-element.elementor-element-36e3182 .elementor-heading-title{font-size:40px;}.elementor-6870 .elementor-element.elementor-element-37512b3 img{width:60%;max-width:60%;}.elementor-6870 .elementor-element.elementor-element-968aaf0 img{width:50%;max-width:50%;}}@media(max-width:767px){.elementor-6870 .elementor-element.elementor-element-5a7d978d{padding:0px 0px 0px 0px;}.elementor-6870 .elementor-element.elementor-element-7b7c0473{width:100%;}.elementor-6870 .elementor-element.elementor-element-36e3182 .elementor-heading-title{font-size:30px;}.elementor-6870 .elementor-element.elementor-element-37512b3{text-align:center;}.elementor-6870 .elementor-element.elementor-element-37512b3 img{width:70%;max-width:70%;}.elementor-6870 .elementor-element.elementor-element-968aaf0{text-align:center;}.elementor-6870 .elementor-element.elementor-element-968aaf0 img{width:70%;max-width:70%;height:61px;}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Rinmsasft","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"AsUzARuw6d\"><a href=\"https:\/\/rss.hr\/rinmsasft\/\">Rinmsasft<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/rinmsasft\/embed\/#?secret=AsUzARuw6d\" width=\"600\" height=\"338\" title=\"“Rinmsasft” — Rijecki sportski savez\" data-secret=\"AsUzARuw6d\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Rinmsasft","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"TnWC1He5Ty\"><a href=\"https:\/\/rss.hr\/rinmsasft\/\">Rinmsasft<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/rinmsasft\/embed\/#?secret=TnWC1He5Ty\" width=\"600\" height=\"338\" title=\"“Rinmsasft” — Rijecki sportski savez\" data-secret=\"TnWC1He5Ty\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Rinmsasft","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"8Wd1pTszzT\"><a href=\"https:\/\/rss.hr\/rinmsasft\/\">Rinmsasft<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/rinmsasft\/embed\/#?secret=8Wd1pTszzT\" width=\"600\" height=\"338\" title=\"“Rinmsasft” — Rijecki sportski savez\" data-secret=\"8Wd1pTszzT\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Rinmsasft","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"zypNasp8mB\"><a href=\"https:\/\/rss.hr\/rinmsasft\/\">Rinmsasft<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/rinmsasft\/embed\/#?secret=zypNasp8mB\" width=\"600\" height=\"338\" title=\"“Rinmsasft” — Rijecki sportski savez\" data-secret=\"zypNasp8mB\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Judo for Peace","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"b0QlF0wgzW\"><a href=\"https:\/\/rss.hr\/judo-for-peace\/\">Judo for Peace<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/judo-for-peace\/embed\/#?secret=b0QlF0wgzW\" width=\"600\" height=\"338\" title=\"“Judo for Peace” — Rijecki sportski savez\" data-secret=\"b0QlF0wgzW\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -48,7 +48,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
</style>
|
</style>
|
||||||
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_091428' media='all' />
|
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_115123' media='all' />
|
||||||
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
||||||
@@ -568,8 +568,8 @@ var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"","is_scroll_to_t
|
|||||||
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
||||||
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_091428" id="event-micromodal-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_115123" id="event-micromodal-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_091428" id="event-custom-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_115123" id="event-custom-js"></script>
|
||||||
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
||||||
<script id="starter-templates-zip-preview-js-extra">
|
<script id="starter-templates-zip-preview-js-extra">
|
||||||
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
||||||
@@ -613,4 +613,4 @@ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 09:14:28 -->
|
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 11:51:23 -->
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
.events-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event {
|
||||||
|
border-bottom: 1px solid #5e5e5e;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date {
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day,
|
||||||
|
.events-list .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day {
|
||||||
|
font-size: 2.1875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .month {}
|
||||||
|
|
||||||
|
.events-list .event .title {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .description {
|
||||||
|
margin: .5rem auto;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.events-list .event .images {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .event-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.events-grid .event {
|
||||||
|
float: left;
|
||||||
|
width: 31.33%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
border: none !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:after {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images .date {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: red;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .day {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content .title {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content p {
|
||||||
|
font-size: .875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .buttons .btn-cta {
|
||||||
|
background-color: red;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px) {
|
||||||
|
.events-grid .event {
|
||||||
|
width: 48.5%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.events-grid .event {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Basic Modal Styles
|
||||||
|
\**************************/
|
||||||
|
|
||||||
|
.modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
width: 900px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #000000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__close {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header .modal__close:before {
|
||||||
|
content: "\2715";
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn {
|
||||||
|
font-size: .875rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: .5rem;
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
border-radius: .25rem;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
text-transform: none;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
will-change: transform;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
transition: -webkit-transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out, -webkit-transform .25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn:focus,
|
||||||
|
.modal__btn:hover {
|
||||||
|
-webkit-transform: scale(1.05);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn-primary {
|
||||||
|
background-color: #00449e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Demo Animation Style
|
||||||
|
\**************************/
|
||||||
|
@keyframes mmfadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmfadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideOut {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__overlay {
|
||||||
|
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__container {
|
||||||
|
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__overlay {
|
||||||
|
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__container {
|
||||||
|
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .modal__container,
|
||||||
|
.micromodal-slide .modal__overlay {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .title {
|
||||||
|
font-size: 1.5625rem;
|
||||||
|
color: #111111;
|
||||||
|
border: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images {
|
||||||
|
margin: .5rem auto 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 350px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
-moz-border-radius: 10px;
|
||||||
|
-ms-border-radius: 10px;
|
||||||
|
-o-border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h2.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h4.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .short_description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
.micromodal-slide .long_description {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period {
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td strong {
|
||||||
|
display: block;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td {
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder td {
|
||||||
|
padding: 0 .5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag {
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag.canceled {
|
||||||
|
background-color: #FF0000;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.free-entry {
|
||||||
|
background-color: #28A745;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.limited {
|
||||||
|
background-color: #1668B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .tags {
|
||||||
|
margin-right: 1rem;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #111111;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Projekti","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"VhGQY4vSnG\"><a href=\"https:\/\/rss.hr\/projekti\/\">Projekti<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/projekti\/embed\/#?secret=VhGQY4vSnG\" width=\"600\" height=\"338\" title=\"“Projekti” — Rijecki sportski savez\" data-secret=\"VhGQY4vSnG\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"Vijesti","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"4sacGcncim\"><a href=\"https:\/\/rss.hr\/vijesti\/\">Vijesti<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/vijesti\/embed\/#?secret=4sacGcncim\" width=\"600\" height=\"338\" title=\"“Vijesti” — Rijecki sportski savez\" data-secret=\"4sacGcncim\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"Iris Spanjol","author_url":"https:\/\/rss.hr\/author\/iris-spanjol\/","title":"BESPLATAN online te\u010daj – DATA projekt","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"90qAIHM75F\"><a href=\"https:\/\/rss.hr\/besplatan-online-tecaj-data-projekt\/\">BESPLATAN online te\u010daj – DATA projekt<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/besplatan-online-tecaj-data-projekt\/embed\/#?secret=90qAIHM75F\" width=\"600\" height=\"338\" title=\"“BESPLATAN online te\u010daj – DATA projekt” — Rijecki sportski savez\" data-secret=\"90qAIHM75F\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n","thumbnail_url":"https:\/\/rss.hr\/wp-content\/uploads\/2026\/02\/Data-768x431.png","thumbnail_width":600,"thumbnail_height":337}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
.events-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event {
|
||||||
|
border-bottom: 1px solid #5e5e5e;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date {
|
||||||
|
padding: 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day,
|
||||||
|
.events-list .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .day {
|
||||||
|
font-size: 2.1875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .date .month {}
|
||||||
|
|
||||||
|
.events-list .event .title {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .description {
|
||||||
|
margin: .5rem auto;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.events-list .event .images {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-list .event .event-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.events-grid .event {
|
||||||
|
float: left;
|
||||||
|
width: 31.33%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
border: none !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:after {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images .date {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: red;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .day {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .date .month {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content .title {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .event-content p {
|
||||||
|
font-size: .875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid .event .buttons .btn-cta {
|
||||||
|
background-color: red;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: .8rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px) {
|
||||||
|
.events-grid .event {
|
||||||
|
width: 48.5%;
|
||||||
|
margin: 1% 0 1% 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.events-grid .event {
|
||||||
|
float: none;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 3%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Basic Modal Styles
|
||||||
|
\**************************/
|
||||||
|
|
||||||
|
.modal__overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
width: 900px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #000000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__close {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header .modal__close:before {
|
||||||
|
content: "\2715";
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__content {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn {
|
||||||
|
font-size: .875rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: .5rem;
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
color: rgba(0, 0, 0, .8);
|
||||||
|
border-radius: .25rem;
|
||||||
|
border-style: none;
|
||||||
|
border-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
text-transform: none;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
will-change: transform;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
transition: -webkit-transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out;
|
||||||
|
transition: transform .25s ease-out, -webkit-transform .25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn:focus,
|
||||||
|
.modal__btn:hover {
|
||||||
|
-webkit-transform: scale(1.05);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__btn-primary {
|
||||||
|
background-color: #00449e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************************\
|
||||||
|
Demo Animation Style
|
||||||
|
\**************************/
|
||||||
|
@keyframes mmfadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmfadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mmslideOut {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__overlay {
|
||||||
|
animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="false"] .modal__container {
|
||||||
|
animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__overlay {
|
||||||
|
animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide[aria-hidden="true"] .modal__container {
|
||||||
|
animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .modal__container,
|
||||||
|
.micromodal-slide .modal__overlay {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .title {
|
||||||
|
font-size: 1.5625rem;
|
||||||
|
color: #111111;
|
||||||
|
border: none;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images {
|
||||||
|
margin: .5rem auto 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 350px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
-moz-border-radius: 10px;
|
||||||
|
-ms-border-radius: 10px;
|
||||||
|
-o-border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .images img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h2.title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide h4.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 auto .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .short_description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: lighter;
|
||||||
|
}
|
||||||
|
.micromodal-slide .long_description {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period {
|
||||||
|
border: none;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td strong {
|
||||||
|
display: block;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period td {
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 1rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide table.period tr.noborder td {
|
||||||
|
padding: 0 .5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag {
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide span.tag.canceled {
|
||||||
|
background-color: #FF0000;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.free-entry {
|
||||||
|
background-color: #28A745;
|
||||||
|
}
|
||||||
|
.micromodal-slide span.tag.limited {
|
||||||
|
background-color: #1668B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.micromodal-slide .tags {
|
||||||
|
margin-right: 1rem;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: .8125rem;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #111111;
|
||||||
|
border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-ms-border-radius: 3px;
|
||||||
|
-o-border-radius: 3px;
|
||||||
|
}
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.0","provider_name":"Rijecki sportski savez","provider_url":"https:\/\/rss.hr","author_name":"RSS","author_url":"https:\/\/rss.hr\/author\/sasa\/","title":"SONKEI Respect in Sport, Respect in Life","type":"rich","width":600,"height":338,"html":"<blockquote class=\"wp-embedded-content\" data-secret=\"FLJPDCh4ik\"><a href=\"https:\/\/rss.hr\/sonkei-respect-in-sport-respect-in-life\/\">SONKEI Respect in Sport, Respect in Life<\/a><\/blockquote><iframe sandbox=\"allow-scripts\" security=\"restricted\" src=\"https:\/\/rss.hr\/sonkei-respect-in-sport-respect-in-life\/embed\/#?secret=FLJPDCh4ik\" width=\"600\" height=\"338\" title=\"“SONKEI Respect in Sport, Respect in Life” — Rijecki sportski savez\" data-secret=\"FLJPDCh4ik\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\" class=\"wp-embedded-content\"><\/iframe><script>\n\/*! This file is auto-generated *\/\n!function(d,l){\"use strict\";l.querySelector&&d.addEventListener&&\"undefined\"!=typeof URL&&(d.wp=d.wp||{},d.wp.receiveEmbedMessage||(d.wp.receiveEmbedMessage=function(e){var t=e.data;if((t||t.secret||t.message||t.value)&&!\/[^a-zA-Z0-9]\/.test(t.secret)){for(var s,r,n,a=l.querySelectorAll('iframe[data-secret=\"'+t.secret+'\"]'),o=l.querySelectorAll('blockquote[data-secret=\"'+t.secret+'\"]'),c=new RegExp(\"^https?:$\",\"i\"),i=0;i<o.length;i++)o[i].style.display=\"none\";for(i=0;i<a.length;i++)s=a[i],e.source===s.contentWindow&&(s.removeAttribute(\"style\"),\"height\"===t.message?(1e3<(r=parseInt(t.value,10))?r=1e3:~~r<200&&(r=200),s.height=r):\"link\"===t.message&&(r=new URL(s.getAttribute(\"src\")),n=new URL(t.value),c.test(n.protocol))&&n.host===r.host&&l.activeElement===s&&(d.top.location.href=t.value))}},d.addEventListener(\"message\",d.wp.receiveEmbedMessage,!1),l.addEventListener(\"DOMContentLoaded\",function(){for(var e,t,s=l.querySelectorAll(\"iframe.wp-embedded-content\"),r=0;r<s.length;r++)(t=(e=s[r]).getAttribute(\"data-secret\"))||(t=Math.random().toString(36).substring(2,12),e.src+=\"#?secret=\"+t,e.setAttribute(\"data-secret\",t)),e.contentWindow.postMessage({message:\"ready\",secret:t},\"*\")},!1)))}(window,document);\n\/\/# sourceURL=https:\/\/rss.hr\/wp-includes\/js\/wp-embed.min.js\n<\/script>\n"}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-1
@@ -405,7 +405,7 @@ def api_kpi():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/dashboard/top-primatelji")
|
@app.get("/api/dashboard/top-primatelji")
|
||||||
def dashboard_top_primatelji(godina: int = 2025, davatelj: str = None, limit: int = 100):
|
def dashboard_top_primatelji(godina: int = 2025, davatelj: str = None, samo_klubovi: bool = True, limit: int = 100):
|
||||||
"""Top primatelji javnih potreba s davatelj filter + PDF link na godišnjak."""
|
"""Top primatelji javnih potreba s davatelj filter + PDF link na godišnjak."""
|
||||||
where = []
|
where = []
|
||||||
params = []
|
params = []
|
||||||
@@ -415,6 +415,8 @@ def dashboard_top_primatelji(godina: int = 2025, davatelj: str = None, limit: in
|
|||||||
if davatelj and davatelj != 'all':
|
if davatelj and davatelj != 'all':
|
||||||
where.append("pn.davatelj = %s")
|
where.append("pn.davatelj = %s")
|
||||||
params.append(davatelj)
|
params.append(davatelj)
|
||||||
|
if samo_klubovi:
|
||||||
|
where.append("(pn.je_klub IS NULL OR pn.je_klub = true)")
|
||||||
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
||||||
|
|
||||||
rows = fetch(f"""
|
rows = fetch(f"""
|
||||||
|
|||||||
@@ -0,0 +1,462 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# hns_api_client.py — HNS Semafor structured client (v1.0)
|
||||||
|
# Author: Damir Radulić <dradulic@outlook.com> / <damir@rinet.one>
|
||||||
|
# Date: 2026-05-05
|
||||||
|
# Description:
|
||||||
|
# Reverse-engineered client for https://semafor.hns.family.
|
||||||
|
# The site is server-rendered ASP.NET (NOT Next.js — no __NEXT_DATA__,
|
||||||
|
# no hydration JSON). The only XHR endpoints exposed are filter helpers
|
||||||
|
# /handlers/getOrganizations/, getCompetitions/, getAgeCategories/,
|
||||||
|
# getCalendarEvents/. All player & club data is rendered into stable
|
||||||
|
# semantic HTML (classes: playerHeader, playerData, playerCompetitionStatsTable,
|
||||||
|
# matchlist, clubHeader, basic_info, playerslist…).
|
||||||
|
#
|
||||||
|
# This module therefore implements a fast HTML→JSON parser using requests
|
||||||
|
# + BeautifulSoup, with a connection-pooled session, polite UA, and a
|
||||||
|
# per-hour cache. It exposes the SDK surface SUB4 was asked for:
|
||||||
|
# fetch_player(hns_id, slug) -> dict
|
||||||
|
# fetch_klub(hns_id, slug) -> dict
|
||||||
|
# fetch_klub_roster(hns_id, slug) -> list
|
||||||
|
# get_buildid() -> str (no buildId on this stack — returns site CSS hash)
|
||||||
|
#
|
||||||
|
# Fallback chain inside _get_html():
|
||||||
|
# 1. requests + polite UA (primary)
|
||||||
|
# 2. requests with referer + cookie (retry on 403/503)
|
||||||
|
# 3. Playwright (lazy import) for JS-only edge cases
|
||||||
|
#
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
BASE = "https://semafor.hns.family"
|
||||||
|
UA = (
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
HEADERS = {
|
||||||
|
"User-Agent": UA,
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "hr,en-US;q=0.7,en;q=0.3",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
}
|
||||||
|
|
||||||
|
_session = requests.Session()
|
||||||
|
_session.headers.update(HEADERS)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _txt(node, default: str = "") -> str:
|
||||||
|
if node is None:
|
||||||
|
return default
|
||||||
|
return re.sub(r"\s+", " ", node.get_text(" ", strip=True)).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _int(s: str | None, default: int | None = None) -> int | None:
|
||||||
|
if s is None:
|
||||||
|
return default
|
||||||
|
m = re.search(r"-?\d+", s.replace("\xa0", " "))
|
||||||
|
return int(m.group()) if m else default
|
||||||
|
|
||||||
|
|
||||||
|
def _href_id(a, pattern: str) -> int | None:
|
||||||
|
if a is None or not a.get("href"):
|
||||||
|
return None
|
||||||
|
m = re.search(pattern, a["href"])
|
||||||
|
return int(m.group(1)) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_html(url: str, *, timeout: int = 20, retries: int = 2) -> str:
|
||||||
|
last_exc: Exception | None = None
|
||||||
|
for attempt in range(retries + 1):
|
||||||
|
try:
|
||||||
|
r = _session.get(url, timeout=timeout)
|
||||||
|
if r.status_code == 200 and r.text:
|
||||||
|
return r.text
|
||||||
|
if r.status_code in (403, 503) and attempt < retries:
|
||||||
|
time.sleep(1.0 + attempt)
|
||||||
|
continue
|
||||||
|
r.raise_for_status()
|
||||||
|
except Exception as e: # pragma: no cover — network paths
|
||||||
|
last_exc = e
|
||||||
|
time.sleep(1.0 + attempt)
|
||||||
|
# Playwright fallback (last resort)
|
||||||
|
try:
|
||||||
|
from playwright.sync_api import sync_playwright # type: ignore
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
ctx = browser.new_context(user_agent=UA, locale="hr-HR")
|
||||||
|
page = ctx.new_page()
|
||||||
|
page.goto(url, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
html = page.content()
|
||||||
|
browser.close()
|
||||||
|
return html
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"_get_html failed for {url}: {last_exc or e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# buildId-equivalent — NOT a Next.js app. We surface a deterministic version
|
||||||
|
# token taken from the cached CSS asset hash; refreshes hourly.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _build_token_cached(hour_bucket: int) -> str:
|
||||||
|
try:
|
||||||
|
html = _get_html(BASE + "/", timeout=15)
|
||||||
|
m = re.search(r"common\.min\.css\?v=([A-Za-z0-9_\-]+)", html)
|
||||||
|
return m.group(1) if m else f"unknown-{hour_bucket}"
|
||||||
|
except Exception:
|
||||||
|
return f"unknown-{hour_bucket}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_buildid() -> str:
|
||||||
|
"""Returns the current site asset version hash (hourly-cached).
|
||||||
|
HNS Semafor is server-rendered ASP.NET, so there is no Next.js buildId.
|
||||||
|
We surface the CSS asset hash as an equivalent cache-busting token."""
|
||||||
|
return _build_token_cached(int(time.time()) // 3600)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Player parser
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _parse_player_header(soup: BeautifulSoup) -> dict[str, Any]:
|
||||||
|
hdr = soup.select_one("div.block.playerHeader")
|
||||||
|
if hdr is None:
|
||||||
|
return {}
|
||||||
|
name = _txt(hdr.select_one(".playerName .name"))
|
||||||
|
surname = _txt(hdr.select_one(".playerName .surname"))
|
||||||
|
shirt = _txt(hdr.select_one(".playerName h3"))
|
||||||
|
img = hdr.select_one(".photo img")
|
||||||
|
photo_url = img["src"] if img and img.get("src") else None
|
||||||
|
club_a = hdr.select_one(".playerData li.club a")
|
||||||
|
club_name = _txt(hdr.select_one(".playerData li.club h4"))
|
||||||
|
club_id = _href_id(club_a, r"/klubovi/(\d+)/")
|
||||||
|
dob_node = hdr.select_one(".playerData li.dob h4")
|
||||||
|
dob_text = _txt(dob_node)
|
||||||
|
dob = None
|
||||||
|
age = None
|
||||||
|
m = re.match(r"(\d{2}\.\d{2}\.\d{4})", dob_text)
|
||||||
|
if m:
|
||||||
|
dob = m.group(1)
|
||||||
|
m_age = re.search(r"\((\d+)\s+godina", dob_text)
|
||||||
|
if m_age:
|
||||||
|
age = int(m_age.group(1))
|
||||||
|
pob = _txt(hdr.select_one(".playerData li.pob h4")) or None
|
||||||
|
return {
|
||||||
|
"name": name or None,
|
||||||
|
"surname": surname or None,
|
||||||
|
"shirt_number": _int(shirt),
|
||||||
|
"photo_url": photo_url,
|
||||||
|
"current_club": {"id": club_id, "name": club_name or None},
|
||||||
|
"dob": dob,
|
||||||
|
"age": age,
|
||||||
|
"place_of_birth": pob,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_season_recap(soup: BeautifulSoup) -> dict[str, Any]:
|
||||||
|
block = soup.select_one("div.player_season_stats_recap")
|
||||||
|
if block is None:
|
||||||
|
return {}
|
||||||
|
season = _txt(block.select_one("h2"))
|
||||||
|
out = {"season": season or None}
|
||||||
|
for li in block.select("ul > li"):
|
||||||
|
cls = (li.get("class") or [None])[0]
|
||||||
|
out[cls] = _int(_txt(li.select_one("h4")))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_player_seasons(soup: BeautifulSoup) -> list[dict[str, Any]]:
|
||||||
|
"""Statistika po sezonama: each season has a stats table + matches table."""
|
||||||
|
seasons: list[dict[str, Any]] = []
|
||||||
|
container = soup.select_one("div.player_profile_matches")
|
||||||
|
if container is None:
|
||||||
|
return seasons
|
||||||
|
titles = container.select("h2.seasonTitle")
|
||||||
|
tabbed = container.select("div.tabbedContent")
|
||||||
|
for i, h2 in enumerate(titles):
|
||||||
|
season = _txt(h2)
|
||||||
|
tab = tabbed[i] if i < len(tabbed) else None
|
||||||
|
comps: list[dict[str, Any]] = []
|
||||||
|
matches: list[dict[str, Any]] = []
|
||||||
|
if tab is not None:
|
||||||
|
# competitions stats table
|
||||||
|
for st in tab.select("div.stats_table"):
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for row in st.select("ul > li.row"):
|
||||||
|
if "header" in (row.get("class") or []):
|
||||||
|
continue
|
||||||
|
title_a = row.select_one(".title a")
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"competition": _txt(row.select_one(".title")) or None,
|
||||||
|
"competition_url": title_a["href"] if title_a and title_a.get("href") else None,
|
||||||
|
"competition_id": _href_id(title_a, r"/natjecanja/(\d+)/"),
|
||||||
|
"apps": _int(_txt(row.select_one(".apps"))),
|
||||||
|
"starter": _int(_txt(row.select_one(".starter"))),
|
||||||
|
"sub": _int(_txt(row.select_one(".sub"))),
|
||||||
|
"minutes": _int(_txt(row.select_one(".minutes"))),
|
||||||
|
"goals": _int(_txt(row.select_one(".goals"))),
|
||||||
|
"yellows": _int(_txt(row.select_one(".yellows"))),
|
||||||
|
"reds": _int(_txt(row.select_one(".reds"))),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if rows:
|
||||||
|
comps.append({"rows": rows})
|
||||||
|
# matches list
|
||||||
|
for ml in tab.select("div.matchlist"):
|
||||||
|
for row in ml.select("li.row, div.row"):
|
||||||
|
if "header" in (row.get("class") or []):
|
||||||
|
continue
|
||||||
|
date = _txt(row.select_one(".date"))
|
||||||
|
c1 = _txt(row.select_one(".club1"))
|
||||||
|
c2 = _txt(row.select_one(".club2"))
|
||||||
|
res = _txt(row.select_one(".result"))
|
||||||
|
comp = _txt(row.select_one(".competitionround"))
|
||||||
|
if not (date or c1 or c2 or res):
|
||||||
|
continue
|
||||||
|
matches.append(
|
||||||
|
{
|
||||||
|
"date": date or None,
|
||||||
|
"home": c1 or None,
|
||||||
|
"away": c2 or None,
|
||||||
|
"result": res or None,
|
||||||
|
"competition_round": comp or None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
seasons.append({"season": season, "competitions": comps, "matches": matches})
|
||||||
|
return seasons
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_player(hns_id: int, slug: str) -> dict[str, Any]:
|
||||||
|
"""Fetch a player profile from semafor.hns.family and return structured JSON."""
|
||||||
|
url = f"{BASE}/igraci/{hns_id}/{slug.strip('/')}/"
|
||||||
|
t0 = time.time()
|
||||||
|
html = _get_html(url)
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
data = {
|
||||||
|
"source_url": url,
|
||||||
|
"hns_igrac_id": hns_id,
|
||||||
|
"slug": slug.strip("/"),
|
||||||
|
"build_token": get_buildid(),
|
||||||
|
"fetched_at": int(time.time()),
|
||||||
|
"fetch_ms": int((time.time() - t0) * 1000),
|
||||||
|
"profile": _parse_player_header(soup),
|
||||||
|
"season_recap": _parse_season_recap(soup),
|
||||||
|
"seasons": _parse_player_seasons(soup),
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Klub parser
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _parse_klub_header(soup: BeautifulSoup) -> dict[str, Any]:
|
||||||
|
hdr = soup.select_one("div.block.clubHeader")
|
||||||
|
if hdr is None:
|
||||||
|
return {}
|
||||||
|
name = _txt(hdr.select_one(".title h1"))
|
||||||
|
long_name = _txt(hdr.select_one(".title h2"))
|
||||||
|
img = hdr.select_one(".logo img")
|
||||||
|
info: dict[str, Any] = {
|
||||||
|
"name": name or None,
|
||||||
|
"full_name": long_name or None,
|
||||||
|
"logo_url": img["src"] if img and img.get("src") else None,
|
||||||
|
}
|
||||||
|
for li in hdr.select(".info ul > li"):
|
||||||
|
cls = (li.get("class") or [None])[0]
|
||||||
|
val = _txt(li.select_one("h3"))
|
||||||
|
info[cls] = val or None
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_klub(hns_id: int, slug: str) -> dict[str, Any]:
|
||||||
|
"""Fetch a club profile (header + meta) from semafor.hns.family."""
|
||||||
|
url = f"{BASE}/klubovi/{hns_id}/{slug.strip('/')}/"
|
||||||
|
t0 = time.time()
|
||||||
|
html = _get_html(url)
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
return {
|
||||||
|
"source_url": url,
|
||||||
|
"hns_klub_id": hns_id,
|
||||||
|
"slug": slug.strip("/"),
|
||||||
|
"build_token": get_buildid(),
|
||||||
|
"fetched_at": int(time.time()),
|
||||||
|
"fetch_ms": int((time.time() - t0) * 1000),
|
||||||
|
"info": _parse_klub_header(soup),
|
||||||
|
"competitions": _parse_klub_competitions(soup),
|
||||||
|
"next_match": _parse_next_match(soup),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_klub_competitions(soup: BeautifulSoup) -> list[dict[str, Any]]:
|
||||||
|
out = []
|
||||||
|
for tbl in soup.select("div.competition_table"):
|
||||||
|
rows = []
|
||||||
|
for li in tbl.select("li.row, div.row"):
|
||||||
|
if "header" in (li.get("class") or []):
|
||||||
|
continue
|
||||||
|
comp_a = li.select_one("a")
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"competition": _txt(li.select_one(".title")) or _txt(comp_a) or None,
|
||||||
|
"competition_id": _href_id(comp_a, r"/natjecanja/(\d+)/"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if rows:
|
||||||
|
out.extend(rows)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_next_match(soup: BeautifulSoup) -> dict[str, Any] | None:
|
||||||
|
nm = soup.select_one("div.scoreboard_next_previous, div.current_results")
|
||||||
|
if nm is None:
|
||||||
|
return None
|
||||||
|
return {"raw_text": _txt(nm)[:400] or None}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Roster parser — the “Igrači” tab (server-rendered with the page)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def fetch_klub_roster(hns_id: int, slug: str) -> list[dict[str, Any]]:
|
||||||
|
"""Return list of players currently rostered at the club."""
|
||||||
|
url = f"{BASE}/klubovi/{hns_id}/{slug.strip('/')}/"
|
||||||
|
html = _get_html(url)
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
roster: list[dict[str, Any]] = []
|
||||||
|
seen: set[int] = set()
|
||||||
|
for block in soup.select("div.playerslist"):
|
||||||
|
for row in block.select("li.row, div.row, tr"):
|
||||||
|
if "header" in (row.get("class") or []):
|
||||||
|
continue
|
||||||
|
a = row.find("a", href=re.compile(r"/igraci/\d+/"))
|
||||||
|
if a is None:
|
||||||
|
continue
|
||||||
|
pid = _href_id(a, r"/igraci/(\d+)/")
|
||||||
|
if pid is None or pid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(pid)
|
||||||
|
slug_m = re.search(r"/igraci/\d+/([^/]+)/", a["href"])
|
||||||
|
roster.append(
|
||||||
|
{
|
||||||
|
"hns_igrac_id": pid,
|
||||||
|
"slug": slug_m.group(1) if slug_m else None,
|
||||||
|
"name": _txt(a) or None,
|
||||||
|
"url": BASE + a["href"] if a["href"].startswith("/") else a["href"],
|
||||||
|
"apps": _int(_txt(row.select_one(".apps"))),
|
||||||
|
"minutes": _int(_txt(row.select_one(".minutes"))),
|
||||||
|
"goals": _int(_txt(row.select_one(".goals"))),
|
||||||
|
"yellows": _int(_txt(row.select_one(".yellows"))),
|
||||||
|
"reds": _int(_txt(row.select_one(".reds"))),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return roster
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI / smoke test
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _bench(label: str, fn, *args):
|
||||||
|
t0 = time.time()
|
||||||
|
out = fn(*args)
|
||||||
|
dt = (time.time() - t0) * 1000
|
||||||
|
print(f"[{label}] {dt:6.1f} ms args={args}", file=sys.stderr)
|
||||||
|
return out, dt
|
||||||
|
|
||||||
|
|
||||||
|
def _slugify(name: str) -> str:
|
||||||
|
name = name.lower()
|
||||||
|
repl = {"č": "c", "ć": "c", "ž": "z", "š": "s", "đ": "d"}
|
||||||
|
for k, v in repl.items():
|
||||||
|
name = name.replace(k, v)
|
||||||
|
name = re.sub(r"[^a-z0-9]+", "-", name).strip("-")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
samples_players = [
|
||||||
|
(88284, "zoran-kurjaga"),
|
||||||
|
(134238, "roko-antesic"),
|
||||||
|
(1223263, "anel-husic"),
|
||||||
|
]
|
||||||
|
samples_klubovi = [
|
||||||
|
(1589, "nk-zamet"),
|
||||||
|
(107150, "nk-pomorac"),
|
||||||
|
(1574, "hnk-lovran"),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"# build_token = {get_buildid()}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("## fetch_player benchmark")
|
||||||
|
p_times: list[float] = []
|
||||||
|
for pid, slug in samples_players:
|
||||||
|
data, dt = _bench("player", fetch_player, pid, slug)
|
||||||
|
p_times.append(dt)
|
||||||
|
prof = data.get("profile", {})
|
||||||
|
recap = data.get("season_recap", {})
|
||||||
|
seasons = data.get("seasons", [])
|
||||||
|
print(
|
||||||
|
f" - id={pid} slug={slug} -> {prof.get('name')} {prof.get('surname')}, "
|
||||||
|
f"club={prof.get('current_club', {}).get('name')}, dob={prof.get('dob')}, "
|
||||||
|
f"recap_season={recap.get('season')} apps={recap.get('apps')}, "
|
||||||
|
f"seasons_count={len(seasons)}"
|
||||||
|
)
|
||||||
|
print(f" avg={sum(p_times)/len(p_times):.1f} ms")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("## fetch_klub benchmark")
|
||||||
|
k_times: list[float] = []
|
||||||
|
for kid, slug in samples_klubovi:
|
||||||
|
data, dt = _bench("klub", fetch_klub, kid, slug)
|
||||||
|
k_times.append(dt)
|
||||||
|
info = data.get("info", {})
|
||||||
|
print(
|
||||||
|
f" - id={kid} slug={slug} -> {info.get('name')} | "
|
||||||
|
f"founded={info.get('foundation_date')} | "
|
||||||
|
f"address={info.get('address')} | "
|
||||||
|
f"comps={len(data.get('competitions', []))}"
|
||||||
|
)
|
||||||
|
print(f" avg={sum(k_times)/len(k_times):.1f} ms")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("## fetch_klub_roster benchmark")
|
||||||
|
r_times: list[float] = []
|
||||||
|
for kid, slug in samples_klubovi:
|
||||||
|
roster, dt = _bench("roster", fetch_klub_roster, kid, slug)
|
||||||
|
r_times.append(dt)
|
||||||
|
print(f" - id={kid} slug={slug} -> {len(roster)} players")
|
||||||
|
for r in roster[:3]:
|
||||||
|
print(f" • {r['name']} (id={r['hns_igrac_id']}) apps={r.get('apps')} goals={r.get('goals')}")
|
||||||
|
print(f" avg={sum(r_times)/len(r_times):.1f} ms")
|
||||||
|
|
||||||
|
# Dump first sample to disk for inspection
|
||||||
|
out_dir = Path("/opt/pgz-sport/cc_tasks/sub4")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
pid0, slug0 = samples_players[0]
|
||||||
|
sample = fetch_player(pid0, slug0)
|
||||||
|
(out_dir / "sample_player.json").write_text(
|
||||||
|
json.dumps(sample, ensure_ascii=False, indent=2)
|
||||||
|
)
|
||||||
|
kid0, kslug0 = samples_klubovi[0]
|
||||||
|
ksample = fetch_klub(kid0, kslug0)
|
||||||
|
(out_dir / "sample_klub.json").write_text(
|
||||||
|
json.dumps(ksample, ensure_ascii=False, indent=2)
|
||||||
|
)
|
||||||
|
rsample = fetch_klub_roster(kid0, kslug0)
|
||||||
|
(out_dir / "sample_roster.json").write_text(
|
||||||
|
json.dumps(rsample, ensure_ascii=False, indent=2)
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
print(f"# sample JSONs written to {out_dir}/")
|
||||||
@@ -0,0 +1,534 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
hns_player_deep.py — SUB3 deep HNS player scraper
|
||||||
|
─────────────────────────────────────────────────
|
||||||
|
Author: dradulic@outlook.com / damir@rinet.one
|
||||||
|
Date: 2026-05-05
|
||||||
|
Version: 1.0
|
||||||
|
|
||||||
|
Scrapes semafor.hns.family/igraci/{id}/{slug}/ for every clanovi.hns_igrac_id row,
|
||||||
|
extracting:
|
||||||
|
• profil meta (datum_rodenja, mjesto_rodenja, broj_dresa, current klub)
|
||||||
|
• per-season stats per natjecanje (UPSERT pgz_sport.hns_player_seasons)
|
||||||
|
• last 30+ matches (UPSERT pgz_sport.hns_player_matches)
|
||||||
|
|
||||||
|
Server-rendered HTML — no Playwright needed → uses requests for 5–10× speedup.
|
||||||
|
Fallback to Playwright if --use-playwright is passed.
|
||||||
|
|
||||||
|
Resume-able: skips clanovi where last_scraped_at > now() - interval N days.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 hns_player_deep.py [--limit 200] [--days 7] [--player HNS_ID] [--use-playwright]
|
||||||
|
"""
|
||||||
|
import os, sys, re, time, json, argparse, traceback
|
||||||
|
from datetime import datetime, date
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
import requests
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor, execute_values
|
||||||
|
|
||||||
|
DSN = os.getenv("RINET_DSN",
|
||||||
|
"host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7")
|
||||||
|
TG_TOKEN = os.getenv("TG_BOT_TOKEN", "8535797835:AAFItT-92jzZ9NWFafLxn0dLa1_n2s-JE5Y")
|
||||||
|
TG_CHAT = os.getenv("TG_CHAT", "7969491558")
|
||||||
|
SLEEP = float(os.getenv("SLEEP", "0.8"))
|
||||||
|
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
LOG_DIR = "/var/log/pgz-sport-debug"
|
||||||
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
LOG_FILE = f"{LOG_DIR}/sub3_{datetime.now().strftime('%Y%m%d_%H%M')}.log"
|
||||||
|
LOG_FH = open(LOG_FILE, "a", encoding="utf-8")
|
||||||
|
|
||||||
|
def log(msg: str, telegram: bool = False) -> None:
|
||||||
|
line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}"
|
||||||
|
print(line, flush=True)
|
||||||
|
LOG_FH.write(line + "\n"); LOG_FH.flush()
|
||||||
|
if telegram and TG_TOKEN and TG_CHAT:
|
||||||
|
try:
|
||||||
|
requests.post(
|
||||||
|
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
|
||||||
|
data={"chat_id": TG_CHAT, "text": msg[:4000]},
|
||||||
|
timeout=8,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── HTTP session ──────────────────────────────────────────────────────────
|
||||||
|
SESSION = requests.Session()
|
||||||
|
SESSION.headers.update({"User-Agent": UA, "Accept-Language": "hr,en;q=0.7"})
|
||||||
|
|
||||||
|
def fetch_html(url: str, timeout: int = 20) -> str | None:
|
||||||
|
try:
|
||||||
|
r = SESSION.get(url, timeout=timeout)
|
||||||
|
if r.status_code != 200:
|
||||||
|
log(f" HTTP {r.status_code} {url}")
|
||||||
|
return None
|
||||||
|
return r.text
|
||||||
|
except Exception as e:
|
||||||
|
log(f" fetch fail {url}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ── Parsers ───────────────────────────────────────────────────────────────
|
||||||
|
def _strip_html(s: str) -> str:
|
||||||
|
s = re.sub(r"<[^>]+>", " ", s)
|
||||||
|
return re.sub(r"\s+", " ", s).strip()
|
||||||
|
|
||||||
|
def parse_profile(html: str) -> dict:
|
||||||
|
"""Extract player profile meta (HNS exposes only birth date / city / jersey / current club)."""
|
||||||
|
out = {
|
||||||
|
"broj_dresa": None,
|
||||||
|
"datum_rodenja": None,
|
||||||
|
"mjesto_rodenja": None,
|
||||||
|
"klub_hns_id": None,
|
||||||
|
"klub_naziv": None,
|
||||||
|
}
|
||||||
|
# playerHeader block (everything from header to first <!--)
|
||||||
|
m = re.search(r'<div class="block playerHeader"[^>]*>(.*?)<!--', html, re.DOTALL)
|
||||||
|
header_html = m.group(1) if m else html
|
||||||
|
|
||||||
|
# Jersey number
|
||||||
|
m = re.search(r'<span class="number"[^>]*>(\d+)</span>', header_html)
|
||||||
|
if not m:
|
||||||
|
# fallback: number in playerHeader text region (first standalone digit before name)
|
||||||
|
text = _strip_html(header_html)
|
||||||
|
mm = re.match(r'^\s*(\d{1,2})\s+[A-ZČĆŠŽĐ]', text)
|
||||||
|
if mm:
|
||||||
|
out["broj_dresa"] = int(mm.group(1))
|
||||||
|
else:
|
||||||
|
out["broj_dresa"] = int(m.group(1))
|
||||||
|
|
||||||
|
# Trenutni klub (first /klubovi/ link in header)
|
||||||
|
m = re.search(r'<a[^>]+href="/klubovi/(\d+)/([\w-]+)/?"[^>]*>([^<]+)<', header_html)
|
||||||
|
if m:
|
||||||
|
out["klub_hns_id"] = m.group(1)
|
||||||
|
out["klub_naziv"] = m.group(3).strip()
|
||||||
|
|
||||||
|
# Datum rođenja (dd.mm.yyyy.)
|
||||||
|
m = re.search(r'(\d{1,2})\.(\d{1,2})\.(\d{4})\.\s*(?:</[^>]+>\s*)?(?:<[^>]+>\s*)*\(?\s*\d+\s*godin', header_html)
|
||||||
|
if not m:
|
||||||
|
# Looser pattern in playerData
|
||||||
|
m = re.search(r'<div[^>]*class="[^"]*birth[^"]*"[^>]*>(\d{1,2})\.(\d{1,2})\.(\d{4})', header_html)
|
||||||
|
if not m:
|
||||||
|
# Fallback: any dd.mm.yyyy. near "Datum rođenja"
|
||||||
|
text = _strip_html(header_html)
|
||||||
|
mm = re.search(r'(\d{1,2})\.(\d{1,2})\.(\d{4})\.\s*\(?\s*\d+\s*godin[ae]?\)?\s*Datum rođenja', text)
|
||||||
|
if mm:
|
||||||
|
m = mm
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
out["datum_rodenja"] = date(int(m.group(3)), int(m.group(2)), int(m.group(1)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Mjesto rođenja: text right before "Mjesto rođenja"
|
||||||
|
text_all = _strip_html(header_html)
|
||||||
|
mm = re.search(r'([A-ZČĆŠŽĐ][\w\sčćšžđČĆŠŽĐ\-]{1,80}?)\s+Mjesto rođenja', text_all)
|
||||||
|
if mm:
|
||||||
|
out["mjesto_rodenja"] = mm.group(1).strip()
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Each season block: "{YYYY/YY} Statistika Utakmice ... <playerCompetitionStatsTable> ... <matchlist>"
|
||||||
|
# We split player_profile_matches by the recurring pattern.
|
||||||
|
SEASON_HEADER_RE = re.compile(
|
||||||
|
r'(?:<[^>]+>\s*)?(20\d{2}/\d{2})(?:\s*<[^>]+>)?\s*Statistika\s+Utakmice',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_seasons_and_matches(html: str) -> tuple[list[dict], list[dict]]:
|
||||||
|
"""Return (season_rows, match_rows) for ALL seasons on the profile page."""
|
||||||
|
# Limit to player_profile_matches block to avoid noise
|
||||||
|
m = re.search(
|
||||||
|
r'<div class="block w1280 matchlist style1 player_profile_matches"[^>]*>(.*?)(?=<!--|<footer)',
|
||||||
|
html, re.DOTALL,
|
||||||
|
)
|
||||||
|
if not m:
|
||||||
|
return [], []
|
||||||
|
block = m.group(1)
|
||||||
|
|
||||||
|
# Find season header positions: <h2 class="seasonTitle ...">YYYY/YY</h2>
|
||||||
|
headers = list(re.finditer(
|
||||||
|
r'<h2\s+class="seasonTitle[^"]*"[^>]*>\s*(20\d{2}/\d{2})\s*</h2>',
|
||||||
|
block,
|
||||||
|
))
|
||||||
|
if not headers:
|
||||||
|
# Fallback: any <h2> with season label
|
||||||
|
headers = list(re.finditer(r'<h2[^>]*>\s*(20\d{2}/\d{2})\s*</h2>', block))
|
||||||
|
if not headers:
|
||||||
|
plain = re.sub(r'<[^>]+>', ' ', block)
|
||||||
|
plain = re.sub(r'\s+', ' ', plain)
|
||||||
|
return _parse_plain(plain)
|
||||||
|
|
||||||
|
sections = []
|
||||||
|
for i, h in enumerate(headers):
|
||||||
|
sezona = h.group(1)
|
||||||
|
start = h.start()
|
||||||
|
end = headers[i + 1].start() if i + 1 < len(headers) else len(block)
|
||||||
|
sections.append((sezona, block[start:end]))
|
||||||
|
|
||||||
|
season_rows: list[dict] = []
|
||||||
|
match_rows: list[dict] = []
|
||||||
|
|
||||||
|
for sezona, sec in sections:
|
||||||
|
# ── Per-season per-natjecanje stats (playerCompetitionStatsTable) ──
|
||||||
|
cs = re.search(
|
||||||
|
r'<div class="block w1280 playerCompetitionStatsTable"[^>]*>(.*?)</div>\s*</div>\s*</div>',
|
||||||
|
sec, re.DOTALL,
|
||||||
|
)
|
||||||
|
if cs:
|
||||||
|
stab = cs.group(1)
|
||||||
|
# Header row → identify columns; body rows have natjecanje + 6 ints
|
||||||
|
# Extract: total row "Ukupno" + per-competition rows
|
||||||
|
# Each row appears as <td>…</td>. Use table-agnostic approach: find every block of
|
||||||
|
# "<td>NATJECANJE</td><td>N</td><td>S</td><td>Z</td><td>G</td><td>YEL</td><td>RED</td>"
|
||||||
|
# but tables here use divs not td. Walk plain text per line.
|
||||||
|
stext = _strip_html(stab)
|
||||||
|
# Split by competition-row pattern: "<label> <int> <int> <int> <int> <int> <int>"
|
||||||
|
for rm in re.finditer(
|
||||||
|
r'([A-ZČĆŠŽĐa-zčćšžđ0-9][^|]*?)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)(?=\s|$)',
|
||||||
|
stext,
|
||||||
|
):
|
||||||
|
label = rm.group(1).strip()
|
||||||
|
if label.lower().startswith("ukupno"):
|
||||||
|
continue # we keep per-natjecanje rows only (UNIQUE prefers natjecanje)
|
||||||
|
if "Nastupi" in label or "Započeo" in label or "Statistika" in label:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
season_rows.append({
|
||||||
|
"sezona": sezona,
|
||||||
|
"natjecanje": label[:200],
|
||||||
|
"nastupi": int(rm.group(2)),
|
||||||
|
"startna": int(rm.group(3)),
|
||||||
|
"zamjena": int(rm.group(4)),
|
||||||
|
"golovi": int(rm.group(5)),
|
||||||
|
"zuti": int(rm.group(6)),
|
||||||
|
"crveni": int(rm.group(7)),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── Matches (matchlist style2) ──
|
||||||
|
ml = re.search(
|
||||||
|
r'<div class="matchlist style2 semafor player[^"]*"[^>]*>(.*?)</ul>',
|
||||||
|
sec, re.DOTALL,
|
||||||
|
)
|
||||||
|
if ml:
|
||||||
|
list_html = ml.group(1)
|
||||||
|
for row in re.finditer(
|
||||||
|
r'<li class="row[^"]*"[^>]*data-match="(\d+)"[^>]*>(.*?)</li>',
|
||||||
|
list_html, re.DOTALL,
|
||||||
|
):
|
||||||
|
row_html = row.group(2)
|
||||||
|
# Date
|
||||||
|
d = re.search(r'<div class="date">([^<]+)</div>', row_html)
|
||||||
|
# club1 / club2
|
||||||
|
c1 = re.search(r'<div class="club1"[^>]*>\s*<a[^>]*>([^<]+?)<', row_html)
|
||||||
|
c2 = re.search(r'<div class="club2"[^>]*>\s*<a[^>]*>([^<]+?)<', row_html)
|
||||||
|
# result
|
||||||
|
r1 = re.search(r'<div class="res1">(\d+)</div>', row_html)
|
||||||
|
r2 = re.search(r'<div class="res2">(\d+)</div>', row_html)
|
||||||
|
# natjecanje
|
||||||
|
cr = re.search(r'<div class="competitionround">([^<]+)</div>', row_html)
|
||||||
|
# goals
|
||||||
|
gl = re.search(r'<div class="goals">(\d+)</div>', row_html)
|
||||||
|
# cards "Y / R"
|
||||||
|
ca = re.search(r'<div class="cards">.*?(\d+)\s*/\s*(\d+).*?</div>', row_html, re.DOTALL)
|
||||||
|
# minutes
|
||||||
|
mn = re.search(r'<div class="minutes">(\d+)</div>', row_html)
|
||||||
|
|
||||||
|
# Parse date dd.mm.yyyy. HH:MM
|
||||||
|
datum = None
|
||||||
|
if d:
|
||||||
|
dm = re.search(r'(\d{1,2})\.(\d{1,2})\.(\d{4})', d.group(1))
|
||||||
|
if dm:
|
||||||
|
try:
|
||||||
|
datum = date(int(dm.group(3)), int(dm.group(2)), int(dm.group(1)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
rezultat = f"{r1.group(1)}:{r2.group(1)}" if r1 and r2 else None
|
||||||
|
|
||||||
|
match_rows.append({
|
||||||
|
"datum": datum,
|
||||||
|
"domacin": (c1.group(1).strip() if c1 else "")[:120],
|
||||||
|
"gost": (c2.group(1).strip() if c2 else "")[:120],
|
||||||
|
"rezultat": rezultat,
|
||||||
|
"natjecanje": (cr.group(1).strip() if cr else "")[:200],
|
||||||
|
"golovi": int(gl.group(1)) if gl else 0,
|
||||||
|
"zuti": int(ca.group(1)) if ca else 0,
|
||||||
|
"crveni": int(ca.group(2)) if ca else 0,
|
||||||
|
"minute_do": int(mn.group(1)) if mn else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return season_rows, match_rows
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_plain(plain_text: str) -> tuple[list[dict], list[dict]]:
|
||||||
|
"""Fallback: parse from already-stripped plain text (no match-row HTML access)."""
|
||||||
|
# Best effort: extract season totals only
|
||||||
|
season_rows: list[dict] = []
|
||||||
|
# Split by season headers
|
||||||
|
parts = re.split(r'(20\d{2}/\d{2})\s+Statistika\s+Utakmice', plain_text)
|
||||||
|
# parts: [pre, season1, body1, season2, body2, ...]
|
||||||
|
for i in range(1, len(parts), 2):
|
||||||
|
sezona = parts[i]
|
||||||
|
body = parts[i + 1] if i + 1 < len(parts) else ""
|
||||||
|
# Find the "Ukupno N N N G Y R" then per-competition lines
|
||||||
|
for rm in re.finditer(
|
||||||
|
r'([A-ZČĆŠŽĐa-zčćšžđ0-9][^|]*?)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)(?=\s|$)',
|
||||||
|
body[:3000],
|
||||||
|
):
|
||||||
|
label = rm.group(1).strip()
|
||||||
|
if label.lower().startswith("ukupno"):
|
||||||
|
continue
|
||||||
|
if "Nastupi" in label or "Statistika" in label:
|
||||||
|
continue
|
||||||
|
season_rows.append({
|
||||||
|
"sezona": sezona,
|
||||||
|
"natjecanje": label[:200],
|
||||||
|
"nastupi": int(rm.group(2)),
|
||||||
|
"startna": int(rm.group(3)),
|
||||||
|
"zamjena": int(rm.group(4)),
|
||||||
|
"golovi": int(rm.group(5)),
|
||||||
|
"zuti": int(rm.group(6)),
|
||||||
|
"crveni": int(rm.group(7)),
|
||||||
|
})
|
||||||
|
return season_rows, []
|
||||||
|
|
||||||
|
# ── DB ────────────────────────────────────────────────────────────────────
|
||||||
|
def db_conn():
|
||||||
|
c = psycopg2.connect(DSN); c.autocommit = True; return c
|
||||||
|
|
||||||
|
def get_targets(conn, limit: int, days: int, force_player: str | None = None) -> list[dict]:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
if force_player:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, hns_igrac_id, ime, prezime, source_url, slug
|
||||||
|
FROM pgz_sport.clanovi
|
||||||
|
WHERE hns_igrac_id = %s
|
||||||
|
LIMIT 1
|
||||||
|
""", (force_player,))
|
||||||
|
else:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, hns_igrac_id, ime, prezime, source_url, slug
|
||||||
|
FROM pgz_sport.clanovi
|
||||||
|
WHERE hns_igrac_id IS NOT NULL
|
||||||
|
AND (last_scraped_at IS NULL OR last_scraped_at < now() - %s::interval)
|
||||||
|
ORDER BY (last_scraped_at IS NULL) DESC, id ASC
|
||||||
|
LIMIT %s
|
||||||
|
""", (f"{days} days", limit))
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
def update_clan(conn, clan_id: int, profile: dict, url: str) -> None:
|
||||||
|
sets, vals = [], []
|
||||||
|
if profile.get("datum_rodenja"):
|
||||||
|
sets.append("datum_rodenja = COALESCE(datum_rodenja, %s)")
|
||||||
|
vals.append(profile["datum_rodenja"])
|
||||||
|
sets.append("datum_rodjenja = COALESCE(datum_rodjenja, %s)")
|
||||||
|
vals.append(profile["datum_rodenja"])
|
||||||
|
if profile.get("mjesto_rodenja"):
|
||||||
|
sets.append("mjesto_rodenja = COALESCE(NULLIF(mjesto_rodenja,''), %s)")
|
||||||
|
vals.append(profile["mjesto_rodenja"])
|
||||||
|
sets.append("mjesto_rodjenja = COALESCE(NULLIF(mjesto_rodjenja,''), %s)")
|
||||||
|
vals.append(profile["mjesto_rodenja"])
|
||||||
|
if profile.get("broj_dresa") is not None:
|
||||||
|
sets.append("broj_dresa = COALESCE(broj_dresa, %s)")
|
||||||
|
vals.append(profile["broj_dresa"])
|
||||||
|
sets.append("source_url = %s"); vals.append(url)
|
||||||
|
sets.append("source = COALESCE(NULLIF(source,''), 'hns_semafor')")
|
||||||
|
sets.append("sport = COALESCE(NULLIF(sport,''), 'nogomet')")
|
||||||
|
sets.append("last_scraped_at = now()")
|
||||||
|
sets.append("source_synced_at = now()")
|
||||||
|
vals.append(clan_id)
|
||||||
|
sql = f"UPDATE pgz_sport.clanovi SET {', '.join(sets)} WHERE id = %s"
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(sql, tuple(vals))
|
||||||
|
|
||||||
|
def upsert_seasons(conn, hns_id: str, clan_id: int, url: str, rows: list[dict]) -> int:
|
||||||
|
if not rows:
|
||||||
|
return 0
|
||||||
|
raw = [
|
||||||
|
(hns_id, clan_id, r["sezona"], None, None, r["natjecanje"][:200],
|
||||||
|
r.get("nastupi", 0), r.get("startna", 0), r.get("zamjena", 0),
|
||||||
|
r.get("golovi", 0), 0, r.get("zuti", 0), r.get("crveni", 0), 0, url)
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
# Dedupe by UNIQUE (hns_igrac_id, sezona, klub_hns_id, natjecanje)
|
||||||
|
dedup: dict[tuple, tuple] = {}
|
||||||
|
for row in raw:
|
||||||
|
k = (row[0], row[2], row[3], row[5])
|
||||||
|
dedup[k] = row
|
||||||
|
data = list(dedup.values())
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
execute_values(cur, """
|
||||||
|
INSERT INTO pgz_sport.hns_player_seasons
|
||||||
|
(hns_igrac_id, clan_id, sezona, klub_hns_id, klub_naziv, natjecanje,
|
||||||
|
nastupi, startna, zamjena, golovi, asistencije, zuti, crveni, minute, source_url)
|
||||||
|
VALUES %s
|
||||||
|
ON CONFLICT (hns_igrac_id, sezona, klub_hns_id, natjecanje) DO UPDATE SET
|
||||||
|
nastupi = EXCLUDED.nastupi,
|
||||||
|
startna = EXCLUDED.startna,
|
||||||
|
zamjena = EXCLUDED.zamjena,
|
||||||
|
golovi = EXCLUDED.golovi,
|
||||||
|
zuti = EXCLUDED.zuti,
|
||||||
|
crveni = EXCLUDED.crveni,
|
||||||
|
source_url = EXCLUDED.source_url,
|
||||||
|
scraped_at = now()
|
||||||
|
""", data)
|
||||||
|
return len(rows)
|
||||||
|
|
||||||
|
def upsert_matches(conn, hns_id: str, clan_id: int, url: str, rows: list[dict]) -> int:
|
||||||
|
if not rows:
|
||||||
|
return 0
|
||||||
|
raw = [
|
||||||
|
(hns_id, clan_id, r["datum"], r["natjecanje"], r["domacin"], r["gost"],
|
||||||
|
r["rezultat"], None, None, None, r.get("minute_do"),
|
||||||
|
r.get("golovi", 0), 0, r.get("zuti", 0), r.get("crveni", 0), url)
|
||||||
|
for r in rows if r["datum"] and r["domacin"] and r["gost"]
|
||||||
|
]
|
||||||
|
# Dedupe by UNIQUE key (hns_igrac_id, datum, domacin, gost) — keep last occurrence
|
||||||
|
dedup: dict[tuple, tuple] = {}
|
||||||
|
for row in raw:
|
||||||
|
k = (row[0], row[2], row[4], row[5])
|
||||||
|
dedup[k] = row
|
||||||
|
data = list(dedup.values())
|
||||||
|
if not data:
|
||||||
|
return 0
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
execute_values(cur, """
|
||||||
|
INSERT INTO pgz_sport.hns_player_matches
|
||||||
|
(hns_igrac_id, clan_id, datum, natjecanje, domacin, gost,
|
||||||
|
rezultat, pozicija, startna, minute_od, minute_do,
|
||||||
|
golovi, asistencije, zuti, crveni, source_url)
|
||||||
|
VALUES %s
|
||||||
|
ON CONFLICT (hns_igrac_id, datum, domacin, gost) DO UPDATE SET
|
||||||
|
rezultat = EXCLUDED.rezultat,
|
||||||
|
natjecanje = EXCLUDED.natjecanje,
|
||||||
|
minute_do = EXCLUDED.minute_do,
|
||||||
|
golovi = EXCLUDED.golovi,
|
||||||
|
zuti = EXCLUDED.zuti,
|
||||||
|
crveni = EXCLUDED.crveni,
|
||||||
|
source_url = EXCLUDED.source_url,
|
||||||
|
scraped_at = now()
|
||||||
|
""", data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
# ── Slug helper ───────────────────────────────────────────────────────────
|
||||||
|
def slugify(text: str) -> str:
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
repl = str.maketrans("čćžšđČĆŽŠĐ", "ccczsdcczsd"[:10])
|
||||||
|
t = text.lower().translate(repl)
|
||||||
|
t = re.sub(r"[^a-z0-9\s-]", "", t)
|
||||||
|
return re.sub(r"\s+", "-", t).strip("-")
|
||||||
|
|
||||||
|
def build_url(t: dict) -> str:
|
||||||
|
if t.get("source_url") and "semafor.hns.family/igraci/" in t["source_url"]:
|
||||||
|
return t["source_url"]
|
||||||
|
slug = (t.get("slug") or slugify(f"{t['ime']} {t['prezime']}")) or "x"
|
||||||
|
return f"https://semafor.hns.family/igraci/{t['hns_igrac_id']}/{slug}/"
|
||||||
|
|
||||||
|
# ── Driver ────────────────────────────────────────────────────────────────
|
||||||
|
def process_one(conn, t: dict) -> dict:
|
||||||
|
url = build_url(t)
|
||||||
|
html = fetch_html(url)
|
||||||
|
if not html or "playerHeader" not in html:
|
||||||
|
log(f" ✗ no playerHeader for {t['ime']} {t['prezime']} ({t['hns_igrac_id']}) → {url}")
|
||||||
|
# Mark as scraped to avoid hot-loop on broken URL
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE pgz_sport.clanovi SET last_scraped_at = now() WHERE id = %s",
|
||||||
|
(t["id"],),
|
||||||
|
)
|
||||||
|
return {"profile": False, "seasons": 0, "matches": 0, "fields": 0}
|
||||||
|
|
||||||
|
profile = parse_profile(html)
|
||||||
|
seasons, matches = parse_seasons_and_matches(html)
|
||||||
|
|
||||||
|
# Update clan profile
|
||||||
|
update_clan(conn, t["id"], profile, url)
|
||||||
|
n_fields = sum(1 for k in ("datum_rodenja", "mjesto_rodenja", "broj_dresa") if profile.get(k))
|
||||||
|
|
||||||
|
n_s = upsert_seasons(conn, t["hns_igrac_id"], t["id"], url, seasons)
|
||||||
|
n_m = upsert_matches(conn, t["hns_igrac_id"], t["id"], url, matches)
|
||||||
|
|
||||||
|
return {"profile": True, "seasons": n_s, "matches": n_m, "fields": n_fields}
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--limit", type=int, default=200)
|
||||||
|
ap.add_argument("--days", type=int, default=7)
|
||||||
|
ap.add_argument("--player", help="Single HNS ID (debug)")
|
||||||
|
ap.add_argument("--missing-matches", action="store_true",
|
||||||
|
help="Only target clanovi without rows in hns_player_matches")
|
||||||
|
ap.add_argument("--no-telegram", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
log(f"SUB3 deep scraper start | limit={args.limit} | days={args.days} | "
|
||||||
|
f"missing_matches={args.missing_matches} | log={LOG_FILE}",
|
||||||
|
telegram=not args.no_telegram)
|
||||||
|
|
||||||
|
conn = db_conn()
|
||||||
|
if args.missing_matches:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, hns_igrac_id, ime, prezime, source_url, slug
|
||||||
|
FROM pgz_sport.clanovi
|
||||||
|
WHERE hns_igrac_id IS NOT NULL
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT clan_id FROM pgz_sport.hns_player_matches WHERE clan_id IS NOT NULL
|
||||||
|
)
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT %s
|
||||||
|
""", (args.limit,))
|
||||||
|
targets = cur.fetchall()
|
||||||
|
else:
|
||||||
|
targets = get_targets(conn, args.limit, args.days, args.player)
|
||||||
|
log(f"Targets: {len(targets)}")
|
||||||
|
|
||||||
|
stats = {"scraped": 0, "seasons": 0, "matches": 0, "fields": 0, "errors": 0}
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
for i, t in enumerate(targets, 1):
|
||||||
|
try:
|
||||||
|
r = process_one(conn, t)
|
||||||
|
stats["scraped"] += 1
|
||||||
|
stats["seasons"] += r["seasons"]
|
||||||
|
stats["matches"] += r["matches"]
|
||||||
|
stats["fields"] += r["fields"]
|
||||||
|
if i % 10 == 0 or r["matches"] > 0:
|
||||||
|
log(f" [{i}/{len(targets)}] {t['ime']} {t['prezime']} "
|
||||||
|
f"→ seasons +{r['seasons']} matches +{r['matches']} fields +{r['fields']} "
|
||||||
|
f"(totals: s={stats['seasons']} m={stats['matches']})")
|
||||||
|
except Exception as e:
|
||||||
|
stats["errors"] += 1
|
||||||
|
log(f" ✗ ERROR {t['ime']} {t['prezime']} ({t['hns_igrac_id']}): {e}")
|
||||||
|
log(traceback.format_exc()[:500])
|
||||||
|
time.sleep(SLEEP)
|
||||||
|
|
||||||
|
dur = time.time() - t0
|
||||||
|
summary = (
|
||||||
|
f"SUB3 done in {dur:.0f}s | scraped={stats['scraped']} "
|
||||||
|
f"seasons +{stats['seasons']} matches +{stats['matches']} "
|
||||||
|
f"fields +{stats['fields']} errors={stats['errors']}"
|
||||||
|
)
|
||||||
|
log(summary, telegram=not args.no_telegram)
|
||||||
|
|
||||||
|
# Result file
|
||||||
|
res_path = "/opt/pgz-sport/cc_tasks/SUB3_RESULT.md"
|
||||||
|
with open(res_path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"\n## Run {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
f.write(f"- batch_limit: {args.limit}\n")
|
||||||
|
f.write(f"- targets: {len(targets)}\n")
|
||||||
|
f.write(f"- scraped: {stats['scraped']}\n")
|
||||||
|
f.write(f"- seasons +{stats['seasons']}\n")
|
||||||
|
f.write(f"- matches +{stats['matches']}\n")
|
||||||
|
f.write(f"- profile fields enriched: +{stats['fields']}\n")
|
||||||
|
f.write(f"- errors: {stats['errors']}\n")
|
||||||
|
f.write(f"- duration: {dur:.0f}s\n")
|
||||||
|
f.write(f"- log: {LOG_FILE}\n")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# hns_youth_categories.py — SUB5 — HNS Semafor youth team scraper (v1.0)
|
||||||
|
# Author: Damir Radulić <dradulic@outlook.com> / <damir@rinet.one>
|
||||||
|
# Date: 2026-05-05
|
||||||
|
# Description:
|
||||||
|
# Discovers per-club age categories (Seniori / U-19 juniori / U-17 kadeti /
|
||||||
|
# U-15 stariji pioniri / U-13 mlađi pioniri / U-11/U-9 početnici) by
|
||||||
|
# scraping HNS COMET Semafor competition pages and matching participating
|
||||||
|
# klubovi with hns_klub_id in pgz_sport.klubovi. For each (klub, kategorija,
|
||||||
|
# sezona) the per-club competition roster is fetched and players are
|
||||||
|
# upserted into pgz_sport.clan_kategorije (M2M player x category x season).
|
||||||
|
#
|
||||||
|
# Strategy:
|
||||||
|
# 1. Hardcoded list of per-season national + 2.NL competitions whose
|
||||||
|
# cid → kategorija mapping is known (PGZ regional 3.NL/ŽNS leagues
|
||||||
|
# are added as discovered).
|
||||||
|
# 2. For each competition, fetch /natjecanja/{cid}/{slug}/ and extract
|
||||||
|
# all participating /klubovi/{kid}/{slug}/ links.
|
||||||
|
# 3. Match against pgz_sport.klubovi (hns_klub_id). For each match,
|
||||||
|
# fetch /klubovi/{kid}/{slug}/?cid={cid} and parse player /igraci/
|
||||||
|
# links — these are the players belonging to this age category.
|
||||||
|
# 4. Upsert each player as clanovi (source=hns_semafor) and write
|
||||||
|
# clan_kategorije(clan_id, klub_id, kategorija, sezona, source,
|
||||||
|
# source_url, scraped_at).
|
||||||
|
#
|
||||||
|
# Run modes:
|
||||||
|
# python hns_youth_categories.py discover # dry-run, only logs
|
||||||
|
# python hns_youth_categories.py run # full scrape + DB upsert
|
||||||
|
# python hns_youth_categories.py klub <db_kid> # one club only
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import unquote
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# Try to use SUB4's hns_api_client for shared session/UA
|
||||||
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||||
|
try:
|
||||||
|
import hns_api_client as hns_api # type: ignore
|
||||||
|
_GET_HTML = hns_api._get_html
|
||||||
|
_UA = hns_api.UA
|
||||||
|
except Exception:
|
||||||
|
_GET_HTML = None
|
||||||
|
_UA = (
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
DB_DSN = dict(
|
||||||
|
host="10.10.0.2", port=6432, dbname="rinet_v3",
|
||||||
|
user="rinet", password="R1net2026!SecureDB#v7",
|
||||||
|
)
|
||||||
|
BASE = "https://semafor.hns.family"
|
||||||
|
RATE_S = 1.0
|
||||||
|
TIMEOUT = 25
|
||||||
|
|
||||||
|
LOG_DIR = Path("/var/log/pgz-sport-debug")
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
LOG_FILE = LOG_DIR / f"sub5_{datetime.now().strftime('%Y%m%d_%H%M')}.log"
|
||||||
|
|
||||||
|
log = logging.getLogger("sub5")
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE, encoding="utf-8"),
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Telegram ───────────────────────────────────────────────────────────────
|
||||||
|
TG_TOKEN = os.environ.get("TG_TOKEN", "8535797835:AAFItT-92jzZ9NWFafLxn0dLa1_n2s-JE5Y")
|
||||||
|
TG_CHAT = os.environ.get("TG_CHAT", "7969491558")
|
||||||
|
|
||||||
|
|
||||||
|
def tg_send(msg: str):
|
||||||
|
if not TG_TOKEN or not TG_CHAT:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
requests.post(
|
||||||
|
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
|
||||||
|
data={"chat_id": TG_CHAT, "text": msg, "parse_mode": "Markdown"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"telegram failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── HTTP fallback ──────────────────────────────────────────────────────────
|
||||||
|
_session = requests.Session()
|
||||||
|
_session.headers.update({"User-Agent": _UA, "Accept-Language": "hr,en;q=0.7"})
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url: str) -> str:
|
||||||
|
if _GET_HTML is not None:
|
||||||
|
return _GET_HTML(url)
|
||||||
|
log.debug(f"GET {url}")
|
||||||
|
r = _session.get(url, timeout=TIMEOUT)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
|
||||||
|
# ── Competition catalogue ─────────────────────────────────────────────────
|
||||||
|
# Each entry: (cid, slug, kategorija, sezona). PGZ-relevant national /
|
||||||
|
# 2.NL leagues per season. Regional ŽNS leagues are discovered dynamically
|
||||||
|
# via discover_pgz_competitions() once we find them inside klub raspored.
|
||||||
|
COMP_CATALOG = [
|
||||||
|
# 2025/2026 season
|
||||||
|
("100454960", "1-nl-juniori", "juniori-u19", "2025/2026"),
|
||||||
|
("100454979", "1-nl-kadeti", "kadeti-u17", "2025/2026"),
|
||||||
|
("100454999", "1-nl-pioniri", "pioniri-u15", "2025/2026"),
|
||||||
|
("100540163", "2-nl-juniori-a", "juniori-u19", "2025/2026"),
|
||||||
|
("100540177", "2-nl-juniori-b", "juniori-u19", "2025/2026"),
|
||||||
|
("100540032", "2-nl-kadeti-a", "kadeti-u17", "2025/2026"),
|
||||||
|
("100540109", "2-nl-kadeti-b", "kadeti-u17", "2025/2026"),
|
||||||
|
("100381663", "kvalifikacije-za-prvu-nl-juniori", "juniori-u19", "2025/2026"),
|
||||||
|
("100381584", "kvalifikacije-za-prvu-nl-kadeti", "kadeti-u17", "2025/2026"),
|
||||||
|
("100381484", "kvalifikacije-za-prvu-nl-pioniri", "pioniri-u15", "2025/2026"),
|
||||||
|
("100569152", "treca-nl-istok", "seniori", "2025/2026"), # Treća NL Istok
|
||||||
|
("100585203", "treca-nl-zapad", "seniori", "2025/2026"), # Treća NL Zapad (PGŽ klubovi)
|
||||||
|
("100391485", "supersport-hnl", "seniori", "2025/2026"),
|
||||||
|
("100413651", "supersport-prva-nl", "seniori", "2025/2026"),
|
||||||
|
("100418001", "supersport-druga-nl", "seniori", "2025/2026"),
|
||||||
|
("100439118", "supersport-hnk", "seniori", "2025/2026"), # Cup, all seniori
|
||||||
|
("101411063", "hrvatski-nogometni-kup", "seniori", "2025/2026"),
|
||||||
|
# 2024/2025 season — same structure, slightly different cids; will be
|
||||||
|
# discovered dynamically per-klub as well.
|
||||||
|
]
|
||||||
|
|
||||||
|
# Map from acat dropdown values (HR semantic labels) → kategorija
|
||||||
|
ACAT_MAP = {
|
||||||
|
"Seniors": "seniori",
|
||||||
|
"Juniors": "juniori-u19",
|
||||||
|
"Juniors 2": "juniori-u19",
|
||||||
|
"Cadets": "kadeti-u17",
|
||||||
|
"Cadets 2": "kadeti-u17",
|
||||||
|
"Pioneers": "pioniri-u15",
|
||||||
|
"Pioneers 2": "pioniri-u15",
|
||||||
|
"Young pioneers": "mladji-pioniri-u13",
|
||||||
|
"Beginners": "pocetnici-u11",
|
||||||
|
"Pre-beginners (6+1, 20min)": "pocetnici-u9",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Heuristic from competition name → kategorija
|
||||||
|
def kategorija_from_name(name: str) -> str:
|
||||||
|
nl = name.lower()
|
||||||
|
if "juniori" in nl or "juniors" in nl:
|
||||||
|
return "juniori-u19"
|
||||||
|
if "kadeti" in nl or "cadets" in nl or "kadetkinje" in nl:
|
||||||
|
return "kadeti-u17"
|
||||||
|
if "stariji pioniri" in nl:
|
||||||
|
return "pioniri-u15"
|
||||||
|
if "mladji pioniri" in nl or "mlađi pioniri" in nl or "young pioneers" in nl:
|
||||||
|
return "mladji-pioniri-u13"
|
||||||
|
if "pioniri" in nl or "pioneers" in nl or "pionirke" in nl:
|
||||||
|
return "pioniri-u15"
|
||||||
|
if "pocetnici u-9" in nl or "pre-beginners" in nl or "pocetnici-u-9" in nl:
|
||||||
|
return "pocetnici-u9"
|
||||||
|
if "pocetnici u-11" in nl or "beginners" in nl or "pocetnici-u-11" in nl:
|
||||||
|
return "pocetnici-u11"
|
||||||
|
return "seniori"
|
||||||
|
|
||||||
|
|
||||||
|
# ── DB helpers ─────────────────────────────────────────────────────────────
|
||||||
|
def conn():
|
||||||
|
return psycopg2.connect(**DB_DSN)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_schema():
|
||||||
|
"""Verify clan_kategorije table exists; the schema in production already
|
||||||
|
matches the M2M shape required (no DDL change needed here)."""
|
||||||
|
with conn() as c, c.cursor() as cu:
|
||||||
|
cu.execute(
|
||||||
|
"""SELECT 1 FROM information_schema.tables
|
||||||
|
WHERE table_schema='pgz_sport' AND table_name='clan_kategorije'"""
|
||||||
|
)
|
||||||
|
if cu.fetchone():
|
||||||
|
log.info("clan_kategorije table verified.")
|
||||||
|
return
|
||||||
|
cu.execute(
|
||||||
|
"""CREATE TABLE pgz_sport.clan_kategorije (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
clan_id INTEGER REFERENCES pgz_sport.clanovi(id) ON DELETE CASCADE,
|
||||||
|
klub_id INTEGER REFERENCES pgz_sport.klubovi(id),
|
||||||
|
kategorija TEXT NOT NULL,
|
||||||
|
sezona TEXT,
|
||||||
|
source TEXT,
|
||||||
|
source_url TEXT,
|
||||||
|
scraped_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
UNIQUE (clan_id, kategorija, sezona, klub_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clan_kat_clan
|
||||||
|
ON pgz_sport.clan_kategorije(clan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clan_kat_sezona
|
||||||
|
ON pgz_sport.clan_kategorije(sezona);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clan_kat_klub
|
||||||
|
ON pgz_sport.clan_kategorije(klub_id);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
c.commit()
|
||||||
|
log.info("Created pgz_sport.clan_kategorije.")
|
||||||
|
|
||||||
|
|
||||||
|
def load_pgz_klubovi() -> dict[int, dict]:
|
||||||
|
"""Returns {hns_klub_id: {db_id, naziv, slug}}, deduped by hns_klub_id
|
||||||
|
(keeping the first / lowest-id row when duplicates exist)."""
|
||||||
|
out: dict[int, dict] = {}
|
||||||
|
with conn() as c, c.cursor() as cu:
|
||||||
|
cu.execute(
|
||||||
|
"""SELECT id, naziv, hns_klub_id, COALESCE(NULLIF(hns_slug,''), slug)
|
||||||
|
FROM pgz_sport.klubovi
|
||||||
|
WHERE hns_klub_id IS NOT NULL
|
||||||
|
ORDER BY id"""
|
||||||
|
)
|
||||||
|
for kid_db, naziv, hns_id, slug in cu.fetchall():
|
||||||
|
if hns_id in out:
|
||||||
|
continue # keep first occurrence
|
||||||
|
out[hns_id] = {
|
||||||
|
"db_id": kid_db,
|
||||||
|
"naziv": naziv,
|
||||||
|
"slug": slug or _slugify(naziv),
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _slugify(name: str) -> str:
|
||||||
|
name = (name or "").lower()
|
||||||
|
repl = {"č": "c", "ć": "c", "ž": "z", "š": "s", "đ": "d"}
|
||||||
|
for k, v in repl.items():
|
||||||
|
name = name.replace(k, v)
|
||||||
|
name = re.sub(r"[^a-z0-9]+", "-", name).strip("-")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_clan(klub_db_id: int, hns_pid: int, ime_prezime: str, slug: str) -> int:
|
||||||
|
"""Upsert a player into pgz_sport.clanovi keyed on (source='hns_semafor',
|
||||||
|
source_id=hns_pid). Returns clan_id."""
|
||||||
|
ime, prezime = "", ""
|
||||||
|
if ime_prezime:
|
||||||
|
parts = ime_prezime.strip().split(" ", 1)
|
||||||
|
ime = parts[0]
|
||||||
|
prezime = parts[1] if len(parts) > 1 else ""
|
||||||
|
url = f"{BASE}/igraci/{hns_pid}/{slug or 'x'}/"
|
||||||
|
with conn() as c, c.cursor() as cu:
|
||||||
|
cu.execute(
|
||||||
|
"""SELECT id FROM pgz_sport.clanovi
|
||||||
|
WHERE source='hns_semafor' AND source_id=%s LIMIT 1""",
|
||||||
|
(str(hns_pid),),
|
||||||
|
)
|
||||||
|
row = cu.fetchone()
|
||||||
|
if row:
|
||||||
|
return row[0]
|
||||||
|
# Try secondary lookup by hns_igrac_id (some rows from earlier runs)
|
||||||
|
# NOTE: hns_igrac_id is varchar in DB, cast to text
|
||||||
|
cu.execute(
|
||||||
|
"SELECT id FROM pgz_sport.clanovi WHERE hns_igrac_id=%s LIMIT 1",
|
||||||
|
(str(hns_pid),),
|
||||||
|
)
|
||||||
|
row = cu.fetchone()
|
||||||
|
if row:
|
||||||
|
cu.execute(
|
||||||
|
"""UPDATE pgz_sport.clanovi
|
||||||
|
SET source='hns_semafor', source_id=%s, source_url=%s,
|
||||||
|
source_synced_at=now()
|
||||||
|
WHERE id=%s""",
|
||||||
|
(str(hns_pid), url, row[0]),
|
||||||
|
)
|
||||||
|
c.commit()
|
||||||
|
return row[0]
|
||||||
|
cu.execute(
|
||||||
|
"""INSERT INTO pgz_sport.clanovi
|
||||||
|
(klub_id, ime, prezime, source, source_id, source_url,
|
||||||
|
source_synced_at, slug, hns_igrac_id, sport, aktivan,
|
||||||
|
verified, created_at, updated_at)
|
||||||
|
VALUES (%s,%s,%s,'hns_semafor',%s,%s,now(),%s,%s,'nogomet',
|
||||||
|
true, false, now(), now())
|
||||||
|
RETURNING id""",
|
||||||
|
(klub_db_id, ime, prezime, str(hns_pid), url, slug or None, hns_pid),
|
||||||
|
)
|
||||||
|
cid = cu.fetchone()[0]
|
||||||
|
c.commit()
|
||||||
|
return cid
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_clan_kategorija(
|
||||||
|
clan_id: int, klub_db_id: int, kategorija: str, sezona: str,
|
||||||
|
source_url: str,
|
||||||
|
):
|
||||||
|
with conn() as c, c.cursor() as cu:
|
||||||
|
cu.execute(
|
||||||
|
"""INSERT INTO pgz_sport.clan_kategorije
|
||||||
|
(clan_id, klub_id, kategorija, sezona, source, source_url,
|
||||||
|
scraped_at)
|
||||||
|
VALUES (%s,%s,%s,%s,'hns_semafor',%s,now())
|
||||||
|
ON CONFLICT (clan_id, kategorija, sezona, klub_id) DO UPDATE
|
||||||
|
SET source_url=EXCLUDED.source_url,
|
||||||
|
scraped_at=now()""",
|
||||||
|
(clan_id, klub_db_id, kategorija, sezona, source_url),
|
||||||
|
)
|
||||||
|
c.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Scrape primitives ─────────────────────────────────────────────────────
|
||||||
|
def parse_competition_klubovi(html: str) -> list[tuple[int, str]]:
|
||||||
|
"""Extract participating klubovi from a /natjecanja/{cid}/ page.
|
||||||
|
Returns list of (hns_klub_id, slug)."""
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for a in soup.find_all("a", href=re.compile(r"^/klubovi/\d+/[a-z0-9-]+/?")):
|
||||||
|
m = re.match(r"^/klubovi/(\d+)/([a-z0-9-]+)/?", a["href"])
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
kid, slug = int(m.group(1)), m.group(2)
|
||||||
|
if kid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(kid)
|
||||||
|
out.append((kid, slug))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def parse_klub_roster(html: str) -> list[tuple[int, str, str]]:
|
||||||
|
"""Extract (hns_pid, slug, name) from a klub-with-cid page."""
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for a in soup.find_all("a", href=re.compile(r"^/?(?:https?://[^/]+)?/igraci/\d+/[a-z0-9-]+/?")):
|
||||||
|
href = a["href"]
|
||||||
|
m = re.search(r"/igraci/(\d+)/([a-z0-9-]+)/?", href)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
pid, slug = int(m.group(1)), m.group(2)
|
||||||
|
if pid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(pid)
|
||||||
|
name = (a.get_text(" ", strip=True) or "").strip()
|
||||||
|
out.append((pid, slug, name))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def parse_klub_competitions(html: str) -> list[tuple[int, str]]:
|
||||||
|
"""From a klub page, parse the cid options dropdown — those are the
|
||||||
|
competitions the club currently participates in (default season+acat
|
||||||
|
only, but useful to discover more cids)."""
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
out = []
|
||||||
|
for opt in soup.select('select#cid option'):
|
||||||
|
val = opt.get("value") or ""
|
||||||
|
m = re.search(r"\?cid=(\d+)", val)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
out.append((int(m.group(1)), opt.get_text(" ", strip=True)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main flow ──────────────────────────────────────────────────────────────
|
||||||
|
def harvest():
|
||||||
|
pgz = load_pgz_klubovi()
|
||||||
|
log.info(
|
||||||
|
f"Loaded {len(pgz)} unique PGŽ klubovi with hns_klub_id "
|
||||||
|
f"({sum(1 for v in pgz.values() if v['slug'])} have slug)."
|
||||||
|
)
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"competitions_processed": 0,
|
||||||
|
"competitions_skipped": 0,
|
||||||
|
"klubovi_matched": 0,
|
||||||
|
"rosters_fetched": 0,
|
||||||
|
"players_upserted": 0,
|
||||||
|
"kategorije_inserted": 0,
|
||||||
|
"errors": 0,
|
||||||
|
"per_kategorija": {},
|
||||||
|
"per_klub": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
discovered_extra: set[tuple[str, str, str]] = set() # (cid, slug, sezona)
|
||||||
|
seen_clan_kat: set[tuple[int, int, str, str]] = set()
|
||||||
|
|
||||||
|
for cid, slug, kategorija, sezona in COMP_CATALOG:
|
||||||
|
comp_url = f"{BASE}/natjecanja/{cid}/{slug}/"
|
||||||
|
try:
|
||||||
|
html = fetch(comp_url)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"comp {cid} fetch failed: {e}")
|
||||||
|
stats["competitions_skipped"] += 1
|
||||||
|
stats["errors"] += 1
|
||||||
|
continue
|
||||||
|
klubovi = parse_competition_klubovi(html)
|
||||||
|
log.info(
|
||||||
|
f"COMP cid={cid} '{slug}' [{kategorija}/{sezona}] -> "
|
||||||
|
f"{len(klubovi)} participating klubovi"
|
||||||
|
)
|
||||||
|
stats["competitions_processed"] += 1
|
||||||
|
time.sleep(RATE_S)
|
||||||
|
|
||||||
|
for hns_kid, k_slug in klubovi:
|
||||||
|
if hns_kid not in pgz:
|
||||||
|
continue
|
||||||
|
klub = pgz[hns_kid]
|
||||||
|
klub_db_id = klub["db_id"]
|
||||||
|
stats["klubovi_matched"] += 1
|
||||||
|
stats["per_klub"].setdefault(klub["naziv"], set()).add(kategorija)
|
||||||
|
|
||||||
|
# Fetch klub roster filtered by this competition cid
|
||||||
|
slug_use = klub["slug"] or k_slug
|
||||||
|
roster_url = f"{BASE}/klubovi/{hns_kid}/{slug_use}/?cid={cid}"
|
||||||
|
try:
|
||||||
|
rhtml = fetch(roster_url)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"roster {hns_kid} cid={cid} failed: {e}")
|
||||||
|
stats["errors"] += 1
|
||||||
|
continue
|
||||||
|
stats["rosters_fetched"] += 1
|
||||||
|
time.sleep(RATE_S)
|
||||||
|
|
||||||
|
# Discover any other cids this klub plays in
|
||||||
|
for ocid, oname in parse_klub_competitions(rhtml):
|
||||||
|
if ocid != int(cid):
|
||||||
|
discovered_extra.add((str(ocid), oname, sezona))
|
||||||
|
|
||||||
|
roster = parse_klub_roster(rhtml)
|
||||||
|
if not roster:
|
||||||
|
log.info(f" {klub['naziv']} cid={cid}: empty roster")
|
||||||
|
continue
|
||||||
|
log.info(
|
||||||
|
f" KLUB '{klub['naziv']}' (db={klub_db_id}, hns={hns_kid}) "
|
||||||
|
f"cid={cid} -> {len(roster)} igraca [{kategorija}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
for hns_pid, p_slug, name in roster:
|
||||||
|
try:
|
||||||
|
clan_id = upsert_clan(klub_db_id, hns_pid, name, p_slug)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"upsert_clan({hns_pid}) fail: {e}")
|
||||||
|
stats["errors"] += 1
|
||||||
|
continue
|
||||||
|
stats["players_upserted"] += 1
|
||||||
|
key = (clan_id, klub_db_id, kategorija, sezona)
|
||||||
|
if key in seen_clan_kat:
|
||||||
|
continue
|
||||||
|
seen_clan_kat.add(key)
|
||||||
|
try:
|
||||||
|
upsert_clan_kategorija(
|
||||||
|
clan_id, klub_db_id, kategorija, sezona, roster_url
|
||||||
|
)
|
||||||
|
stats["kategorije_inserted"] += 1
|
||||||
|
stats["per_kategorija"][kategorija] = (
|
||||||
|
stats["per_kategorija"].get(kategorija, 0) + 1
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(
|
||||||
|
f"upsert_clan_kategorija(clan={clan_id} "
|
||||||
|
f"klub={klub_db_id} kat={kategorija}) fail: {e}"
|
||||||
|
)
|
||||||
|
stats["errors"] += 1
|
||||||
|
|
||||||
|
# Summarize discovered extra cids (not yet in catalog) for next run
|
||||||
|
if discovered_extra:
|
||||||
|
log.info(
|
||||||
|
f"Discovered {len(discovered_extra)} extra cids not in catalog "
|
||||||
|
f"(top 15 below):"
|
||||||
|
)
|
||||||
|
for cid, name, sezona in list(discovered_extra)[:15]:
|
||||||
|
log.info(f" + cid={cid} '{name}' sezona={sezona}")
|
||||||
|
|
||||||
|
# Convert per_klub sets to lists for JSON serialisation
|
||||||
|
stats["per_klub"] = {k: sorted(v) for k, v in stats["per_klub"].items()}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global load_pgz_klubovi # noqa: PLW0603
|
||||||
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "run"
|
||||||
|
log.info(f"=== SUB5 hns_youth_categories START cmd={cmd} log={LOG_FILE} ===")
|
||||||
|
ensure_schema()
|
||||||
|
|
||||||
|
if cmd == "discover":
|
||||||
|
pgz = load_pgz_klubovi()
|
||||||
|
log.info(f"PGŽ klubovi with hns_klub_id: {len(pgz)}")
|
||||||
|
for hk, v in list(pgz.items())[:10]:
|
||||||
|
log.info(f" hns={hk} db={v['db_id']} slug={v['slug']} naziv={v['naziv']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if cmd == "klub" and len(sys.argv) > 2:
|
||||||
|
# narrow-scope debug mode — monkey-patch loader before harvest()
|
||||||
|
target_db = int(sys.argv[2])
|
||||||
|
_orig = load_pgz_klubovi
|
||||||
|
pgz = {k: v for k, v in _orig().items() if v["db_id"] == target_db}
|
||||||
|
log.info(f"Restricted to db_id={target_db}: {len(pgz)} match")
|
||||||
|
load_pgz_klubovi = lambda: pgz # type: ignore
|
||||||
|
try:
|
||||||
|
stats = harvest()
|
||||||
|
finally:
|
||||||
|
load_pgz_klubovi = _orig # type: ignore
|
||||||
|
else:
|
||||||
|
stats = harvest()
|
||||||
|
|
||||||
|
log.info("=== SUMMARY ===")
|
||||||
|
log.info(json.dumps(stats, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
# Write SUB5_RESULT.md
|
||||||
|
md_path = Path("/opt/pgz-sport/cc_tasks/SUB5_RESULT.md")
|
||||||
|
md_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
md = render_summary_md(stats)
|
||||||
|
md_path.write_text(md, encoding="utf-8")
|
||||||
|
log.info(f"Result MD written → {md_path}")
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
tg_send(
|
||||||
|
"*SUB5 — HNS youth categories*\n"
|
||||||
|
f"Klubovi matched: *{stats['klubovi_matched']}*\n"
|
||||||
|
f"Rosters fetched: *{stats['rosters_fetched']}*\n"
|
||||||
|
f"Players upserted: *{stats['players_upserted']}*\n"
|
||||||
|
f"clan_kategorije: *{stats['kategorije_inserted']}*\n"
|
||||||
|
f"Errors: {stats['errors']}\n"
|
||||||
|
f"Log: `{LOG_FILE.name}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_summary_md(stats: dict) -> str:
|
||||||
|
lines = [
|
||||||
|
"# SUB5 — HNS youth categories result",
|
||||||
|
"",
|
||||||
|
f"_Generated: {datetime.now().isoformat(timespec='seconds')}_",
|
||||||
|
"",
|
||||||
|
"## High-level counters",
|
||||||
|
"",
|
||||||
|
f"- Competitions processed: **{stats['competitions_processed']}**",
|
||||||
|
f"- Competitions skipped: {stats['competitions_skipped']}",
|
||||||
|
f"- Klubovi (DB) matched in competitions: **{stats['klubovi_matched']}**",
|
||||||
|
f"- Rosters fetched: **{stats['rosters_fetched']}**",
|
||||||
|
f"- Players upserted into `clanovi`: **{stats['players_upserted']}**",
|
||||||
|
f"- M2M rows written into `clan_kategorije`: **{stats['kategorije_inserted']}**",
|
||||||
|
f"- Errors: {stats['errors']}",
|
||||||
|
"",
|
||||||
|
"## Per kategorija",
|
||||||
|
"",
|
||||||
|
"| Kategorija | M2M zapisa |",
|
||||||
|
"|---|---:|",
|
||||||
|
]
|
||||||
|
for k in sorted(stats["per_kategorija"].keys()):
|
||||||
|
lines.append(f"| {k} | {stats['per_kategorija'][k]} |")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Per klub — kategorije pronadjene")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("| Klub | Kategorije |")
|
||||||
|
lines.append("|---|---|")
|
||||||
|
for klub in sorted(stats["per_klub"].keys()):
|
||||||
|
kats = ", ".join(stats["per_klub"][klub])
|
||||||
|
lines.append(f"| {klub} | {kats} |")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"_Log: `{LOG_FILE}`_")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"FATAL: {e}")
|
||||||
|
tg_send(f"*SUB5 FATAL*: {e}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# Fajl: sub1_hns_fix_and_extract.py | v1.0.0 | 05.05.2026
|
||||||
|
# Lokacija: /opt/pgz-sport/scripts/sub1_hns_fix_and_extract.py
|
||||||
|
# Autor: dradulic@outlook.com / damir@rinet.one
|
||||||
|
# Svrha: SUB1 finalize — (a) rollback false positives,
|
||||||
|
# (b) extract hns_klub_id iz već postojećeg source_url,
|
||||||
|
# (c) verify presence preko HEAD i upsert.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
"""SUB1 fix-up: false-positive rollback + source_url-based extraction."""
|
||||||
|
import os, re, sys, time, json, subprocess, urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
|
DSN = os.getenv("RINET_DSN",
|
||||||
|
"host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7")
|
||||||
|
TG = os.getenv("TG_BOT_TOKEN", "8535797835:AAFItT-92jzZ9NWFafLxn0dLa1_n2s-JE5Y")
|
||||||
|
TG_CHAT = os.getenv("TG_CHAT", "7969491558")
|
||||||
|
UA = "PGZ-Sport-Bot/1.0 (+https://api.rinet.one/sport/; contact dradulic@outlook.com)"
|
||||||
|
LOG_PATH = f"/var/log/pgz-sport-debug/sub1_fix_{datetime.now().strftime('%Y%m%d_%H%M')}.log"
|
||||||
|
LOG = open(LOG_PATH, "a")
|
||||||
|
|
||||||
|
# False positives to ROLLBACK (cleared and marked not_found)
|
||||||
|
FALSE_POS = {
|
||||||
|
2572: "NK Hajduk Tovarnik (matched HNK Hajduk Split — different club)",
|
||||||
|
600: "Ženski NK XXL Kraljevica (matched men's NK Kraljevica — wrong sex)",
|
||||||
|
}
|
||||||
|
|
||||||
|
def log(msg, telegram=False):
|
||||||
|
line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}"
|
||||||
|
print(line, flush=True); LOG.write(line+"\n"); LOG.flush()
|
||||||
|
if telegram:
|
||||||
|
try:
|
||||||
|
subprocess.run(["curl","-s","-X","POST",
|
||||||
|
f"https://api.telegram.org/bot{TG}/sendMessage",
|
||||||
|
"-d", f"chat_id={TG_CHAT}",
|
||||||
|
"--data-urlencode", f"text={msg[:3500]}"],
|
||||||
|
timeout=8, capture_output=True)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def http_head_or_get(url, timeout=12):
|
||||||
|
"""Verify URL exists. Return (status, title)."""
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
html = r.read().decode("utf-8", errors="replace")
|
||||||
|
m = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
|
||||||
|
title = m.group(1).strip() if m else None
|
||||||
|
return r.status, title
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, None
|
||||||
|
except Exception as e:
|
||||||
|
return 0, str(e)
|
||||||
|
|
||||||
|
URL_RE = re.compile(r'/klubovi/(\d+)/([a-z0-9-]*)/?')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log(f"=== SUB1 fix start; log={LOG_PATH} ===")
|
||||||
|
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Phase 1: Rollback false positives
|
||||||
|
rb = 0
|
||||||
|
for kid, reason in FALSE_POS.items():
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE pgz_sport.klubovi
|
||||||
|
SET hns_klub_id = NULL,
|
||||||
|
hns_slug = NULL,
|
||||||
|
scrape_source = 'hns_not_found',
|
||||||
|
last_scraped_at = now()
|
||||||
|
WHERE id = %s
|
||||||
|
""", (kid,))
|
||||||
|
log(f" ROLLBACK [{kid}] — {reason}")
|
||||||
|
rb += 1
|
||||||
|
|
||||||
|
# Phase 2: Extract hns_klub_id from existing source_url
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, naziv, source_url
|
||||||
|
FROM pgz_sport.klubovi
|
||||||
|
WHERE sport='nogomet' AND pgz_sufinanciran=true
|
||||||
|
AND hns_klub_id IS NULL
|
||||||
|
AND source_url ~ 'semafor\\.hns\\.family/klubovi/[0-9]+'
|
||||||
|
ORDER BY id
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
log(f"Source-URL extraction candidates: {len(rows)}")
|
||||||
|
|
||||||
|
extracted = 0; verify_fail = 0
|
||||||
|
for r in rows:
|
||||||
|
kid, naziv, url = r['id'], r['naziv'], r['source_url']
|
||||||
|
m = URL_RE.search(url)
|
||||||
|
if not m:
|
||||||
|
log(f" SKIP [{kid}] no match in {url}")
|
||||||
|
continue
|
||||||
|
hns_id = int(m.group(1))
|
||||||
|
slug = m.group(2) or None
|
||||||
|
# Verify
|
||||||
|
verify_url = f"https://semafor.hns.family/klubovi/{hns_id}/"
|
||||||
|
status, title = http_head_or_get(verify_url)
|
||||||
|
time.sleep(0.8)
|
||||||
|
if status != 200 or not title:
|
||||||
|
log(f" VERIFY FAIL [{kid}] {naziv} -> {hns_id}: status={status} title={title}")
|
||||||
|
verify_fail += 1
|
||||||
|
continue
|
||||||
|
# If slug missing, try inferring from title
|
||||||
|
if not slug and title:
|
||||||
|
slug = re.sub(r'[^a-z0-9]+', '-',
|
||||||
|
title.lower()
|
||||||
|
.replace('č','c').replace('ć','c').replace('š','s').replace('ž','z').replace('đ','d')
|
||||||
|
).strip('-')
|
||||||
|
canonical = f"https://semafor.hns.family/klubovi/{hns_id}/{slug}/" if slug else verify_url
|
||||||
|
try:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE pgz_sport.klubovi
|
||||||
|
SET hns_klub_id = %s,
|
||||||
|
hns_slug = %s,
|
||||||
|
source_url = %s,
|
||||||
|
scrape_source = 'hns_semafor',
|
||||||
|
last_scraped_at = now()
|
||||||
|
WHERE id = %s
|
||||||
|
""", (hns_id, slug, canonical, kid))
|
||||||
|
log(f" EXTRACT [{kid}] {naziv} -> HNS {hns_id} '{title}' (slug={slug})")
|
||||||
|
extracted += 1
|
||||||
|
except Exception as e:
|
||||||
|
log(f" UPDATE fail [{kid}]: {e}")
|
||||||
|
|
||||||
|
# Phase 3: Final stats
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) FILTER (WHERE hns_klub_id IS NOT NULL) AS mapped,
|
||||||
|
COUNT(*) FILTER (WHERE hns_klub_id IS NULL AND scrape_source='hns_not_found') AS marked_nf,
|
||||||
|
COUNT(*) FILTER (WHERE hns_klub_id IS NULL AND (scrape_source IS NULL OR scrape_source != 'hns_not_found')) AS untouched
|
||||||
|
FROM pgz_sport.klubovi
|
||||||
|
WHERE sport='nogomet' AND pgz_sufinanciran=true
|
||||||
|
AND naziv !~* 'Malonogometni|Mini Nogomet|Mali Nogomet|Američkog|Plaž|Pijesku|HMNK|MNK|preteča|povijesn|1903|1906|1908|1917|1919|1926|1929'
|
||||||
|
""")
|
||||||
|
stats = cur.fetchone()
|
||||||
|
log(f"=== Final state (real football, PGŽ priority): mapped={stats['mapped']}, marked_not_found={stats['marked_nf']}, untouched={stats['untouched']} ===")
|
||||||
|
|
||||||
|
msg = (f"SUB1 fix done: rollback={rb}, source_url-extracted={extracted}, "
|
||||||
|
f"verify_fail={verify_fail}. Final mapped={stats['mapped']} / "
|
||||||
|
f"not_found={stats['marked_nf']} / untouched={stats['untouched']}")
|
||||||
|
log(msg, telegram=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# Fajl: sub1_hns_link_harvester.py | v1.0.0 | 05.05.2026
|
||||||
|
# Lokacija: /opt/pgz-sport/scripts/sub1_hns_link_harvester.py
|
||||||
|
# Autor: dradulic@outlook.com / damir@rinet.one
|
||||||
|
# Svrha: SUB1 — Pronađi semafor.hns.family link za PGŽ priority
|
||||||
|
# nogometne klubove koji nemaju hns_klub_id.
|
||||||
|
# Strategija:
|
||||||
|
# 1. Enumerate ŽNS Primorsko-goranski (oid=51) competitions across
|
||||||
|
# seasons, plus 4. NL NS Rijeka, 3. HNL Zapad arhive
|
||||||
|
# 2. Za svaki natjecanje GET /natjecanja/{cid}/{cname}/ i izvuci
|
||||||
|
# sve <a href="/klubovi/{id}/{slug}/">{naziv}</a>
|
||||||
|
# 3. Build catalog (hns_id, slug, naziv) — skup unique
|
||||||
|
# 4. Fuzzy match candidate klubovi: normalize, drop NK/HNK/GNK
|
||||||
|
# prefiks, ukloni dijakritike, pa equality + substring + ratio
|
||||||
|
# 5. UPDATE pgz_sport.klubovi za matche; mark not_found za ostalo
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
"""SUB1 — HNS link harvester for PGŽ priority football clubs."""
|
||||||
|
import os, re, sys, time, json, traceback, subprocess, difflib
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import quote
|
||||||
|
import urllib.request, urllib.error
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
|
DSN = os.getenv("RINET_DSN",
|
||||||
|
"host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7")
|
||||||
|
TG = os.getenv("TG_BOT_TOKEN", "8535797835:AAFItT-92jzZ9NWFafLxn0dLa1_n2s-JE5Y")
|
||||||
|
TG_CHAT = os.getenv("TG_CHAT", "7969491558")
|
||||||
|
UA = "PGZ-Sport-Bot/1.0 (+https://api.rinet.one/sport/; legitimni interes; analitika sporta PGZ; contact dradulic@outlook.com)"
|
||||||
|
SLEEP = 1.1
|
||||||
|
BASE = "https://semafor.hns.family"
|
||||||
|
|
||||||
|
LOG_PATH = f"/var/log/pgz-sport-debug/sub1_{datetime.now().strftime('%Y%m%d_%H%M')}.log"
|
||||||
|
LOG = open(LOG_PATH, "a")
|
||||||
|
|
||||||
|
def log(msg, telegram=False):
|
||||||
|
line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}"
|
||||||
|
print(line, flush=True)
|
||||||
|
LOG.write(line + "\n"); LOG.flush()
|
||||||
|
if telegram:
|
||||||
|
try:
|
||||||
|
subprocess.run(["curl","-s","-X","POST",
|
||||||
|
f"https://api.telegram.org/bot{TG}/sendMessage",
|
||||||
|
"-d", f"chat_id={TG_CHAT}",
|
||||||
|
"--data-urlencode", f"text={msg[:3500]}"],
|
||||||
|
timeout=8, capture_output=True)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"TG error: {e}")
|
||||||
|
|
||||||
|
def http_get(url, accept_json=False, timeout=25):
|
||||||
|
req = urllib.request.Request(url, headers={
|
||||||
|
"User-Agent": UA,
|
||||||
|
"Accept": "application/json, */*" if accept_json else "text/html,*/*",
|
||||||
|
"X-Requested-With": "XMLHttpRequest" if accept_json else "",
|
||||||
|
})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
return r.read().decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
# ── Normalization for fuzzy match ──
|
||||||
|
DIACRITIC_MAP = str.maketrans({
|
||||||
|
'č':'c','ć':'c','ž':'z','š':'s','đ':'d',
|
||||||
|
'Č':'c','Ć':'c','Ž':'z','Š':'s','Đ':'d',
|
||||||
|
'á':'a','é':'e','í':'i','ó':'o','ú':'u',
|
||||||
|
})
|
||||||
|
PREFIX_RE = re.compile(
|
||||||
|
r'^(hrvatski\s+nogometni\s+klub|hrvatski\s+nogometni\s+klub\.?|'
|
||||||
|
r'nogometni\s+klub|nogometna\s+akademija|nogometna\s+škola|'
|
||||||
|
r'sportska\s+akademija|Ženski\s+nogometni\s+klub|'
|
||||||
|
r'hnk|nk|gnk|znk|žnk|nk\.?|hnk\.?)\s+',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
SUFFIX_NOISE_RE = re.compile(
|
||||||
|
r'\b(veterani|veterana|gornji\s+zamet|grada\s+crikvenice|'
|
||||||
|
r'gomirje\s+gomirje|mrkopalj\s+mrkopalj|snježnik\s+gerovo|'
|
||||||
|
r'-?\s*\d{4}\s*$)', re.IGNORECASE)
|
||||||
|
|
||||||
|
def norm(s):
|
||||||
|
if not s: return ""
|
||||||
|
s = s.lower().strip()
|
||||||
|
s = s.translate(DIACRITIC_MAP)
|
||||||
|
s = re.sub(r'["\'`]', '', s)
|
||||||
|
s = re.sub(r'\s+', ' ', s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def core_name(naziv):
|
||||||
|
"""Strip prefixes and noise; return core token list + joined."""
|
||||||
|
s = norm(naziv)
|
||||||
|
# remove prefix(es) (sometimes nested e.g. "Nogometni Klub HNK ...")
|
||||||
|
for _ in range(3):
|
||||||
|
s2 = PREFIX_RE.sub('', s)
|
||||||
|
if s2 == s: break
|
||||||
|
s = s2
|
||||||
|
s = SUFFIX_NOISE_RE.sub('', s).strip()
|
||||||
|
s = re.sub(r'\s+', ' ', s).strip()
|
||||||
|
return s
|
||||||
|
|
||||||
|
def slugify(s):
|
||||||
|
s = core_name(s)
|
||||||
|
s = re.sub(r'[^a-z0-9]+', '-', s).strip('-')
|
||||||
|
return s
|
||||||
|
|
||||||
|
# ── Catalog harvest ──
|
||||||
|
def get_pgz_competitions(season):
|
||||||
|
"""Fetch list of competitions for ŽNS Primorsko-goranski (oid=51) for a season."""
|
||||||
|
t = int(time.time()*1000)
|
||||||
|
url = (f"{BASE}/handlers/getCompetitions/"
|
||||||
|
f"?season={quote(season)}&oid=51&teamch=Club"
|
||||||
|
f"&linkType=competitions&linkConstructor={quote(BASE+'/natjecanja/{cid}/{cname}/')}"
|
||||||
|
f"&lang=hr&t={t}")
|
||||||
|
try:
|
||||||
|
body = http_get(url, accept_json=True)
|
||||||
|
return json.loads(body)
|
||||||
|
except Exception as e:
|
||||||
|
log(f" comps fetch fail {season}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_organizations(season):
|
||||||
|
"""List all organizations (regional federations) for a season."""
|
||||||
|
t = int(time.time()*1000)
|
||||||
|
url = (f"{BASE}/handlers/getOrganizations/"
|
||||||
|
f"?season={quote(season)}&teamch=Club&lang=hr&t={t}")
|
||||||
|
try:
|
||||||
|
body = http_get(url, accept_json=True)
|
||||||
|
return json.loads(body)
|
||||||
|
except Exception as e:
|
||||||
|
log(f" orgs fetch fail {season}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Match <a href="/klubovi/{id}/{slug}/">NAME<div...>...</a> — name is anything before first child element
|
||||||
|
CLUB_LINK_RE2 = re.compile(
|
||||||
|
r'<a[^>]+href="(?:https?://semafor\.hns\.family)?/klubovi/(\d+)/([a-z0-9-]*)/?"[^>]*>([^<]{1,150})(?:<|</a>)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
def harvest_competition(cid):
|
||||||
|
"""GET natjecanje page and extract all club refs."""
|
||||||
|
# The dynamic linkConstructor returned literal {cid}/{cname} — try direct id
|
||||||
|
url = f"{BASE}/natjecanja/{cid}/x/"
|
||||||
|
try:
|
||||||
|
html = http_get(url)
|
||||||
|
except Exception as e:
|
||||||
|
log(f" nat fetch fail {cid}: {e}")
|
||||||
|
return []
|
||||||
|
found = []
|
||||||
|
for m in CLUB_LINK_RE2.finditer(html):
|
||||||
|
hns_id, slug, naziv = m.group(1), m.group(2), m.group(3).strip()
|
||||||
|
# filter: real club name (not "Klubovi" navigation etc.)
|
||||||
|
if len(naziv) > 1 and not naziv.lower().startswith('klubov'):
|
||||||
|
found.append((hns_id, slug, naziv))
|
||||||
|
return found
|
||||||
|
|
||||||
|
# ── Match logic ──
|
||||||
|
def match_score(candidate_naziv, candidate_grad, hns_naziv):
|
||||||
|
"""Score 0-100 how well candidate matches an HNS club entry."""
|
||||||
|
cand_core = core_name(candidate_naziv)
|
||||||
|
hns_core = core_name(hns_naziv)
|
||||||
|
if not cand_core or not hns_core:
|
||||||
|
return 0
|
||||||
|
if cand_core == hns_core:
|
||||||
|
return 100
|
||||||
|
# ratio
|
||||||
|
r = difflib.SequenceMatcher(None, cand_core, hns_core).ratio()
|
||||||
|
score = int(r * 100)
|
||||||
|
# bonus if grad in HNS naziv (e.g. "NK Borac (Ba)" + grad="Bakar")
|
||||||
|
if candidate_grad:
|
||||||
|
gnorm = norm(candidate_grad)
|
||||||
|
if gnorm and (gnorm[:3] in norm(hns_naziv) or norm(hns_naziv).endswith('('+gnorm[:1]+')')):
|
||||||
|
score = min(100, score + 5)
|
||||||
|
# substring containment bonus (one fully contained)
|
||||||
|
if cand_core in hns_core or hns_core in cand_core:
|
||||||
|
score = max(score, 85)
|
||||||
|
return score
|
||||||
|
|
||||||
|
# ── Main ──
|
||||||
|
def main():
|
||||||
|
log(f"=== SUB1 HNS link harvester start; log={LOG_PATH} ===")
|
||||||
|
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# 1) Get candidate clubs
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, naziv, grad
|
||||||
|
FROM pgz_sport.klubovi
|
||||||
|
WHERE sport='nogomet' AND pgz_sufinanciran=true
|
||||||
|
AND hns_klub_id IS NULL
|
||||||
|
AND naziv !~* 'Malonogometni|Mini Nogomet|Mali Nogomet|Američkog|Plaž|Pijesku|HMNK|MNK|preteča|povijesn|1903|1906|1908|1917|1919|1926|1929'
|
||||||
|
ORDER BY naziv
|
||||||
|
""")
|
||||||
|
candidates = cur.fetchall()
|
||||||
|
log(f"Candidates: {len(candidates)}")
|
||||||
|
|
||||||
|
# 2) Build HNS catalog from PGŽ competitions across recent seasons
|
||||||
|
SEASONS = ["2025/2026","2024/2025","2023/2024","2022/2023","2021/2022","2020/2021","2019/2020","2018/2019","2017/2018"]
|
||||||
|
catalog = {} # hns_id -> {slug, naziv, sources:set}
|
||||||
|
|
||||||
|
seen_cids = set()
|
||||||
|
for season in SEASONS:
|
||||||
|
log(f"-- season {season}")
|
||||||
|
comps = get_pgz_competitions(season)
|
||||||
|
time.sleep(SLEEP)
|
||||||
|
log(f" PGŽ comps: {len(comps)}")
|
||||||
|
for c in comps:
|
||||||
|
cid = str(c.get('id',''))
|
||||||
|
if not cid or cid in seen_cids: continue
|
||||||
|
seen_cids.add(cid)
|
||||||
|
cname = c.get('value','')
|
||||||
|
try:
|
||||||
|
clubs = harvest_competition(cid)
|
||||||
|
except Exception as e:
|
||||||
|
log(f" {cid} ({cname}) fetch error: {e}")
|
||||||
|
clubs = []
|
||||||
|
for hns_id, slug, naziv in clubs:
|
||||||
|
if hns_id not in catalog:
|
||||||
|
catalog[hns_id] = {'slug': slug, 'naziv': naziv, 'sources': set()}
|
||||||
|
else:
|
||||||
|
if slug and not catalog[hns_id]['slug']:
|
||||||
|
catalog[hns_id]['slug'] = slug
|
||||||
|
catalog[hns_id]['sources'].add(f"{season}:{cname[:30]}")
|
||||||
|
log(f" {cid} '{cname[:40]}' -> {len(clubs)} clubs (catalog={len(catalog)})")
|
||||||
|
time.sleep(SLEEP)
|
||||||
|
# also sweep top-tier comps to catch HNK Rijeka-tier (though those usually mapped)
|
||||||
|
# Also: 3.HNL Zapad / 4.NL NS Rijeka by oid=178180 (NS Rijeka)
|
||||||
|
log("-- NS Rijeka oid=178180 sweep")
|
||||||
|
for season in SEASONS:
|
||||||
|
t = int(time.time()*1000)
|
||||||
|
url = (f"{BASE}/handlers/getCompetitions/"
|
||||||
|
f"?season={quote(season)}&oid=178180&teamch=Club"
|
||||||
|
f"&linkType=competitions&linkConstructor={quote(BASE+'/natjecanja/{cid}/{cname}/')}"
|
||||||
|
f"&lang=hr&t={t}")
|
||||||
|
try:
|
||||||
|
comps = json.loads(http_get(url, accept_json=True))
|
||||||
|
except Exception as e:
|
||||||
|
log(f" ns_rijeka {season} fail: {e}"); comps = []
|
||||||
|
time.sleep(SLEEP)
|
||||||
|
for c in comps:
|
||||||
|
cid = str(c.get('id',''))
|
||||||
|
if not cid or cid in seen_cids: continue
|
||||||
|
seen_cids.add(cid)
|
||||||
|
cname = c.get('value','')
|
||||||
|
try:
|
||||||
|
clubs = harvest_competition(cid)
|
||||||
|
except Exception as e:
|
||||||
|
clubs = []
|
||||||
|
for hns_id, slug, naziv in clubs:
|
||||||
|
if hns_id not in catalog:
|
||||||
|
catalog[hns_id] = {'slug': slug, 'naziv': naziv, 'sources': set()}
|
||||||
|
catalog[hns_id]['sources'].add(f"NSR:{season}:{cname[:30]}")
|
||||||
|
log(f" NSR {cid} '{cname[:40]}' -> {len(clubs)} (cat={len(catalog)})")
|
||||||
|
time.sleep(SLEEP)
|
||||||
|
|
||||||
|
log(f"=== Catalog built: {len(catalog)} unique HNS clubs ===")
|
||||||
|
|
||||||
|
# Save catalog snapshot
|
||||||
|
snap = {hid: {'slug': v['slug'], 'naziv': v['naziv'], 'sources': sorted(v['sources'])[:5]}
|
||||||
|
for hid,v in catalog.items()}
|
||||||
|
with open("/opt/pgz-sport/cc_tasks/sub1_hns_catalog.json","w") as f:
|
||||||
|
json.dump(snap, f, ensure_ascii=False, indent=2)
|
||||||
|
log(f"Catalog snapshot -> /opt/pgz-sport/cc_tasks/sub1_hns_catalog.json")
|
||||||
|
|
||||||
|
# 3) Match candidates
|
||||||
|
matched = [] # (db_id, db_naziv, hns_id, slug, hns_naziv, score)
|
||||||
|
not_found = []
|
||||||
|
ambiguous = []
|
||||||
|
|
||||||
|
for cand in candidates:
|
||||||
|
db_id, naziv, grad = cand['id'], cand['naziv'], cand['grad']
|
||||||
|
ranked = []
|
||||||
|
for hid, v in catalog.items():
|
||||||
|
sc = match_score(naziv, grad, v['naziv'])
|
||||||
|
if sc >= 70:
|
||||||
|
ranked.append((sc, hid, v['slug'], v['naziv']))
|
||||||
|
ranked.sort(reverse=True)
|
||||||
|
if not ranked:
|
||||||
|
not_found.append((db_id, naziv, grad))
|
||||||
|
log(f" NOT FOUND: [{db_id}] {naziv} ({grad})")
|
||||||
|
continue
|
||||||
|
top = ranked[0]
|
||||||
|
if len(ranked) > 1 and ranked[1][0] >= top[0] - 3 and top[0] < 95:
|
||||||
|
ambiguous.append((db_id, naziv, grad, ranked[:3]))
|
||||||
|
log(f" AMBIGUOUS: [{db_id}] {naziv} -> top: {top[3]} ({top[0]}), 2nd: {ranked[1][3]} ({ranked[1][0]})")
|
||||||
|
# Skip ambiguous, mark not_found for safety
|
||||||
|
not_found.append((db_id, naziv, grad))
|
||||||
|
continue
|
||||||
|
matched.append((db_id, naziv, top[1], top[2], top[3], top[0]))
|
||||||
|
log(f" MATCH [{db_id}] {naziv} -> HNS {top[1]} '{top[3]}' (slug={top[2]}, score={top[0]})")
|
||||||
|
|
||||||
|
log(f"=== Match results: {len(matched)} matched, {len(not_found)} not_found, {len(ambiguous)} ambiguous ===")
|
||||||
|
|
||||||
|
# 4) Apply UPDATEs
|
||||||
|
upd_ok, upd_fail = 0, 0
|
||||||
|
for db_id, naziv, hns_id, slug, hns_naziv, sc in matched:
|
||||||
|
try:
|
||||||
|
source_url = f"{BASE}/klubovi/{hns_id}/{slug}/" if slug else f"{BASE}/klubovi/{hns_id}/"
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE pgz_sport.klubovi
|
||||||
|
SET hns_klub_id = %s,
|
||||||
|
hns_slug = %s,
|
||||||
|
source_url = COALESCE(source_url, %s),
|
||||||
|
scrape_source = 'hns_semafor',
|
||||||
|
last_scraped_at = now()
|
||||||
|
WHERE id = %s
|
||||||
|
""", (int(hns_id), slug or None, source_url, db_id))
|
||||||
|
upd_ok += 1
|
||||||
|
except Exception as e:
|
||||||
|
upd_fail += 1
|
||||||
|
log(f" UPDATE fail [{db_id}] {naziv}: {e}")
|
||||||
|
|
||||||
|
# Mark not_found
|
||||||
|
nf_ok = 0
|
||||||
|
for db_id, naziv, grad in not_found:
|
||||||
|
try:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE pgz_sport.klubovi
|
||||||
|
SET scrape_source = 'hns_not_found',
|
||||||
|
last_scraped_at = now()
|
||||||
|
WHERE id = %s AND hns_klub_id IS NULL
|
||||||
|
""", (db_id,))
|
||||||
|
nf_ok += 1
|
||||||
|
except Exception as e:
|
||||||
|
log(f" not_found mark fail [{db_id}]: {e}")
|
||||||
|
|
||||||
|
# 5) Write result md
|
||||||
|
res_path = "/opt/pgz-sport/cc_tasks/SUB1_RESULT.md"
|
||||||
|
with open(res_path, "w") as f:
|
||||||
|
f.write(f"# SUB1 — HNS Link Harvest Result\n\n")
|
||||||
|
f.write(f"Date: {datetime.now().isoformat(timespec='seconds')}\n\n")
|
||||||
|
f.write(f"- Candidates processed: **{len(candidates)}**\n")
|
||||||
|
f.write(f"- HNS catalog built: **{len(catalog)}** unique clubs from {len(seen_cids)} competitions\n")
|
||||||
|
f.write(f"- Matched: **{len(matched)}** (DB updated: {upd_ok}, fail: {upd_fail})\n")
|
||||||
|
f.write(f"- Ambiguous (skipped to safety): **{len(ambiguous)}**\n")
|
||||||
|
f.write(f"- Not found (marked hns_not_found): **{len(not_found)}** (mark ok: {nf_ok})\n\n")
|
||||||
|
f.write(f"## Matched\n\n| db_id | DB naziv | HNS id | HNS naziv | slug | score |\n|---|---|---|---|---|---|\n")
|
||||||
|
for db_id, naziv, hns_id, slug, hns_naziv, sc in sorted(matched, key=lambda x: -x[5]):
|
||||||
|
f.write(f"| {db_id} | {naziv} | {hns_id} | {hns_naziv} | {slug} | {sc} |\n")
|
||||||
|
f.write(f"\n## Ambiguous (manual review)\n\n")
|
||||||
|
for db_id, naziv, grad, ranked in ambiguous:
|
||||||
|
f.write(f"- **[{db_id}] {naziv}** ({grad})\n")
|
||||||
|
for sc, hid, slug, hns_naziv in ranked:
|
||||||
|
f.write(f" - {sc}: HNS {hid} '{hns_naziv}' (slug={slug})\n")
|
||||||
|
f.write(f"\n## Not Found\n\n")
|
||||||
|
for db_id, naziv, grad in not_found:
|
||||||
|
f.write(f"- [{db_id}] {naziv} ({grad})\n")
|
||||||
|
f.write(f"\n## Log\n\n`{LOG_PATH}`\n")
|
||||||
|
log(f"Result -> {res_path}")
|
||||||
|
|
||||||
|
# 6) Telegram notify
|
||||||
|
msg = (f"SUB1 HNS done: matched {len(matched)}, not_found {len(not_found)}, "
|
||||||
|
f"ambiguous {len(ambiguous)}. Catalog={len(catalog)}. "
|
||||||
|
f"DB upd ok={upd_ok}/fail={upd_fail}. See SUB1_RESULT.md")
|
||||||
|
log(msg, telegram=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e:
|
||||||
|
log(f"FATAL: {e}\n{traceback.format_exc()}", telegram=True)
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# Fajl: sub1_hns_manual_overrides.py | v1.0.0 | 05.05.2026
|
||||||
|
# Lokacija: /opt/pgz-sport/scripts/sub1_hns_manual_overrides.py
|
||||||
|
# Autor: dradulic@outlook.com / damir@rinet.one
|
||||||
|
# Svrha: SUB1 — Manual high-confidence overrides za klubove koje
|
||||||
|
# fuzzy match nije uhvatio (ali postoje u HNS-u).
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
"""SUB1 manual overrides — verified mapping for special cases."""
|
||||||
|
import os, re, sys, time, urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
DSN = os.getenv("RINET_DSN",
|
||||||
|
"host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7")
|
||||||
|
UA = "PGZ-Sport-Bot/1.0 (+https://api.rinet.one/sport/; contact dradulic@outlook.com)"
|
||||||
|
|
||||||
|
# Manual mappings — verified by visiting semafor.hns.family
|
||||||
|
# Format: db_id -> (hns_id, slug, naziv-na-HNS, reason)
|
||||||
|
OVERRIDES = {
|
||||||
|
9: (3440, "znk-rijeka", "ŽNK Rijeka", "Ženski NK Rijeka — same modern club"),
|
||||||
|
101: (3440, "znk-rijeka", "ŽNK Rijeka", "Ženski NK Rijeka 'Jack Pot' — sponsor naming, same club"),
|
||||||
|
574: (5239, "nk-medicinar", "NK Medicinar", "NK Medicinar Rijeka (osnovan 1996, SRC Belveder)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def http_check(url, timeout=10):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
html = r.read().decode("utf-8", errors="replace")
|
||||||
|
m = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
|
||||||
|
return r.status, (m.group(1).strip() if m else None)
|
||||||
|
except Exception as e:
|
||||||
|
return 0, str(e)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||||
|
cur = conn.cursor()
|
||||||
|
print(f"[{datetime.now().isoformat(timespec='seconds')}] Manual overrides start")
|
||||||
|
ok = 0; fail = 0
|
||||||
|
for kid, (hns_id, slug, naziv, reason) in OVERRIDES.items():
|
||||||
|
url = f"https://semafor.hns.family/klubovi/{hns_id}/{slug}/"
|
||||||
|
status, title = http_check(url)
|
||||||
|
time.sleep(0.8)
|
||||||
|
if status != 200:
|
||||||
|
print(f" VERIFY FAIL [{kid}] {hns_id}: {status} {title}")
|
||||||
|
fail += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE pgz_sport.klubovi
|
||||||
|
SET hns_klub_id = %s,
|
||||||
|
hns_slug = %s,
|
||||||
|
source_url = %s,
|
||||||
|
scrape_source = 'hns_semafor_manual',
|
||||||
|
last_scraped_at = now()
|
||||||
|
WHERE id = %s
|
||||||
|
""", (hns_id, slug, url, kid))
|
||||||
|
print(f" OVERRIDE [{kid}] -> HNS {hns_id} '{title}' ({reason})")
|
||||||
|
ok += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f" UPDATE fail [{kid}]: {e}")
|
||||||
|
fail += 1
|
||||||
|
print(f"Done: ok={ok}, fail={fail}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -269,6 +269,27 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
|||||||
/* Center mobile content */
|
/* Center mobile content */
|
||||||
.container, main { padding: 8px !important; max-width: 100% !important; margin: 0 !important; }
|
.container, main { padding: 8px !important; max-width: 100% !important; margin: 0 !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PANEL EXPAND (CRISIS V6) — drill-down panel BIGGER za HNS karijera */
|
||||||
|
#panel, #dpanel {
|
||||||
|
width: 70vw !important;
|
||||||
|
max-width: 1100px !important;
|
||||||
|
min-width: 600px !important;
|
||||||
|
}
|
||||||
|
@media (min-width: 1400px){
|
||||||
|
#panel, #dpanel { width: 60vw !important; }
|
||||||
|
}
|
||||||
|
@media (max-width: 768px){
|
||||||
|
#panel, #dpanel {
|
||||||
|
width: 100vw !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HNS karijera tabela full-width */
|
||||||
|
#panel table, #dpanel table { width: 100%; font-size: 12px; }
|
||||||
|
#panel .hns-stats td { padding: 4px 6px; }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||||
<script src="/static/shared/sidebar.js" defer data-active="dashboard"></script>
|
<script src="/static/shared/sidebar.js" defer data-active="dashboard"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user