SlimSome CMS

Description

For over a year, I've been managing a Counter-Strike 1.6 game project, and I contemplated the idea of establishing a dedicated website. This platform would serve as a central hub, encompassing essential information such as online status, news, an online shop, and statistics for players, administrators, and banned individuals.

Despite the availability of numerous ready-made options, driven by a desire for hands-on experience, I opted to develop my own system entirely from scratch. Thus, SlimSome CMS was created—an entirely free system designed to facilitate the easy construction of a website tailored for Counter-Strike 1.6 game projects.

Details

Technologies PHP, SQL, SQLite, CRON, NodeJS, Gulp, jQuery, Ajax, SASS, PostCSS, SVG, API, ChartJS, Wysiwyg, CyrToLat, Pawn
Features Own CMS: pages, news, services and user management, Online shop with real-time game server integration, Personal Account, Server status, Online chat from game, Group chat on site, Admins list, Bans list, Detailed player statistics, Email notifications, SEO optimized, Search, Pagination, Hot/Top news/comments, Payments history, Charts for analytics, Accordion, Slider, Gallery, Form files download status and many more...
Date May 2021
Link http://web.archive.org/web/20230613082011/https://awesomecs.com/
Github https://github.com/zakandaiev/slimsome-cms

Code snippets

clike User Expiration Date - amxx plugin for CS 1.6 server written in Pawn language

#include <amxmodx>

new const USERS_FILE[] = "addons/amxmodx/configs/users.ini";

new Trie: g_trie;

new g_iAdminExpired[MAX_PLAYERS + 1];

public plugin_init() {
  register_plugin("SlimSome CMS: User Expiration Date", "1.0", "szawesome");
  
  g_trie = TrieCreate();

  CheckDays();
  GetDays();
}

public plugin_end() {
  TrieDestroy(g_trie);
}

public client_authorized(id) {
  g_iAdminExpired[id] = GetTrieParsedTime(id);
}

public plugin_natives() {
  register_native("admin_expired", "admin_expired_callback", true);
}

public admin_expired_callback(id) {
  return g_iAdminExpired[id];
}
  
CheckDays() {
  new file = fopen(USERS_FILE, "r+");

  if(!file) return 0;

  new buffer[256], curr_line;
  new nickname[64], password[64], flags[32], access[32], exp_date[32];
  
  while(!feof(file)) {
    fgets(file, buffer, charsmax(buffer));
    trim(buffer);

    curr_line++;

    if(!buffer[0]) {
      continue;
    }

    if(buffer[0] != '^"') {
      continue;
    }

    parse(buffer, 
      nickname, charsmax(nickname),
      password, charsmax(password),
      flags, charsmax(flags),
      access, charsmax(access),
      exp_date, charsmax(exp_date));

    if(str_to_num(exp_date) == 0 || str_to_num(exp_date) > get_systime()) {
      formatex(buffer, charsmax(buffer), "^"%s^" ^"%s^" ^"%s^" ^"%s^" ^"%s^"", nickname, password, flags, access, exp_date);
    } else {
      formatex(buffer, charsmax(buffer), ";^"%s^" ^"%s^" ^"%s^" ^"%s^" ^"%s^"", nickname, password, flags, access, exp_date);
    }

    write_file(USERS_FILE, buffer, curr_line-1);
  }
  
  // server_cmd("amx_reloadadmins"); - если плагин поставить выше admin.amxx тогда можно и не посылать эту команду
  return fclose(file);
}

GetDays() {
  new file = fopen(USERS_FILE, "r");

  if(!file) return 0;

  new buffer[256], admin_id[32], exp_date[32];
  
  while(!feof(file)) {
    fgets(file, buffer, charsmax(buffer));
    trim(buffer);

    if(!buffer[0] || buffer[0] != '^"')
      continue;
      
    for(new i; i<5;i++) {
      if(strlen(buffer) <= 0)
        break;
      if(!i) {
        argbreak(buffer, admin_id, charsmax(admin_id), buffer, charsmax(buffer));
      } else {
        argbreak(buffer, exp_date, charsmax(exp_date), buffer, charsmax(buffer));
      }
    }
    
    TrieSetCell(g_trie, admin_id, str_to_num(exp_date) == 0 ? 0 : str_to_num(exp_date));
  }
  
  return fclose(file);
}

