From 83ef0d5ab3d4610eef4392823abf73671a97a6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BC=D0=B0=D1=80=D0=BE=D0=B2=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB=20=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87=206?= Date: Fri, 29 May 2026 04:59:58 +0300 Subject: [PATCH] init --- .dockerignore | 7 + .gitignore | 4 + Dockerfile | 36 + README.md | 208 +++ client/.env | 1 + client/.env.development | 1 + client/index.html | 12 + client/package-lock.json | 1719 ++++++++++++++++++++++++ client/package.json | 19 + client/src/App.css | 17 + client/src/App.jsx | 75 ++ client/src/components/Chat.css | 52 + client/src/components/Chat.jsx | 137 ++ client/src/components/Login.css | 65 + client/src/components/Login.jsx | 36 + client/src/components/Message.css | 129 ++ client/src/components/Message.jsx | 113 ++ client/src/components/MessageInput.css | 58 + client/src/components/MessageInput.jsx | 71 + client/src/components/MessageList.jsx | 22 + client/src/main.jsx | 10 + client/vite.config.js | 9 + docker-entrypoint.sh | 16 + nginx.conf | 29 + server/package-lock.json | 36 + server/package.json | 12 + server/server.js | 129 ++ server/users.json | 4 + 28 files changed, 3027 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 client/.env create mode 100644 client/.env.development create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/src/App.css create mode 100644 client/src/App.jsx create mode 100644 client/src/components/Chat.css create mode 100644 client/src/components/Chat.jsx create mode 100644 client/src/components/Login.css create mode 100644 client/src/components/Login.jsx create mode 100644 client/src/components/Message.css create mode 100644 client/src/components/Message.jsx create mode 100644 client/src/components/MessageInput.css create mode 100644 client/src/components/MessageInput.jsx create mode 100644 client/src/components/MessageList.jsx create mode 100644 client/src/main.jsx create mode 100644 client/vite.config.js create mode 100644 docker-entrypoint.sh create mode 100644 nginx.conf create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/server.js create mode 100644 server/users.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d2d2215 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +.git +.gitignore +**/dist +client/node_modules +server/node_modules diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ff9f26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env.local +npm-debug.log* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..83719f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# Stage 1: Build client +FROM node:18-alpine AS client-builder +WORKDIR /client +COPY client/package*.json ./ +RUN npm ci +COPY client/ . +RUN npm run build + +# Stage 2: Production image +FROM nginx:alpine + +# Install Node.js for the server +RUN apk add --no-cache nodejs npm + +# Remove default nginx config +RUN rm /etc/nginx/conf.d/default.conf + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy client build +COPY --from=client-builder /client/dist /usr/share/nginx/html + +# Copy server +WORKDIR /server +COPY server/package*.json ./ +RUN npm ci --omit=dev +COPY server/ . + +# Copy entrypoint +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + +EXPOSE 80 + +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2fe4c6 --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# Чат с общей комнатой + +Веб-приложение чата с одной общей комнатой на WebSocket. Поддерживает текстовые сообщения, отправку файлов и отображение изображений. + +**Сервер:** Node.js + `ws` +**Клиент:** React + Vite +**Прокси:** nginx (production-сборка) + +--- + +## Быстрый запуск (разработка) + +### Сервер + +```bash +cd server +npm install +npm start +``` + +Запускает WebSocket сервер на `ws://localhost:8080`. + +### Клиент + +```bash +cd client +npm install +npx vite +``` + +Запускает dev-сервер на `http://localhost:3000`. +В режиме разработки клиент подключается напрямую к `ws://localhost:8080`. + +### Пользователи по умолчанию + +| Логин | Пароль | +|-------|--------| +| alice | 123 | +| bob | 456 | + +--- + +## Production (Docker) + +### Сборка образа + +```bash +docker build -t chat-app . +``` + +### Запуск контейнера + +```bash +docker run -d -p 80:80 chat-app +``` + +Открой `http://localhost`. + +### Как это работает + +- **nginx** на порту 80 раздаёт статику React и проксирует WebSocket +- Путь `/` → статические файлы клиента (`index.html`, JS, CSS) +- Путь `/ws` → прокси на Node.js WebSocket сервер (порт 8080 внутри контейнера) +- nginx поддерживает Upgrade до WebSocket, таймаут соединения — 24 часа +- Максимальный размер загружаемых файлов через nginx — 50 МБ +- После остановки Node.js контейнер завершается (через `wait`) + +--- + +## Переменные окружения + +### Сервер + +| Переменная | По умолчанию | Описание | +|-----------|-------------|---------| +| `PORT` | `8080` | Порт WebSocket сервера | +| `MAX_FILE_SIZE` | `10485760` (10 МБ) | Максимальный размер файла в байтах | +| `USERS_FILE` | `users.json` | Путь к файлу с пользователями | + +Пример: + +```bash +$env:PORT=9090; $env:MAX_FILE_SIZE=2097152; node server.js +``` + +### Клиент + +| Переменная | По умолчанию (dev) | По умолчанию (production) | Описание | +|-----------|-------------------|--------------------------|---------| +| `VITE_WS_URL` | `ws://localhost:8080` | `/ws` | URL WebSocket сервера | + +Настройки хранятся в файлах: +- `client/.env.development` — для `npx vite` (dev-режим) +- `client/.env` — для `vite build` (production-сборка) + +--- + +## Протокол обмена + +### Аутентификация + +Клиент → Сервер: + +```json +{ "type": "auth", "login": "alice", "password": "123" } +``` + +Сервер → Клиент: + +```json +{ "type": "auth_result", "success": true } +``` + +```json +{ "type": "auth_result", "success": false, "reason": "Invalid login or password" } +``` + +### Текстовое сообщение + +Клиент → Сервер: + +```json +{ "type": "text", "text": "Привет!" } +``` + +Сервер → Все: + +```json +{ "type": "text", "from": "alice", "timestamp": 1717000000000, "text": "Привет!" } +``` + +### Файл / изображение + +Клиент → Сервер: + +```json +{ + "type": "file", + "filename": "cat.png", + "mime": "image/png", + "data": "" +} +``` + +Сервер → Все (такая же структура, добавлены `from` и `timestamp`): + +```json +{ + "type": "file", + "from": "alice", + "timestamp": 1717000000000, + "filename": "cat.png", + "mime": "image/png", + "data": "" +} +``` + +### Системные сообщения + +Сервер → Все: + +```json +{ "type": "system", "text": "alice joined the chat" } +``` + +--- + +## Функциональность + +- **Текстовые сообщения** — отображаются с логином отправителя и временем +- **Файлы** — любые типы, до 10 МБ (настраивается). Файлы передаются через base64 в JSON +- **Изображения** — автоматически определяются по MIME-типу (`image/*`), рендерятся как ``, клик открывает в новой вкладке +- **Не-изображения** — отображаются как ссылка для скачивания с именем файла +- **Уведомления на неактивной вкладке** — звуковой сигнал (Web Audio API) и красная точка на favicon +- **Системные сообщения** — подключение / отключение участников +- **Скролл** — автоматический, кастомный тонкий скроллбар в теме приложения + +--- + +## Структура проекта + +``` +chat/ +├── server/ +│ ├── server.js # WebSocket сервер +│ ├── users.json # Хардкоженые логины/пароли +│ └── package.json +├── client/ +│ ├── src/ +│ │ ├── App.jsx # Корневой компонент +│ │ ├── App.css +│ │ ├── main.jsx # Точка входа +│ │ └── components/ +│ │ ├── Login.jsx / .css +│ │ ├── Chat.jsx / .css +│ │ ├── MessageList.jsx +│ │ ├── Message.jsx / .css +│ │ └── MessageInput.jsx / .css +│ ├── .env # Production настройки +│ ├── .env.development # Dev настройки +│ ├── index.html +│ ├── vite.config.js +│ └── package.json +├── Dockerfile # Многостадийная сборка +├── docker-entrypoint.sh # Точка входа контейнера +├── nginx.conf # Конфигурация nginx +└── README.md +``` diff --git a/client/.env b/client/.env new file mode 100644 index 0000000..9edf2f5 --- /dev/null +++ b/client/.env @@ -0,0 +1 @@ +VITE_WS_URL=/ws diff --git a/client/.env.development b/client/.env.development new file mode 100644 index 0000000..2ec742f --- /dev/null +++ b/client/.env.development @@ -0,0 +1 @@ +VITE_WS_URL=ws://localhost:8080 diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..8fd9d83 --- /dev/null +++ b/client/index.html @@ -0,0 +1,12 @@ + + + + + + Chat Room + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..fb7fa3d --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,1719 @@ +{ + "name": "chat-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-client", + "version": "1.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.363", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.363.tgz", + "integrity": "sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "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.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..178cce2 --- /dev/null +++ b/client/package.json @@ -0,0 +1,19 @@ +{ + "name": "chat-client", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.1.0" + } +} diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..da32751 --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,17 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body, #root { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, sans-serif; + background: #1a1a2e; + color: #e0e0e0; +} + +a { + color: #7ec8e3; +} diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..1bd3090 --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,75 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import Login from './components/Login'; +import Chat from './components/Chat'; + +const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8080'; + +export default function App() { + const [loggedIn, setLoggedIn] = useState(false); + const [login, setLogin] = useState(''); + const [error, setError] = useState(''); + const wsRef = useRef(null); + + const handleLogin = useCallback((userLogin, password) => { + setError(''); + const ws = new WebSocket(WS_URL); + + ws.onopen = () => { + ws.send(JSON.stringify({ type: 'auth', login: userLogin, password })); + }; + + ws.onmessage = (event) => { + let msg; + try { + msg = JSON.parse(event.data); + } catch { + return; + } + + if (msg.type === 'auth_result') { + if (msg.success) { + setLogin(userLogin); + setLoggedIn(true); + } else { + setError(msg.reason || 'Authentication failed'); + ws.close(); + } + } + }; + + ws.onerror = () => { + setError('Cannot connect to server'); + }; + + ws.onclose = () => { + if (!loggedIn) { + setError('Connection closed'); + } + }; + + wsRef.current = ws; + }, [loggedIn]); + + const handleLogout = useCallback(() => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + setLoggedIn(false); + setLogin(''); + }, []); + + useEffect(() => { + return () => { + if (wsRef.current) { + wsRef.current.close(); + } + }; + }, []); + + if (!loggedIn) { + return ; + } + + return ; +} diff --git a/client/src/components/Chat.css b/client/src/components/Chat.css new file mode 100644 index 0000000..045bb82 --- /dev/null +++ b/client/src/components/Chat.css @@ -0,0 +1,52 @@ +.chat-container { + display: flex; + flex-direction: column; + height: 100%; + max-width: 800px; + margin: 0 auto; + padding: 0 1rem; +} + +.chat-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 0; + border-bottom: 1px solid #2a2a4a; +} + +.chat-header h2 { + font-size: 1.1rem; + font-weight: 500; +} + +.logout-btn { + padding: 0.4rem 1rem; + border: 1px solid #5c5c7a; + border-radius: 6px; + background: transparent; + color: #e0e0e0; + cursor: pointer; + font-size: 0.85rem; + transition: background 0.2s; +} + +.logout-btn:hover { + background: #2a2a4a; +} + +.chat-error { + background: #5c1a1a; + color: #ff8a8a; + padding: 0.5rem 1rem; + text-align: center; + font-size: 0.9rem; +} + +.chat-notification { + background: #1a3a5c; + color: #8ab8ff; + padding: 0.5rem 1rem; + text-align: center; + font-size: 0.9rem; +} diff --git a/client/src/components/Chat.jsx b/client/src/components/Chat.jsx new file mode 100644 index 0000000..3a57b05 --- /dev/null +++ b/client/src/components/Chat.jsx @@ -0,0 +1,137 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import MessageList from './MessageList'; +import MessageInput from './MessageInput'; +import './Chat.css'; + +function setFavicon(hasNotification) { + const canvas = document.createElement('canvas'); + canvas.width = 32; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#4a9eff'; + ctx.beginPath(); + ctx.arc(16, 16, 14, 0, 2 * Math.PI); + ctx.fill(); + + ctx.fillStyle = '#fff'; + ctx.font = 'bold 18px sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('CH', 16, 16); + + if (hasNotification) { + ctx.fillStyle = '#ff4444'; + ctx.beginPath(); + ctx.arc(26, 6, 5, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = '#1a1a2e'; + ctx.lineWidth = 2; + ctx.stroke(); + } + + let link = document.querySelector('link[rel="icon"]'); + if (!link) { + link = document.createElement('link'); + link.rel = 'icon'; + document.head.appendChild(link); + } + link.href = canvas.toDataURL(); +} + +export default function Chat({ ws, login, onLogout }) { + const [messages, setMessages] = useState([]); + const [connected, setConnected] = useState(true); + const [error, setError] = useState(''); + const hasNotifRef = useRef(false); + const messagesRef = useRef(messages); + messagesRef.current = messages; + + const addMessage = useCallback((msg) => { + setMessages(prev => [...prev, { ...msg, id: Date.now() + Math.random() }]); + if ( + document.hidden && + msg.from && + msg.from !== login && + (msg.type === 'text' || msg.type === 'file') + ) { + if (!hasNotifRef.current) { + hasNotifRef.current = true; + setFavicon(true); + } + try { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.connect(gain); + gain.connect(ctx.destination); + osc.frequency.value = 520; + gain.gain.value = 0.15; + osc.start(); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15); + osc.stop(ctx.currentTime + 0.15); + osc.onended = () => ctx.close(); + } catch {} + } + }, [login]); + + useEffect(() => { + setFavicon(false); + const onVisible = () => { + if (hasNotifRef.current) { + hasNotifRef.current = false; + setFavicon(false); + } + }; + document.addEventListener('visibilitychange', onVisible); + return () => { + document.removeEventListener('visibilitychange', onVisible); + }; + }, []); + + useEffect(() => { + if (!ws) return; + + ws.onmessage = (event) => { + let msg; + try { + msg = JSON.parse(event.data); + } catch { + return; + } + + if (msg.type === 'text') { + addMessage(msg); + } else if (msg.type === 'file') { + addMessage(msg); + } else if (msg.type === 'system') { + addMessage(msg); + } else if (msg.type === 'error') { + setError(msg.message); + } + }; + + ws.onclose = () => { + setConnected(false); + setError('Connection lost'); + }; + + ws.onerror = () => { + setConnected(false); + setError('Connection error'); + }; + }, [ws, addMessage]); + + return ( +
+
+

