Udemy - Improved Course Library

Adds current ratings and other detailed data to all courses in your Udemy library.

  1. // ==UserScript==
  2. // @name Udemy - Improved Course Library
  3. // @name:de Udemy - Verbesserte Kursbibliothek
  4. // @name:fr Udemy - Bibliothèque de cours améliorée
  5. // @name:es Udemy - Biblioteca de cursos mejorada
  6. // @name:it Udemy - Libreria dei corsi migliorata
  7. // @name:ja Udemy - コースライブラリの改良
  8. // @description Adds current ratings and other detailed data to all courses in your Udemy library.
  9. // @description:de Fügt aktuelle Bewertungen und andere detaillierte Informationen zu allen Kursen in deiner Udemy-Bibliothek hinzu.
  10. // @description:fr Ajoute les évaluations actuelles et d'autres données détaillées à tous les cours de ta bibliothèque Udemy.
  11. // @description:es Añade valoraciones actuales y otros datos detallados a todos los cursos de tu biblioteca Udemy.
  12. // @description:it Aggiunge valutazioni attuali e altri dati dettagliati a tutti i corsi nella tua libreria Udemy.
  13. // @description:ja Udemyのライブラリにある全てのコースに現在の評価やその他の詳細情報を追加します。
  14. // @namespace https://github.com/tadwohlrapp
  15. // @author Tad Wohlrapp
  16. // @version 1.1.2
  17. // @license MIT
  18. // @homepageURL https://github.com/tadwohlrapp/udemy-improved-course-library
  19. // @supportURL https://github.com/tadwohlrapp/udemy-improved-course-library/issues
  20. // @icon https://github.com/tadwohlrapp/udemy-improved-course-library/raw/main/src/icon48.png
  21. // @icon64 https://github.com/tadwohlrapp/udemy-improved-course-library/raw/main/src/icon64.png
  22. // @run-at document-end
  23. // @match https://www.udemy.com/home/my-courses/*
  24. // @compatible firefox Tested on Firefox v117.0 with Violentmonkey v2.15.0 and Tampermonkey v4.19.0
  25. // @compatible chrome Tested on Chrome v115.0 with Violentmonkey v2.15.0 and Tampermonkey v4.19.0
  26. // ==/UserScript==
  27.  
  28. fetchCourses();
  29.  
  30. const mutationObserver = new MutationObserver(fetchCourses);
  31. const observerConfig = {
  32. childList: true,
  33. subtree: true
  34. };
  35. mutationObserver.observe(document, observerConfig);
  36.  
  37. const i18n = loadTranslations();
  38. const lang = getLang(document.documentElement.lang);
  39.  
  40. function fetchCourses() {
  41. listenForArchiveToggle();
  42. const courseContainers = document.querySelectorAll('[class^="enrolled-course-card--container--"]:not(.details-done)');
  43. if (courseContainers.length == 0) return;
  44. [...courseContainers].forEach((courseContainer) => {
  45.  
  46. const isPartialRefresh = courseContainer.classList.contains('partial-refresh');
  47.  
  48. const courseId = courseContainer.querySelector('h3[data-purpose="course-title-url"]>a').href.replace('https://www.udemy.com/course-dashboard-redirect/?course_id=', '');
  49.  
  50. const courseCustomDiv = document.createElement('div');
  51. courseCustomDiv.classList.add('improved-course-card--additional-details', 'js-removepartial');
  52.  
  53. const innerContainer = courseContainer.querySelector('div[data-purpose="container"]')
  54. innerContainer.appendChild(courseCustomDiv);
  55.  
  56. courseContainer.classList.add('details-done');
  57. courseContainer.classList.add('improved-course-card--container');
  58. courseContainer.classList.remove('partial-refresh');
  59.  
  60. // Add Link to course overview to options dropdown
  61. const courseLinkLi = document.createElement('li');
  62. courseLinkLi.innerHTML = `
  63. <a class="udlite-btn udlite-btn-large udlite-btn-ghost udlite-text-sm udlite-block-list-item udlite-block-list-item-small udlite-block-list-item-neutral" role="menuitem" tabindex="-1" href="https://www.udemy.com/course/${courseId}/" target="_blank" rel="noopener">
  64. <span class="udi-small udi udi-explore udlite-block-list-item-icon"></span>
  65. <div class="udlite-block-list-item-content card__course-link">${i18n[lang].overview}
  66. <svg fill="#686f7a" width="12" height="16" viewBox="0 0 24 24" style="vertical-align: bottom; margin-left: 5px;" xmlns="http://www.w3.org/2000/svg">
  67. <path d="M19 19H5V5h7V3H5a2 2 0 00-2 2v14c0 1.1.9 2 2 2h14a2 2 0 002-2v-7h-2v7zM14 3v2h3.6l-9.8 9.8 1.4 1.4L19 6.4V10h2V3h-7z"></path>
  68. </svg>
  69. </div>
  70. </a>
  71. `;
  72. courseLinkLi.classList.add('js-removepartial');
  73.  
  74. const allDropdowns = courseContainer.parentElement.querySelectorAll('.udlite-block-list');
  75. if (allDropdowns[1]) {
  76. allDropdowns[1].appendChild(courseLinkLi);
  77. }
  78.  
  79. // Find existing elements in DOM
  80. const imageWrapper = courseContainer.querySelector('div[class^="course-card-module--image-container--"]');
  81. imageWrapper.classList.add('improved-course-card--image-container');
  82.  
  83. const mainContent = courseContainer.querySelector('div[class^="course-card-module--main-content--"]');
  84. mainContent.classList.add('improved-course-card--main-content');
  85.  
  86. const courseTitle = courseContainer.querySelector('h3[data-purpose="course-title-url"]');
  87. courseTitle.classList.add('improved-course-card--course-title');
  88.  
  89. const priceTextContainer = courseContainer.querySelector('div[class^="course-card-module--price-text-container--"]');
  90. if (priceTextContainer) priceTextContainer.parentNode.removeChild(priceTextContainer);
  91.  
  92. const courseBadges = courseContainer.querySelector('div[class^="course-card-module--badges-container--"]');
  93. if (courseBadges) courseBadges.parentNode.removeChild(courseBadges);
  94.  
  95. const progressBar = courseContainer.querySelector('div[class^="enrolled-course-card--meter--"]');
  96. progressBar?.classList.add('improved-course-card--meter');
  97.  
  98. const progressAndRating = courseContainer.querySelector('div[class*="enrolled-course-card--progress-and-rating--"]');
  99. progressAndRating?.classList.add('improved-course-card--progress-and-rating');
  100.  
  101. const progressText = progressAndRating.firstChild;
  102. const progressMade = /%/.test(progressText.textContent);
  103.  
  104. if (!progressMade) progressAndRating.parentNode.removeChild(progressAndRating);
  105.  
  106. // If progress made
  107. if (progressMade) {
  108. // Add progress bar below thumbnail
  109. const progressBarSpan = document.createElement('span');
  110. progressBarSpan.classList.add('impr__progress-bar', 'js-removepartial');
  111. progressBarSpan.innerHTML = progressBar.innerHTML;
  112. imageWrapper.appendChild(progressBarSpan);
  113. // Add progress percentage to thumbnail bottom right
  114. const progressTextSpan = document.createElement('span');
  115. progressTextSpan.classList.add('card__thumb-overlay', 'card__course-runtime', 'hover-show', 'js-removepartial');
  116. progressTextSpan.textContent = progressText.textContent;
  117. imageWrapper.appendChild(progressTextSpan);
  118. // Remove existing progress percentage
  119. progressText.parentNode.removeChild(progressText);
  120. }
  121.  
  122. // Remove existing progress bar
  123. if (!isPartialRefresh) {
  124. progressBar.parentNode.removeChild(progressBar);
  125. }
  126.  
  127. // If course page has draft status, do not even to fetch its data via API
  128. if (courseContainer.querySelector('[data-purpose="course-title-url"] a').href.includes('/draft/')) {
  129. courseContainer.querySelector('.card__course-link').style.textDecoration = "line-through";
  130. courseCustomDiv.classList.add('card__nodata');
  131. courseCustomDiv.innerHTML += i18n[lang].notavailable;
  132. // We're done with this course
  133. return;
  134. }
  135.  
  136. const fetchUrl = 'https://www.udemy.com/api-2.0/courses/' + courseId + '?fields[course]=rating,num_reviews,num_subscribers,content_length_video,last_update_date,created,locale,has_closed_caption,caption_languages,num_published_lectures';
  137. fetch(fetchUrl)
  138. .then(response => {
  139. if (response.ok) {
  140. return response.json();
  141. } else {
  142. throw new Error(response.status);
  143. }
  144. })
  145. .then(json => {
  146. if (typeof json === 'undefined') { return; }
  147.  
  148. // Get everything from JSON and put it in variables
  149. const rating = json.rating.toFixed(1);
  150. const reviews = json.num_reviews;
  151. const enrolled = json.num_subscribers;
  152. const runtime = json.content_length_video;
  153. const date = json.last_update_date ?? json.created.slice(0, 10); // 'created' comes as full iso string with time
  154. const locale = json.locale.title;
  155. const localeCode = json.locale.locale;
  156. const hasCaptions = json.has_closed_caption;
  157. const captionsLangs = json.caption_languages;
  158.  
  159. // Format "Last updated / Created" Dates
  160. const updateDateShort = date ? date.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2\/$1') : '';
  161. const updateDateLong = date ? new Date(date).toLocaleDateString(lang, { year: 'numeric', month: 'long', day: 'numeric' }) : '';
  162.  
  163. // Small helper for rating strip color
  164. const getColor = v => `hsl(${(Math.round((1 - v) * 120))},100%,45%)`;
  165. const colorValue = r => Math.min(Math.max((5 - r) / 2, 0), 1);
  166.  
  167. // If captions are available, create the tag for it. We'll add it in template string later
  168. let captionsTag = '';
  169. if (hasCaptions) {
  170. const captionsString = captionsLangs.join('&#013;&#010;');
  171. captionsTag = `
  172. <div class="impr__badge" data-tooltip="${captionsString}">
  173. <svg aria-hidden="true" focusable="false" class="ud-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  174. <path d="M21 4H3v16h18V4zm-10 7H9.5v-.5h-2v3h2V13H11v2H6V9h5v2zm7 0h-1.5v-.5h-2v3h2V13H18v2h-5V9h5v2z"/>
  175. </svg>
  176. </div>
  177. `;
  178. }
  179.  
  180. // Returns true or false depending if stars are visible
  181. const reviewButton = courseContainer.querySelector('[data-purpose="review-button"]');
  182.  
  183. // Now let's handle own ratings
  184.  
  185. // Set up empty html
  186. let myRatingHtml = '';
  187. let ratingButton;
  188. let ratingOwn = 0;
  189.  
  190. // If ratings stars ARE visible, proceed to build own rating stars
  191. if (reviewButton != null) {
  192.  
  193. // Find the rating-button, and remove its css class
  194. ratingButton = reviewButton;
  195.  
  196. // If I have voted, count the stars and tell me how I voted
  197. ratingOwn = getRatingFromSvg(ratingButton.querySelector('svg')); // between 0 and 5
  198.  
  199. // Remove the old stars from ratingButton
  200. ratingButton.removeChild(ratingButton.querySelector('span'));
  201.  
  202. // Build the html
  203. myRatingHtml = `
  204. <div class="impr__rating-row">
  205. <span class="impr__star-wrapper">
  206. <span class="ud-sr-only">Rating: ${ratingOwn} out of 5</span>
  207. ${buildSvgStars(courseId.toString() + '-own', ratingOwn)}
  208. <span class="ud-heading-sm impr__rating-number">${setDecimal(ratingOwn, lang)}</span>
  209. </span>
  210. <span class="ud-text-xs impr__rating-count">(<span class="review-button"></span>)</span>
  211. </div>
  212. `;
  213. }
  214.  
  215. const ratingStripColor = ratingOwn > 0 ? ratingOwn : rating;
  216.  
  217. let updateDateInfo = '';
  218. if (updateDateShort !== '' && updateDateLong !== '') {
  219. updateDateInfo = `
  220. <div class="impr__badge" data-tooltip="${i18n[lang].updated}${updateDateLong}">
  221. <svg aria-hidden="true" focusable="false" class="ud-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  222. <path d="M11 8v5l4.3 2.5.7-1.3-3.5-2V8H11zm10 2V3l-2.6 2.6A9 9 0 1 0 21 12h-2a7 7 0 1 1-2-5l-3 3h7z"/>
  223. </svg><span>${updateDateShort}</span>
  224. </div>
  225. `;
  226. }
  227.  
  228. courseCustomDiv.innerHTML = `
  229. <div class="impr__rating">
  230. <div class="impr__rating-row">
  231. <span class="impr__star-wrapper">
  232. <span class="ud-sr-only">Rating: ${rating} out of 5</span>
  233. ${buildSvgStars(courseId, rating)}
  234. <span class="ud-heading-sm impr__rating-number">${setDecimal(rating, lang)}</span>
  235. </span>
  236. <span class="ud-text-xs impr__rating-count">(${setSeparator(reviews, lang)})</span>
  237. </div>
  238. ${myRatingHtml}
  239. </div>
  240. <div class="impr__rating-strip" style="background-color:${getColor(colorValue(ratingStripColor))}"></div>
  241. <div class="impr__stats">
  242. <div class="impr__badge" data-tooltip="${setSeparator(enrolled, lang)} ${i18n[lang].enrolled}">
  243. <svg aria-hidden="true" focusable="false" class="ud-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  244. <path d="M16 11c1.7 0 3-1.3 3-3s-1.3-3-3-3-3 1.3-3 3 1.3 3 3 3zm-8 0c1.7 0 3-1.3 3-3S9.7 5 8 5 5 6.3 5 8s1.3 3 3 3zm0 2c-2.3 0-7 1.2-7 3.5V19h14v-2.5c0-2.3-4.7-3.5-7-3.5zm8 0h-1c1.2.9 2 2 2 3.5V19h6v-2.5c0-2.3-4.7-3.5-7-3.5z"/>
  245. </svg><span>${setSeparator(enrolled, lang)}</span>
  246. </div>
  247. ${updateDateInfo}
  248. ${captionsTag}
  249. </div>
  250. `;
  251.  
  252. if (reviewButton != null) {
  253. const reviewButtonContainer = courseCustomDiv.querySelector('.review-button');
  254. ratingButton.style.display = 'inline';
  255. reviewButtonContainer.appendChild(ratingButton);
  256. }
  257.  
  258. // Hide language badge if language is English
  259. if (localeCode.slice(0, 2) !== 'en') {
  260. const localeSpan = document.createElement('span');
  261. localeSpan.classList.add('card__thumb-overlay', 'card__course-locale', 'hover-hide', 'js-removepartial');
  262. localeSpan.innerHTML = `<span style="margin-right: 3px;vertical-align: bottom;font-size: 14px;line-height: 13px;">${getFlagEmoji(localeCode.slice(-2))}</span>${locale}`;
  263. imageWrapper.appendChild(localeSpan);
  264. }
  265.  
  266. // Add course runtime from API to thumbnail bottom right
  267. const runtimeSpan = document.createElement('span');
  268. runtimeSpan.classList.add('card__thumb-overlay', 'card__course-runtime', 'hover-hide', 'js-removepartial');
  269. runtimeSpan.innerHTML = parseRuntime(runtime, lang);
  270. imageWrapper.appendChild(runtimeSpan);
  271. })
  272. .catch(error => {
  273. courseCustomDiv.classList.add('card__nodata');
  274. courseCustomDiv.innerHTML += `<div><b>${error}</b><br>${i18n[lang].notavailable}</div>`;
  275. });
  276. });
  277. }
  278.  
  279. function listenForArchiveToggle() {
  280.  
  281. document.querySelectorAll('[data-purpose="toggle-archived"]').forEach(item => {
  282. item.addEventListener('click', event => {
  283.  
  284. // super super dirty quickfix for broken archiving. I am sorry
  285. setTimeout(() => {
  286. location.reload();
  287. }, 500)
  288.  
  289. });
  290. });
  291. }
  292.  
  293. function setSeparator(int, lang) {
  294. return int.toString().replace(/\B(?=(\d{3})+(?!\d))/g, i18n[lang].separator);
  295. }
  296.  
  297. function setDecimal(rating, lang) {
  298. return rating.toString().replace('.', i18n[lang].decimal);
  299. }
  300.  
  301. function getLang(lang) {
  302. return i18n.hasOwnProperty(lang) ? lang : 'en-us';
  303. }
  304.  
  305. function buildSvgStars(courseId, rating) {
  306. return (`
  307. <svg aria-hidden="true" viewBox="0 0 70 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="impr__svg-stars">
  308. <mask id="mask-${courseId}" data-purpose="star-rating-mask">
  309. <rect x="0" y="0" width="${rating * 20}%" height="100%" fill="white"></rect>
  310. </mask>
  311. <g fill="#e59819" mask="url(#mask-${courseId})" data-purpose="star-filled">
  312. <use xlink:href="#icon-rating-star" width="14" height="14" x="0"></use>
  313. <use xlink:href="#icon-rating-star" width="14" height="14" x="14"></use>
  314. <use xlink:href="#icon-rating-star" width="14" height="14" x="28"></use>
  315. <use xlink:href="#icon-rating-star" width="14" height="14" x="42"></use>
  316. <use xlink:href="#icon-rating-star" width="14" height="14" x="56"></use>
  317. </g>
  318. <g fill="transparent" stroke="#e59819" stroke-width="2" data-purpose="star-bordered">
  319. <use xlink:href="#icon-rating-star" width="12" height="12" x="1" y="1"></use>
  320. <use xlink:href="#icon-rating-star" width="12" height="12" x="15" y="1"></use>
  321. <use xlink:href="#icon-rating-star" width="12" height="12" x="29" y="1"></use>
  322. <use xlink:href="#icon-rating-star" width="12" height="12" x="43" y="1"></use>
  323. <use xlink:href="#icon-rating-star" width="12" height="12" x="57" y="1"></use>
  324. </g>
  325. </svg>
  326. `);
  327. }
  328.  
  329. function parseRuntime(seconds, lang) {
  330. if (seconds % 60 > 29) { seconds += 30; }
  331. let hours = Math.floor(seconds / 60 / 60);
  332. let minutes = Math.floor(seconds / 60) - (hours * 60);
  333. let hoursFormatted = hours > 0 ? hours.toString() + i18n[lang].hours : '';
  334. let minutesFormatted = minutes > 0 ? ' ' + minutes.toString() + i18n[lang].mins : '';
  335. return hoursFormatted + minutesFormatted;
  336. }
  337.  
  338. function getRatingFromSvg(svgElement) {
  339. let percentage = svgElement.querySelector('mask rect').getAttribute('width');
  340. let rating = parseFloat(percentage) / 100 * 5;
  341. return rating;
  342. }
  343.  
  344. function loadTranslations() {
  345. return {
  346. 'en-us': {
  347. 'overview': 'Course overview',
  348. 'enrolled': 'students',
  349. 'updated': 'Last updated ',
  350. 'notavailable': 'Course info not available',
  351. 'separator': ',',
  352. 'decimal': '.',
  353. 'hours': 'h',
  354. 'mins': 'm'
  355. },
  356. 'de-de': {
  357. 'overview': 'Kursübersicht',
  358. 'enrolled': 'Teilnehmer',
  359. 'updated': 'Zuletzt aktualisiert ',
  360. 'notavailable': 'Kursinfo nicht verfügbar',
  361. 'separator': '.',
  362. 'decimal': ',',
  363. 'hours': ' Std',
  364. 'mins': ' Min'
  365. },
  366. 'es-es': {
  367. 'overview': 'Descripción del curso',
  368. 'enrolled': 'estudiantes',
  369. 'updated': 'Última actualización ',
  370. 'notavailable': 'La información del curso no está disponible',
  371. 'separator': '.',
  372. 'decimal': ',',
  373. 'hours': ' h',
  374. 'mins': ' m'
  375. },
  376. 'fr-fr': {
  377. 'overview': 'Aperçu du cours',
  378. 'enrolled': 'participants',
  379. 'updated': 'Dernière mise à jour : ',
  380. 'notavailable': 'Informations sur les cours non disponibles',
  381. 'separator': ' ',
  382. 'decimal': ',',
  383. 'hours': ' h',
  384. 'mins': ' min'
  385. },
  386. 'it-it': {
  387. 'overview': 'Panoramica del corso',
  388. 'enrolled': 'studenti',
  389. 'updated': 'Ultimo aggiornamento ',
  390. 'notavailable': 'Informazioni sul corso non disponibili',
  391. 'separator': '.',
  392. 'decimal': ',',
  393. 'hours': ' h',
  394. 'mins': ' min'
  395. },
  396. 'ja-jp': {
  397. 'overview': 'コースの概要',
  398. 'enrolled': '受講生',
  399. 'updated': '最終更新日 ',
  400. 'notavailable': 'コースの情報はありません。',
  401. 'separator': ',',
  402. 'decimal': '.',
  403. 'hours': '時間',
  404. 'mins': '分'
  405. }
  406. };
  407. }
  408.  
  409. function getFlagEmoji(countryCode) {
  410. const codePoints = countryCode
  411. .split('')
  412. .map(char => 127397 + char.charCodeAt());
  413. return String.fromCodePoint(...codePoints);
  414. }
  415.  
  416. const style = document.createElement('style');
  417. style.textContent = `
  418. .improved-course-card--container {
  419. border: 1px solid #d1d7dc;
  420. }
  421.  
  422. .improved-course-card--container:hover {
  423. background-color: #f7f9fa;
  424. }
  425.  
  426. .improved-course-card--container .improved-course-card--image-container {
  427. border-width: 0 0 1px 0;
  428. }
  429.  
  430. .improved-course-card--main-content {
  431. padding: 0 6px;
  432. min-height: 68px;
  433. }
  434.  
  435. .card--learning__details {
  436. border-top: 1px solid #e8e9eb;
  437. }
  438.  
  439. .card__details {
  440. padding: 12px;
  441. height: 66px;
  442. white-space: initial;
  443. }
  444.  
  445. .improved-course-card--course-title {
  446. font-size: 1.4rem !important;
  447. }
  448.  
  449. span[class^='leave-rating--helper-text'] {
  450. font-size: 10px;
  451. white-space: nowrap;
  452. }
  453.  
  454. .card__thumb-overlay {
  455. position: absolute;
  456. display: inline-block;
  457. font-size: 10px;
  458. font-weight: 700;
  459. margin: 4px;
  460. padding: 2px 4px;
  461. border-radius: 2px;
  462. transition: opacity linear 100ms;
  463. }
  464.  
  465. .card__course-link {
  466. font-size: 1.4rem;
  467. }
  468.  
  469. .card__course-runtime {
  470. bottom: 0;
  471. right: 0;
  472. background-color: rgba(20, 30, 46, 0.75);
  473. color: #ffffff;
  474. }
  475.  
  476. .impr__progress-bar ~ .card__course-runtime {
  477. bottom: 4px;
  478. }
  479.  
  480. .card__course-locale {
  481. top: 0;
  482. left: 0;
  483. background-color: rgba(255, 255, 255, 0.9);
  484. box-shadow: 0 0 1px 1px rgba(20, 23, 28, 0.1);
  485. color: #29303b;
  486. font-weight: 600;
  487. }
  488.  
  489. .improved-course-card--container .hover-hide {
  490. opacity: 1;
  491. }
  492.  
  493. .improved-course-card--container .hover-show {
  494. opacity: 0;
  495. }
  496.  
  497. .improved-course-card--container:hover .hover-hide {
  498. opacity: 0;
  499. }
  500.  
  501. .improved-course-card--container:hover .hover-show {
  502. opacity: 1;
  503. }
  504.  
  505. .impr__progress-bar {
  506. display: block;
  507. position: absolute;
  508. bottom: 0;
  509. right: 0;
  510. left: 0;
  511. height: 5px;
  512. background: rgba(20, 30, 46, 0.75);
  513. }
  514.  
  515. .impr__progress-bar .progress__bar {
  516. background: #a435ef !important;
  517. }
  518.  
  519. .improved-course-card--additional-details {
  520. width: 100%;
  521. font-size: 1.2rem;
  522. color: #464b53;
  523. height: 82px;
  524. }
  525.  
  526. .impr__rating .impr__rating-number {
  527. margin-left: 0.4rem;
  528. font-size: 1.3rem;
  529. color: #505763;
  530. }
  531.  
  532. .impr__rating-count {
  533. color: #6a6f73;
  534. margin-left: 0.4rem;
  535. }
  536.  
  537. .impr__rating {
  538. display: flex;
  539. flex-direction: column;
  540. justify-content: space-between;
  541. padding: 0 6px 6px;
  542. height: 42px;
  543. }
  544.  
  545. .impr__rating-strip {
  546. height: 5px;
  547. }
  548.  
  549. .impr__stats {
  550. font-weight: 500;
  551. padding: 6px;
  552. line-height: 1.7;
  553. display: flex;
  554. }
  555.  
  556. .impr__badge {
  557. display: inline-flex;
  558. position: relative;
  559. flex-direction: row;
  560. justify-content: center;
  561. align-items: center;
  562. gap: 4px;
  563. background: #f7f8fa;
  564. padding: 0 5px;
  565. margin-right: 5px;
  566. border-radius: 2px;
  567. border: 1px solid #e7e7e8;
  568. cursor: default;
  569. }
  570.  
  571. .impr__badge .ud-icon {
  572. width: 1.4rem;
  573. height: 1.4rem;
  574. opacity: 0.75;
  575. }
  576.  
  577. .impr__svg-stars {
  578. display: block;
  579. width: 7rem;
  580. height: 1.6rem;
  581. }
  582.  
  583. .card__nodata {
  584. font-size: 13px;
  585. display: flex;
  586. justify-content: center;
  587. align-items: center;
  588. text-align: center;
  589. height: 75px;
  590. margin-top: 10px;
  591. padding: 12px;
  592. background: #fbf4f4;
  593. color: #521822;
  594. }
  595.  
  596. .impr__badge:hover:after {
  597. display: flex;
  598. justify-content: center;
  599. background: #4f5662;
  600. border-radius: 3px;
  601. color: #fff;
  602. content: attr(data-tooltip);
  603. bottom: 24px;
  604. margin: 0;
  605. font-size: 11px;
  606. padding: 2px 6px;
  607. position: absolute;
  608. z-index: 10;
  609. white-space: pre;
  610. }
  611.  
  612. .impr__badge:hover:before {
  613. border: solid;
  614. border-color: #4f5662 transparent;
  615. content: '';
  616. left: 50%;
  617. margin-left: -4px;
  618. position: absolute;
  619. top: -4px;
  620. border-width: 6px 4px 0;
  621. }
  622.  
  623. .impr__rating-row {
  624. margin: 0;
  625. padding: 0;
  626. display: flex;
  627. }
  628.  
  629. .impr__star-wrapper {
  630. display: inline-flex;
  631. align-items: center;
  632. }`;
  633. document.documentElement.appendChild(style);