stock GetTrieParsedTime(id) {
  static _id[32], cell;

  get_user_name(id, _id, charsmax(_id));
  
  if(TrieGetCell(g_trie, _id, cell)) {
    return cell;
  }

  get_user_authid(id, _id, charsmax(_id));

  if(TrieGetCell(g_trie, _id, cell)) {
    return cell;
  }
    
  get_user_ip(id,  _id, charsmax(_id), 1);

  if(TrieGetCell(g_trie, _id, cell)) {
    return cell;
  }

  return -1;
}

sql DB structure install query

SET NAMES UTF8;

CREATE TABLE IF NOT EXISTS `%prefix%_settings` ( 
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL,
  `value` TEXT DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE `name` (name)
) ENGINE = InnoDB;

INSERT INTO `%prefix%_settings` (`name`, `value`) VALUES
('db_host', '%db_host%'),
('db_user', '%db_user%'),
('db_pass', '%db_pass%'),
('db_name', '%db_name%'),
('db_prefix', '%prefix%'),
('t_zone', '%t_zone%'),
('cron_pass', '%cron_pass%'),
('site_name', '%site_name%'),
('site_url', '%site_url%'),
('site_email', '%site_email%'),
('site_logo', '%site_logo%'),
('site_background', null),
('site_background_styles', null),
('site_color_accent', '#313946'),
('site_color_accent_2', '#fe3f99'),
('site_color_body', '#faf9fa'),
('site_color_text', '#000'),
('site_description', 'Сайт игрового сервера по игре CS 1.6'),
('site_keywords', 'сайт сервера, сервер кс, кс 1.6, cs 1.6, counter-strike 1.6'),
('site_analytics_gtag', null),
('site_chat_enabled', 'true'),
('site_chat_enabled_for_unregistereds', 'true'),
('server_ip', '%server_ip%'),
('ftp_host', '%ftp_host%'),
('ftp_login', '%ftp_login%'),
('ftp_pass', '%ftp_pass%'),
('ftp_users_path', '%ftp_users_path%'),
('ftp_bans_path', '%ftp_bans_path%'),
('ftp_stats_path', '%ftp_stats_path%'),
('payments', '%payments%'),
('socials', '[{"icon": "vk.svg", "url": "https://vk.com/szawesome", "blank": true},{"icon": "dev-cs.svg", "url": "https://dev-cs.ru/members/7458/", "blank": true},{"icon": "email.svg", "url": "mailto:szawesome95@gmail.com", "blank": true}]'),
('server_online_info', null);