Chat room — logged in as {login}

+ +
+ {error && !connected &&
{error}
} + {error && connected &&
{error}
} + + +
+ ); +} diff --git a/client/src/components/Login.css b/client/src/components/Login.css new file mode 100644 index 0000000..fb9a496 --- /dev/null +++ b/client/src/components/Login.css @@ -0,0 +1,65 @@ +.login-container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.login-form { + background: #16213e; + padding: 2.5rem; + border-radius: 12px; + width: 100%; + max-width: 360px; + display: flex; + flex-direction: column; + gap: 1rem; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.login-form h1 { + text-align: center; + margin-bottom: 0.5rem; + font-size: 1.6rem; + color: #e0e0e0; +} + +.login-error { + background: #5c1a1a; + color: #ff8a8a; + padding: 0.6rem 1rem; + border-radius: 8px; + font-size: 0.9rem; + text-align: center; +} + +.login-form input { + padding: 0.75rem 1rem; + border: 1px solid #2a2a4a; + border-radius: 8px; + background: #1a1a2e; + color: #e0e0e0; + font-size: 1rem; + outline: none; + transition: border-color 0.2s; +} + +.login-form input:focus { + border-color: #4a9eff; +} + +.login-form button { + padding: 0.75rem; + border: none; + border-radius: 8px; + background: #4a9eff; + color: #fff; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.login-form button:hover { + background: #357abd; +} diff --git a/client/src/components/Login.jsx b/client/src/components/Login.jsx new file mode 100644 index 0000000..0d9585c --- /dev/null +++ b/client/src/components/Login.jsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import './Login.css'; + +export default function Login({ onLogin, error }) { + const [login, setLogin] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = (e) => { + e.preventDefault(); + if (!login.trim() || !password.trim()) return; + onLogin(login.trim(), password); + }; + + return ( +
+
+

Chat Login

+ {error &&
{error}
} + setLogin(e.target.value)} + autoFocus + /> + setPassword(e.target.value)} + /> + +
+
+ ); +} diff --git a/client/src/components/Message.css b/client/src/components/Message.css new file mode 100644 index 0000000..5a84d56 --- /dev/null +++ b/client/src/components/Message.css @@ -0,0 +1,129 @@ +.message-list { + flex: 1; + overflow-y: auto; + padding: 1rem 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + scrollbar-width: thin; + scrollbar-color: #3a3a5a transparent; +} + +.message-list::-webkit-scrollbar { + width: 6px; +} + +.message-list::-webkit-scrollbar-track { + background: transparent; +} + +.message-list::-webkit-scrollbar-thumb { + background: #3a3a5a; + border-radius: 3px; +} + +.message-list::-webkit-scrollbar-thumb:hover { + background: #4a4a6a; +} + +.message-list-empty { + text-align: center; + color: #6a6a8a; + margin-top: 3rem; + font-size: 0.95rem; +} + +.message-system { + text-align: center; + color: #6a6a8a; + font-size: 0.85rem; + padding: 0.3rem 0; + font-style: italic; +} + +.message { + max-width: 75%; + padding: 0.6rem 1rem; + border-radius: 12px; + word-wrap: break-word; +} + +.message-own { + align-self: flex-end; + background: #2b4a7a; + border-bottom-right-radius: 4px; +} + +.message-other { + align-self: flex-start; + background: #2a2a4a; + border-bottom-left-radius: 4px; +} + +.message-header { + display: flex; + align-items: baseline; + gap: 0.5rem; + margin-bottom: 0.3rem; +} + +.message-author { + font-weight: 600; + font-size: 0.85rem; + color: #8ab8ff; +} + +.message-time { + font-size: 0.7rem; + color: #6a6a8a; +} + +.message-body p { + margin-bottom: 0.3rem; + line-height: 1.4; +} + +.message-body p:last-child { + margin-bottom: 0; +} + +.message-image { + max-width: 100%; + max-height: 300px; + border-radius: 8px; + cursor: pointer; + display: block; + margin-top: 0.3rem; + transition: opacity 0.2s; +} + +.message-image:hover { + opacity: 0.9; +} + +.message-file-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.4rem 0.6rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 6px; + text-decoration: none; + color: #7ec8e3; + font-size: 0.9rem; + margin-top: 0.3rem; +} + +.message-file-link:hover { + background: rgba(255, 255, 255, 0.1); +} + +.file-icon { + font-size: 1.1rem; +} + +.message-file-error { + color: #ff6a6a; + font-size: 0.85rem; + margin-top: 0.3rem; +} diff --git a/client/src/components/Message.jsx b/client/src/components/Message.jsx new file mode 100644 index 0000000..257eedc --- /dev/null +++ b/client/src/components/Message.jsx @@ -0,0 +1,113 @@ +import React, { useMemo } from 'react'; +import './Message.css'; + +function formatTime(ts) { + const d = new Date(ts); + return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +const URL_REGEX = /(https?:\/\/[^\s<]+[^\s<.,!?)}\]"'\\])/gi; + +function linkifyText(text) { + const parts = []; + let lastIndex = 0; + let match; + URL_REGEX.lastIndex = 0; + while ((match = URL_REGEX.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(text.slice(lastIndex, match.index)); + } + parts.push( + + {match[0]} + + ); + lastIndex = match.index + match[0].length; + } + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + return parts.length ? parts : text; +} + +function dataURLToBlobUrl(dataUrl) { + try { + const arr = dataUrl.split(','); + const mime = arr[0].match(/:(.*?);/)[1]; + const bstr = atob(arr[1]); + const n = bstr.length; + const u8arr = new Uint8Array(n); + for (let i = 0; i < n; i++) { + u8arr[i] = bstr.charCodeAt(i); + } + const blob = new Blob([u8arr], { type: mime }); + return URL.createObjectURL(blob); + } catch { + return null; + } +} + +export default function Message({ message, isOwn }) { + const isImage = message.mime && message.mime.startsWith('image/'); + const isSystem = message.type === 'system'; + const isFile = message.type === 'file'; + + const blobUrl = useMemo(() => { + if (isFile && message.data) { + return dataURLToBlobUrl(`data:${message.mime};base64,${message.data}`); + } + return null; + }, [isFile, message.data, message.mime]); + + const downloadUrl = useMemo(() => { + if (isFile && message.data && !isImage) { + return dataURLToBlobUrl(`data:${message.mime};base64,${message.data}`); + } + return null; + }, [isFile, message.data, message.mime, isImage]); + + if (isSystem) { + return ( +
+ {message.text} +
+ ); + } + + return ( +
+
+ {message.from} + {formatTime(message.timestamp)} +
+
+ {message.text &&

{linkifyText(message.text)}

} + {isFile && isImage && blobUrl && ( + + {message.filename} + + )} + {isFile && isImage && !blobUrl && ( +
Failed to load image
+ )} + {isFile && !isImage && downloadUrl && ( + + 📎 + {message.filename} + + )} + {isFile && !isImage && !downloadUrl && ( +
Failed to load file
+ )} +
+
+ ); +} diff --git a/client/src/components/MessageInput.css b/client/src/components/MessageInput.css new file mode 100644 index 0000000..cdea5be --- /dev/null +++ b/client/src/components/MessageInput.css @@ -0,0 +1,58 @@ +.message-input-container { + padding: 1rem 0; + border-top: 1px solid #2a2a4a; +} + +.message-input-form { + display: flex; + gap: 0.5rem; +} + +.message-input-field { + flex: 1; + padding: 0.7rem 1rem; + border: 1px solid #2a2a4a; + border-radius: 8px; + background: #16213e; + color: #e0e0e0; + font-size: 0.95rem; + outline: none; + transition: border-color 0.2s; +} + +.message-input-field:focus { + border-color: #4a9eff; +} + +.attach-btn, .send-btn { + padding: 0.7rem 1.2rem; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 0.95rem; + transition: background 0.2s; +} + +.attach-btn { + background: #2a2a4a; + color: #e0e0e0; + font-size: 1.1rem; +} + +.attach-btn:hover { + background: #3a3a5a; +} + +.send-btn { + background: #4a9eff; + color: #fff; + font-weight: 600; +} + +.send-btn:hover { + background: #357abd; +} + +.file-input-hidden { + display: none; +} diff --git a/client/src/components/MessageInput.jsx b/client/src/components/MessageInput.jsx new file mode 100644 index 0000000..de10638 --- /dev/null +++ b/client/src/components/MessageInput.jsx @@ -0,0 +1,71 @@ +import React, { useState, useRef } from 'react'; +import './MessageInput.css'; + +const MAX_FILE_SIZE = 10 * 1024 * 1024; + +export default function MessageInput({ ws }) { + const [text, setText] = useState(''); + const fileInputRef = useRef(null); + + const sendText = (e) => { + e.preventDefault(); + if (!text.trim()) return; + ws.send(JSON.stringify({ type: 'text', text: text.trim() })); + setText(''); + }; + + const handleFileSelect = (e) => { + const file = e.target.files[0]; + if (!file) return; + + if (file.size > MAX_FILE_SIZE) { + alert('File too large. Maximum is 10 MB.'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result.split(',')[1]; + ws.send(JSON.stringify({ + type: 'file', + filename: file.name, + mime: file.type || 'application/octet-stream', + data: base64 + })); + }; + reader.onerror = () => { + alert('Failed to read file'); + }; + reader.readAsDataURL(file); + e.target.value = ''; + }; + + const handleAttachClick = () => { + fileInputRef.current?.click(); + }; + + return ( +
+
+ setText(e.target.value)} + /> + + +
+ +
+ ); +} diff --git a/client/src/components/MessageList.jsx b/client/src/components/MessageList.jsx new file mode 100644 index 0000000..305d78d --- /dev/null +++ b/client/src/components/MessageList.jsx @@ -0,0 +1,22 @@ +import React, { useRef, useEffect } from 'react'; +import Message from './Message'; + +export default function MessageList({ messages, currentUser }) { + const bottomRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + return ( +
+ {messages.length === 0 && ( +
No messages yet. Start chatting!
+ )} + {messages.map((msg) => ( + + ))} +
+
+ ); +} diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 0000000..64c5fb9 --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './App.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..f675a8e --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000 + } +}); diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..839a3c2 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +# Start nginx in foreground +nginx -g 'daemon off;' & +NGINX_PID=$! + +# Start WebSocket server +node /server/server.js & +NODE_PID=$! + +# Forward signals to both processes +trap 'kill $NGINX_PID $NODE_PID 2>/dev/null; exit' SIGTERM SIGINT + +# Exit if node server stops +wait $NODE_PID diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..e2eedf3 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 50M; + + gzip on; + gzip_types text/css application/javascript image/svg+xml; + gzip_min_length 256; + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "public, max-age=3600"; + } + + location /ws { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 86400; + } +} diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..a70e76a --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "chat-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-server", + "version": "1.0.0", + "dependencies": { + "ws": "^8.16.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..20a2936 --- /dev/null +++ b/server/package.json @@ -0,0 +1,12 @@ +{ + "name": "chat-server", + "version": "1.0.0", + "description": "WebSocket chat server with file sharing", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "ws": "^8.16.0" + } +} diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..7bffe0b --- /dev/null +++ b/server/server.js @@ -0,0 +1,129 @@ +const fs = require('fs'); +const path = require('path'); +const { WebSocketServer } = require('ws'); + +const PORT = parseInt(process.env.PORT, 10) || 8080; +const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE, 10) || 10 * 1024 * 1024; + +const usersPath = path.join(__dirname, process.env.USERS_FILE || 'users.json'); +let validUsers; +try { + validUsers = JSON.parse(fs.readFileSync(usersPath, 'utf-8')); + console.log(`Loaded ${validUsers.length} users from users.json`); +} catch (err) { + console.error('Failed to load users.json:', err.message); + process.exit(1); +} + +function authenticate(login, password) { + return validUsers.find(u => u.login === login && u.password === password) || null; +} + +const wss = new WebSocketServer({ port: PORT }); +console.log(`WebSocket server running on ws://localhost:${PORT}`); + +wss.on('connection', (ws, req) => { + const clientIp = req.socket.remoteAddress; + console.log(`New connection from ${clientIp}`); + + let authenticated = false; + let userLogin = null; + + ws.on('message', (data) => { + let parsed; + try { + parsed = JSON.parse(data.toString()); + } catch (e) { + ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' })); + return; + } + + if (!authenticated) { + if (parsed.type === 'auth') { + const user = authenticate(parsed.login, parsed.password); + if (user) { + authenticated = true; + userLogin = parsed.login; + console.log(`User authenticated: ${userLogin}`); + ws.send(JSON.stringify({ type: 'auth_result', success: true })); + broadcast({ + type: 'system', + text: `${userLogin} joined the chat` + }, null); + } else { + console.log(`Auth failed for login: ${parsed.login}`); + ws.send(JSON.stringify({ + type: 'auth_result', + success: false, + reason: 'Invalid login or password' + })); + } + } else { + ws.send(JSON.stringify({ type: 'error', message: 'Authenticate first' })); + } + return; + } + + if (parsed.type === 'text') { + if (!parsed.text || typeof parsed.text !== 'string') { + ws.send(JSON.stringify({ type: 'error', message: 'Invalid text message' })); + return; + } + console.log(`Text from ${userLogin}: ${parsed.text.substring(0, 50)}`); + broadcast({ + type: 'text', + from: userLogin, + timestamp: Date.now(), + text: parsed.text + }, null); + return; + } + + if (parsed.type === 'file') { + if (!parsed.filename || !parsed.mime || !parsed.data) { + ws.send(JSON.stringify({ type: 'error', message: 'Invalid file message' })); + return; + } + const size = Math.ceil((parsed.data.length * 3) / 4); + if (size > MAX_FILE_SIZE) { + ws.send(JSON.stringify({ type: 'error', message: 'File too large (max 10 MB)' })); + return; + } + console.log(`File from ${userLogin}: ${parsed.filename} (${parsed.mime}, ~${size} bytes)`); + broadcast({ + type: 'file', + from: userLogin, + timestamp: Date.now(), + filename: parsed.filename, + mime: parsed.mime, + data: parsed.data + }, null); + return; + } + + ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' })); + }); + + ws.on('close', (code, reason) => { + console.log(`Connection closed${userLogin ? ` for ${userLogin}` : ''} (code: ${code})`); + if (userLogin) { + broadcast({ + type: 'system', + text: `${userLogin} left the chat` + }, null); + } + }); + + ws.on('error', (err) => { + console.error(`WebSocket error${userLogin ? ` for ${userLogin}` : ''}:`, err.message); + }); +}); + +function broadcast(message, excludeWs) { + const json = JSON.stringify(message); + wss.clients.forEach(client => { + if (client !== excludeWs && client.readyState === 1) { + client.send(json); + } + }); +} diff --git a/server/users.json b/server/users.json new file mode 100644 index 0000000..6744151 --- /dev/null +++ b/server/users.json @@ -0,0 +1,4 @@ +[ + { "login": "alice", "password": "123" }, + { "login": "bob", "password": "456" } +]