Реализация модуля выбора города

По проекту была поставлена задача реализовать модуль выбора города в шапке сайта. При выборе города из списка, адрес сайта будет добавлять нужный домен к адресной строке.

Примерно это должно выглядеть так. Адрес в строке «https://example.ru» должен превратиться в «https://dallas.example.ru» при выборе города Dallas. Или немного сложнее: «https://сайт.рф» будет переписан в строку «https://москва.сайт.рф» при выборе — Москва. Ну и поместить данный модуль нужно было в шапку сайта.

Сначала разметка HTML

HTML
<!-- CITY CHANGER -->
 <div class="city-changer">
			<a href="#" class="city-changer__link">
				<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
				<span data-role="current-city">Ваш город</span>
			</a>
			<div class="cities city-changer__menu">
				<ul class="cities__list"></ul>
			</div>
</div>
<!-- CITY CHANGER -->

По сути. Это простой блок, в котором лежит ссылка с будущим названием города (по умолчанию «Ваш город»). А также блок с раскрывающимся меню-списком городов. Будем генерировать налету — поэтому список пока пустой.

Стилизация модуля в CSS

CSS
/* CITY CHANGER */
.city-changer {
	--link-color: #fff;
	--menu-link-color: #000;
	--bg-color: #fff;
	--menu-link-hover: #0038a5;
	width: fit-content;
	position: relative;
	margin-left: 32px;
}

.city-changer__link {
	display: flex;
	align-items: center;
	gap: 3px;
	color: var(--link-color);
}

.city-changer__link.active {
	opacity: 1;
}

.city-changer a {
	font-size: 14px;
	text-decoration: none;
	opacity: 0.6;
}

.city-changer a:hover {
	opacity: 1;
}

.city-changer__menu {
	display: none;
	position: absolute;
	top: calc(100% + 2ch);
	left: -2ch;
}

.city-changer__menu.active {
	display: block;
}

.city-changer__menu a {
	color: var(--menu-link-color);
	padding-block: 2px;
}
.city-changer__menu a:hover {
	color: var(--menu-link-hover);
}

.cities {
	border: 1px solid #00000022;
	border-radius: 8px;
	padding: 16px 32px;
	width: fit-content;
	background-color: var(--bg-color);
	box-shadow: 0 0 16px #00000033;
}

.cities__list {
	padding: 0;
	margin: 0;
	white-space: nowrap;
	line-height: 1.3;
	columns: 3;
	column-gap: 20px;
}
.cities__list-item{
	margin-left: 20px;
	margin-bottom: 6px;
}
.cities__list-item::marker {
	color: #00000066;
}
.cities__list-item.accent::marker {
	color: #000000aa;
}
.cities__list-item:hover::marker{
	color: var(--menu-link-hover);
}
.cities__list-item:hover{
	color: var(--menu-link-hover);
}
.cities__list-item.accent {
	font-weight: 700;
}
.cities__list-item.accent:has(+ :not(.accent)) {
	margin-bottom: calc(1lh + 12px);
}
/* CITY CHANGER */

Тут стандартная реализация блока. Выставляем кастомные свойства цветов — так проще оперировать далее при стилизации. Абсолютом стилизуем всплывашку. Реализуем разбивку на колонки самого списка городов — их будет много (будут добавляться постепенно).

JavaScript код для функционирования самого модуля

Сначала массив с данными городов (объект)

JavaScript
let cities = {
"Москва": ["москва", "xn--80adxhks", true],
"Нижний Новгород": ["нн", "xn--m1aa", true],
"Краснодар": ["краснодар", "xn--80aalwqglfe"]
}

Флаг true в коде будет подсказывать, что этот город в разметке нужно будет выделить жирным, так как он наиболее важен.

Ключ объекта нам нужен для подстановки его в разметку в виде названия города в ссылке. В качестве значения у нас будет массив, в котором первое значение — строка, которая выступит в рли поддомена, а второе — к сожалению, необходимая строка для идентификации города через адресную строку (в браузерах, кириллица копируется с помощью кодировки).

