diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml
new file mode 100644
index 0000000..6f3eff1
--- /dev/null
+++ b/.github/workflows/deploy-pages.yml
@@ -0,0 +1,56 @@
+# Сборка и публикация на GitHub Pages при пуше в ветку deploy
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches:
+ - deploy
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+ cache: "npm"
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Ensure index.html at project root
+ run: |
+ if [ ! -f index.html ] && [ -f static/index.html ]; then
+ cp static/index.html index.html
+ fi
+
+ - name: Build
+ run: npm run build
+ env:
+ BASE_PATH: /GilgaChat/
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: dist
+
+ deploy:
+ environment: github_pages
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3adb713
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env
+file.env
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..8fdd954
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+22
\ No newline at end of file
diff --git a/README.md b/README.md
index bcd1a13..459ada4 100644
--- a/README.md
+++ b/README.md
@@ -1,83 +1,186 @@
-### Ветка, в которой делаете задания спринта, должна называться sprint_i, где i - номер спринта. Не переименовывайте её.
+
GilgaChat
-### Откройте pull request в ветку main из ветки, где вы разрабатывали проект, и добавьте ссылку на этот pr в README.md в ветке main.
-### ВАЖНО: pull request должен называться “Sprint i” (i — номер спринта).
+Современный интерфейс мессенджера на TypeScript, Handlebars и Vite.
+Репозиторий содержит фронтенд GilgaChat: экран авторизации, переиспользуемые компоненты и страницы, которые можно развивать в полноценный мессенджер.
-### Например, задания для проектной работы во втором спринте вы делаете в ветке sprint_2. Открываете из неё pull request в ветку main. Ссылку на этот pr добавляете в README.md в ветке main. После этого на платформе Практикума нажимаете «Проверить задание».
+**Демо:** [https://maximste.github.io/GilgaChat/](https://maximste.github.io/GilgaChat/)
-### Также не забудьте проверить, что репозиторий публичный.
---
+## Обзор
-Даже законченный проект остаётся только заготовкой, пока им не начнут пользоваться. Но сначала пользователь должен понять, зачем ему пользоваться вашим кодом. В этом помогает файл README.
+GilgaChat — учебный фронтенд-проект, в котором показано, как собрать небольшую компонентную систему без тяжёлого фреймворка.
+Вместо React/Vue используются:
-README — первое, что прочитает пользователь, когда попадёт в репозиторий на «Гитхабе». Хороший REAMDE отвечает на четыре вопроса:
+- **TypeScript** — типизация.
+- **Handlebars** — шаблоны и партиалы.
+- **Vite** — сборка и dev-сервер.
+- **SCSS** — стили.
-- Готов ли проект к использованию?
-- В чём его польза?
-- Как установить?
-- Как применять?
+В приложении реализована **навигация по hash** и есть **главная страница** (демо с ссылками), **layout мессенджера** (сайдбар и заглушка), формы **входа** и **регистрации**, страница **профиля**, страницы ошибок **404** и **500** — всё на базе переиспользуемых UI-компонентов.
-## Бейджи
+## Возможности
-Быстро понять статус проекта помогают бейджи на «Гитхабе». Иногда разработчики ограничиваются парой бейджев, которые сообщат о статусе тестов кода:
+- **Главная страница**
+ - Временное сообщение о демо и ссылки на все сверстанные страницы: Мессенджер, Вход, Регистрация, Профиль, 404, 500.
-
+- **Layout мессенджера** (`#messenger`)
+ - Слева: сайдбар с названием приложения и выпадающим меню, поиском, списком **личных сообщений** (аватар, имя, превью последнего сообщения, индикатор статуса), списком **групп** (иконка, название, превью) и блоком текущего пользователя (имя, статус, ссылка на настройки).
+ - Справа: заглушка **NoChatStub** («Чат не выбран» — место под будущий экран переписки).
-Если пользователь увидит ошибку в работе тестов, то поймёт: использовать текущую версию в важном проекте — не лучшая идея.
+- **Авторизация и профиль**
+ - **Вход** (`#auth`) — форма входа с логотипом, полями email/пароль и ссылкой «Забыли пароль?».
+ - **Регистрация** (`#register`) — форма регистрации (login, display_name, email, имя, фамилия, телефон, пароль).
+ - **Профиль** (`#profile`) — карточка профиля, блок Profile Information (login, display name, email, имя, фамилия, телефон). Кнопка «Edit Profile» открывает форму редактирования (данные пользователя и опционально смена пароля: current password, new password).
-Бейджи помогают похвастаться достижениями: насколько популярен проект, как много разработчиков создавало этот код. Через бейджи можно даже пригласить пользователя в чат:
+- **Страницы ошибок**
+ - **404** (`#404`) — «Oops! You're Lost in Cyberspace» с изображением, подсказками и кнопками «Take Me Home» / «Go Back».
+ - **500** (`#500`) — «Houston, we have a problem!» с карточками статуса и кнопками «Try Again» / «Go Home».
-
+- **Переиспользуемые UI-компоненты**
+ - `Button`, `Input`, `Label`, `Link`, `FormField` — базовые компоненты для форм и layout’ов.
+ - У каждого: класс на TypeScript, шаблон Handlebars (`.hbs`), стили SCSS.
-В README **Webpack** строка бейджев подробно рассказывает о покрытии кода тестами. Когда проект протестирован, это вызывает доверие пользователя. Последний бейдж приглашает присоединиться к разработке.
+- **Layout’ы**
+ - **MainLayout** — шапка (GilgaChat, Вход / Регистрация) и область контента для авторизации, профиля, ошибок и главной.
+ - **MessengerLayout** — сайдбар и основная область (например, NoChatStub или будущий экран чата).
-Другая строка убедит пользователя в стабильности инфраструктуры и популярности проекта. Последний бейдж зовёт в чат проекта.
+- **Разработка и деплой**
+ - Dev-сервер Vite с hot reload, сборка TypeScript + Vite.
+ - Netlify: публикация из `dist/`.
+ - GitHub Actions для CI (тесты по спринтам).
-## Описание
+## Стек
-Краткое опишите, какую задачу решает проект. Пользователь не верит обещаниям и не готов читать «полотна» текста. Поэтому в описании достаточно нескольких строк:
+- **Язык:** TypeScript
+- **Сборка и dev-сервер:** Vite
+- **Шаблоны:** Handlebars
+- **Стили:** SCSS
+- **Деплой:** [GitHub Pages](https://maximste.github.io/GilgaChat/) (актуальная версия). Netlify (`https://gilgachat.netlify.app/`) — неактуальная версия; проект на Netlify не деплоится из-за проблем с работой сервиса.
-
+## Дизайн и прототипы
-Авторы **React** дробят описание на абзацы и списки — так проще пробежаться глазами по тексту и найти ключевую информацию.
+- Макет в Figma: [GilgaChat UI](https://www.figma.com/design/sbXZfnJcFjbmh9L6nxRv7W/GilgaChat?node-id=13-9452&t=vIzquu2G5jxpVV9o-1)
-Если у проекта есть сайт, добавьте ссылку в заголовок.
+## Начало работы
-## Установка
+### Требования
-Лучше всего пользователя убеждает собственный опыт. Чем быстрее он начнёт пользоваться проектом, тем раньше почувствует пользу. Для этого помогите ему установить приложение: напишите краткую пошаговую инструкцию.
+- **Node.js**: 22.x (рекомендуется, совпадает с настройками CI)
+- **npm**: поставляется с Node.js
-Если проект предназначен для разработчиков, добавьте информацию об установке тестовых версий. Например:
+### Установка
-- `npm install` — установка стабильной версии,
-- `npm start` — запуск версии для разработчика,
-- `npm run build:prod` — сборка стабильной версии.
+```bash
+git clone https://github.com/maximste/GilgaChat.git
+cd GilgaChat
+npm install
+```
-## **Примеры использования**
+### Скрипты
-Хорошо, если сразу после установки пользователь сможет решить свои задачи без изучения проекта. Это особенно верно, если ваш пользователь — не профессиональный разработчик. Но даже профессионал поймёт вас лучше, если показать примеры использования:
+Запуск dev-сервера:
-
+```bash
+npm run dev
+```
-Для более подробных инструкции добавьте новые разделы или ссылки:
+Сборка для продакшена:
-- на документацию,
-- вики проекта,
-- описание API.
+```bash
+npm run build
+```
-В учебном проекте будут полезен раздел с описанием стиля кода и правилами разработки: как работать с ветками, пул-реквестами и релизами.
+Локальный просмотр продакшен-сборки:
-### **Команда**
+```bash
+npm run preview
+```
-Если вы работаете в команде, укажите основных участников: им будет приятно, а новые разработчики охотнее присоединятся к проекту. «Гитхаб» — не просто инструмент, это социальная сеть разработчиков.
+Сборка и просмотр одной командой:
-
+```bash
+npm run start
+```
-### **Примеры README**
+## Структура проекта
-- «[Реакт](https://github.com/facebook/react)»,
-- «[Эхо](https://github.com/labstack/echo)»,
-- «[Вебпак](https://github.com/webpack/webpack)»,
-- «[ТДенгине](https://github.com/taosdata/TDengine)»,
-- «[Соул-хантинг](https://github.com/vladpereskokov/soul-hunting/)».
+```text
+.
+├── index.html # Корневой HTML (точка входа Vite)
+├── public
+│ └── images/ # Статика (логотип, изображение для 404)
+├── src
+│ ├── main.ts # Точка входа: роутинг по hash, MainLayout / MessengerLayout, экраны
+│ ├── style.scss # Глобальные стили
+│ ├── components
+│ │ ├── AuthForm/ # Форма входа
+│ │ ├── RegisterForm/ # Форма регистрации (login, display_name, email, имя, фамилия, телефон, пароль)
+│ │ ├── ProfilePage/ # Карточка профиля и блок Profile Information (Edit Profile, Logout)
+│ │ ├── EditProfileForm/ # Форма редактирования профиля (данные + old_password, new_password)
+│ │ ├── NotFoundPage/ # Страница 404
+│ │ ├── ServerErrorPage/ # Страница 500
+│ │ ├── NoChatStub/ # Заглушка «Чат не выбран»
+│ │ ├── Button/ # Кнопка (TS + HBS + SCSS)
+│ │ ├── Input/ # Поле ввода
+│ │ ├── Label/ # Подпись
+│ │ ├── Link/ # Ссылка
+│ │ └── FormField/ # Подпись + поле ввода
+│ ├── layout
+│ │ ├── main/ # MainLayout (шапка + контент: авторизация, профиль, ошибки, главная)
+│ │ └── messenger/ # MessengerLayout (сайдбар + основная область)
+│ ├── styles/ # Общие SCSS (цвета, типографика, отступы, переменные)
+│ ├── types/ # Общие типы TypeScript
+│ └── utils/mydash/ # Вспомогательные функции
+├── vite.config.ts # Конфиг Vite (base, build, preview port: 3000)
+├── netlify.toml # Netlify: команда сборки, каталог публикации dist
+└── .github/workflows/ # CI (тесты по спринтам)
+```
+
+## Как устроено
+
+- В `src/main.ts` подписаны события `DOMContentLoaded` и `hashchange`; в зависимости от `window.location.hash` выбирается нужный layout и экран.
+- **Роутинг:**
+ - `#messenger` → полностью **MessengerLayout** (сайдбар и **NoChatStub** в основной области).
+ - `#auth`, `#register`, `#profile`, `#404`, `#500` → **MainLayout** с **AuthForm**, **RegisterForm**, **ProfilePage**, **NotFoundPage** или **ServerErrorPage** в области контента.
+ - Без hash или неизвестный hash → MainLayout с **главной** (демо-сообщение и ссылки).
+- При переходе с `#messenger` на другой маршрут корень перерисовывается с MainLayout, чтобы отображались нужная шапка и контент.
+- Компоненты (AuthForm, RegisterForm, ProfilePage, EditProfileForm и др.) регистрируют партиалы Handlebars, компилируют свой шаблон `.hbs` с пропсами и вставляют HTML в элемент контента layout’а. Логика интерфейса — в классах TypeScript, разметка — в шаблонах Handlebars. Форма редактирования профиля рендерится по клику на «Edit Profile» в область контента профиля.
+
+## Разработка и соглашения
+
+- Проект изначально был учебным по спринтам, поэтому могут встречаться ветки вроде `sprint_1`, `sprint_2` и т.п.
+- Workflow GitHub Actions запускает автоматические тесты для этих веток при pull request в `main`.
+- Для своей разработки или open-source можно оставить этот workflow или настроить свою стратегию веток.
+
+## Деплой
+
+Актуальная версия проекта раздаётся с **GitHub Pages** (см. раздел ниже). Конфигурация для Netlify в репозитории сохранена, но из-за проблем с работой сервиса Netlify проект там не деплоится; ссылка [gilgachat.netlify.app](https://gilgachat.netlify.app/) может вести на неактуальную версию.
+
+- Соберите проект локально:
+
+ ```bash
+ npm run build
+ ```
+
+- Результат сборки попадает в `dist/`. Содержимое `public/` (например, `images/`) копируется в `dist/`.
+- Раздавать можно вручную: содержимое `dist/` подойдёт для любого статического хостинга (GitHub Pages, Vercel, nginx и т.д.).
+
+### GitHub Pages (автодеплой из ветки deploy)
+
+1. В репозитории: **Settings → Pages**.
+2. В блоке **Build and deployment** выберите **Source: GitHub Actions**.
+3. Ветка **deploy**: при каждом пуше в неё workflow `.github/workflows/deploy-pages.yml` запускает сборку на GitHub (Node.js 22, `npm ci`, `npm run build`) и публикует каталог `dist/` на GitHub Pages.
+
+Сайт будет доступен по адресу вида `https://.github.io//`. Для SPA важно задать **base** в `vite.config.ts` равным `'//'` (например, `'/GilgaChat/'`), иначе маршруты по hash и статика будут отдаваться с неправильного пути.
+
+## Планы
+
+- **Сделано:** экран регистрации, страница профиля, 404/500, layout мессенджера (сайдбар и заглушка), роутинг по hash, редиректы и заголовки для Netlify.
+- **Дальше:** полноценный экран чата в основной области мессенджера (сообщения, поле ввода), валидация форм и обработка ошибок, восстановление пароля.
+- **Позже:** бэкенд-API для авторизации и чата.
+
+## Лицензия
+
+Проект создан в учебных целях и пока не содержит явной open-source лицензии.
+Если планируете использовать его в продакшене или как основу для open-source проекта, добавьте файл лицензии (например, MIT) по своему выбору.
diff --git a/dist/index.html b/dist/index.html
deleted file mode 100644
index 3892a08..0000000
--- a/dist/index.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- Hello
-
-
-
- My static 2.0.
-
-
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..396d3c8
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ GilgaChat
+
+
+ Loading…
+
+
+
diff --git a/netlify.toml b/netlify.toml
index 220ff2e..5fa1ec3 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -5,5 +5,7 @@
# “command” is your build command.
# “publish” is the directory to publish (relative to the root of your repo).
+# Use "deploy" branch in Netlify. Leave "Base directory" EMPTY in Netlify UI.
[build]
- publish = "dist"
\ No newline at end of file
+ command = "npm run build"
+ publish = "dist"
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..c3d7acb
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1697 @@
+{
+ "name": "gilgachat",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "gilgachat",
+ "version": "0.0.0",
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^7.2.0",
+ "dotenv": "^17.3.1",
+ "handlebars": "^4.7.8"
+ },
+ "devDependencies": {
+ "@types/node": "^25.3.2",
+ "sass": "^1.97.3",
+ "typescript": "~5.9.3",
+ "vite": "^7.3.1"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-free": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz",
+ "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==",
+ "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
+ "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3",
+ "is-glob": "^4.0.3",
+ "node-addon-api": "^7.0.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.6",
+ "@parcel/watcher-darwin-arm64": "2.5.6",
+ "@parcel/watcher-darwin-x64": "2.5.6",
+ "@parcel/watcher-freebsd-x64": "2.5.6",
+ "@parcel/watcher-linux-arm-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm-musl": "2.5.6",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm64-musl": "2.5.6",
+ "@parcel/watcher-linux-x64-glibc": "2.5.6",
+ "@parcel/watcher-linux-x64-musl": "2.5.6",
+ "@parcel/watcher-win32-arm64": "2.5.6",
+ "@parcel/watcher-win32-ia32": "2.5.6",
+ "@parcel/watcher-win32-x64": "2.5.6"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+ "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+ "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+ "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+ "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+ "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+ "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+ "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+ "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+ "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+ "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+ "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+ "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+ "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.3.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz",
+ "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
+ "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
+ "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT"
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/sass": {
+ "version": "1.97.3",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
+ "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ef360ab
--- /dev/null
+++ b/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "gilgachat",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "engines": {
+ "node": ">=22"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "start": "npm run build && npm run preview"
+ },
+ "devDependencies": {
+ "@types/node": "^25.3.2",
+ "sass": "^1.97.3",
+ "typescript": "~5.9.3",
+ "vite": "^7.3.1"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^7.2.0",
+ "dotenv": "^17.3.1",
+ "handlebars": "^4.7.8"
+ }
+}
diff --git a/public/images/404-dune.png b/public/images/404-dune.png
new file mode 100644
index 0000000..bcec976
Binary files /dev/null and b/public/images/404-dune.png differ
diff --git a/public/images/logo.png b/public/images/logo.png
new file mode 100644
index 0000000..3f35c01
Binary files /dev/null and b/public/images/logo.png differ
diff --git a/src/components/AuthForm/AuthForm.hbs b/src/components/AuthForm/AuthForm.hbs
new file mode 100644
index 0000000..f90ff69
--- /dev/null
+++ b/src/components/AuthForm/AuthForm.hbs
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/src/components/AuthForm/AuthForm.scss b/src/components/AuthForm/AuthForm.scss
new file mode 100644
index 0000000..ced017f
--- /dev/null
+++ b/src/components/AuthForm/AuthForm.scss
@@ -0,0 +1,86 @@
+@use '../../styles/variables' as *;
+
+.auth-window {
+ width: 386px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $gap-size-32;
+ border: 1px solid $border-color-dark;
+ background-color: $background-color-on-surface-black;
+ border-radius: $border-radius-8;
+ padding: $padding-32;
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+
+ &__header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $gap-size-8;
+ }
+
+ &__icon-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 64px;
+ height: 64px;
+ border-radius: 14px;
+ overflow: hidden;
+ box-shadow:
+ 0 0 0 1px rgba(255, 255, 255, 0.06),
+ 0 2px 8px rgba(0, 0, 0, 0.4);
+ }
+
+ &__icon {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ object-position: center center;
+ display: block;
+ }
+
+ &__title {
+ color: $text-color-primary;
+ font-size: $font-size-24;
+ line-height: $line-height-32;
+ margin: 0;
+ font-weight: 700;
+ }
+
+ &__sub-title {
+ color: $text-color-secondary;
+ font-size: $font-size-14;
+ }
+}
+
+.login-form {
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: $gap-size-12;
+ align-self: start;
+ width: 100%;
+
+ &__footer {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-12;
+ width: 100%;
+ }
+
+ &__link {
+ text-decoration: none;
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ }
+
+ &__submit-btn {
+ background-color: $color-accent;
+ color: $text-color-primary;
+
+ &:hover {
+ background-color: $color-accent-dark;
+ }
+ }
+}
diff --git a/src/components/AuthForm/AuthForm.ts b/src/components/AuthForm/AuthForm.ts
new file mode 100644
index 0000000..45ebc88
--- /dev/null
+++ b/src/components/AuthForm/AuthForm.ts
@@ -0,0 +1,113 @@
+import Handlebars from 'handlebars';
+
+import template from './AuthForm.hbs?raw';
+import {
+ Button,
+ ButtonTemplate,
+ FormField,
+ FormFieldTemplate,
+ Input,
+ InputTemplate,
+ Label,
+ LabelTemplate,
+ Link,
+ LinkTemplate
+} from '..';
+
+import './AuthForm.scss'
+
+interface AuthFormProps {
+ title: string;
+ subtitle?: string;
+};
+
+export class AuthForm {
+ private container: HTMLElement;
+ private props: AuthFormProps;
+ private signInButton: Button;
+ private emailLabel: Label;
+ private emailInput: Input;
+ private passwordLabel: Label;
+ private passwordInput: Input;
+ private emailFormField: FormField;
+ private passwordFormField: FormField;
+ private restorePasswordLink: Link;
+
+ constructor(container: HTMLElement, props: AuthFormProps) {
+ this.container = container;
+ this.props = props;
+
+ this.emailLabel = new Label({
+ text: 'Email',
+ for: 'userEmail',
+ className: 'login-form__label',
+ });
+
+ this.passwordLabel = new Label ({
+ text: 'Password',
+ for: 'userPassword',
+ className: 'login-form__label',
+ });
+
+ this.emailInput = new Input({
+ id: 'userEmail',
+ type: 'email',
+ name: 'email',
+ required: true,
+ className: "login-form__input",
+ });
+
+ this.passwordInput = new Input({
+ id: 'userPassword',
+ type: 'password',
+ name: 'password',
+ required: true,
+ className: "login-form__input",
+ });
+
+ this.signInButton = new Button({
+ type: 'submit',
+ text: 'Sign In',
+ className: 'login-form__submit-btn',
+ });
+
+ this.emailFormField = new FormField({
+ label: this.emailLabel.getData(),
+ input: this.emailInput.getData(),
+ className: 'login-form__field',
+ icon: 'fa-solid fa-envelope',
+ });
+
+ this.passwordFormField = new FormField({
+ label: this.passwordLabel.getData(),
+ input: this.passwordInput.getData(),
+ className: 'login-form__field',
+ icon: 'fa-solid fa-lock',
+ });
+
+ this.restorePasswordLink = new Link({
+ text: 'Forgot password?',
+ href: '/recovery',
+ className: 'login-form__link',
+ });
+ }
+
+ public render(): void {
+ Handlebars.registerPartial("Button", ButtonTemplate);
+ Handlebars.registerPartial("Input", InputTemplate);
+ Handlebars.registerPartial("Label", LabelTemplate);
+ Handlebars.registerPartial("FormField", FormFieldTemplate);
+ Handlebars.registerPartial("Link", LinkTemplate);
+
+ const compiledTemplate = Handlebars.compile(template)({
+ title: this.props.title,
+ subtitle: this.props.subtitle,
+ signInButton: this.signInButton.getData(),
+ emailFormField: this.emailFormField.getData(),
+ passwordFormField: this.passwordFormField.getData(),
+ restorePasswordLink: this.restorePasswordLink.getData(),
+ });
+
+ this.container.innerHTML = compiledTemplate;
+ }
+};
diff --git a/src/components/Button/Button.hbs b/src/components/Button/Button.hbs
new file mode 100644
index 0000000..953cca5
--- /dev/null
+++ b/src/components/Button/Button.hbs
@@ -0,0 +1,6 @@
+
+ {{text}}
+
diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss
new file mode 100644
index 0000000..b33c341
--- /dev/null
+++ b/src/components/Button/Button.scss
@@ -0,0 +1,14 @@
+@use '../../styles/variables' as *;
+
+.button {
+ border-radius: $border-radius-6;
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ padding: 8px 16px;
+ border: none;
+ transition: background-color ease-in-out 0.25s;
+
+ &:hover {
+ cursor: pointer;
+ }
+}
diff --git a/src/components/Button/Button.ts b/src/components/Button/Button.ts
new file mode 100644
index 0000000..620645c
--- /dev/null
+++ b/src/components/Button/Button.ts
@@ -0,0 +1,28 @@
+import Handlebars from 'handlebars';
+import template from './Button.hbs?raw';
+import './Button.scss';
+
+interface ButtonProps {
+ type: 'reset' | 'submit' | 'button'
+ text: string;
+ disabled?: boolean;
+ className?: string
+};
+
+class Button {
+ private props: ButtonProps;
+
+ constructor(props: ButtonProps){
+ this.props = props;
+ }
+
+ public getData(): ButtonProps {
+ return this.props;
+ }
+
+ public render(props: ButtonProps): string {
+ return Handlebars.compile(template)(props);
+ }
+};
+
+export { Button };
diff --git a/src/components/EditProfileForm/EditProfileForm.hbs b/src/components/EditProfileForm/EditProfileForm.hbs
new file mode 100644
index 0000000..5d6b860
--- /dev/null
+++ b/src/components/EditProfileForm/EditProfileForm.hbs
@@ -0,0 +1,21 @@
+
diff --git a/src/components/EditProfileForm/EditProfileForm.scss b/src/components/EditProfileForm/EditProfileForm.scss
new file mode 100644
index 0000000..9d878c7
--- /dev/null
+++ b/src/components/EditProfileForm/EditProfileForm.scss
@@ -0,0 +1,62 @@
+@use '../../styles/variables' as *;
+
+.edit-profile {
+ background-color: $background-color-on-surface-black;
+ border: 1px solid $border-color-dark;
+ border-radius: $border-radius-8;
+ padding: $padding-32;
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.edit-profile__header {
+ margin-bottom: $gap-size-24;
+}
+
+.edit-profile__title {
+ margin: 0;
+ font-size: $font-size-24;
+ font-weight: 700;
+ color: $text-color-primary;
+}
+
+.edit-profile__form {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-12;
+ width: 100%;
+}
+
+.edit-profile__field {
+ width: 100%;
+}
+
+.edit-profile__input {
+ width: 100%;
+}
+
+.edit-profile__footer {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $gap-size-12;
+ margin-top: $gap-size-8;
+}
+
+.edit-profile__btn {
+ &--primary {
+ background-color: $color-accent;
+ color: $text-color-primary;
+
+ &:hover {
+ background-color: $color-accent-dark;
+ }
+ }
+
+ &--secondary {
+ background-color: $background-color-input;
+ color: $text-color-primary;
+
+ &:hover {
+ filter: brightness(0.95);
+ }
+ }
+}
diff --git a/src/components/EditProfileForm/EditProfileForm.ts b/src/components/EditProfileForm/EditProfileForm.ts
new file mode 100644
index 0000000..5d29a66
--- /dev/null
+++ b/src/components/EditProfileForm/EditProfileForm.ts
@@ -0,0 +1,227 @@
+import Handlebars from 'handlebars';
+
+import template from './EditProfileForm.hbs?raw';
+import {
+ Button,
+ ButtonTemplate,
+ FormField,
+ FormFieldTemplate,
+ Input,
+ InputTemplate,
+ Label,
+ LabelTemplate,
+} from '..';
+
+import './EditProfileForm.scss';
+
+export interface EditProfileFormProps {
+ login: string;
+ displayName: string;
+ email: string;
+ firstName: string;
+ surname: string;
+ phone: string;
+ oldPassword?: string;
+ newPassword?: string;
+}
+
+export interface EditProfileFormCallbacks {
+ onCancel: () => void;
+ onSave?: (data: EditProfileFormProps) => void;
+}
+
+export class EditProfileForm {
+ private container: HTMLElement;
+ private callbacks: EditProfileFormCallbacks;
+ private loginFormField: FormField;
+ private displayNameFormField: FormField;
+ private emailFormField: FormField;
+ private firstNameFormField: FormField;
+ private secondNameFormField: FormField;
+ private phoneFormField: FormField;
+ private oldPasswordFormField: FormField;
+ private newPasswordFormField: FormField;
+ private cancelButton: Button;
+ private saveButton: Button;
+
+ constructor(
+ container: HTMLElement,
+ props: EditProfileFormProps,
+ callbacks: EditProfileFormCallbacks
+ ) {
+ this.container = container;
+ this.callbacks = callbacks;
+
+ const fieldClass = 'edit-profile__field';
+ const labelClass = 'edit-profile__label';
+ const inputClass = 'edit-profile__input';
+
+ this.loginFormField = new FormField({
+ label: new Label({ text: 'Login', for: 'editProfileLogin', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfileLogin',
+ type: 'text',
+ name: 'login',
+ value: props.login,
+ required: true,
+ placeholder: 'Login',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-user',
+ });
+
+ this.displayNameFormField = new FormField({
+ label: new Label({ text: 'Display name', for: 'editProfileDisplayName', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfileDisplayName',
+ type: 'text',
+ name: 'display_name',
+ value: props.displayName,
+ required: true,
+ placeholder: 'Display name',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-user',
+ });
+
+ this.emailFormField = new FormField({
+ label: new Label({ text: 'Email', for: 'editProfileEmail', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfileEmail',
+ type: 'email',
+ name: 'email',
+ value: props.email,
+ required: true,
+ placeholder: 'you@example.com',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-envelope',
+ });
+
+ this.firstNameFormField = new FormField({
+ label: new Label({ text: 'Name', for: 'editProfileFirstName', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfileFirstName',
+ type: 'text',
+ name: 'first_name',
+ value: props.firstName,
+ required: true,
+ placeholder: 'Name',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-user',
+ });
+
+ this.secondNameFormField = new FormField({
+ label: new Label({ text: 'Surname', for: 'editProfileSecondName', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfileSecondName',
+ type: 'text',
+ name: 'second_name',
+ value: props.surname,
+ required: true,
+ placeholder: 'Surname',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-user',
+ });
+
+ this.phoneFormField = new FormField({
+ label: new Label({ text: 'Phone', for: 'editProfilePhone', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfilePhone',
+ type: 'tel',
+ name: 'phone',
+ value: props.phone,
+ required: true,
+ placeholder: '+1 (555) 000-0000',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-phone',
+ });
+
+ this.oldPasswordFormField = new FormField({
+ label: new Label({ text: 'Current password', for: 'editProfileOldPassword', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfileOldPassword',
+ type: 'password',
+ name: 'old_password',
+ placeholder: 'Leave blank to keep current',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-lock',
+ });
+
+ this.newPasswordFormField = new FormField({
+ label: new Label({ text: 'New password', for: 'editProfileNewPassword', className: labelClass }).getData(),
+ input: new Input({
+ id: 'editProfileNewPassword',
+ type: 'password',
+ name: 'new_password',
+ placeholder: 'Leave blank to keep current',
+ className: inputClass,
+ }).getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-lock',
+ });
+
+ this.cancelButton = new Button({
+ type: 'button',
+ text: 'Cancel',
+ className: 'edit-profile__btn edit-profile__btn--secondary',
+ });
+
+ this.saveButton = new Button({
+ type: 'submit',
+ text: 'Save',
+ className: 'edit-profile__btn edit-profile__btn--primary',
+ });
+ }
+
+ public render(): void {
+ Handlebars.registerPartial('Button', ButtonTemplate);
+ Handlebars.registerPartial('FormField', FormFieldTemplate);
+ Handlebars.registerPartial('Input', InputTemplate);
+ Handlebars.registerPartial('Label', LabelTemplate);
+
+ this.container.innerHTML = Handlebars.compile(template)({
+ loginFormField: this.loginFormField.getData(),
+ displayNameFormField: this.displayNameFormField.getData(),
+ emailFormField: this.emailFormField.getData(),
+ firstNameFormField: this.firstNameFormField.getData(),
+ secondNameFormField: this.secondNameFormField.getData(),
+ phoneFormField: this.phoneFormField.getData(),
+ oldPasswordFormField: this.oldPasswordFormField.getData(),
+ newPasswordFormField: this.newPasswordFormField.getData(),
+ cancelButton: this.cancelButton.getData(),
+ saveButton: this.saveButton.getData(),
+ });
+
+ this.container.querySelector('[type="button"]')?.addEventListener('click', () => {
+ this.callbacks.onCancel();
+ });
+
+ this.container.querySelector('#editProfileForm')?.addEventListener('submit', (e) => {
+ e.preventDefault();
+ const form = e.target as HTMLFormElement;
+ const data: EditProfileFormProps = {
+ login: (form.elements.namedItem('login') as HTMLInputElement).value.trim(),
+ displayName: (form.elements.namedItem('display_name') as HTMLInputElement).value.trim(),
+ email: (form.elements.namedItem('email') as HTMLInputElement).value.trim(),
+ firstName: (form.elements.namedItem('first_name') as HTMLInputElement).value.trim(),
+ surname: (form.elements.namedItem('second_name') as HTMLInputElement).value.trim(),
+ phone: (form.elements.namedItem('phone') as HTMLInputElement).value.trim(),
+ oldPassword: (form.elements.namedItem('old_password') as HTMLInputElement).value || undefined,
+ newPassword: (form.elements.namedItem('new_password') as HTMLInputElement).value || undefined,
+ };
+ this.callbacks.onSave?.(data);
+ });
+ }
+}
diff --git a/src/components/FormField/FormField.hbs b/src/components/FormField/FormField.hbs
new file mode 100644
index 0000000..4bfcdfb
--- /dev/null
+++ b/src/components/FormField/FormField.hbs
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/src/components/FormField/FormField.scss b/src/components/FormField/FormField.scss
new file mode 100644
index 0000000..6e5fd7e
--- /dev/null
+++ b/src/components/FormField/FormField.scss
@@ -0,0 +1,64 @@
+@use '../../styles/variables' as *;
+
+.form-field {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-8;
+ width: 100%;
+
+ &__input-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background-color: $background-color-input;
+ border-radius: $border-radius-6;
+ padding-left: 12px;
+
+ &:focus-within {
+ outline: 1px solid $color-accent;
+ outline-offset: -1px;
+ }
+ }
+
+ &__icon {
+ flex-shrink: 0;
+ width: 1.25rem;
+ text-align: center;
+ color: $text-color-secondary;
+ font-size: 0.875rem;
+ }
+
+ &__label {
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ color: $text-color-primary;
+ }
+
+ &__input {
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ color: $text-color-primary;
+ background-color: transparent;
+ border: none;
+ border-radius: $border-radius-6;
+ padding: 8px 16px;
+ flex: 1;
+ min-width: 0;
+
+ &:focus {
+ outline: none;
+ }
+
+ &--with-icon {
+ padding-left: 0;
+ }
+
+ &--error {
+ border: 1px solid $color-error;
+ }
+
+ &--success {
+ border: 1px solid $color-success;
+ }
+ }
+}
diff --git a/src/components/FormField/FormField.ts b/src/components/FormField/FormField.ts
new file mode 100644
index 0000000..5b8bc8f
--- /dev/null
+++ b/src/components/FormField/FormField.ts
@@ -0,0 +1,38 @@
+import Handlebars from 'handlebars';
+import template from './FormField.hbs?raw';
+import './FormField.scss';
+import type { InputProps, LabelProps } from '../../types';
+import InputTemplate from '../Input/Input.hbs?raw';
+import LabelTemplate from '../Label/Label.hbs?raw';
+
+interface FormFieldProps {
+ input: InputProps;
+ label: LabelProps;
+ className?: string;
+ /** Font Awesome icon classes, e.g. "fa-solid fa-envelope" */
+ icon?: string;
+}
+
+class FormField {
+ private props: FormFieldProps;
+
+ constructor(props: FormFieldProps){
+ this.props = props;
+ }
+
+ public getData(): FormFieldProps {
+ const input = this.props.icon
+ ? { ...this.props.input, className: [this.props.input.className, 'form-field__input--with-icon'].filter(Boolean).join(' ') }
+ : this.props.input;
+ return { ...this.props, input };
+ }
+
+ public render(): string {
+ Handlebars.registerPartial("Input", InputTemplate);
+ Handlebars.registerPartial("Label", LabelTemplate);
+ const data = this.getData();
+ return Handlebars.compile(template)(data);
+ }
+};
+
+export { FormField };
diff --git a/src/components/Input/Input.hbs b/src/components/Input/Input.hbs
new file mode 100644
index 0000000..8ff4994
--- /dev/null
+++ b/src/components/Input/Input.hbs
@@ -0,0 +1,9 @@
+
diff --git a/src/components/Input/Input.ts b/src/components/Input/Input.ts
new file mode 100644
index 0000000..d7cdbec
--- /dev/null
+++ b/src/components/Input/Input.ts
@@ -0,0 +1,21 @@
+import Handlebars from 'handlebars';
+import template from './Input.hbs?raw';
+import type { InputProps } from '../../types';
+
+class Input {
+ private props: InputProps;
+
+ constructor(props: InputProps){
+ this.props = props;
+ }
+
+ public getData(): InputProps {
+ return this.props;
+ }
+
+ public render(props: InputProps): string {
+ return Handlebars.compile(template)(props);
+ }
+};
+
+export { Input };
diff --git a/src/components/Label/Label.hbs b/src/components/Label/Label.hbs
new file mode 100644
index 0000000..624971d
--- /dev/null
+++ b/src/components/Label/Label.hbs
@@ -0,0 +1,3 @@
+
+ {{text}}
+
diff --git a/src/components/Label/Label.ts b/src/components/Label/Label.ts
new file mode 100644
index 0000000..f507bbd
--- /dev/null
+++ b/src/components/Label/Label.ts
@@ -0,0 +1,21 @@
+import Handlebars from 'handlebars';
+import template from './Label.hbs?raw';
+import type { LabelProps } from '../../types';
+
+class Label {
+ private props: LabelProps;
+
+ constructor(props: LabelProps){
+ this.props = props;
+ }
+
+ public getData(): LabelProps {
+ return this.props;
+ }
+
+ public render(props: LabelProps): string {
+ return Handlebars.compile(template)(props);
+ }
+};
+
+export { Label };
diff --git a/src/components/Link/Link.hbs b/src/components/Link/Link.hbs
new file mode 100644
index 0000000..77650fb
--- /dev/null
+++ b/src/components/Link/Link.hbs
@@ -0,0 +1 @@
+{{text}}
\ No newline at end of file
diff --git a/src/components/Link/Link.ts b/src/components/Link/Link.ts
new file mode 100644
index 0000000..4d11cde
--- /dev/null
+++ b/src/components/Link/Link.ts
@@ -0,0 +1,21 @@
+import Handlebars from 'handlebars';
+import template from './Link.hbs?raw';
+import type { LinkProps } from '../../types';
+
+class Link {
+ private props: LinkProps;
+
+ constructor(props: LinkProps){
+ this.props = props;
+ }
+
+ public getData(): LinkProps {
+ return this.props;
+ }
+
+ public render(props: LinkProps): string {
+ return Handlebars.compile(template)(props);
+ }
+};
+
+export { Link };
diff --git a/src/components/NoChatStub/NoChatStub.hbs b/src/components/NoChatStub/NoChatStub.hbs
new file mode 100644
index 0000000..84dd912
--- /dev/null
+++ b/src/components/NoChatStub/NoChatStub.hbs
@@ -0,0 +1,7 @@
+
+
+
+
+
No chat selected
+
Select a conversation from the sidebar to start messaging
+
diff --git a/src/components/NoChatStub/NoChatStub.scss b/src/components/NoChatStub/NoChatStub.scss
new file mode 100644
index 0000000..486d773
--- /dev/null
+++ b/src/components/NoChatStub/NoChatStub.scss
@@ -0,0 +1,35 @@
+@use '../../styles/variables' as *;
+
+.no-chat-stub {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: $gap-size-32;
+ text-align: center;
+}
+
+.no-chat-stub__icon-wrap {
+ margin-bottom: $gap-size-32;
+}
+
+.no-chat-stub__icon {
+ font-size: 80px;
+ color: $text-color-secondary;
+ opacity: 0.5;
+}
+
+.no-chat-stub__title {
+ margin: 0 0 $gap-size-12;
+ font-size: $font-size-24;
+ font-weight: 700;
+ color: $text-color-primary;
+}
+
+.no-chat-stub__hint {
+ margin: 0;
+ font-size: $font-size-14;
+ line-height: 1.5;
+ color: $text-color-secondary;
+}
diff --git a/src/components/NoChatStub/NoChatStub.ts b/src/components/NoChatStub/NoChatStub.ts
new file mode 100644
index 0000000..43aaaa6
--- /dev/null
+++ b/src/components/NoChatStub/NoChatStub.ts
@@ -0,0 +1,17 @@
+import Handlebars from 'handlebars';
+
+import template from './NoChatStub.hbs?raw';
+
+import './NoChatStub.scss';
+
+export class NoChatStub {
+ private container: HTMLElement;
+
+ constructor(container: HTMLElement) {
+ this.container = container;
+ }
+
+ public render(): void {
+ this.container.innerHTML = Handlebars.compile(template)({});
+ }
+}
diff --git a/src/components/NotFoundPage/NotFoundPage.hbs b/src/components/NotFoundPage/NotFoundPage.hbs
new file mode 100644
index 0000000..ba04cd5
--- /dev/null
+++ b/src/components/NotFoundPage/NotFoundPage.hbs
@@ -0,0 +1,44 @@
+
+
404
+
Oops! You're Lost in Cyberspace
+
+ Looks like this page went on vacation without leaving a forwarding address.
+ Even our best digital detectives couldn't find it!
+ 🕵️ 🐛
+
+
+
+
+
+
+ 🤔
+ Maybe you typed something wrong?
+
+
+ 🗺️
+ This page is off the map!
+
+
+ 👻
+ Or it never existed... spooky!
+
+
+
+
+ "Not all who wander are lost... but you definitely are right now."
+ !
+
+
diff --git a/src/components/NotFoundPage/NotFoundPage.scss b/src/components/NotFoundPage/NotFoundPage.scss
new file mode 100644
index 0000000..0ca1852
--- /dev/null
+++ b/src/components/NotFoundPage/NotFoundPage.scss
@@ -0,0 +1,144 @@
+@use '../../styles/variables' as *;
+
+.not-found {
+ position: relative;
+ width: 100%;
+ max-width: 640px;
+ margin: 0 auto;
+ padding: $gap-size-32;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $gap-size-32;
+ text-align: center;
+}
+
+.not-found__bg-number {
+ position: absolute;
+ top: -0.2em;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: clamp(6rem, 20vw, 12rem);
+ font-weight: 700;
+ line-height: 1;
+ color: rgba($color-accent, 0.12);
+ pointer-events: none;
+ user-select: none;
+}
+
+.not-found__title {
+ position: relative;
+ margin: 0;
+ font-size: $font-size-24;
+ font-weight: 700;
+ color: $text-color-primary;
+ line-height: 1.3;
+}
+
+.not-found__message {
+ position: relative;
+ margin: 0;
+ font-size: $font-size-14;
+ line-height: 1.5;
+ color: $text-color-secondary;
+}
+
+.not-found__emoji {
+ display: inline-block;
+ margin-left: 0.25em;
+}
+
+.not-found__image-wrap {
+ position: relative;
+ width: 100%;
+ border-radius: $border-radius-8;
+ overflow: hidden;
+ background-color: $background-color-on-surface-black;
+}
+
+.not-found__image {
+ display: block;
+ width: 100%;
+ height: auto;
+ vertical-align: middle;
+}
+
+.not-found__reasons {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: $gap-size-12;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.not-found__reason {
+ display: flex;
+ align-items: center;
+ gap: $gap-size-8;
+ font-size: $font-size-14;
+ color: $text-color-secondary;
+}
+
+.not-found__reason-icon {
+ font-size: 1.2em;
+}
+
+.not-found__actions {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: $gap-size-12;
+}
+
+.not-found__btn {
+ display: inline-flex;
+ align-items: center;
+ gap: $gap-size-8;
+ padding: 12px 20px;
+ font-size: $font-size-14;
+ font-weight: 600;
+ text-decoration: none;
+ border: none;
+ border-radius: $border-radius-6;
+ cursor: pointer;
+ transition: filter 0.15s ease, background-color 0.15s ease;
+
+ i {
+ font-size: 1em;
+ }
+
+ &--primary {
+ background-color: $color-accent;
+ color: $text-color-primary;
+
+ &:hover {
+ filter: brightness(1.1);
+ }
+ }
+
+ &--secondary {
+ background-color: $background-color-on-surface-black;
+ color: $text-color-primary;
+
+ &:hover {
+ filter: brightness(1.08);
+ }
+ }
+}
+
+.not-found__quote {
+ position: relative;
+ margin: 0;
+ font-size: 12px;
+ font-style: italic;
+ color: $text-color-secondary;
+ opacity: 0.9;
+}
+
+.not-found__quote-mark {
+ color: $color-error;
+}
diff --git a/src/components/NotFoundPage/NotFoundPage.ts b/src/components/NotFoundPage/NotFoundPage.ts
new file mode 100644
index 0000000..f905096
--- /dev/null
+++ b/src/components/NotFoundPage/NotFoundPage.ts
@@ -0,0 +1,30 @@
+import Handlebars from 'handlebars';
+
+import template from './NotFoundPage.hbs?raw';
+
+import './NotFoundPage.scss';
+
+export class NotFoundPage {
+ private container: HTMLElement;
+
+ constructor(container: HTMLElement) {
+ this.container = container;
+ }
+
+ public render(): void {
+ this.container.innerHTML = Handlebars.compile(template)({});
+
+ this.container.querySelector('[data-go-home]')?.addEventListener('click', (e) => {
+ e.preventDefault();
+ window.location.hash = '';
+ });
+
+ this.container.querySelector('[data-go-back]')?.addEventListener('click', () => {
+ if (window.history.length > 1) {
+ window.history.back();
+ } else {
+ window.location.hash = '';
+ }
+ });
+ }
+}
diff --git a/src/components/ProfilePage/ProfilePage.hbs b/src/components/ProfilePage/ProfilePage.hbs
new file mode 100644
index 0000000..84c1af7
--- /dev/null
+++ b/src/components/ProfilePage/ProfilePage.hbs
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+ Profile Information
+
+
+ Login
+
+
+ {{login}}
+
+
+
+ Display name
+
+
+ {{displayName}}
+
+
+
+ Email
+
+
+ {{email}}
+
+
+
+ Name
+
+
+ {{firstName}}
+
+
+
+ Surname
+
+
+ {{surname}}
+
+
+
+ Phone
+
+
+ {{phone}}
+
+
+
+
+
+
diff --git a/src/components/ProfilePage/ProfilePage.scss b/src/components/ProfilePage/ProfilePage.scss
new file mode 100644
index 0000000..6e76a53
--- /dev/null
+++ b/src/components/ProfilePage/ProfilePage.scss
@@ -0,0 +1,181 @@
+@use 'sass:color';
+@use '../../styles/variables' as *;
+
+.profile-page {
+ width: 100%;
+ max-width: 800px;
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-32;
+}
+
+.profile-page__topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.profile-page__theme-btn {
+ background: none;
+ border: none;
+ padding: $gap-size-8;
+ cursor: pointer;
+ color: $text-color-secondary;
+ font-size: $font-size-24;
+ line-height: 1;
+
+ &:hover {
+ color: $text-color-primary;
+ }
+}
+
+.profile-page__theme-icon {
+ display: block;
+}
+
+.profile-page__content {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-32;
+}
+
+.profile-card {
+ background-color: $background-color-on-surface-black;
+ border: 1px solid $border-color-dark;
+ border-radius: $border-radius-8;
+ padding: $padding-32;
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.profile-card__header {
+ display: flex;
+ align-items: flex-start;
+ gap: $gap-size-32;
+}
+
+.profile-card__avatar {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background-color: $color-accent;
+ flex-shrink: 0;
+}
+
+.profile-card__identity {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-8;
+ flex: 1;
+ min-width: 0;
+}
+
+.profile-card__name {
+ margin: 0;
+ font-size: $font-size-24;
+ line-height: $line-height-32;
+ font-weight: 700;
+ color: $text-color-primary;
+}
+
+.profile-card__username {
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ color: $text-color-secondary;
+}
+
+.profile-card__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $gap-size-12;
+ margin-top: $gap-size-8;
+}
+
+.profile-info {
+ background-color: $background-color-on-surface-black;
+ border: 1px solid $border-color-dark;
+ border-radius: $border-radius-8;
+ padding: $padding-32;
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+}
+
+.profile-info__title {
+ margin: 0 0 $gap-size-12;
+ font-size: $font-size-24;
+ line-height: $line-height-32;
+ font-weight: 700;
+ color: $text-color-primary;
+}
+
+.profile-info__grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: $gap-size-12 $gap-size-32;
+}
+
+.profile-info__row {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-8;
+}
+
+.profile-info__label {
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ color: $text-color-secondary;
+}
+
+.profile-info__value {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ color: $text-color-primary;
+ background-color: $background-color-input;
+ border-radius: $border-radius-6;
+ padding: 8px 16px;
+}
+
+.profile-info__icon {
+ flex-shrink: 0;
+ width: 1.25rem;
+ text-align: center;
+ color: $text-color-secondary;
+ font-size: 0.875rem;
+}
+
+.profile-page__btn {
+ &--primary {
+ background-color: $color-accent;
+ color: $text-color-primary;
+
+ &:hover {
+ background-color: $color-accent-dark;
+ }
+ }
+
+ &--secondary {
+ background-color: $background-color-input;
+ color: $text-color-primary;
+
+ &:hover {
+ background-color: color.adjust($background-color-input, $lightness: -4%);
+ }
+ }
+
+ &--danger {
+ background-color: $color-danger;
+ color: #fff;
+
+ &:hover {
+ background-color: color.adjust($color-danger, $lightness: -8%);
+ }
+ }
+}
+
+@media (max-width: 520px) {
+ .profile-info__grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/components/ProfilePage/ProfilePage.ts b/src/components/ProfilePage/ProfilePage.ts
new file mode 100644
index 0000000..68df79d
--- /dev/null
+++ b/src/components/ProfilePage/ProfilePage.ts
@@ -0,0 +1,83 @@
+import Handlebars from 'handlebars';
+
+import template from './ProfilePage.hbs?raw';
+import { Button, ButtonTemplate, EditProfileForm, LinkTemplate } from '..';
+
+import './ProfilePage.scss';
+
+export interface ProfilePageProps {
+ name: string;
+ username: string;
+ displayName: string;
+ login: string;
+ email: string;
+ firstName: string;
+ surname: string;
+ phone: string;
+}
+
+export class ProfilePage {
+ private container: HTMLElement;
+ private props: ProfilePageProps;
+ private backLink: { href: string; text: string; className: string };
+ private editProfileButton: Button;
+ private logoutButton: Button;
+
+ constructor(container: HTMLElement, props: ProfilePageProps) {
+ this.container = container;
+ this.props = props;
+
+ this.backLink = {
+ href: '#',
+ text: '← Back to Messenger',
+ className: 'profile-page__back-link',
+ };
+
+ this.editProfileButton = new Button({
+ type: 'button',
+ text: 'Edit Profile',
+ className: 'profile-page__btn profile-page__btn--primary',
+ });
+
+ this.logoutButton = new Button({
+ type: 'button',
+ text: 'Logout',
+ className: 'profile-page__btn profile-page__btn--danger',
+ });
+ }
+
+ public render(): void {
+ Handlebars.registerPartial('Link', LinkTemplate);
+ Handlebars.registerPartial('Button', ButtonTemplate);
+
+ const compiledTemplate = Handlebars.compile(template)({
+ ...this.props,
+ backLink: this.backLink,
+ editProfileButton: this.editProfileButton.getData(),
+ logoutButton: this.logoutButton.getData(),
+ });
+
+ this.container.innerHTML = compiledTemplate;
+
+ const contentEl = this.container.querySelector('.profile-page__content');
+ this.container.querySelector('.profile-page__btn--primary')?.addEventListener('click', () => {
+ if (!contentEl || !(contentEl instanceof HTMLElement)) return;
+ const editForm = new EditProfileForm(
+ contentEl,
+ {
+ login: this.props.login,
+ displayName: this.props.displayName,
+ email: this.props.email,
+ firstName: this.props.firstName,
+ surname: this.props.surname,
+ phone: this.props.phone,
+ },
+ {
+ onCancel: () => this.render(),
+ onSave: () => this.render(),
+ }
+ );
+ editForm.render();
+ });
+ }
+}
diff --git a/src/components/RegisterForm/RegisterForm.hbs b/src/components/RegisterForm/RegisterForm.hbs
new file mode 100644
index 0000000..934e239
--- /dev/null
+++ b/src/components/RegisterForm/RegisterForm.hbs
@@ -0,0 +1,27 @@
+
diff --git a/src/components/RegisterForm/RegisterForm.scss b/src/components/RegisterForm/RegisterForm.scss
new file mode 100644
index 0000000..30143ea
--- /dev/null
+++ b/src/components/RegisterForm/RegisterForm.scss
@@ -0,0 +1,41 @@
+@use '../../styles/variables' as *;
+
+.register-form {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: $gap-size-12;
+ align-self: stretch;
+ width: 100%;
+
+ &__footer {
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-12;
+ width: 100%;
+ }
+
+ &__sign-in-text,
+ &__legal {
+ margin: 0;
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ color: $text-color-secondary;
+ }
+
+ &__link {
+ text-decoration: none;
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ }
+
+ &__submit-btn {
+ background-color: $color-accent;
+ color: $text-color-primary;
+ width: 100%;
+
+ &:hover {
+ background-color: $color-accent-dark;
+ }
+ }
+}
diff --git a/src/components/RegisterForm/RegisterForm.ts b/src/components/RegisterForm/RegisterForm.ts
new file mode 100644
index 0000000..1ba4f1e
--- /dev/null
+++ b/src/components/RegisterForm/RegisterForm.ts
@@ -0,0 +1,192 @@
+import Handlebars from 'handlebars';
+
+import template from './RegisterForm.hbs?raw';
+import {
+ Button,
+ ButtonTemplate,
+ FormField,
+ FormFieldTemplate,
+ Input,
+ InputTemplate,
+ Label,
+ LabelTemplate,
+ Link,
+ LinkTemplate,
+} from '..';
+
+import './RegisterForm.scss';
+
+interface RegisterFormProps {
+ title: string;
+ subtitle?: string;
+}
+
+export class RegisterForm {
+ private container: HTMLElement;
+ private props: RegisterFormProps;
+ private createAccountButton: Button;
+ private signInLink: Link;
+ private termsLink: Link;
+ private privacyLink: Link;
+ private formFields: FormField[];
+
+ constructor(container: HTMLElement, props: RegisterFormProps) {
+ this.container = container;
+ this.props = props;
+
+ const fieldClass = 'register-form__field';
+ const labelClass = 'register-form__label';
+ const inputClass = 'register-form__input';
+
+ const loginLabel = new Label({ text: 'Login', for: 'userLogin', className: labelClass });
+ const loginInput = new Input({
+ id: 'userLogin',
+ type: 'text',
+ name: 'login',
+ required: true,
+ placeholder: 'Choose a login',
+ className: inputClass,
+ });
+
+ const displayNameLabel = new Label({ text: 'Display name', for: 'userDisplayName', className: labelClass });
+ const displayNameInput = new Input({
+ id: 'userDisplayName',
+ type: 'text',
+ name: 'display_name',
+ required: true,
+ placeholder: 'Display name',
+ className: inputClass,
+ });
+
+ const emailLabel = new Label({ text: 'Email', for: 'userEmail', className: labelClass });
+ const emailInput = new Input({
+ id: 'userEmail',
+ type: 'email',
+ name: 'email',
+ required: true,
+ placeholder: 'you@example.com',
+ className: inputClass,
+ });
+
+ const nameLabel = new Label({ text: 'Name', for: 'userName', className: labelClass });
+ const nameInput = new Input({
+ id: 'userName',
+ type: 'text',
+ name: 'first_name',
+ required: true,
+ placeholder: 'Enter your name',
+ className: inputClass,
+ });
+
+ const surnameLabel = new Label({ text: 'Surname', for: 'userSurname', className: labelClass });
+ const surnameInput = new Input({
+ id: 'userSurname',
+ type: 'text',
+ name: 'second_name',
+ required: true,
+ placeholder: 'Enter your surname',
+ className: inputClass,
+ });
+
+ const phoneLabel = new Label({ text: 'Phone', for: 'userPhone', className: labelClass });
+ const phoneInput = new Input({
+ id: 'userPhone',
+ type: 'tel',
+ name: 'phone',
+ required: true,
+ placeholder: 'Enter your phone number',
+ className: inputClass,
+ });
+
+ const passwordLabel = new Label({ text: 'Password', for: 'userPassword', className: labelClass });
+ const passwordInput = new Input({
+ id: 'userPassword',
+ type: 'password',
+ name: 'password',
+ required: true,
+ placeholder: 'Enter your password',
+ className: inputClass,
+ });
+
+ const confirmPasswordLabel = new Label({
+ text: 'Confirm Password',
+ for: 'userConfirmPassword',
+ className: labelClass,
+ });
+ const confirmPasswordInput = new Input({
+ id: 'userConfirmPassword',
+ type: 'password',
+ name: 'password_confirm',
+ required: true,
+ placeholder: 'Confirm your password',
+ className: inputClass,
+ });
+
+ this.formFields = [
+ new FormField({ label: loginLabel.getData(), input: loginInput.getData(), className: fieldClass, icon: 'fa-solid fa-user' }),
+ new FormField({ label: displayNameLabel.getData(), input: displayNameInput.getData(), className: fieldClass, icon: 'fa-solid fa-user' }),
+ new FormField({ label: emailLabel.getData(), input: emailInput.getData(), className: fieldClass, icon: 'fa-solid fa-envelope' }),
+ new FormField({ label: nameLabel.getData(), input: nameInput.getData(), className: fieldClass, icon: 'fa-solid fa-user' }),
+ new FormField({ label: surnameLabel.getData(), input: surnameInput.getData(), className: fieldClass, icon: 'fa-solid fa-user' }),
+ new FormField({ label: phoneLabel.getData(), input: phoneInput.getData(), className: fieldClass, icon: 'fa-solid fa-phone' }),
+ new FormField({ label: passwordLabel.getData(), input: passwordInput.getData(), className: fieldClass, icon: 'fa-solid fa-lock' }),
+ new FormField({
+ label: confirmPasswordLabel.getData(),
+ input: confirmPasswordInput.getData(),
+ className: fieldClass,
+ icon: 'fa-solid fa-lock',
+ }),
+ ];
+
+ this.createAccountButton = new Button({
+ type: 'submit',
+ text: 'Create Account',
+ className: 'register-form__submit-btn',
+ });
+
+ this.signInLink = new Link({
+ text: 'Sign in',
+ href: '#auth',
+ className: 'register-form__link',
+ });
+
+ this.termsLink = new Link({
+ text: 'Terms of Service',
+ href: '#terms',
+ className: 'register-form__link',
+ });
+
+ this.privacyLink = new Link({
+ text: 'Privacy Policy',
+ href: '#privacy',
+ className: 'register-form__link',
+ });
+ }
+
+ public render(): void {
+ Handlebars.registerPartial('Button', ButtonTemplate);
+ Handlebars.registerPartial('Input', InputTemplate);
+ Handlebars.registerPartial('Label', LabelTemplate);
+ Handlebars.registerPartial('FormField', FormFieldTemplate);
+ Handlebars.registerPartial('Link', LinkTemplate);
+
+ const compiledTemplate = Handlebars.compile(template)({
+ title: this.props.title,
+ subtitle: this.props.subtitle,
+ loginFormField: this.formFields[0].getData(),
+ displayNameFormField: this.formFields[1].getData(),
+ emailFormField: this.formFields[2].getData(),
+ nameFormField: this.formFields[3].getData(),
+ surnameFormField: this.formFields[4].getData(),
+ phoneFormField: this.formFields[5].getData(),
+ passwordFormField: this.formFields[6].getData(),
+ confirmPasswordFormField: this.formFields[7].getData(),
+ createAccountButton: this.createAccountButton.getData(),
+ signInLink: this.signInLink.getData(),
+ termsLink: this.termsLink.getData(),
+ privacyLink: this.privacyLink.getData(),
+ });
+
+ this.container.innerHTML = compiledTemplate;
+ }
+}
diff --git a/src/components/ServerErrorPage/ServerErrorPage.hbs b/src/components/ServerErrorPage/ServerErrorPage.hbs
new file mode 100644
index 0000000..ccefbb6
--- /dev/null
+++ b/src/components/ServerErrorPage/ServerErrorPage.hbs
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ Server Status: On Fire
+ (Metaphorically... we hope)
+
+
+
+ Engineers: Panicking
+ But with style!
+
+
+
+
+
+
+ "It's not a bug, it's an unexpected feature!" — Every developer ever
+
+
diff --git a/src/components/ServerErrorPage/ServerErrorPage.scss b/src/components/ServerErrorPage/ServerErrorPage.scss
new file mode 100644
index 0000000..c5664af
--- /dev/null
+++ b/src/components/ServerErrorPage/ServerErrorPage.scss
@@ -0,0 +1,176 @@
+@use '../../styles/variables' as *;
+
+.server-error {
+ width: 100%;
+ max-width: 640px;
+ margin: 0 auto;
+ padding: $gap-size-32;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $gap-size-32;
+ text-align: center;
+}
+
+.server-error__header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $gap-size-12;
+}
+
+.server-error__code {
+ display: flex;
+ align-items: center;
+ gap: $gap-size-8;
+ margin: 0;
+ font-size: clamp(2rem, 6vw, 3rem);
+ font-weight: 700;
+ color: #a85c5c; // Softer red, less aggressive than $color-danger
+
+ i {
+ font-size: 0.8em;
+ }
+}
+
+.server-error__title {
+ margin: 0;
+ font-size: $font-size-24;
+ font-weight: 700;
+ color: $text-color-primary;
+ line-height: 1.3;
+}
+
+.server-error__icons {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25em;
+ margin-left: 0.25em;
+
+ .fa-fire {
+ color: #e25822;
+ }
+
+ .fa-rocket {
+ color: #5865f2;
+ }
+
+ .fa-screwdriver-wrench {
+ color: #94a3b8;
+ }
+}
+
+.server-error__message {
+ margin: 0;
+ font-size: $font-size-14;
+ line-height: 1.5;
+ color: $text-color-secondary;
+}
+
+.server-error__status-list {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: $gap-size-12;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ width: 100%;
+}
+
+.server-error__status {
+ flex: 1;
+ min-width: 160px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: $gap-size-8;
+ padding: $gap-size-12;
+ background-color: $background-color-on-surface-black;
+ border-radius: $border-radius-8;
+ text-align: center;
+}
+
+.server-error__status-icon {
+ font-size: 1.5em;
+
+ i {
+ display: inline-block;
+ }
+}
+
+.server-error__status:nth-child(1) .server-error__status-icon {
+ color: #e25822;
+}
+
+.server-error__status:nth-child(2) .server-error__status-icon {
+ color: #22c55e;
+}
+
+.server-error__status:nth-child(3) .server-error__status-icon {
+ color: #eab308;
+}
+
+.server-error__status-label {
+ font-size: $font-size-14;
+ font-weight: 600;
+ color: $text-color-primary;
+}
+
+.server-error__status-note {
+ font-size: 12px;
+ color: $text-color-secondary;
+ opacity: 0.9;
+}
+
+.server-error__actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: $gap-size-12;
+}
+
+.server-error__btn {
+ display: inline-flex;
+ align-items: center;
+ gap: $gap-size-8;
+ padding: 12px 20px;
+ font-size: $font-size-14;
+ font-weight: 600;
+ text-decoration: none;
+ border: none;
+ border-radius: $border-radius-6;
+ cursor: pointer;
+ transition: filter 0.15s ease, background-color 0.15s ease;
+ color: $text-color-primary;
+
+ i {
+ font-size: 1em;
+ }
+
+ &--primary {
+ background-color: $color-accent;
+ color: $text-color-primary;
+
+ &:hover {
+ filter: brightness(1.1);
+ }
+ }
+
+ &--secondary {
+ background-color: $background-color-on-surface-black;
+ color: $text-color-primary;
+
+ &:hover {
+ filter: brightness(1.08);
+ }
+ }
+}
+
+.server-error__quote {
+ margin: 0;
+ font-size: 12px;
+ font-style: italic;
+ color: $text-color-secondary;
+ opacity: 0.9;
+}
diff --git a/src/components/ServerErrorPage/ServerErrorPage.ts b/src/components/ServerErrorPage/ServerErrorPage.ts
new file mode 100644
index 0000000..b3d73b2
--- /dev/null
+++ b/src/components/ServerErrorPage/ServerErrorPage.ts
@@ -0,0 +1,26 @@
+import Handlebars from 'handlebars';
+
+import template from './ServerErrorPage.hbs?raw';
+
+import './ServerErrorPage.scss';
+
+export class ServerErrorPage {
+ private container: HTMLElement;
+
+ constructor(container: HTMLElement) {
+ this.container = container;
+ }
+
+ public render(): void {
+ this.container.innerHTML = Handlebars.compile(template)({});
+
+ this.container.querySelector('[data-go-home]')?.addEventListener('click', (e) => {
+ e.preventDefault();
+ window.location.hash = '';
+ });
+
+ this.container.querySelector('[data-try-again]')?.addEventListener('click', () => {
+ window.location.reload();
+ });
+ }
+}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..a43e3cb
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,30 @@
+import { Button } from './Button/Button';
+import { FormField } from './FormField/FormField';
+import { Input } from './Input/Input';
+import { Label } from './Label/Label';
+import { Link } from './Link/Link';
+
+import ButtonTemplate from './Button/Button.hbs?raw';
+import InputTemplate from './Input/Input.hbs?raw';
+import LabelTemplate from './Label/Label.hbs?raw';
+import FormFieldTemplate from './FormField/FormField.hbs?raw';
+import LinkTemplate from './Link/Link.hbs?raw';
+
+export {
+ Button,
+ ButtonTemplate,
+ FormField,
+ FormFieldTemplate,
+ Input,
+ InputTemplate,
+ Label,
+ LabelTemplate,
+ Link,
+ LinkTemplate,
+};
+export { RegisterForm } from './RegisterForm/RegisterForm';
+export { EditProfileForm } from './EditProfileForm/EditProfileForm';
+export { ProfilePage } from './ProfilePage/ProfilePage';
+export { NoChatStub } from './NoChatStub/NoChatStub';
+export { NotFoundPage } from './NotFoundPage/NotFoundPage';
+export { ServerErrorPage } from './ServerErrorPage/ServerErrorPage';
diff --git a/src/layout/main/MainLayout.hbs b/src/layout/main/MainLayout.hbs
new file mode 100644
index 0000000..6448cdd
--- /dev/null
+++ b/src/layout/main/MainLayout.hbs
@@ -0,0 +1,11 @@
+
+
+ GilgaChat
+
+ {{> Link goBackLink }}
+
+
+
+ {{{content}}}
+
+
diff --git a/src/layout/main/MainLayout.scss b/src/layout/main/MainLayout.scss
new file mode 100644
index 0000000..1db1f83
--- /dev/null
+++ b/src/layout/main/MainLayout.scss
@@ -0,0 +1,63 @@
+@use '../../styles/variables' as *;
+
+.main-layout {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ min-height: 100%;
+ width: 100%;
+}
+
+.main-layout__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ max-width: 1200px;
+ padding: $gap-size-12 $padding-32;
+ box-sizing: border-box;
+}
+
+.main-layout__title {
+ margin: 0;
+ font-size: $font-size-24;
+ line-height: $line-height-32;
+ font-weight: 700;
+ color: $text-color-primary;
+}
+
+.main-layout__nav {
+ display: flex;
+ align-items: center;
+ gap: $gap-size-12;
+}
+
+.main-layout__content {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ padding: $gap-size-32;
+ box-sizing: border-box;
+}
+
+.main-layout__welcome-block {
+ text-align: center;
+ max-width: 420px;
+}
+
+.main-layout__welcome-notice {
+ margin: 0 0 $gap-size-12;
+ font-size: $font-size-14;
+ line-height: 1.5;
+ color: $text-color-secondary;
+}
+
+.main-layout__welcome {
+ margin: 0;
+ font-size: $font-size-14;
+ line-height: $line-height-20;
+ color: $text-color-secondary;
+}
diff --git a/src/layout/main/MainLayout.ts b/src/layout/main/MainLayout.ts
new file mode 100644
index 0000000..f0d98a7
--- /dev/null
+++ b/src/layout/main/MainLayout.ts
@@ -0,0 +1,28 @@
+import Handlebars from 'handlebars';
+import template from './MainLayout.hbs?raw';
+import { LinkTemplate } from '../../components';
+import type { LinkProps } from '../../types';
+import './MainLayout.scss';
+
+export interface MainLayoutProps {
+ goBackLink: LinkProps;
+ content?: string;
+}
+
+class MainLayout {
+ private props: MainLayoutProps;
+
+ constructor(props: MainLayoutProps) {
+ this.props = {
+ ...props,
+ content: props.content ?? '',
+ };
+ }
+
+ public render(): string {
+ Handlebars.registerPartial('Link', LinkTemplate);
+ return Handlebars.compile(template)(this.props);
+ }
+}
+
+export { MainLayout };
diff --git a/src/layout/messenger/MessengerLayout.hbs b/src/layout/messenger/MessengerLayout.hbs
new file mode 100644
index 0000000..769fffd
--- /dev/null
+++ b/src/layout/messenger/MessengerLayout.hbs
@@ -0,0 +1,80 @@
+
+
+
+ {{{content}}}
+
+
diff --git a/src/layout/messenger/MessengerLayout.scss b/src/layout/messenger/MessengerLayout.scss
new file mode 100644
index 0000000..21fb85a
--- /dev/null
+++ b/src/layout/messenger/MessengerLayout.scss
@@ -0,0 +1,276 @@
+@use '../../styles/variables' as *;
+
+.messenger-layout {
+ display: flex;
+ width: 100%;
+ min-height: 100vh;
+ background-color: $background-color;
+}
+
+.messenger-sidebar {
+ width: 280px;
+ min-width: 280px;
+ display: flex;
+ flex-direction: column;
+ background-color: $background-color-on-surface-black;
+ border-right: 1px solid $border-color-dark;
+}
+
+.messenger-sidebar__top {
+ padding: $gap-size-12;
+ display: flex;
+ flex-direction: column;
+ gap: $gap-size-12;
+ flex-shrink: 0;
+}
+
+.messenger-sidebar__top-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $gap-size-8;
+}
+
+.messenger-sidebar__top-link {
+ font-size: 12px;
+ color: $text-color-secondary;
+
+ &:hover {
+ color: $text-color-primary;
+ }
+}
+
+.messenger-sidebar__app-title {
+ display: flex;
+ align-items: center;
+ gap: $gap-size-8;
+ width: 100%;
+ padding: $gap-size-8 0;
+ background: none;
+ border: none;
+ font-size: $font-size-14;
+ font-weight: 700;
+ color: $text-color-primary;
+ cursor: pointer;
+ text-align: left;
+
+ &:hover {
+ color: $text-color-primary;
+ opacity: 0.9;
+ }
+}
+
+.messenger-sidebar__chevron {
+ font-size: 12px;
+ opacity: 0.8;
+}
+
+.messenger-sidebar__search-wrap {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.messenger-sidebar__search-icon {
+ position: absolute;
+ left: 10px;
+ font-size: 12px;
+ color: $text-color-placeholder;
+ pointer-events: none;
+}
+
+.messenger-sidebar__search {
+ width: 100%;
+ padding: 8px 10px 8px 32px;
+ background-color: $background-color-input;
+ border: none;
+ border-radius: $border-radius-6;
+ font-size: $font-size-14;
+ color: $text-color-primary;
+ outline: none;
+
+ &::placeholder {
+ color: $text-color-placeholder;
+ }
+}
+
+.messenger-sidebar__nav {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 $gap-size-12 $gap-size-12;
+}
+
+.messenger-sidebar__section {
+ margin-bottom: $gap-size-12;
+}
+
+.messenger-sidebar__section-head {
+ width: 100%;
+ padding: $gap-size-8 0;
+ background: none;
+ border: none;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ color: $text-color-secondary;
+ cursor: pointer;
+ text-align: left;
+
+ &:hover {
+ color: $text-color-primary;
+ }
+}
+
+.messenger-sidebar__list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.messenger-sidebar__list li {
+ margin: 0;
+}
+
+.messenger-sidebar__item {
+ width: 100%;
+ display: flex;
+ align-items: flex-start;
+ gap: $gap-size-8;
+ padding: $gap-size-8;
+ background: none;
+ border: none;
+ border-radius: $border-radius-6;
+ font: inherit;
+ color: inherit;
+ cursor: pointer;
+ text-align: left;
+ transition: background-color 0.15s ease;
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.06);
+ }
+}
+
+.messenger-sidebar__item-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.messenger-sidebar__item-name {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: $font-size-14;
+ font-weight: 500;
+ color: $text-color-primary;
+ line-height: 1.3;
+}
+
+.messenger-sidebar__item-meta {
+ font-size: 12px;
+ color: $text-color-secondary;
+ line-height: 1.3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.messenger-sidebar__item-avatar {
+ flex-shrink: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $background-color-input;
+ border-radius: 50%;
+ font-size: 14px;
+ color: $text-color-secondary;
+
+ &--user {
+ // Аватар пользователя (пустой круг или позже — фото)
+ }
+}
+
+.messenger-sidebar__status-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: 4px;
+ vertical-align: middle;
+
+ &--green {
+ background-color: $color-success;
+ }
+
+ &--yellow {
+ background-color: #b8a200;
+ }
+
+ &--gray {
+ background-color: $text-color-placeholder;
+ }
+}
+
+.messenger-sidebar__user {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: $gap-size-8;
+ padding: $gap-size-12;
+ border-top: 1px solid $border-color-dark;
+ background-color: $background-color-on-surface-black;
+}
+
+.messenger-sidebar__user-avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background-color: $background-color-input;
+ flex-shrink: 0;
+}
+
+.messenger-sidebar__user-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.messenger-sidebar__user-name {
+ display: block;
+ font-size: $font-size-14;
+ font-weight: 600;
+ color: $text-color-primary;
+}
+
+.messenger-sidebar__user-status {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ color: $text-color-secondary;
+ margin-top: 2px;
+}
+
+.messenger-sidebar__user-settings {
+ padding: $gap-size-8;
+ color: $text-color-secondary;
+ text-decoration: none;
+ border-radius: $border-radius-6;
+ transition: color 0.15s ease, background-color 0.15s ease;
+
+ &:hover {
+ color: $text-color-primary;
+ background-color: rgba(255, 255, 255, 0.06);
+ }
+}
+
+.messenger-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ background-color: $background-color;
+}
diff --git a/src/layout/messenger/MessengerLayout.ts b/src/layout/messenger/MessengerLayout.ts
new file mode 100644
index 0000000..11e3be4
--- /dev/null
+++ b/src/layout/messenger/MessengerLayout.ts
@@ -0,0 +1,76 @@
+import Handlebars from 'handlebars';
+
+import template from './MessengerLayout.hbs?raw';
+import { LinkTemplate } from '../../components';
+import type { LinkProps } from '../../types';
+
+import './MessengerLayout.scss';
+
+export interface DirectMessageItem {
+ firstName: string;
+ lastName: string;
+ preview: string;
+ statusType: 'green' | 'yellow' | 'gray';
+}
+
+export interface GroupItem {
+ name: string;
+ preview: string;
+ iconClass: string;
+}
+
+export interface MessengerLayoutProps {
+ appTitle: string;
+ topLinks: LinkProps[];
+ currentUser: {
+ firstName: string;
+ lastName: string;
+ status: string;
+ };
+ directMessages: DirectMessageItem[];
+ groups: GroupItem[];
+ content?: string;
+}
+
+const defaultTopLinks: LinkProps[] = [
+ { href: '#auth', text: 'Sign in', className: 'messenger-sidebar__top-link' },
+ { href: '#register', text: 'Sign up', className: 'messenger-sidebar__top-link' },
+ { href: '#404', text: '404', className: 'messenger-sidebar__top-link' },
+ { href: '#500', text: '500', className: 'messenger-sidebar__top-link' },
+];
+
+const defaultDirectMessages: DirectMessageItem[] = [
+ { firstName: 'Sarah', lastName: 'Chen', preview: 'typing...', statusType: 'green' },
+ { firstName: 'Marcus', lastName: 'Johnson', preview: "Sure, I'll check it out later today", statusType: 'yellow' },
+];
+
+const defaultGroups: GroupItem[] = [
+ { name: 'Project Alpha Team', preview: "Tomorrow's meeting is at 2 PM", iconClass: 'fa-user-group' },
+ { name: 'Weekend Plans', preview: "I'm in! What time? 1h", iconClass: 'fa-bolt' },
+];
+
+class MessengerLayout {
+ private props: MessengerLayoutProps;
+
+ constructor(props: Partial = {}) {
+ this.props = {
+ appTitle: props.appTitle ?? 'Messenger',
+ topLinks: props.topLinks ?? defaultTopLinks,
+ currentUser: props.currentUser ?? {
+ firstName: 'Alex',
+ lastName: 'Morgan',
+ status: 'Playing games',
+ },
+ directMessages: props.directMessages ?? defaultDirectMessages,
+ groups: props.groups ?? defaultGroups,
+ content: props.content ?? '',
+ };
+ }
+
+ public render(): string {
+ Handlebars.registerPartial('Link', LinkTemplate);
+ return Handlebars.compile(template)(this.props);
+ }
+}
+
+export { MessengerLayout };
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..c8c6057
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,142 @@
+import '@fortawesome/fontawesome-free/css/all.min.css';
+import { AuthForm } from './components/AuthForm/AuthForm';
+import { MainLayout } from './layout/main/MainLayout';
+import { MessengerLayout } from './layout/messenger/MessengerLayout';
+import { NoChatStub } from './components/NoChatStub/NoChatStub';
+import { NotFoundPage } from './components/NotFoundPage/NotFoundPage';
+import { ProfilePage } from './components/ProfilePage/ProfilePage';
+import { RegisterForm } from './components/RegisterForm/RegisterForm';
+import { ServerErrorPage } from './components/ServerErrorPage/ServerErrorPage';
+import './style.scss';
+
+function showError(message: string): void {
+ const app = document.getElementById('app');
+ if (app) {
+ app.innerHTML = `App error: ${message}
`;
+ }
+ console.error('[GilgaChat]', message);
+}
+
+class App {
+ private layoutContent: HTMLElement | null = null;
+
+ constructor() {
+ this.init();
+ }
+
+ private init(): void {
+ const run = (): void => {
+ try {
+ this.renderLayout();
+ this.setupNavigation();
+ this.renderCurrentView();
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ showError(msg);
+ throw err;
+ }
+ };
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', run);
+ } else {
+ run();
+ }
+ }
+
+ private renderLayout(): void {
+ const container = document.getElementById('app');
+ if (!container) {
+ showError('App container #app not found');
+ return;
+ }
+
+ const layout = new MainLayout({
+ goBackLink: {
+ href: '#',
+ text: 'Go back',
+ className: 'main-layout__go-back',
+ },
+ content: '',
+ });
+
+ container.innerHTML = layout.render();
+ this.layoutContent = document.getElementById('layout-content');
+ if (!this.layoutContent) {
+ showError('Layout content #layout-content not found');
+ }
+ }
+
+ private setupNavigation(): void {
+ window.addEventListener('hashchange', () => this.renderCurrentView());
+ }
+
+ // TODO: временное решение до внедрения роутинга
+ private renderCurrentView(): void {
+ const container = document.getElementById('app');
+ if (!container) return;
+
+ const hash = window.location.hash;
+ const isMessengerView = hash === '' || hash === '#messenger';
+
+ if (isMessengerView) {
+ const layout = new MessengerLayout();
+ container.innerHTML = layout.render();
+ const contentEl = document.getElementById('messenger-content');
+ if (contentEl) new NoChatStub(contentEl).render();
+ this.layoutContent = null;
+ return;
+ }
+
+ if (!this.layoutContent) {
+ this.renderLayout();
+ }
+ if (!this.layoutContent) return;
+
+ if (hash === '#auth') {
+ const authForm = new AuthForm(this.layoutContent, {
+ title: 'Welcome back',
+ subtitle: 'Sign in to continue to GilgaChat',
+ });
+ authForm.render();
+ } else if (hash === '#register') {
+ const registerForm = new RegisterForm(this.layoutContent, {
+ title: 'Create Account',
+ subtitle: 'Sign up to get started',
+ });
+ registerForm.render();
+ } else if (hash === '#profile') {
+ const profilePage = new ProfilePage(this.layoutContent, {
+ name: 'John Smith',
+ username: '@johnsmith',
+ displayName: 'John Smith',
+ login: 'johnsmith',
+ email: 'john.smith@example.com',
+ firstName: 'John',
+ surname: 'Smith',
+ phone: '+1 (555) 123-4567',
+ });
+ profilePage.render();
+ } else if (hash === '#404') {
+ const notFoundPage = new NotFoundPage(this.layoutContent);
+ notFoundPage.render();
+ } else if (hash === '#500') {
+ const serverErrorPage = new ServerErrorPage(this.layoutContent);
+ serverErrorPage.render();
+ } else {
+ // Unknown hash — show messenger as default
+ const layout = new MessengerLayout();
+ container.innerHTML = layout.render();
+ const contentEl = document.getElementById('messenger-content');
+ if (contentEl) new NoChatStub(contentEl).render();
+ this.layoutContent = null;
+ return;
+ }
+ }
+}
+
+try {
+ new App();
+} catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ showError(msg);
+}
diff --git a/src/style.scss b/src/style.scss
new file mode 100644
index 0000000..dfeb253
--- /dev/null
+++ b/src/style.scss
@@ -0,0 +1,24 @@
+@use 'styles/variables' as *;
+
+// General / global styles
+.body {
+ margin: 0;
+ background-color: $background-color;
+ height: 100vh;
+}
+
+.app {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+}
+
+.link {
+ color: $color-accent;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
diff --git a/src/styles/_colors.scss b/src/styles/_colors.scss
new file mode 100644
index 0000000..5599b6b
--- /dev/null
+++ b/src/styles/_colors.scss
@@ -0,0 +1,16 @@
+// Color palette
+$background-color: #313338;
+$background-color-on-surface-black: #2b2d31;
+$background-color-input: #1e1f22;
+
+$text-color-placeholder: #80848e;
+$text-color-primary: #dbdee1;
+$text-color-secondary: #b5bac1;
+
+$color-accent: #5865f2;
+$color-accent-dark: #424cb5;
+$color-success: green;
+$color-error: red;
+$color-danger: #dc3545;
+
+$border-color-dark: #1e1f22;
diff --git a/src/styles/_sizing.scss b/src/styles/_sizing.scss
new file mode 100644
index 0000000..9c70e19
--- /dev/null
+++ b/src/styles/_sizing.scss
@@ -0,0 +1,10 @@
+// Spacing, gaps, border radius
+$border-radius-6: 6px;
+$border-radius-8: 8px;
+
+$padding-32: 32px;
+
+$gap-size-8: 8px;
+$gap-size-12: 12px;
+$gap-size-24: 24px;
+$gap-size-32: 32px;
diff --git a/src/styles/_typography.scss b/src/styles/_typography.scss
new file mode 100644
index 0000000..e52eaec
--- /dev/null
+++ b/src/styles/_typography.scss
@@ -0,0 +1,6 @@
+// Font sizes and line heights
+$font-size-14: 14px;
+$font-size-24: 24px;
+
+$line-height-20: 20px;
+$line-height-32: 32px;
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
new file mode 100644
index 0000000..9120048
--- /dev/null
+++ b/src/styles/variables.scss
@@ -0,0 +1,3 @@
+@forward 'colors';
+@forward 'typography';
+@forward 'sizing';
diff --git a/src/types/InputTypes.ts b/src/types/InputTypes.ts
new file mode 100644
index 0000000..81a76f0
--- /dev/null
+++ b/src/types/InputTypes.ts
@@ -0,0 +1,17 @@
+type InputType =
+ | "text" | "password" | "email" | "number" | "tel" | "url" | "search"
+ | "checkbox" | "radio" | "range" | "color"
+ | "date" | "time" | "datetime-local" | "month" | "week"
+ | "file" | "submit" | "reset" | "button" | "hidden" | "image";
+
+interface InputProps {
+ id: string;
+ type: InputType;
+ name: string;
+ value?: string;
+ required?: boolean;
+ className?: string;
+ placeholder?: string;
+}
+
+export type { InputProps, InputType };
diff --git a/src/types/LabelTypes.ts b/src/types/LabelTypes.ts
new file mode 100644
index 0000000..65ac034
--- /dev/null
+++ b/src/types/LabelTypes.ts
@@ -0,0 +1,7 @@
+interface LabelProps {
+ for: string;
+ text: string;
+ className?: string;
+};
+
+export type { LabelProps };
diff --git a/src/types/LinkTypes.ts b/src/types/LinkTypes.ts
new file mode 100644
index 0000000..ea4c88c
--- /dev/null
+++ b/src/types/LinkTypes.ts
@@ -0,0 +1,9 @@
+interface LinkProps {
+ href: string;
+ text: string;
+ className?: string;
+ target?: '_blank' | '_top' | '_self' | '_parent'
+ rel?: string;
+}
+
+export type { LinkProps };
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..68aec7f
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,5 @@
+import type { InputProps, InputType } from './InputTypes';
+import type { LabelProps } from './LabelTypes';
+import type { LinkProps } from './LinkTypes';
+
+export type { InputProps, InputType, LabelProps, LinkProps };
diff --git a/src/utils/mydash/first.ts b/src/utils/mydash/first.ts
new file mode 100644
index 0000000..64df5ae
--- /dev/null
+++ b/src/utils/mydash/first.ts
@@ -0,0 +1,5 @@
+function first(list: T[]): T | undefined {
+ return Array.isArray(list) ? list[0] : undefined
+}
+
+export { first };
diff --git a/src/utils/mydash/last.ts b/src/utils/mydash/last.ts
new file mode 100644
index 0000000..52840aa
--- /dev/null
+++ b/src/utils/mydash/last.ts
@@ -0,0 +1,5 @@
+function last(list: T[]): T | undefined {
+ return Array.isArray(list) ? list.pop() : undefined
+}
+
+export { last };
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..52403e5
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": [
+ "src",
+ "*.d.ts"
+ ]
+}
diff --git a/types/vite.d.ts b/types/vite.d.ts
new file mode 100644
index 0000000..5ba42ae
--- /dev/null
+++ b/types/vite.d.ts
@@ -0,0 +1,5 @@
+declare module '*.hbs'{
+ import type { TemplateDelegate } from 'handlebars'
+ const template: TemplateDelegate;
+ export default template;
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..e926795
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite'
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const PORT = process.env.PORT ? +process.env.PORT : 3000;
+
+export default defineConfig({
+ base: process.env.BASE_PATH || '/',
+ server: {
+ open: true,
+ port: PORT,
+ },
+ preview: {
+ port: PORT,
+ },
+ build: {
+ rollupOptions: {
+ input: 'index.html',
+ output: {
+ hashCharacters: 'base36',
+ },
+ },
+ outDir: 'dist',
+ },
+ assetsInclude: ['**/*.hbs'],
+})