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. -![Бэйджи](https://github.com/yandex-praktikum/mf.messenger.praktikum.yandex.images/blob/master/mf/b.png) +- **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». -![Версии](https://github.com/yandex-praktikum/mf.messenger.praktikum.yandex.images/blob/master/mf/vers.png) +- **Переиспользуемые 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 не деплоится из-за проблем с работой сервиса. -![Описание](https://github.com/yandex-praktikum/mf.messenger.praktikum.yandex.images/blob/master/mf/desc.png) +## Дизайн и прототипы -Авторы **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-сервера: -![Ссылки](https://github.com/yandex-praktikum/mf.messenger.praktikum.yandex.images/blob/master/mf/link.png) +```bash +npm run dev +``` -Для более подробных инструкции добавьте новые разделы или ссылки: +Сборка для продакшена: -- на документацию, -- вики проекта, -- описание API. +```bash +npm run build +``` -В учебном проекте будут полезен раздел с описанием стиля кода и правилами разработки: как работать с ветками, пул-реквестами и релизами. +Локальный просмотр продакшен-сборки: -### **Команда** +```bash +npm run preview +``` -Если вы работаете в команде, укажите основных участников: им будет приятно, а новые разработчики охотнее присоединятся к проекту. «Гитхаб» — не просто инструмент, это социальная сеть разработчиков. +Сборка и просмотр одной командой: -![Команда](https://github.com/yandex-praktikum/mf.messenger.praktikum.yandex.images/blob/master/mf/team.png) +```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 @@ +
+
+
+ + GilgaChat + +

{{title}}

+ {{subtitle}} +
+ + + +
+ +
+
+
\ 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 @@ + 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 @@ +
+
+

Edit Profile

+
+ +
+ {{> FormField loginFormField }} + {{> FormField displayNameFormField }} + {{> FormField emailFormField }} + {{> FormField firstNameFormField }} + {{> FormField secondNameFormField }} + {{> FormField phoneFormField }} + {{> FormField oldPasswordFormField }} + {{> FormField newPasswordFormField }} + +
+ {{> Button cancelButton }} + {{> Button saveButton }} +
+
+
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 @@ +
+ {{> Label label }} +
+ {{#if icon}}{{/if}} + {{> Input input }} +
+
\ 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 @@ + 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 @@ +
+ +

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! + +

+
+ Person standing on a sand dune in vast landscape +
+
    +
  • + + Maybe you typed something wrong? +
  • +
  • + + This page is off the map! +
  • +
  • + + Or it never existed... spooky! +
  • +
+
+ + + Take Me Home + + +
+

+ "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 @@ +
+
+ {{> Link backLink }} +
+ +
+
+
+ +
+

{{name}}

+ {{username}} +
+ {{> Button editProfileButton }} + {{> Button logoutButton }} +
+
+
+
+ +
+

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 @@ +
+
+
+ + GilgaChat + +

{{title}}

+ {{subtitle}} +
+ +
+ {{> FormField loginFormField }} + {{> FormField displayNameFormField }} + {{> FormField emailFormField }} + {{> FormField nameFormField }} + {{> FormField surnameFormField }} + {{> FormField phoneFormField }} + {{> FormField passwordFormField }} + {{> FormField confirmPasswordFormField }} + +
+ {{> Button createAccountButton }} + +
+
+
+
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 @@ +
+
+

+ 5XX +

+

+ Houston, we have a problem! +

+

+ Our servers are having an existential crisis right now. They're questioning their purpose, their existence, and why they can't just be a toaster instead. We're working on it! + +

+
+ +
    +
  • + + Server Status: On Fire + (Metaphorically... we hope) +
  • +
  • + + Engineers: Panicking + But with style! +
  • +
+ +
+ + + + Go Home + +
+ +

+ "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

+ +
+
+ {{{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'], +})