Создадим переменные

JavaScript
let changerLink = document.querySelector(".city-changer__link");
changerLink.addEventListener("click", openCitiesLst);
let changerMenu = document.querySelector('.city-changer__menu');
let list = changerMenu.querySelector(".cities__list");

changerLink — сама ссылка с текущим городом. Поставим на нее прослушиватель click и коллбэк.

changerMenu — всплывающая плашка для выбора города.

list — список городов, который будем отрисовывать с помощью JavaScript.

Функция для замены или добавления поддомена

JavaScript
function replaceSubdomain(city) {
	// Получаем текущий URL
	const currentUrl = window.location.href;
	// Разбираем URL на компоненты
	const url = new URL(currentUrl);
	// Получаем хост (например, "москва.сайт.рф" или "сайт.рф")
	const hostParts = url.hostname.split(".");
	// Определяем, есть ли уже поддомен (если хостов > 2)
	let newHostname;
	if (hostParts.length > 2) {
		// Уже есть поддомен, заменяем его: "москва.сайт.рф" -> "краснодар.сайт.рф"
		hostParts[0] = city;
		newHostname = hostParts.join(".");
	} else {
		// Нет поддомена, добавляем: "социалка52.рф" -> "москва.сайт.рф"
		newHostname = `${city}.${url.hostname}`;
	}
	// Устанавливаем новый хост
	url.hostname = newHostname;
	// Возвращаем готовый URL
	return url.toString();
}

В целом, функция не очень сложная. Но для экономии времени, была написана с помощью ИИ.

Добавляем города в список

Нам нужен список такого вида:

JavaScript
for (let [key, value] of Object.entries(cities)) {
	let newHref = replaceSubdomain(value[0]);
	if (typeof value == "object" && value[2]) {
		list.insertAdjacentHTML(
			"beforeend",
			`<li class="cities__list-item accent"><a href="${newHref}">${key}</a></li>`
		);
	} else {
		list.insertAdjacentHTML(
			"beforeend",
			`<li class="cities__list-item"><a href="${newHref}">${key}</a></li>`
		);
	}
}

Производим перебор объекта по каждому элементу. Разбиваем объект на ключ—значение. Применяем функцию, описанную выше для генерации значения href в будущей ссылке. И, с помощью insertAdjacentHTML добавляем элементы в список.

Открытие окна выбора города

JavaScript
function openCitiesList(e) {
	e.preventDefault();
	this.classList.contains('active') ? this.classList.remove('active') : this.classList.add('active');
	changerMenu.classList.contains('active') ? changerMenu.classList.remove('active') : changerMenu.classList.add('active');
}

Функция установки текущего города

JavaScript
function setCurrentCity() {
  let currentCity = changerLink.querySelector('[data-role="current-city"]');
  // Получаем текущий URL
	let currentUrl = window.location.href;
	// Разбираем URL на компоненты
	const url = new URL(currentUrl);
	// Получаем хост (например, "москва.социалка52.рф" или "социалка52.рф")
	const hostParts = url.hostname.split(".");
	// Определяем, есть ли уже поддомен (если хостов > 2)
	let newHostname;
	if (hostParts.length > 2) {
        // Уже есть поддомен, заменяем его: "москва.социалка52.рф" -> "тверь.социалка52.рф"
        for (let [key, value] of Object.entries(cities)) {
            if(hostParts[0] == value[1]){
                currentCity.innerText = key;
            }
        }
    }
}

setCurrentCity();

Описана функция, для замены названия города в ссылке (основной) открытия списка. И ее запуск.

Для удобства

JavaScript
document.addEventListener('click', function(e){
	if(e.target.closest('.city-changer')) return;
	changerMenu.classList.remove('active');
	changerLink.classList.remove('active');
});

Тут простое делегирование события по экрану, чтобы окно с выбором города закрывалось.

Как работает данный модуль можно посмотреть на этом сайте:

посмотреть готовое решение

Полный JavaScript код

JavaScript
let cities = {
"Москва": ["москва", "xn--80adxhks", true],
"Нижний Новгород": ["нн", "xn--m1aa", true],
"Краснодар": ["краснодар", "xn--80aalwqglfe", true],
"Казань": ["казань", "xn--80aauks4g", true],
"Екатеринбург": ["екатеринбург", "xn--80acgfbsl1azdqr", true],
"Самара": ["самара", "xn--80aaa0cvac", true],
"Санкт-Петербург": ["спб", "xn--90a1af", true],
"Челябинск": ["челябинск", "xn--90ahkico3a5b9d", true],
"Воронеж": ["воронеж", "xn--b1agd0aean", true],
"Ростов-на-Дону": ["ростов", "xn--b1axaggg", true],
"Новосибирск": ["новосибирск", "xn--90absbknhbvge", true],
"Уфа": ["уфа", "xn--80a1bd", true],
"Омск": ["омск", "xn--j1adfn", true],
"Пермь": ["пермь", "xn--e1aohf5d", true]
// и так далее...
};

let changerLink = document.querySelector(".city-changer__link");
changerLink.addEventListener("click", openCitiesList);
let changerMenu = document.querySelector('.city-changer__menu');
let list = changerMenu.querySelector(".cities__list");

function replaceSubdomain(city) {


	// Получаем текущий URL
	const currentUrl = window.location.href;

	// Разбираем URL на компоненты
	const url = new URL(currentUrl);

	// Получаем хост (например, "москва.социалка52.рф" или "социалка52.рф")
	const hostParts = url.hostname.split(".");

	// Определяем, есть ли уже поддомен (если хостов > 2)
	let newHostname;

	if (hostParts.length > 2) {
		// Уже есть поддомен, заменяем его: "москва.социалка52.рф" -> "тверь.социалка52.рф"
		hostParts[0] = city;

		newHostname = hostParts.join(".");
	} else {
		// Нет поддомена, добавляем: "социалка52.рф" -> "москва.социалка52.рф"
		newHostname = `${city}.${url.hostname}`;
	}

	// Устанавливаем новый хост
	url.hostname = newHostname;

	// Возвращаем готовый URL
	return url.toString();
}

for (let [key, value] of Object.entries(cities)) {
	let newHref = replaceSubdomain(value[0]);
	if (typeof value == "object" && value[2]) {
		list.insertAdjacentHTML(
			"beforeend",
			`<li class="cities__list-item accent"><a href="${newHref}">${key}</a></li>`
		);
	} else {
		list.insertAdjacentHTML(
			"beforeend",
			`<li class="cities__list-item"><a href="${newHref}">${key}</a></li>`
		);
	}
}

function openCitiesList(e) {
	e.preventDefault();
	this.classList.contains('active') ? this.classList.remove('active') : this.classList.add('active');
	changerMenu.classList.contains('active') ? changerMenu.classList.remove('active') : changerMenu.classList.add('active');
}

function setCurrentCity() {
    let currentCity = changerLink.querySelector('[data-role="current-city"]');
    // Получаем текущий URL
	let currentUrl = window.location.href;

	// Разбираем URL на компоненты
	const url = new URL(currentUrl);

	// Получаем хост (например, "москва.социалка52.рф" или "социалка52.рф")
	const hostParts = url.hostname.split(".");

	// Определяем, есть ли уже поддомен (если хостов > 2)
	let newHostname;

	if (hostParts.length > 2) {
        // Уже есть поддомен, заменяем его: "москва.социалка52.рф" -> "тверь.социалка52.рф"
        for (let [key, value] of Object.entries(cities)) {
            if(hostParts[0] == value[1]){
                currentCity.innerText = key;
            }

        }
    }
}

setCurrentCity();
document.addEventListener('click', function(e){
	if(e.target.closest('.city-changer')) return;
	changerMenu.classList.remove('active');
	changerLink.classList.remove('active');
});