CREATE TABLE IF NOT EXISTS `%prefix%_users` ( 
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `login` VARCHAR(32) NOT NULL,
  `password` VARCHAR(32) NOT NULL,
  `email` VARCHAR(128) DEFAULT NULL,
  `nick` VARCHAR(64) DEFAULT NULL,
  `steam_id` VARCHAR(32) DEFAULT NULL,
  `name` VARCHAR(32) DEFAULT NULL,
  `service_id` INT DEFAULT NULL,
  `service_start` TIMESTAMP NULL DEFAULT NULL,
  `service_end` TIMESTAMP NULL DEFAULT NULL,
  `service_nolimit` BOOLEAN NOT NULL DEFAULT FALSE,
  `service_bind_type` INT DEFAULT NULL,
  `isadmin` BOOLEAN NOT NULL DEFAULT FALSE,
  `ismoder` BOOLEAN NOT NULL DEFAULT FALSE,
  `ip` VARCHAR(32) DEFAULT NULL,
  `last_sign` TIMESTAMP NULL DEFAULT NULL,
  `cdate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE `login` (`login`),
  UNIQUE `email` (`email`),
  UNIQUE `nick` (`nick`),
  UNIQUE `steam_id` (`steam_id`)
) ENGINE = InnoDB;

INSERT INTO `%prefix%_users` (`login`, `password`, `email`, `name`, `isadmin`, `ismoder`) VALUES
('%adm_login%', '%adm_pass%', '%adm_email%', '%adm_name%', true, true);

CREATE TABLE IF NOT EXISTS `%prefix%_services` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL,
  `flags` VARCHAR(32) NOT NULL,
  `days` JSON NOT NULL COMMENT 'days,price_ua,price_rub',
  `images` JSON DEFAULT NULL,
  `user_avatar` TEXT DEFAULT NULL,
  `description` TEXT DEFAULT NULL,
  `buyable` BOOLEAN NOT NULL DEFAULT TRUE,
  `enabled` BOOLEAN NOT NULL DEFAULT TRUE,
  PRIMARY KEY (`id`),
  UNIQUE `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE IF NOT EXISTS `%prefix%_chat` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` INT NOT NULL,
  `message` TEXT NOT NULL,
  `refference` INT DEFAULT NULL,
  `cdate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE IF NOT EXISTS `%prefix%_payments` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL,
  `user_id` INT DEFAULT NULL,
  `user_data` JSON DEFAULT NULL,
  `service_id` INT NOT NULL,
  `service_name` VARCHAR(128) NOT NULL,
  `days` INT NOT NULL,
  `price` INT NOT NULL,
  `currency` INT NOT NULL,
  `prolong` BOOLEAN NOT NULL DEFAULT FALSE,
  `status` INT NOT NULL,
  `cdate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE IF NOT EXISTS `%prefix%_pages` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL,
  `url` VARCHAR(64) NOT NULL,
  `content` TEXT DEFAULT NULL,
  `template` VARCHAR(64) DEFAULT NULL,
  `enabled` BOOLEAN NOT NULL DEFAULT TRUE,
  `page_order` INT UNSIGNED NULL DEFAULT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE `name` (`name`),
  UNIQUE `url` (`url`),
  UNIQUE `page_order` (`page_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

INSERT INTO `%prefix%_pages` (`name`, `url`, `template`, `page_order`) VALUES
('Информация', 'info', 'info', 1),
('Правила', 'rules', 'rules', 2);

CREATE TABLE IF NOT EXISTS `%prefix%_bans` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `nick` VARCHAR(128) NOT NULL,
  `ip` VARCHAR(32) NOT NULL,
  `steam_id` VARCHAR(32) NOT NULL,
  `reason` VARCHAR(128) NOT NULL,
  `created` INT DEFAULT NULL,
  `length` INT DEFAULT NULL,
  `admin_nick` VARCHAR(128) NOT NULL,
  `unbanned` BOOLEAN NOT NULL DEFAULT FALSE,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE IF NOT EXISTS `%prefix%_news` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `author` INT NOT NULL,
  `title` VARCHAR(64) NOT NULL,
  `url` VARCHAR(128) NOT NULL,
  `body` TEXT DEFAULT NULL,
  `image` TEXT DEFAULT NULL,
  `meta_description` TEXT DEFAULT NULL,
  `meta_keywords` TEXT DEFAULT NULL,
  `enabled` BOOLEAN NOT NULL DEFAULT TRUE,
  `cdate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE `url` (`url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE IF NOT EXISTS `%prefix%_comments` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `news_id` INT NOT NULL,
  `author` INT NOT NULL,
  `comment` TEXT NOT NULL,
  `cdate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

CREATE TABLE IF NOT EXISTS `%prefix%_stats` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `nick` VARCHAR(128) NOT NULL,
  `uniq` VARCHAR(32) NOT NULL,
  `teamkill` INT DEFAULT NULL,
  `damage` INT DEFAULT NULL,
  `deaths` INT DEFAULT NULL,
  `kills` INT DEFAULT NULL,
  `shots` INT DEFAULT NULL,
  `hits` INT DEFAULT NULL,
  `headshots` INT DEFAULT NULL,
  `defusions` INT DEFAULT NULL,
  `defused` INT DEFAULT NULL,
  `plants` INT DEFAULT NULL,
  `explosions` INT DEFAULT NULL,
  `head` INT DEFAULT NULL,
  `chest` INT DEFAULT NULL,
  `stomach` INT DEFAULT NULL,
  `leftarm` INT DEFAULT NULL,
  `rightarm` INT DEFAULT NULL,
  `leftleg` INT DEFAULT NULL,
  `rightleg` INT DEFAULT NULL,
  `rank` INT DEFAULT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

php Queries for charts on Bans page

<?php
	$bans_top_nicks = $pdo->query("
		SELECT
			COUNT(id) as bans_count, nick
		FROM ".$prefix."_bans
		GROUP BY nick
		ORDER BY bans_count DESC
		LIMIT 10
	")->fetchAll(PDO::FETCH_ASSOC);

	$bans_top_reasons = $pdo->query("
		SELECT
			COUNT(id) as bans_count, reason
		FROM ".$prefix."_bans
		GROUP BY reason
		ORDER BY bans_count DESC
		LIMIT 5
	")->fetchAll(PDO::FETCH_ASSOC);

	$bans_per_days = $pdo->query("
		SELECT 
			(
				SELECT COUNT(id)
				FROM ".$prefix."_bans
				WHERE FROM_UNIXTIME(created) >= date_sub(NOW(), INTERVAL 1 DAY)
			) as today,
			(
				SELECT COUNT(id)
				FROM ".$prefix."_bans
				WHERE FROM_UNIXTIME(created) >= date_sub(NOW(), INTERVAL 1 MONTH)
			) as month
	")->fetch(PDO::FETCH_ASSOC);

	/*$bans_per_months = $pdo->query("
		SELECT * FROM (
			SELECT
				COUNT(id) as bans_count, EXTRACT(month FROM FROM_UNIXTIME(created)) as month, EXTRACT(year FROM FROM_UNIXTIME(created)) as year
			FROM ".$prefix."_bans
			GROUP BY month, year
			ORDER BY year DESC, month DESC
			LIMIT 5
		) t1 ORDER BY t1.year, t1.month
	")->fetchAll(PDO::FETCH_ASSOC);*/
	$bans_per_months = $pdo->query("
		SELECT coalesce(bans_count, 0) as bans_count, t2.year, t2.month FROM (
			SELECT
				EXTRACT(year FROM FROM_UNIXTIME(created)) as year, EXTRACT(month FROM FROM_UNIXTIME(created)) as month, count(*) as bans_count
			FROM ".$prefix."_bans
			GROUP BY year, month
		) t1
		RIGHT JOIN (
			SELECT EXTRACT(year FROM tt.Date) as year, EXTRACT(month FROM tt.Date) as month
				FROM (
						SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a) + (1000 * d.a) ) DAY as Date
						FROM (SELECT 0 as a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) as a
						CROSS JOIN (SELECT 0 as a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) as b
						CROSS JOIN (SELECT 0 as a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) as c
						CROSS JOIN (SELECT 0 as a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) as d
				) tt
				WHERE tt.Date BETWEEN date_sub(NOW(), INTERVAL 4 month) AND NOW()
				GROUP BY year, month
				ORDER BY year, month
		) t2 ON t2.month=t1.month AND t1.year=t2.year
		WHERE t2.month IS NOT NULL
		ORDER BY t2.year, t2.month
		LIMIT 5
	")->fetchAll(PDO::FETCH_ASSOC);
?>

<div>
	... ... ...
	skip html layout
	... ... ...
</div>

<script>
	const bans_reasons_labels = [
	<? foreach($bans_top_reasons as $row): ?>
		'<?=$row["reason"]?>: <?=$row["bans_count"]?>',
	<? endforeach; ?>
	];
	const bans_reasons_data = [
	<? foreach($bans_top_reasons as $row): ?>
		<?=$row["bans_count"]?>,
	<? endforeach; ?>
	];
	let bans_reasons = new Chart(
		$("#chart-bans-reasons"),
		{
			type: 'pie',
			data: {
				labels: bans_reasons_labels,
				datasets: [{
					label: 'Топ бан-причиин',
					data: bans_reasons_data,
					fill: false,
					borderColor: '#fff',
					backgroundColor: [
						'#ff6384',
						'#36a2eb',
						'#ffcd56',
						'#fe3f99',
						'#00b96c'
					],
					hoverOffset: 3
				}]
			},
			options: {
				plugins: {
					legend: {
						position: 'bottom',
						align: 'start',
						onHover: pieHandleHover,
						onLeave: pieHandleLeave
					},
					tooltip: {
						 callbacks: {
								label: function(context) {
									var label = context.label;
									var dataset = context.dataset;
									var total = dataset.data.reduce(function(previousValue, currentValue, currentIndex, array) {
										return previousValue + currentValue;
									});
									var currentValue = dataset.data[context.dataIndex];
									var percentage = Math.floor(((currentValue/total) * 100)+0.5);
									return label + " (" + percentage + "%)";
								}
						 }
					}
				}
			}
		}
	);
	function pieHandleHover(evt, item, legend) {
		legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => {
			colors[index] = index === item.index || color.length === 9 ? color : color + '4D';
		});
		legend.chart.update();
	}
	function pieHandleLeave(evt, item, legend) {
		legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => {
			colors[index] = color.length === 9 ? color.slice(0, -2) : color;
		});
		legend.chart.update();
	}
	</script>
	<script>
	const bans_per_month_labels = [
	<? foreach($bans_per_months as $row): ?>
		'<?=getMonthName($row["month"])?>',
	<? endforeach; ?>
	];
	const bans_per_month_data = [
	<? foreach($bans_per_months as $row): ?>
		<?=$row["bans_count"]?>,
	<? endforeach; ?>
	];
	var bans_per_month = new Chart(
		$("#chart-bans-per-month"),
		{
			type: 'line',
			data: {
				labels: bans_per_month_labels,
				datasets: [{
					label: 'Количество забаненых',
					data: bans_per_month_data,
					fill: false,
					borderColor: '#fe3f99',
					backgroundColor: '#313946',
					tension: 0.3
				}]
			},
			options: {
				elements: {
					point: {
						radius: 5
					}
				},
				plugins: {
					legend: {
						display: false
					}
				}
			}
		}
	);
</script>

php Service description with carousel & gallery snippet on Buy page

<div class="buy-content__right">
	<section class="block">
		<h2 class="block__title">Описание</h2>
		<?php foreach($db_services_buyable as $rows): ?>
			<div id="service_desc_<?=$rows["id"]?>">
				<? if(!empty($rows["images"])): ?>
					<div class="buy-content__img appear-right">
						<div class="carousel">
							<? foreach(json_decode($rows["images"], true) as $image): ?>
								<div class="carousel__img"><img src="<?=urlEncodeSpaces($image)?>" alt="Привилегия <?=$rows["name"]?>" class="buy-content" data-zoomable></div>
							<? endforeach; ?>
						</div>
					</div>
				<? endif; ?>
				<div class="buy-content__description appear-right anim-delay-1">
					<div class="well well_primary">
						<h3 class="text-center"><?=$rows["name"]?></h3>
						<?php foreach(json_decode($rows["days"], true) as $days): ?>
							<?php
								if ($days["days"] == 0) {
									$days_count = 'навсегда';
								} else {
									$days_count = 'за '.$days["days"].' дней';
								}
							?>
							<p><span class="label label-info"><?=$days["price_rub"]?> руб.</span> или <span class="label label-info"><?=$days["price_uah"]?> грн.</span> <?=$days_count?>.</p>
						<? endforeach; ?>
						<? if(isset($rows["description"])): ?>
							<?=$rows["description"]?>
						<? else: ?>
							<p>Описание не заполнено.</p>
							<? if($is_user_admin): ?>
								<a href="profile?section=services&edit=<?=$rows["id"]?>" class="btn">Заполнить</a>
							<? endif; ?>
						<? endif; ?>
					</div>
				</div>
			</div>
		<? endforeach; ?>
	</section>
</div>

php Hitbox model layout

<div class="hitbox">
	<div class="hitbox__item head"><span class="hitbox__tooltip">Попаданий в голову: <?= $player_arr["head"] ?> (<?= round(100 * $player_arr["head"] / $hitbox_sum, 2) ?>%)</span></div>
	<div class="hitbox__item chest"><span class="hitbox__tooltip">Попаданий в грудь: <?= $player_arr["chest"] ?> (<?= round(100 * $player_arr["chest"] / $hitbox_sum, 2) ?>%)</span></div>
	<div class="hitbox__item stomach"><span class="hitbox__tooltip">Попаданий в живот: <?= $player_arr["stomach"] ?> (<?= round(100 * $player_arr["stomach"] / $hitbox_sum, 2) ?>%)</span></div>
	<div class="hitbox__item larm"><span class="hitbox__tooltip">Попаданий в левую руку: <?= $player_arr["leftarm"] ?> (<?= round(100 * $player_arr["leftarm"] / $hitbox_sum, 2) ?>%)</span></div>
	<div class="hitbox__item rarm"><span class="hitbox__tooltip">Попаданий в правую руку: <?= $player_arr["rightarm"] ?> (<?= round(100 * $player_arr["rightarm"] / $hitbox_sum, 2) ?>%)</span></div>
	<div class="hitbox__item lleg"><span class="hitbox__tooltip">Попаданий в левую ногу: <?= $player_arr["leftleg"] ?> (<?= round(100 * $player_arr["leftleg"] / $hitbox_sum, 2) ?>%)</span></div>
	<div class="hitbox__item rleg"><span class="hitbox__tooltip">Попаданий в правую ногу: <?= $player_arr["rightleg"] ?> (<?= round(100 * $player_arr["rightleg"] / $hitbox_sum, 2) ?>%)</span></div>
	<img class="hitbox__image" src="img/stats/player_model.png" alt="hitbox">
</div>

scss Hitbox model styles

.hitbox {
	display: block;
	width: 100%;
	position: relative;
	&__image {
		display: block;
		width: 100%;
		height: 100%;
		filter: drop-shadow(0 3px 4px rgba(10, 31, 68, 0.1));
	}
	&__item {
		position: absolute;
		z-index: 1;
		cursor: pointer;
		&.head {
			width: 14%;
			height: 17%;
			top: 0;
			left: 50%;
			transform: translateX(-50%);
		}
		&.chest {
			width: 24%;
			height: 18%;
			top: 17%;
			left: 50%;
			transform: translateX(-50%);
		}
		&.stomach {
			width: 24%;
			height: 20%;
			top: 35%;
			left: 50%;
			transform: translateX(-50%);
		}
		&.larm {
			width: 38%;
			height: 14%;
			top: 17%;
			right: 0;
		}
		&.rarm {
			width: 38%;
			height: 14%;
			top: 17%;
			left: 0;
		}
		&.lleg {
			width: 14%;
			height: 45%;
			top: 55%;
			right: 36%;
		}
		&.rleg {
			width: 14%;
			height: 45%;
			top: 55%;
			left: 36%;
		}
	}
	&__tooltip {
		// positioning
		position: absolute;
		z-index: 2;
		top: 0;
		left: 50%;
		transform: translate(-50%, -110%);
		position: absolute;
		white-space: nowrap;
		text-overflow: ellipsis;
		// stylings
		background: rgba(0, 0, 0, 0.9);
		color: #fff;
		text-align: center;
		padding: 5px;
		border-radius: $brs;
		box-shadow: 0 3px 4px rgba(10, 31, 68, 0.1), 0 0 1px rgba(10, 31, 68, 0.08);
		cursor: default;
		&::after {
			content: "";
			position: absolute;
			top: 100%;
			left: 50%;
			margin-left: -5px;
			border-width: 5px;
			border-style: solid;
			border-color: rgba(0, 0, 0, 0.9) transparent transparent transparent;
		}
		// appearance
		visibility: hidden;
		opacity: 0;
		transition: all .3s;
	}
	.hitbox__item:hover .hitbox__tooltip {
		visibility: visible;
		opacity: 1;
	}
}

js Accordion handler

$(document).ready(function() {
	// ACCODRION
	const accondionsContent = $('.accordion__content').hide();
	$('.accordion__title').click(function() {
		if ($(this).hasClass("active")) {
			$(this).next().slideUp();
		} else {
			$(this).next().slideDown();
		}
		$(this).toggleClass("active");
	});
});

scss Accordion styles

.accordion {
	display: block;
	width: 100%;
	&__title {
		display: block;
		width: 100%;
		cursor: pointer;
		position: relative;
		&::before, &::after {
			content: "";
			position: absolute;
			top: 50%;
			transform: translateY(-50%);
			background-color: #000;
			transition: all .3s;
		}
		&::before {
			right: 9px;
			width: 2px;
			height: 20px;
		}
		&::after {
			right: 0;
			width: 20px;
			height: 2px;
		}
		&.active::before {
			transform: translateY(-50%) rotate(90deg);
		}
		&.active::after {
			transform: translateY(-50%) rotate(90deg);
			opacity: 0;
			visibility: hidden;
		}
	}
	&__content {
		display: block;
		width: 100%;
	}
}

php News prev&next buttons query

<?php

$news_extra_query = $pdo->prepare("
	SELECT
		JSON_OBJECT(
			'title', prev_title,
			'url', prev_url,
			'image', prev_image
		) as extra_prev,
		JSON_OBJECT(
			'title', next_title,
			'url', next_url,
			'image', next_image
		) as extra_next
	FROM (
		SELECT id,
			LAG(title) OVER (ORDER BY cdate) as prev_title,
			LAG(url) OVER (ORDER BY cdate) as prev_url,
			LAG(image) OVER (ORDER BY cdate) as prev_image,
			LEAD(title) OVER (ORDER BY cdate) as next_title,
			LEAD(url) OVER (ORDER BY cdate) as next_url,
			LEAD(image) OVER (ORDER BY cdate) as next_image
		FROM ".$prefix."_news
		WHERE enabled IS TRUE
	) t
	WHERE id=:current_id;
");
$news_extra_query->bindParam(":current_id", $current_post["id"]);
$news_extra_query->execute();
$news_extra = $news_extra_query->fetch(PDO::FETCH_LAZY);
$extra_prev = json_decode($news_extra->extra_prev, true);
$extra_next = json_decode($news_extra->extra_next, true);

php Player skill formula

function getPlayerSkill($kills, $deaths) {
	$sum = $kills + $deaths;
	if($sum == 0) {
		return "?";
	}
	return round(100 * $kills / $sum, 2);
}

php Replace smile codes with images

function replaceSmiles($text) {
	$smiles = array(
		':ban:' => '<img src="/img/smiles/ban.gif" alt="ban">',
		':beer:' => '<img src="/img/smiles/beer.gif" alt="beer">',
		':bomb:' => '<img src="/img/smiles/bomb.gif" alt="bomb">',
		':cool:' => '<img src="/img/smiles/cool.gif" alt="cool">',
		'8)' => '<img src="/img/smiles/cool.gif" alt="cool">',
		':crazy:' => '<img src="/img/smiles/crazy.gif" alt="crazy">',
		'oO' => '<img src="/img/smiles/crazy.gif" alt="crazy">',
		'o0' => '<img src="/img/smiles/crazy.gif" alt="crazy">',
		'Oo' => '<img src="/img/smiles/crazy.gif" alt="crazy">',
		'0o' => '<img src="/img/smiles/crazy.gif" alt="crazy">',
		':devil:' => '<img src="/img/smiles/devil.gif" alt="devil">',
		'>:)' => '<img src="/img/smiles/devil.gif" alt="devil">',
		':eek:' => '<img src="/img/smiles/eek.gif" alt="eek">',
		'OO' => '<img src="/img/smiles/eek.gif" alt="eek">',
		':fu:' => '<img src="/img/smiles/fu.gif" alt="fu">',
		':lol:' => '<img src="/img/smiles/lol.gif" alt="lol">',
		'xD' => '<img src="/img/smiles/lol.gif" alt="lol">',
		':love:' => '<img src="/img/smiles/love.gif" alt="love">',
		'<3' => '<img src="/img/smiles/love.gif" alt="love">',
		':ms:' => '<img src="/img/smiles/ms.gif" alt="ms">',
		':nice:' => '<img src="/img/smiles/nice.gif" alt="nice">',
		':razz:' => '<img src="/img/smiles/razz.gif" alt="razz">',
		':p' => '<img src="/img/smiles/razz.gif" alt="razz">',
		':red:' => '<img src="/img/smiles/red.gif" alt="red">',
		':.' => '<img src="/img/smiles/red.gif" alt="red">',
		':sad:' => '<img src="/img/smiles/sad.gif" alt="sad">',
		':(' => '<img src="/img/smiles/sad.gif" alt="sad">',
		':shrug:' => '<img src="/img/smiles/shrug.gif" alt="shrug">',
		':smile:' => '<img src="/img/smiles/smile.gif" alt="smile">',
		':)' => '<img src="/img/smiles/smile.gif" alt="smile">',
		':sos:' => '<img src="/img/smiles/sos.gif" alt="sos">',
		':sps:' => '<img src="/img/smiles/sps.gif" alt="sps">',
		':stop:' => '<img src="/img/smiles/stop.gif" alt="stop">',
		':uzi:' => '<img src="/img/smiles/uzi.gif" alt="uzi">',
		':wink:' => '<img src="/img/smiles/wink.gif" alt="wink">',
		';)' => '<img src="/img/smiles/wink.gif" alt="wink">',
		':woohoo:' => '<img src="/img/smiles/woohoo.gif" alt="woohoo">',
		':xd:' => '<img src="/img/smiles/xd.gif" alt="xd">',
		':D' => '<img src="/img/smiles/xd.gif" alt="xd">'
	);
	$output = $text;
	foreach($smiles as $smile => $image) {
		/*$smile = preg_quote($smile);
		$output = preg_replace("~\b$smile\b~", $image, $output);*/
		$output = str_replace($smile, $image, $output);
	}
	return $output;
}

php Get numerical noun form

function get_numerical_noun_form($number) {
	// Nominative - комментарий
	// Singular - комментария
	// Plural - комментариев
	if ($number > 10 && (($number % 100) / 10) == 1) {
		return "Plural";
	}
	$number = $number % 10;
	if ($number == 1) {
		return "Nominative";
	} else if ($number == 2 || $number == 3 || $number == 4) {
		return "Singular";
	} else {
		return "Plural";
	}
}

php Is number in range

function isNumInRange($number, $min, $max) {
	if ($number >= $min && $number < $max) {
		return true;
	}
	return false;
}

php Generate a password

function generatePassword($length) {
	$string = "";
	$chars = "abcdefghijklmanopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
	$size = strlen($chars);
	for ($i = 0; $i < $length; $i++) {
		$string .= $chars[rand(0, $size - 1)];
	}
	return $string; 
}

js Chat load more on scroll

$(document).ready(function() {
	// chat auto scroll to bottom on page load
	if ($(".chat__messages").length) {
		$(".chat__messages").scrollTop($(".chat__messages")[0].scrollHeight);
	}
	// chat load more
	let is_chat_load_more_enabled = true;
	$(".chat__messages").on("scroll", function() {
		const $this = $(this);
		let messages_count = $(this).find(".chat__message").length;
		if ($(this).scrollTop() < 300 && is_chat_load_more_enabled) {
			is_chat_load_more_enabled = false;
			$.ajax({
				method: "POST",
				url: "../core/db_chat_load_more.php",
				data: { from: messages_count }
			}).done(function(response) {
				const jsonData = JSON.parse(response);
				if (jsonData.success == "1") {
					is_chat_load_more_enabled = true;
					$this.prepend(jsonData.body);
				}
			});
		}
	});
});

js Inputs with live color changer

$(document).ready(function() {
	// COLORS LIVE CHANGE
	$("input[name='site_color_accent']").on("input", function() {
		$(":root").css("--accent-color", $(this).val());
	});
	$("input[name='site_color_accent_2']").on("input", function() {
		$(":root").css("--accent-color-2", $(this).val());
	});
	$("input[name='site_color_body']").on("input", function() {
		$(":root").css("--body-color", $(this).val());
	});
	$("input[name='site_color_text']").on("input", function() {
		$(":root").css("--text-color", $(this).val());
	});
});

js Copy to clipboard data-attribute handler

$(document).ready(function() {
	// COPY TO CLIPBOARD
	// usage: <span data-copy="text to copy">text to show</span>
	$("[data-copy]").on("click", function() {
		const $temp = $("<input>");
		$("body").append($temp);
		$temp.val($(this).data("copy")).select();
		document.execCommand("copy");
		$temp.remove();
		if ($(this).data("copy-toast")) {
			toastr["info"]($(this).data("copy-toast"), null, {"positionClass": "toast-bottom-right"});
		} else {
			toastr["info"]("Скопировано", null, {"positionClass": "toast-bottom-right"});
		}
	});
});