From b4e513e996c9f13eb0722fed721407c6bf94b04f Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Sun, 30 Nov 2025 00:53:15 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Korea=20Tourism=20Agency=20-=20C?= =?UTF-8?q?omplete=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ Features: - Modern tourism website with responsive design - AdminJS admin panel with image editor integration - PostgreSQL database with comprehensive schema - Docker containerization - Image upload and gallery management 🛠 Tech Stack: - Backend: Node.js + Express.js - Database: PostgreSQL 13+ - Frontend: HTML/CSS/JS with responsive design - Admin: AdminJS with custom components - Deployment: Docker + Docker Compose - Image Processing: Sharp with optimization 📱 Admin Features: - Routes/Tours management (city, mountain, fishing) - Guides profiles with specializations - Articles and blog system - Image editor with upload/gallery/URL options - User management and authentication - Responsive admin interface 🎨 Design: - Korean tourism focused branding - Mobile-first responsive design - Custom CSS with modern aesthetics - Image optimization and gallery - SEO-friendly structure 🔒 Security: - Helmet.js security headers - bcrypt password hashing - Input validation and sanitization - CORS protection - Environment variables --- .dockerignore | 11 + .gitignore | 13 +- database/apply-image-fix.js | 27 + database/check-admins.cjs | 58 + database/check-article-categories.js | 40 + database/check-articles-schema.cjs | 31 + database/check-tables.js | 51 + database/create-admin.sql | 7 + database/create-test-admin.cjs | 25 + database/image-triggers-fix.sql | 57 + database/init-database.js | 13 + database/update-test-images.js | 36 + package-lock.json | 1497 +++++++++++++++++++---- package.json | 9 +- public/admin-image-editor-demo.html | 187 +++ public/css/admin-custom.css | 86 ++ public/image-editor-compact.html | 543 ++++++++ public/image-editor.html | 448 +++++++ public/image-system-docs.html | 275 +++++ public/images/placeholders/no-image.svg | 13 + public/js/admin-custom.js | 118 ++ public/js/admin-image-loader.js | 13 + public/js/admin-image-selector-fixed.js | 314 +++++ public/js/admin-image-selector.js | 300 +++++ public/js/image-editor.js | 690 +++++++++++ public/test-editor.html | 103 ++ public/test-image-editor.html | 191 +++ src/app.js | 16 +- src/components/ImageEditor.jsx | 271 ++++ src/components/ImageSelector.jsx | 130 ++ src/config/adminjs-simple.js | 31 +- src/routes/crud.js | 653 ++++++++++ src/routes/image-upload.js | 246 ++++ src/routes/images.js | 259 ++++ test-crud.js | 370 ++++++ views/layout.ejs | 1 + 36 files changed, 6894 insertions(+), 239 deletions(-) create mode 100644 .dockerignore create mode 100644 database/apply-image-fix.js create mode 100644 database/check-admins.cjs create mode 100644 database/check-article-categories.js create mode 100644 database/check-articles-schema.cjs create mode 100644 database/check-tables.js create mode 100644 database/create-admin.sql create mode 100644 database/create-test-admin.cjs create mode 100644 database/image-triggers-fix.sql create mode 100644 database/update-test-images.js create mode 100644 public/admin-image-editor-demo.html create mode 100644 public/image-editor-compact.html create mode 100644 public/image-editor.html create mode 100644 public/image-system-docs.html create mode 100644 public/images/placeholders/no-image.svg create mode 100644 public/js/admin-image-loader.js create mode 100644 public/js/admin-image-selector-fixed.js create mode 100644 public/js/admin-image-selector.js create mode 100644 public/js/image-editor.js create mode 100644 public/test-editor.html create mode 100644 public/test-image-editor.html create mode 100644 src/components/ImageEditor.jsx create mode 100644 src/components/ImageSelector.jsx create mode 100644 src/routes/crud.js create mode 100644 src/routes/image-upload.js create mode 100644 src/routes/images.js create mode 100644 test-crud.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d2cad89 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.DS_Store +*.log +coverage/ +.nyc_output +.vscode/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index a1fcdf3..39e5b00 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ yarn-error.log* *.sqlite *.sqlite3 *.db +database/backups/ # Node modules node_modules/ @@ -41,13 +42,21 @@ build/ # History .history/ -# Docker -.dockerignore +# Docker volumes +docker-data/ +postgres-data/ # Temporary files tmp/ temp/ +# Uploaded files (keep structure but ignore content) +public/uploads/* +!public/uploads/.gitkeep + +# Runtime files +*.pid + # Uploaded files public/uploads/* !public/uploads/.gitkeep diff --git a/database/apply-image-fix.js b/database/apply-image-fix.js new file mode 100644 index 0000000..d5e60d7 --- /dev/null +++ b/database/apply-image-fix.js @@ -0,0 +1,27 @@ +import db from '../src/config/database.js'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function applyImageTriggersFix() { + try { + console.log('🖼️ Applying image triggers fix migration...'); + + const migrationPath = path.join(__dirname, 'image-triggers-fix.sql'); + const sql = fs.readFileSync(migrationPath, 'utf8'); + + await db.query(sql); + console.log('✅ Image triggers fix applied successfully'); + + } catch (error) { + console.log('ℹ️ Some changes may already be applied:', error.message); + } + + process.exit(0); +} + +applyImageTriggersFix(); \ No newline at end of file diff --git a/database/check-admins.cjs b/database/check-admins.cjs new file mode 100644 index 0000000..eee1e61 --- /dev/null +++ b/database/check-admins.cjs @@ -0,0 +1,58 @@ +require('dotenv').config(); +const { Pool } = require('pg'); + +async function checkAdminsTable() { + const pool = new Pool({ + connectionString: process.env.DATABASE_URL + }); + + try { + console.log('🔍 Проверяем таблицу admins...'); + + // Проверяем, существует ли таблица + const tableExists = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'admins' + ) + `); + + console.log('📋 Таблица admins существует:', tableExists.rows[0].exists); + + if (tableExists.rows[0].exists) { + // Получаем структуру таблицы + const structure = await pool.query(` + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = 'admins' + ORDER BY ordinal_position + `); + + console.log('\n📋 Структура таблицы admins:'); + structure.rows.forEach(row => { + console.log(` - ${row.column_name}: ${row.data_type} ${row.is_nullable === 'NO' ? '(NOT NULL)' : '(nullable)'}`); + }); + + // Проверяем количество записей + const count = await pool.query('SELECT COUNT(*) FROM admins'); + console.log(`\n📊 Записей в таблице: ${count.rows[0].count}`); + + // Получаем несколько записей для примера + const sample = await pool.query('SELECT id, username, name, role, is_active FROM admins LIMIT 3'); + console.log('\n👥 Примеры записей:'); + sample.rows.forEach(admin => { + console.log(` - ID: ${admin.id}, Username: ${admin.username}, Name: ${admin.name}, Role: ${admin.role}, Active: ${admin.is_active}`); + }); + } else { + console.log('❌ Таблица admins не существует! Нужно создать её.'); + } + + } catch (error) { + console.error('❌ Ошибка:', error.message); + } finally { + await pool.end(); + } +} + +checkAdminsTable(); \ No newline at end of file diff --git a/database/check-article-categories.js b/database/check-article-categories.js new file mode 100644 index 0000000..b8640b6 --- /dev/null +++ b/database/check-article-categories.js @@ -0,0 +1,40 @@ +import db from '../src/config/database.js'; + +async function checkArticleCategories() { + try { + console.log('🔍 Проверяем ограничения на категории articles...'); + + // Проверяем constraint (для новых версий PostgreSQL) + const constraints = await db.query(` + SELECT conname, pg_get_constraintdef(oid) as definition + FROM pg_constraint + WHERE conrelid = 'articles'::regclass + AND contype = 'c' + `); + + console.log('\n📋 Ограничения articles:'); + constraints.rows.forEach(row => { + console.log(` - ${row.conname}: ${row.definition}`); + }); + + // Проверяем существующие категории + const existingCategories = await db.query(` + SELECT DISTINCT category + FROM articles + WHERE category IS NOT NULL + ORDER BY category + `); + + console.log('\n📂 Существующие категории:'); + existingCategories.rows.forEach(row => { + console.log(` - ${row.category}`); + }); + + } catch (error) { + console.error('❌ Ошибка:', error.message); + } + + process.exit(0); +} + +checkArticleCategories(); \ No newline at end of file diff --git a/database/check-articles-schema.cjs b/database/check-articles-schema.cjs new file mode 100644 index 0000000..8e89cf6 --- /dev/null +++ b/database/check-articles-schema.cjs @@ -0,0 +1,31 @@ +require('dotenv').config(); +const { Pool } = require('pg'); + +async function checkArticlesSchema() { + const pool = new Pool({ + connectionString: process.env.DATABASE_URL + }); + + try { + console.log('🔍 Проверяем структуру таблицы articles...'); + + const result = await pool.query(` + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = 'articles' + ORDER BY ordinal_position + `); + + console.log('\n📋 Структура таблицы articles:'); + result.rows.forEach(row => { + console.log(` - ${row.column_name}: ${row.data_type} ${row.is_nullable === 'NO' ? '(NOT NULL)' : '(nullable)'} ${row.column_default ? `default: ${row.column_default}` : ''}`); + }); + + } catch (error) { + console.error('❌ Ошибка:', error.message); + } finally { + await pool.end(); + } +} + +checkArticlesSchema(); \ No newline at end of file diff --git a/database/check-tables.js b/database/check-tables.js new file mode 100644 index 0000000..ef19596 --- /dev/null +++ b/database/check-tables.js @@ -0,0 +1,51 @@ +import db from '../src/config/database.js'; + +async function checkTables() { + try { + console.log('🔍 Проверяем структуру таблиц...'); + + // Проверяем guides + const guidesDesc = await db.query(` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'guides' + ORDER BY ordinal_position + `); + + console.log('\n📋 Таблица guides:'); + guidesDesc.rows.forEach(row => { + console.log(` - ${row.column_name}: ${row.data_type}`); + }); + + // Проверяем articles + const articlesDesc = await db.query(` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'articles' + ORDER BY ordinal_position + `); + + console.log('\n📰 Таблица articles:'); + articlesDesc.rows.forEach(row => { + console.log(` - ${row.column_name}: ${row.data_type}`); + }); + + // Проверяем тип поля languages в guides + const languagesType = await db.query(` + SELECT data_type, column_default + FROM information_schema.columns + WHERE table_name = 'guides' AND column_name = 'languages' + `); + + if (languagesType.rows.length > 0) { + console.log(`\n🔤 Поле languages: ${languagesType.rows[0].data_type}`); + } + + } catch (error) { + console.error('❌ Ошибка:', error.message); + } + + process.exit(0); +} + +checkTables(); \ No newline at end of file diff --git a/database/create-admin.sql b/database/create-admin.sql new file mode 100644 index 0000000..b59a43d --- /dev/null +++ b/database/create-admin.sql @@ -0,0 +1,7 @@ +-- Создание тестового администратора для проверки +INSERT INTO admins (username, password, name, email, role) VALUES +('admin', '$2a$10$rOjLbFbCqbCQPZdJQWb1gO6WvhzJP1O5VuItXwDJV4tTJYg4oEGoC', 'Главный администратор', 'admin@koreatour.ru', 'admin') +ON CONFLICT (username) DO UPDATE SET + name = EXCLUDED.name, + email = EXCLUDED.email, + role = EXCLUDED.role; \ No newline at end of file diff --git a/database/create-test-admin.cjs b/database/create-test-admin.cjs new file mode 100644 index 0000000..b3d32c2 --- /dev/null +++ b/database/create-test-admin.cjs @@ -0,0 +1,25 @@ +// Создание тестового администратора для AdminJS +const crypto = require('crypto'); + +function generateBcryptHash(password) { + // Упрощенная версия - используем готовый хеш + // Пароль: admin123 + return '$2a$10$rOjLbFbCqbCQPZdJQWb1gO6WvhzJP1O5VuItXwDJV4tTJYg4oEGoC'; +} + +function createTestAdmin() { + const hashedPassword = generateBcryptHash('admin123'); + + console.log('🔐 Создаем тестового администратора...'); + console.log('📧 Email: admin@koreatour.ru'); + console.log('👤 Username: admin'); + console.log('🔑 Password: admin123'); + console.log('🔒 Hashed password:', hashedPassword); + + const sql = `INSERT INTO admins (username, password, name, email, role, is_active) VALUES ('admin', '${hashedPassword}', 'Главный администратор', 'admin@koreatour.ru', 'admin', true) ON CONFLICT (username) DO UPDATE SET password = EXCLUDED.password, name = EXCLUDED.name, email = EXCLUDED.email, role = EXCLUDED.role, is_active = EXCLUDED.is_active;`; + + console.log('\n▶️ Запустите эту команду:'); + console.log(`docker-compose exec db psql -U tourism_user -d korea_tourism -c "${sql}"`); +} + +createTestAdmin(); \ No newline at end of file diff --git a/database/image-triggers-fix.sql b/database/image-triggers-fix.sql new file mode 100644 index 0000000..a59d257 --- /dev/null +++ b/database/image-triggers-fix.sql @@ -0,0 +1,57 @@ +-- Исправление триггеров для таблиц с изображениями +-- Запускается автоматически при инициализации базы данных + +-- Сначала удаляем существующие проблемные триггеры если они есть +DROP TRIGGER IF EXISTS routes_updated_at_trigger ON routes; +DROP TRIGGER IF EXISTS guides_updated_at_trigger ON guides; + +-- Создаем функцию для обновления updated_at (если её нет) +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Создаем триггеры для обновления updated_at при изменении записей +CREATE TRIGGER routes_updated_at_trigger + BEFORE UPDATE ON routes + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER guides_updated_at_trigger + BEFORE UPDATE ON guides + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Добавляем индексы для оптимизации запросов к изображениям +CREATE INDEX IF NOT EXISTS idx_routes_image_url ON routes(image_url) WHERE image_url IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_guides_image_url ON guides(image_url) WHERE image_url IS NOT NULL; + +-- Создаем таблицу для метаданных загруженных изображений (опционально) +CREATE TABLE IF NOT EXISTS image_metadata ( + id SERIAL PRIMARY KEY, + url VARCHAR(500) NOT NULL UNIQUE, + filename VARCHAR(255) NOT NULL, + original_name VARCHAR(255), + size_bytes INTEGER, + width INTEGER, + height INTEGER, + mime_type VARCHAR(100), + entity_type VARCHAR(50), -- 'routes', 'guides', 'articles', etc. + entity_id INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Индекс для поиска изображений по типу сущности +CREATE INDEX IF NOT EXISTS idx_image_metadata_entity ON image_metadata(entity_type, entity_id); + +-- Триггер для image_metadata +CREATE TRIGGER image_metadata_updated_at_trigger + BEFORE UPDATE ON image_metadata + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +COMMIT; \ No newline at end of file diff --git a/database/init-database.js b/database/init-database.js index eed3776..1d12c30 100644 --- a/database/init-database.js +++ b/database/init-database.js @@ -79,6 +79,19 @@ export async function initDatabase() { } } + // 4. Apply image triggers fix migration + try { + console.log('🖼️ Installing image triggers fix...'); + const imageTriggersMigrationPath = path.join(__dirname, 'image-triggers-fix.sql'); + if (fs.existsSync(imageTriggersMigrationPath)) { + const imageTriggersMigration = fs.readFileSync(imageTriggersMigrationPath, 'utf8'); + await db.query(imageTriggersMigration); + console.log('✅ Image triggers fix applied successfully'); + } + } catch (error) { + console.log('ℹ️ Image triggers fix - some changes may already be applied:', error.message); + } + console.log('✨ Database initialization completed successfully!'); } catch (error) { diff --git a/database/update-test-images.js b/database/update-test-images.js new file mode 100644 index 0000000..171d39d --- /dev/null +++ b/database/update-test-images.js @@ -0,0 +1,36 @@ +import db from '../src/config/database.js'; + +async function updateTestImages() { + try { + console.log('🖼️ Обновляем тестовые данные с изображениями...'); + + // Обновляем первый тур с изображением + await db.query('UPDATE routes SET image_url = $1 WHERE id = 1', ['/uploads/routes/seoul-city-tour.jpg']); + + // Обновляем первого гида с изображением + await db.query('UPDATE guides SET image_url = $1 WHERE id = 1', ['/uploads/guides/guide-profile.jpg']); + + console.log('✅ Изображения добавлены в базу данных'); + + // Проверяем результат + const routes = await db.query('SELECT id, title, image_url FROM routes WHERE image_url IS NOT NULL LIMIT 3'); + const guides = await db.query('SELECT id, name, image_url FROM guides WHERE image_url IS NOT NULL LIMIT 3'); + + console.log('📋 Туры с изображениями:'); + routes.rows.forEach(row => { + console.log(` - ${row.title}: ${row.image_url}`); + }); + + console.log('👨‍🏫 Гиды с изображениями:'); + guides.rows.forEach(row => { + console.log(` - ${row.name}: ${row.image_url}`); + }); + + } catch (error) { + console.error('❌ Ошибка:', error); + } + + process.exit(0); +} + +updateTestImages(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d953362..9af7b9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@adminjs/express": "^6.1.0", "@adminjs/sequelize": "^4.1.1", "@adminjs/upload": "^4.0.2", + "@babel/core": "^7.28.5", + "@babel/preset-react": "^7.28.5", "adminjs": "^7.5.0", "bcryptjs": "^2.4.3", "bootstrap": "^5.3.2", @@ -29,62 +31,19 @@ "method-override": "^3.0.0", "moment": "^2.29.4", "morgan": "^1.10.0", - "multer": "^2.0.0", + "multer": "^1.4.5-lts.1", "pg": "^8.11.3", "pg-hstore": "^2.3.4", + "react": "^19.2.0", + "react-dom": "^19.2.0", "sequelize": "^6.37.7", - "sequelize-cli": "^6.6.3" + "sequelize-cli": "^6.6.3", + "sharp": "^0.33.5" }, "devDependencies": { "nodemon": "^3.0.2" } }, - "node_modules/@adminjs/design-system": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "@hypnosphi/create-react-context": "^0.3.1", - "@tinymce/tinymce-react": "^4.3.2", - "@tiptap/core": "2.1.13", - "@tiptap/extension-bubble-menu": "2.1.13", - "@tiptap/extension-character-count": "2.1.13", - "@tiptap/extension-code": "2.1.13", - "@tiptap/extension-document": "2.1.13", - "@tiptap/extension-floating-menu": "2.1.13", - "@tiptap/extension-heading": "2.1.13", - "@tiptap/extension-image": "2.1.13", - "@tiptap/extension-link": "2.1.13", - "@tiptap/extension-table": "2.1.13", - "@tiptap/extension-table-cell": "2.1.13", - "@tiptap/extension-table-header": "2.1.13", - "@tiptap/extension-table-row": "2.1.13", - "@tiptap/extension-text": "2.1.13", - "@tiptap/extension-text-align": "2.1.13", - "@tiptap/extension-typography": "2.1.13", - "@tiptap/pm": "2.1.13", - "@tiptap/react": "2.1.13", - "@tiptap/starter-kit": "2.1.13", - "date-fns": "^2.29.3", - "flat": "^5.0.2", - "hoist-non-react-statics": "3.3.2", - "jw-paginate": "^1.0.4", - "lodash": "^4.17.21", - "polished": "^4.2.2", - "react-currency-input-field": "^3.6.10", - "react-datepicker": "^4.10.0", - "react-feather": "^2.0.10", - "react-phone-input-2": "^2.15.1", - "react-select": "^5.8.0", - "react-text-mask": "^5.5.0", - "styled-components": "5.3.9", - "styled-system": "^5.1.5", - "text-mask-addons": "^3.8.0" - }, - "peerDependencies": { - "react": "^18.1.0", - "react-dom": "^18.1.0" - } - }, "node_modules/@adminjs/express": { "version": "6.1.1", "license": "SEE LICENSE IN LICENSE", @@ -111,6 +70,8 @@ }, "node_modules/@adminjs/upload": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@adminjs/upload/-/upload-4.0.2.tgz", + "integrity": "sha512-SYnedQBcLuq0YOOek7AyasTSA+fQwXJk4pe2yimocBqbQDelDtl/0T3hZwi94A0qr9bN+jpZQefSRA79mqJjkg==", "license": "MIT", "optionalDependencies": { "@aws-sdk/client-s3": "^3.301.0", @@ -1009,6 +970,8 @@ }, "node_modules/@babel/core": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -2316,6 +2279,8 @@ }, "node_modules/@babel/preset-react": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -2412,8 +2377,20 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", @@ -2431,10 +2408,14 @@ }, "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, "node_modules/@emotion/cache": { "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", @@ -2446,10 +2427,14 @@ }, "node_modules/@emotion/hash": { "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" @@ -2457,10 +2442,14 @@ }, "node_modules/@emotion/memoize": { "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", "license": "MIT" }, "node_modules/@emotion/react": { "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -2483,6 +2472,8 @@ }, "node_modules/@emotion/serialize": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", @@ -2494,18 +2485,26 @@ }, "node_modules/@emotion/sheet": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", "license": "MIT" }, "node_modules/@emotion/stylis": { "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", "license": "MIT" }, "node_modules/@emotion/unitless": { "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", "license": "MIT", "peerDependencies": { "react": ">=16.8.0" @@ -2513,10 +2512,14 @@ }, "node_modules/@emotion/utils": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, "node_modules/@esbuild/linux-x64": { @@ -2535,6 +2538,8 @@ }, "node_modules/@floating-ui/core": { "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" @@ -2542,6 +2547,8 @@ }, "node_modules/@floating-ui/dom": { "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.3", @@ -2550,6 +2557,8 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@google-cloud/paginator": { @@ -2636,25 +2645,10 @@ "license": "MIT", "optional": true }, - "node_modules/@hello-pangea/dnd": { - "version": "16.6.0", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.24.1", - "css-box-model": "^1.2.1", - "memoize-one": "^6.0.0", - "raf-schd": "^4.0.3", - "react-redux": "^8.1.3", - "redux": "^4.2.1", - "use-memo-one": "^1.1.3" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@hypnosphi/create-react-context": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", + "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==", "license": "MIT", "dependencies": { "gud": "^1.0.0", @@ -2665,6 +2659,367 @@ "react": ">=0.14.0" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2806,6 +3161,8 @@ }, "node_modules/@remirror/core-constants": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz", + "integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==", "license": "MIT" }, "node_modules/@remix-run/router": { @@ -3655,6 +4012,8 @@ }, "node_modules/@styled-system/background": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz", + "integrity": "sha512-jtwH2C/U6ssuGSvwTN3ri/IyjdHb8W9X/g8Y0JLcrH02G+BW3OS8kZdHphF1/YyRklnrKrBT2ngwGUK6aqqV3A==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3662,6 +4021,8 @@ }, "node_modules/@styled-system/border": { "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@styled-system/border/-/border-5.1.5.tgz", + "integrity": "sha512-JvddhNrnhGigtzWRCVuAHepniyVi6hBlimxWDVAdcTuk7aRn9BYJUwfHslURtwYFsF5FoEs8Zmr1oZq2M1AP0A==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3669,6 +4030,8 @@ }, "node_modules/@styled-system/color": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/color/-/color-5.1.2.tgz", + "integrity": "sha512-1kCkeKDZkt4GYkuFNKc7vJQMcOmTl3bJY3YBUs7fCNM6mMYJeT1pViQ2LwBSBJytj3AB0o4IdLBoepgSgGl5MA==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3676,6 +4039,8 @@ }, "node_modules/@styled-system/core": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/core/-/core-5.1.2.tgz", + "integrity": "sha512-XclBDdNIy7OPOsN4HBsawG2eiWfCcuFt6gxKn1x4QfMIgeO6TOlA2pZZ5GWZtIhCUqEPTgIBta6JXsGyCkLBYw==", "license": "MIT", "dependencies": { "object-assign": "^4.1.1" @@ -3683,10 +4048,14 @@ }, "node_modules/@styled-system/css": { "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@styled-system/css/-/css-5.1.5.tgz", + "integrity": "sha512-XkORZdS5kypzcBotAMPBoeckDs9aSZVkvrAlq5K3xP8IMAUek+x2O4NtwoSgkYkWWzVBu6DGdFZLR790QWGG+A==", "license": "MIT" }, "node_modules/@styled-system/flexbox": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/flexbox/-/flexbox-5.1.2.tgz", + "integrity": "sha512-6hHV52+eUk654Y1J2v77B8iLeBNtc+SA3R4necsu2VVinSD7+XY5PCCEzBFaWs42dtOEDIa2lMrgL0YBC01mDQ==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3694,6 +4063,8 @@ }, "node_modules/@styled-system/grid": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/grid/-/grid-5.1.2.tgz", + "integrity": "sha512-K3YiV1KyHHzgdNuNlaw8oW2ktMuGga99o1e/NAfTEi5Zsa7JXxzwEnVSDSBdJC+z6R8WYTCYRQC6bkVFcvdTeg==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3701,6 +4072,8 @@ }, "node_modules/@styled-system/layout": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/layout/-/layout-5.1.2.tgz", + "integrity": "sha512-wUhkMBqSeacPFhoE9S6UF3fsMEKFv91gF4AdDWp0Aym1yeMPpqz9l9qS/6vjSsDPF7zOb5cOKC3tcKKOMuDCPw==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3708,6 +4081,8 @@ }, "node_modules/@styled-system/position": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/position/-/position-5.1.2.tgz", + "integrity": "sha512-60IZfMXEOOZe3l1mCu6sj/2NAyUmES2kR9Kzp7s2D3P4qKsZWxD1Se1+wJvevb+1TP+ZMkGPEYYXRyU8M1aF5A==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3715,6 +4090,8 @@ }, "node_modules/@styled-system/shadow": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/shadow/-/shadow-5.1.2.tgz", + "integrity": "sha512-wqniqYb7XuZM7K7C0d1Euxc4eGtqEe/lvM0WjuAFsQVImiq6KGT7s7is+0bNI8O4Dwg27jyu4Lfqo/oIQXNzAg==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3722,6 +4099,8 @@ }, "node_modules/@styled-system/space": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/space/-/space-5.1.2.tgz", + "integrity": "sha512-+zzYpR8uvfhcAbaPXhH8QgDAV//flxqxSjHiS9cDFQQUSznXMQmxJegbhcdEF7/eNnJgHeIXv1jmny78kipgBA==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3729,6 +4108,8 @@ }, "node_modules/@styled-system/typography": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@styled-system/typography/-/typography-5.1.2.tgz", + "integrity": "sha512-BxbVUnN8N7hJ4aaPOd7wEsudeT7CxarR+2hns8XCX1zp0DFfbWw4xYa/olA0oQaqx7F1hzDg+eRaGzAJbF+jOg==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2" @@ -3736,26 +4117,18 @@ }, "node_modules/@styled-system/variant": { "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@styled-system/variant/-/variant-5.1.5.tgz", + "integrity": "sha512-Yn8hXAFoWIro8+Q5J8YJd/mP85Teiut3fsGVR9CAxwgNfIAiqlYxsk5iHU7VHJks/0KjL4ATSjmbtCDC/4l1qw==", "license": "MIT", "dependencies": { "@styled-system/core": "^5.1.2", "@styled-system/css": "^5.1.5" } }, - "node_modules/@tinymce/tinymce-react": { - "version": "4.3.2", - "license": "MIT", - "dependencies": { - "prop-types": "^15.6.2", - "tinymce": "^6.0.0 || ^5.5.1" - }, - "peerDependencies": { - "react": "^18.0.0 || ^17.0.1 || ^16.7.0", - "react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0" - } - }, "node_modules/@tiptap/core": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.1.13.tgz", + "integrity": "sha512-cMC8bgTN63dj1Mv82iDeeLl6sa9kY0Pug8LSalxVEptRmyFVsVxGgu2/6Y3T+9aCYScxfS06EkA8SdzFMAwYTQ==", "license": "MIT", "funding": { "type": "github", @@ -3767,6 +4140,8 @@ }, "node_modules/@tiptap/extension-bubble-menu": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.1.13.tgz", + "integrity": "sha512-Hm7e1GX3AI6lfaUmr6WqsS9MMyXIzCkhh+VQi6K8jj4Q4s8kY4KPoAyD/c3v9pZ/dieUtm2TfqrOCkbHzsJQBg==", "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" @@ -3782,6 +4157,8 @@ }, "node_modules/@tiptap/extension-character-count": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.1.13.tgz", + "integrity": "sha512-FxPxS/Uqd4MgndInxXOcgNd225541Nsk1lT5e2uNTSNiQnG7dj7cSFG5KXGcSGLpGGt6e/E28WR6KLV+0/u+WA==", "license": "MIT", "funding": { "type": "github", @@ -3794,6 +4171,8 @@ }, "node_modules/@tiptap/extension-code": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.1.13.tgz", + "integrity": "sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w==", "license": "MIT", "funding": { "type": "github", @@ -3805,6 +4184,8 @@ }, "node_modules/@tiptap/extension-document": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.1.13.tgz", + "integrity": "sha512-wLwiTWsVmZTGIE5duTcHRmW4ulVxNW4nmgfpk95+mPn1iKyNGtrVhGWleLhBlTj+DWXDtcfNWZgqZkZNzhkqYQ==", "license": "MIT", "funding": { "type": "github", @@ -3816,6 +4197,8 @@ }, "node_modules/@tiptap/extension-floating-menu": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.1.13.tgz", + "integrity": "sha512-9Oz7pk1Nts2+EyY+rYfnREGbLzQ5UFazAvRhF6zAJdvyuDmAYm0Jp6s0GoTrpV0/dJEISoFaNpPdMJOb9EBNRw==", "license": "MIT", "dependencies": { "tippy.js": "^6.3.7" @@ -3831,6 +4214,8 @@ }, "node_modules/@tiptap/extension-heading": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.1.13.tgz", + "integrity": "sha512-PEmc19QLmlVUTiHWoF0hpgNTNPNU0nlaFmMKskzO+cx5Df4xvHmv/UqoIwp7/UFbPMkfVJT1ozQU7oD1IWn9Hg==", "license": "MIT", "funding": { "type": "github", @@ -3842,6 +4227,8 @@ }, "node_modules/@tiptap/extension-image": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.1.13.tgz", + "integrity": "sha512-7oVAos+BU4KR/zQsfltrd8hgIxKxyxZ19dhwb1BJI2Nt3Mnx+yFPRlRSehID6RT9dYqgW4UW5d6vh/3HQcYYYw==", "license": "MIT", "funding": { "type": "github", @@ -3853,6 +4240,8 @@ }, "node_modules/@tiptap/extension-link": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.1.13.tgz", + "integrity": "sha512-wuGMf3zRtMHhMrKm9l6Tft5M2N21Z0UP1dZ5t1IlOAvOeYV2QZ5UynwFryxGKLO0NslCBLF/4b/HAdNXbfXWUA==", "license": "MIT", "dependencies": { "linkifyjs": "^4.1.0" @@ -3868,6 +4257,8 @@ }, "node_modules/@tiptap/extension-table": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.1.13.tgz", + "integrity": "sha512-yMWt2LqotOsWJhLwFNo8fyTwJNLPtnk+eCUxKLlMXP23mJ/lpF+jvTihhHVVic5GqV9vLYZFU2Tn+5k/Vd5P1w==", "license": "MIT", "funding": { "type": "github", @@ -3880,6 +4271,8 @@ }, "node_modules/@tiptap/extension-table-cell": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.1.13.tgz", + "integrity": "sha512-30pyVt2PxGAk8jmsXKxDheql8K/xIRA9FiDo++kS2Kr6Y7I42/kNPQttJ2W+Q1JdRJvedNfQtziQfKWDRLLCNA==", "license": "MIT", "funding": { "type": "github", @@ -3891,6 +4284,8 @@ }, "node_modules/@tiptap/extension-table-header": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.1.13.tgz", + "integrity": "sha512-FwIV5iso5kmpu01QyvrPCjJqZfqxRTjtjMsDyut2uIgx9v5TXk0V5XvMWobx435ANIDJoGTYCMRlIqcgtyqwAQ==", "license": "MIT", "funding": { "type": "github", @@ -3902,6 +4297,8 @@ }, "node_modules/@tiptap/extension-table-row": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.1.13.tgz", + "integrity": "sha512-27Mb9/oYbiLd+/BUFMhQzRIqMd2Z5j1BZMYsktwtDG8vGdYVlaW257UVaoNR9TmiXyIzd3Dh1mOil8G35+HRHg==", "license": "MIT", "funding": { "type": "github", @@ -3913,6 +4310,8 @@ }, "node_modules/@tiptap/extension-text": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.1.13.tgz", + "integrity": "sha512-zzsTTvu5U67a8WjImi6DrmpX2Q/onLSaj+LRWPh36A1Pz2WaxW5asZgaS+xWCnR+UrozlCALWa01r7uv69jq0w==", "license": "MIT", "funding": { "type": "github", @@ -3924,6 +4323,8 @@ }, "node_modules/@tiptap/extension-text-align": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.1.13.tgz", + "integrity": "sha512-ZmbGpi5FHGsWyzt+8DceXERr/Vwxhjpm2VKWZyFTVz8uNJVj+/ou196JQJZqxbp5VtKkS7UYujaO++G5eflb0Q==", "license": "MIT", "funding": { "type": "github", @@ -3935,6 +4336,8 @@ }, "node_modules/@tiptap/extension-typography": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.1.13.tgz", + "integrity": "sha512-//90Gzkci4/77CCmdWYyRGTcMUvsQ64jv3mqlL+JqWgLCffMHvWPGKhPMgSzoyHRlAIIACMhxniRtB7HixhTHQ==", "license": "MIT", "funding": { "type": "github", @@ -3946,6 +4349,8 @@ }, "node_modules/@tiptap/pm": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.1.13.tgz", + "integrity": "sha512-zNbA7muWsHuVg12GrTgN/j119rLePPq5M8dZgkKxUwdw8VmU3eUyBp1SihPEXJ2U0MGdZhNhFX7Y74g11u66sg==", "license": "MIT", "dependencies": { "prosemirror-changeset": "^2.2.0", @@ -3972,26 +4377,10 @@ "url": "https://github.com/sponsors/ueberdosis" } }, - "node_modules/@tiptap/react": { - "version": "2.1.13", - "license": "MIT", - "dependencies": { - "@tiptap/extension-bubble-menu": "^2.1.13", - "@tiptap/extension-floating-menu": "^2.1.13" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.0.0", - "@tiptap/pm": "^2.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, "node_modules/@tiptap/starter-kit": { "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.1.13.tgz", + "integrity": "sha512-ph/mUR/OwPtPkZ5rNHINxubpABn8fHnvJSdhXFrY/q6SKoaO11NZXgegRaiG4aL7O6Sz4LsZVw6Sm0Ae+GJmrg==", "license": "MIT", "dependencies": { "@tiptap/core": "^2.1.13", @@ -4021,11 +4410,15 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@remirror/core-constants": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", "license": "MIT", "peer": true }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/core": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz", + "integrity": "sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==", "license": "MIT", "funding": { "type": "github", @@ -4037,6 +4430,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-blockquote": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.27.1.tgz", + "integrity": "sha512-QrUX3muElDrNjKM3nqCSAtm3H3pT33c6ON8kwRiQboOAjT/9D57Cs7XEVY7r6rMaJPeKztrRUrNVF9w/w/6B0A==", "license": "MIT", "funding": { "type": "github", @@ -4048,6 +4443,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-bold": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.27.1.tgz", + "integrity": "sha512-g4l4p892x/r7mhea8syp3fNYODxsDrimgouQ+q4DKXIgQmm5+uNhyuEPexP3I8TFNXqQ4DlMNFoM9yCqk97etQ==", "license": "MIT", "funding": { "type": "github", @@ -4059,6 +4456,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-bullet-list": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.27.1.tgz", + "integrity": "sha512-5FmnfXkJ76wN4EbJNzBhAlmQxho8yEMIJLchTGmXdsD/n/tsyVVtewnQYaIOj/Z7naaGySTGDmjVtLgTuQ+Sxw==", "license": "MIT", "funding": { "type": "github", @@ -4070,6 +4469,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-code-block": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.27.1.tgz", + "integrity": "sha512-wCI5VIOfSAdkenCWFvh4m8FFCJ51EOK+CUmOC/PWUjyo2Dgn8QC8HMi015q8XF7886T0KvYVVoqxmxJSUDAYNg==", "license": "MIT", "funding": { "type": "github", @@ -4082,6 +4483,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-dropcursor": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.27.1.tgz", + "integrity": "sha512-3MBQRGHHZ0by3OT0CWbLKS7J3PH9PpobrXjmIR7kr0nde7+bHqxXiVNuuIf501oKU9rnEUSedipSHkLYGkmfsA==", "license": "MIT", "funding": { "type": "github", @@ -4094,6 +4497,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-gapcursor": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.27.1.tgz", + "integrity": "sha512-A9e1jr+jGhDWzNSXtIO6PYVYhf5j/udjbZwMja+wCE/3KvZU9V3IrnGKz1xNW+2Q2BDOe1QO7j5uVL9ElR6nTA==", "license": "MIT", "funding": { "type": "github", @@ -4106,6 +4511,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-hard-break": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.27.1.tgz", + "integrity": "sha512-W4hHa4Io6QCTwpyTlN6UAvqMIQ7t56kIUByZhyY9EWrg/+JpbfpxE1kXFLPB4ZGgwBknFOw+e4bJ1j3oAbTJFw==", "license": "MIT", "funding": { "type": "github", @@ -4117,6 +4524,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-history": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.27.1.tgz", + "integrity": "sha512-K8PHC9gegSAt0wzSlsd4aUpoEyIJYOmVVeyniHr1P1mIblW1KYEDbRGbDlrLALTyUEfMcBhdIm8zrB9X2Nihvg==", "license": "MIT", "funding": { "type": "github", @@ -4129,6 +4538,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-horizontal-rule": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.27.1.tgz", + "integrity": "sha512-WxXWGEEsqDmGIF2o9av+3r9Qje4CKrqrpeQY6aRO5bxvWX9AabQCfasepayBok6uwtvNzh3Xpsn9zbbSk09dNA==", "license": "MIT", "funding": { "type": "github", @@ -4141,6 +4552,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-italic": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.27.1.tgz", + "integrity": "sha512-rcm0GyniWW0UhcNI9+1eIK64GqWQLyIIrWGINslvqSUoBc+WkfocLvv4CMpRkzKlfsAxwVIBuH2eLxHKDtAREA==", "license": "MIT", "funding": { "type": "github", @@ -4152,6 +4565,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-list-item": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.27.1.tgz", + "integrity": "sha512-dtsxvtzxfwOJP6dKGf0vb2MJAoDF2NxoiWzpq0XTvo7NGGYUHfuHjX07Zp0dYqb4seaDXjwsi5BIQUOp3+WMFQ==", "license": "MIT", "funding": { "type": "github", @@ -4163,6 +4578,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-ordered-list": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.27.1.tgz", + "integrity": "sha512-U1/sWxc2TciozQsZjH35temyidYUjvroHj3PUPzPyh19w2fwKh1NSbFybWuoYs6jS3XnMSwnM2vF52tOwvfEmA==", "license": "MIT", "funding": { "type": "github", @@ -4174,6 +4591,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-paragraph": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.27.1.tgz", + "integrity": "sha512-R3QdrHcUdFAsdsn2UAIvhY0yWyHjqGyP/Rv8RRdN0OyFiTKtwTPqreKMHKJOflgX4sMJl/OpHTpNG1Kaf7Lo2A==", "license": "MIT", "funding": { "type": "github", @@ -4185,6 +4604,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/extension-strike": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.27.1.tgz", + "integrity": "sha512-S9I//K8KPgfFTC5I5lorClzXk0g4lrAv9y5qHzHO5EOWt7AFl0YTg2oN8NKSIBK4bHRnPIrjJJKv+dDFnUp5jQ==", "license": "MIT", "funding": { "type": "github", @@ -4196,6 +4617,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/@tiptap/pm": { "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.1.tgz", + "integrity": "sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==", "license": "MIT", "peer": true, "dependencies": { @@ -4225,6 +4648,8 @@ }, "node_modules/@tiptap/starter-kit/node_modules/prosemirror-trailing-node": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", "license": "MIT", "peer": true, "dependencies": { @@ -4260,6 +4685,8 @@ }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", "license": "MIT", "dependencies": { "hoist-non-react-statics": "^3.3.0" @@ -4270,10 +4697,14 @@ }, "node_modules/@types/linkify-it": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", "dependencies": { "@types/linkify-it": "^5", @@ -4282,6 +4713,8 @@ }, "node_modules/@types/mdurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, "node_modules/@types/ms": { @@ -4301,6 +4734,8 @@ }, "node_modules/@types/parse-json": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, "node_modules/@types/prop-types": { @@ -4319,6 +4754,8 @@ }, "node_modules/@types/react-transition-group": { "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -4328,8 +4765,17 @@ "version": "1.20.2", "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", "license": "MIT" }, "node_modules/@types/validator": { @@ -4431,6 +4877,284 @@ "node": ">=16.0.0" } }, + "node_modules/adminjs/node_modules/@adminjs/design-system": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@adminjs/design-system/-/design-system-4.1.1.tgz", + "integrity": "sha512-aUJjoOwXlBgJWckqDcgUDrRCiDnSrtSHnjFIYzfhhm0dFV107ToHNBemvh72/+krXHtMhRuWCkYkKnrJKh1Bjw==", + "license": "MIT", + "dependencies": { + "@hypnosphi/create-react-context": "^0.3.1", + "@tinymce/tinymce-react": "^4.3.2", + "@tiptap/core": "2.1.13", + "@tiptap/extension-bubble-menu": "2.1.13", + "@tiptap/extension-character-count": "2.1.13", + "@tiptap/extension-code": "2.1.13", + "@tiptap/extension-document": "2.1.13", + "@tiptap/extension-floating-menu": "2.1.13", + "@tiptap/extension-heading": "2.1.13", + "@tiptap/extension-image": "2.1.13", + "@tiptap/extension-link": "2.1.13", + "@tiptap/extension-table": "2.1.13", + "@tiptap/extension-table-cell": "2.1.13", + "@tiptap/extension-table-header": "2.1.13", + "@tiptap/extension-table-row": "2.1.13", + "@tiptap/extension-text": "2.1.13", + "@tiptap/extension-text-align": "2.1.13", + "@tiptap/extension-typography": "2.1.13", + "@tiptap/pm": "2.1.13", + "@tiptap/react": "2.1.13", + "@tiptap/starter-kit": "2.1.13", + "date-fns": "^2.29.3", + "flat": "^5.0.2", + "hoist-non-react-statics": "3.3.2", + "jw-paginate": "^1.0.4", + "lodash": "^4.17.21", + "polished": "^4.2.2", + "react-currency-input-field": "^3.6.10", + "react-datepicker": "^4.10.0", + "react-feather": "^2.0.10", + "react-phone-input-2": "^2.15.1", + "react-select": "^5.8.0", + "react-text-mask": "^5.5.0", + "styled-components": "5.3.9", + "styled-system": "^5.1.5", + "text-mask-addons": "^3.8.0" + }, + "peerDependencies": { + "react": "^18.1.0", + "react-dom": "^18.1.0" + } + }, + "node_modules/adminjs/node_modules/@adminjs/design-system/node_modules/styled-components": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.9.tgz", + "integrity": "sha512-Aj3kb13B75DQBo2oRwRa/APdB5rSmwUfN5exyarpX+x/tlM/rwZA2vVk2vQgVSP6WKaZJHWwiFrzgHt+CLtB4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/adminjs/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/adminjs/node_modules/@hello-pangea/dnd": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.6.0.tgz", + "integrity": "sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.24.1", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "use-memo-one": "^1.1.3" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/adminjs/node_modules/@tinymce/tinymce-react": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-4.3.2.tgz", + "integrity": "sha512-wJHZhPf2Mk3yTtdVC/uIGh+kvDgKuTw/qV13uzdChTNo68JI1l7jYMrSQOpyimDyn5LHAw0E1zFByrm1WHAVeA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.2", + "tinymce": "^6.0.0 || ^5.5.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^17.0.1 || ^16.7.0", + "react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0" + } + }, + "node_modules/adminjs/node_modules/@tiptap/react": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.1.13.tgz", + "integrity": "sha512-Dq3f8EtJnpImP3iDtJo+7bulnN9SJZRZcVVzxHXccLcC2MxtmDdlPGZjP+wxO800nd8toSIOd5734fPNf/YcfA==", + "license": "MIT", + "dependencies": { + "@tiptap/extension-bubble-menu": "^2.1.13", + "@tiptap/extension-floating-menu": "^2.1.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/adminjs/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/adminjs/node_modules/react-datepicker": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz", + "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, + "node_modules/adminjs/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/adminjs/node_modules/react-onclickoutside": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", + "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, + "node_modules/adminjs/node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "license": "MIT", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/adminjs/node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/adminjs/node_modules/react-text-mask": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-text-mask/-/react-text-mask-5.5.0.tgz", + "integrity": "sha512-SLJlJQxa0uonMXsnXRpv5abIepGmHz77ylQcra0GNd7Jtk4Wj2Mtp85uGQHv1avba2uI8ZvRpIEQPpJKsqRGYw==", + "license": "Unlicense", + "dependencies": { + "prop-types": "^15.5.6" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/adminjs/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/adminjs/node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/adminjs/node_modules/uuid": { "version": "9.0.1", "funding": [ @@ -4507,6 +5231,8 @@ }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/array-flatten": { @@ -4557,6 +5283,8 @@ }, "node_modules/babel-plugin-macros": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", @@ -4603,6 +5331,8 @@ }, "node_modules/babel-plugin-styled-components": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -4617,6 +5347,8 @@ }, "node_modules/babel-plugin-styled-components/node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -4897,6 +5629,8 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "license": "MIT", "engines": { "node": ">=6" @@ -4904,6 +5638,8 @@ }, "node_modules/camelize": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4962,6 +5698,8 @@ }, "node_modules/classnames": { "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, "node_modules/cli-cursor": { @@ -5038,6 +5776,19 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5056,6 +5807,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "license": "MIT", @@ -5120,18 +5881,50 @@ "license": "MIT" }, "node_modules/concat-stream": { - "version": "2.0.0", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "engines": [ - "node >= 6.0" + "node >= 0.8" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^3.0.2", + "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -5185,6 +5978,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "license": "MIT", @@ -5198,6 +5997,8 @@ }, "node_modules/cosmiconfig": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", @@ -5212,6 +6013,8 @@ }, "node_modules/crelt": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, "node_modules/cross-fetch": { @@ -5237,6 +6040,8 @@ }, "node_modules/css-box-model": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", "license": "MIT", "dependencies": { "tiny-invariant": "^1.0.6" @@ -5244,6 +6049,8 @@ }, "node_modules/css-color-keywords": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", "license": "ISC", "engines": { "node": ">=4" @@ -5251,6 +6058,8 @@ }, "node_modules/css-to-react-native": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", "license": "MIT", "dependencies": { "camelize": "^1.0.0", @@ -5268,6 +6077,8 @@ }, "node_modules/date-fns": { "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.21.0" @@ -5334,8 +6145,19 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", @@ -5503,6 +6325,8 @@ }, "node_modules/entities": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -5513,6 +6337,8 @@ }, "node_modules/error-ex": { "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -5612,6 +6438,8 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", "engines": { "node": ">=10" @@ -5876,6 +6704,8 @@ }, "node_modules/find-root": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, "node_modules/find-up": { @@ -6186,6 +7016,8 @@ }, "node_modules/gud": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==", "license": "MIT" }, "node_modules/has-flag": { @@ -6366,6 +7198,8 @@ }, "node_modules/import-fresh": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -6414,6 +7248,8 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-binary-path": { @@ -6545,6 +7381,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6681,6 +7523,8 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, "node_modules/json5": { @@ -6707,6 +7551,8 @@ }, "node_modules/jw-paginate": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/jw-paginate/-/jw-paginate-1.0.4.tgz", + "integrity": "sha512-W0bv782exgCoynUL/egbRpaYwf/r6T6e02H870H5u3hfSgEYrxgz5POwmFF5aApS6iPi6yhZ0VF8IbafNFsntA==", "license": "MIT" }, "node_modules/jwa": { @@ -6737,10 +7583,14 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/linkify-it": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -6748,6 +7598,8 @@ }, "node_modules/linkifyjs": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", "license": "MIT" }, "node_modules/locate-path": { @@ -6771,14 +7623,20 @@ }, "node_modules/lodash.memoize": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "license": "MIT" }, "node_modules/lodash.reduce": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", "license": "MIT" }, "node_modules/lodash.startswith": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz", + "integrity": "sha512-XClYR1h4/fJ7H+mmCKppbiBmljN/nGs73iq2SjCT9SF4CBPoUHzLvWmH1GtZMhMBZSiRkHXfeA2RY1eIlJ75ww==", "license": "MIT" }, "node_modules/log-symbols": { @@ -6839,6 +7697,8 @@ }, "node_modules/markdown-it": { "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "license": "MIT", "dependencies": { "argparse": "^2.0.1", @@ -6861,6 +7721,8 @@ }, "node_modules/mdurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, "node_modules/media-typer": { @@ -7049,19 +7911,41 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.4", + "xtend": "^4.0.0" }, "engines": { - "node": ">= 10.16.0" + "node": ">= 6.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/negotiator": { @@ -7259,6 +8143,8 @@ }, "node_modules/orderedmap": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", "license": "MIT" }, "node_modules/p-limit": { @@ -7313,6 +8199,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -7323,6 +8211,8 @@ }, "node_modules/parse-json": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -7392,6 +8282,8 @@ }, "node_modules/path-type": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "license": "MIT", "engines": { "node": ">=8" @@ -7522,6 +8414,8 @@ }, "node_modules/polished": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.17.8" @@ -7530,8 +8424,39 @@ "node": ">=10" } }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, "node_modules/postgres-array": { @@ -7565,6 +8490,12 @@ "node": ">=0.10.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", @@ -7580,6 +8511,8 @@ }, "node_modules/prosemirror-changeset": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", "license": "MIT", "dependencies": { "prosemirror-transform": "^1.0.0" @@ -7587,6 +8520,8 @@ }, "node_modules/prosemirror-collab": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0" @@ -7594,6 +8529,8 @@ }, "node_modules/prosemirror-commands": { "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", @@ -7603,6 +8540,8 @@ }, "node_modules/prosemirror-dropcursor": { "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", @@ -7612,6 +8551,8 @@ }, "node_modules/prosemirror-gapcursor": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", "license": "MIT", "dependencies": { "prosemirror-keymap": "^1.0.0", @@ -7622,6 +8563,8 @@ }, "node_modules/prosemirror-history": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.2.2", @@ -7632,6 +8575,8 @@ }, "node_modules/prosemirror-inputrules": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", @@ -7640,6 +8585,8 @@ }, "node_modules/prosemirror-keymap": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", @@ -7648,6 +8595,8 @@ }, "node_modules/prosemirror-markdown": { "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", "license": "MIT", "dependencies": { "@types/markdown-it": "^14.0.0", @@ -7657,6 +8606,8 @@ }, "node_modules/prosemirror-menu": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", "license": "MIT", "dependencies": { "crelt": "^1.0.0", @@ -7667,6 +8618,8 @@ }, "node_modules/prosemirror-model": { "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", "dependencies": { "orderedmap": "^2.0.0" @@ -7674,6 +8627,8 @@ }, "node_modules/prosemirror-schema-basic": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.25.0" @@ -7681,6 +8636,8 @@ }, "node_modules/prosemirror-schema-list": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", @@ -7690,6 +8647,8 @@ }, "node_modules/prosemirror-state": { "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", @@ -7699,6 +8658,8 @@ }, "node_modules/prosemirror-tables": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.1.tgz", + "integrity": "sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==", "license": "MIT", "dependencies": { "prosemirror-keymap": "^1.2.2", @@ -7710,6 +8671,8 @@ }, "node_modules/prosemirror-trailing-node": { "version": "2.0.9", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz", + "integrity": "sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg==", "license": "MIT", "dependencies": { "@remirror/core-constants": "^2.0.2", @@ -7723,6 +8686,8 @@ }, "node_modules/prosemirror-transform": { "version": "1.10.5", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz", + "integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.21.0" @@ -7730,6 +8695,8 @@ }, "node_modules/prosemirror-view": { "version": "1.41.3", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.3.tgz", + "integrity": "sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", @@ -7772,6 +8739,8 @@ }, "node_modules/punycode.js": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", "engines": { "node": ">=6" @@ -7792,6 +8761,8 @@ }, "node_modules/raf-schd": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, "node_modules/random-bytes": { @@ -7822,51 +8793,39 @@ } }, "node_modules/react": { - "version": "18.3.1", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-currency-input-field": { "version": "3.10.0", + "resolved": "https://registry.npmjs.org/react-currency-input-field/-/react-currency-input-field-3.10.0.tgz", + "integrity": "sha512-GRmZogHh1e1LrmgXg/fKHSuRLYUnj/c/AumfvfuDMA0UX1mDR6u2NR0fzDemRdq4tNHNLucJeJ2OKCr3ehqyDA==", "license": "MIT", "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-datepicker": { - "version": "4.25.0", - "license": "MIT", - "dependencies": { - "@popperjs/core": "^2.11.8", - "classnames": "^2.2.6", - "date-fns": "^2.30.0", - "prop-types": "^15.7.2", - "react-onclickoutside": "^6.13.0", - "react-popper": "^2.3.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17 || ^18", - "react-dom": "^16.9.0 || ^17 || ^18" - } - }, "node_modules/react-dom": { - "version": "18.3.1", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.0" } }, "node_modules/react-fast-compare": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, "node_modules/react-feather": { @@ -7903,20 +8862,10 @@ "version": "18.3.1", "license": "MIT" }, - "node_modules/react-onclickoutside": { - "version": "6.13.2", - "license": "MIT", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" - }, - "peerDependencies": { - "react": "^15.5.x || ^16.x || ^17.x || ^18.x", - "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" - } - }, "node_modules/react-phone-input-2": { "version": "2.15.1", + "resolved": "https://registry.npmjs.org/react-phone-input-2/-/react-phone-input-2-2.15.1.tgz", + "integrity": "sha512-W03abwhXcwUoq+vUFvC6ch2+LJYMN8qSOiO889UH6S7SyMCQvox/LF3QWt+cZagZrRdi5z2ON3omnjoCUmlaYw==", "license": "MIT", "dependencies": { "classnames": "^2.2.6", @@ -7931,56 +8880,6 @@ "react-dom": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0" } }, - "node_modules/react-popper": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "react-fast-compare": "^3.0.1", - "warning": "^4.0.2" - }, - "peerDependencies": { - "@popperjs/core": "^2.0.0", - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" - } - }, - "node_modules/react-redux": { - "version": "8.1.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.1", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/use-sync-external-store": "^0.0.3", - "hoist-non-react-statics": "^3.3.2", - "react-is": "^18.0.0", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^16.8 || ^17.0 || ^18.0", - "@types/react-dom": "^16.8 || ^17.0 || ^18.0", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0", - "react-native": ">=0.59", - "redux": "^4 || ^5.0.0-beta.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, "node_modules/react-router": { "version": "6.30.2", "license": "MIT", @@ -7996,6 +8895,8 @@ }, "node_modules/react-router-dom": { "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.1", @@ -8011,6 +8912,8 @@ }, "node_modules/react-select": { "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.0", @@ -8028,18 +8931,10 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-text-mask": { - "version": "5.5.0", - "license": "Unlicense", - "dependencies": { - "prop-types": "^15.5.6" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-transition-group": { "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -8169,6 +9064,8 @@ }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "license": "MIT", "engines": { "node": ">=4" @@ -8278,6 +9175,8 @@ }, "node_modules/rope-sequence": { "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", "license": "MIT" }, "node_modules/safe-buffer": { @@ -8319,11 +9218,10 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.23.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -8516,8 +9414,61 @@ }, "node_modules/shallowequal": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8607,6 +9558,21 @@ "version": "3.0.7", "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "dev": true, @@ -8641,11 +9607,23 @@ }, "node_modules/source-map": { "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "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==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "license": "MIT", @@ -8837,22 +9815,24 @@ "optional": true }, "node_modules/styled-components": { - "version": "5.3.9", + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" }, "engines": { - "node": ">=10" + "node": ">= 16" }, "funding": { "type": "opencollective", @@ -8860,16 +9840,58 @@ }, "peerDependencies": { "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" + "react-dom": ">= 16.8.0" } }, + "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/styled-components/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT", + "peer": true + }, "node_modules/styled-components/node_modules/@emotion/unitless": { - "version": "0.7.5", - "license": "MIT" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/styled-components/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT", + "peer": true + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT", + "peer": true + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD", + "peer": true }, "node_modules/styled-system": { "version": "5.1.5", + "resolved": "https://registry.npmjs.org/styled-system/-/styled-system-5.1.5.tgz", + "integrity": "sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A==", "license": "MIT", "dependencies": { "@styled-system/background": "^5.1.2", @@ -8889,6 +9911,8 @@ }, "node_modules/stylis": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, "node_modules/supports-color": { @@ -8940,18 +9964,26 @@ }, "node_modules/text-mask-addons": { "version": "3.8.0", + "resolved": "https://registry.npmjs.org/text-mask-addons/-/text-mask-addons-3.8.0.tgz", + "integrity": "sha512-VSZSdc/tKn4zGxgpJ+uNBzoW1t472AoAFIlbw1K7hSNXz0DfSBYDJNRxLqgxOfWw1BY2z6DQpm7g0sYZn5qLpg==", "license": "Unlicense" }, "node_modules/tiny-invariant": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, "node_modules/tinymce": { "version": "6.8.6", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.6.tgz", + "integrity": "sha512-++XYEs8lKWvZxDCjrr8Baiw7KiikraZ5JkLMg6EdnUVNKJui0IsrAADj5MsyUeFkcEryfn2jd3p09H7REvewyg==", "license": "MIT" }, "node_modules/tippy.js": { "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", "license": "MIT", "dependencies": { "@popperjs/core": "^2.9.0" @@ -9010,10 +10042,14 @@ }, "node_modules/typedarray": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, "node_modules/uc.micro": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, "node_modules/uid-safe": { @@ -9133,6 +10169,8 @@ }, "node_modules/use-isomorphic-layout-effect": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9143,15 +10181,10 @@ } } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9200,10 +10233,14 @@ }, "node_modules/w3c-keyname": { "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, "node_modules/warning": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" @@ -9373,6 +10410,8 @@ }, "node_modules/yaml": { "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "license": "ISC", "engines": { "node": ">= 6" diff --git a/package.json b/package.json index 8cf124d..3f76ce7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@adminjs/express": "^6.1.0", "@adminjs/sequelize": "^4.1.1", "@adminjs/upload": "^4.0.2", + "@babel/core": "^7.28.5", + "@babel/preset-react": "^7.28.5", "adminjs": "^7.5.0", "bcryptjs": "^2.4.3", "bootstrap": "^5.3.2", @@ -39,11 +41,14 @@ "method-override": "^3.0.0", "moment": "^2.29.4", "morgan": "^1.10.0", - "multer": "^2.0.0", + "multer": "^1.4.5-lts.1", "pg": "^8.11.3", "pg-hstore": "^2.3.4", + "react": "^19.2.0", + "react-dom": "^19.2.0", "sequelize": "^6.37.7", - "sequelize-cli": "^6.6.3" + "sequelize-cli": "^6.6.3", + "sharp": "^0.33.5" }, "devDependencies": { "nodemon": "^3.0.2" diff --git a/public/admin-image-editor-demo.html b/public/admin-image-editor-demo.html new file mode 100644 index 0000000..3b9c0d2 --- /dev/null +++ b/public/admin-image-editor-demo.html @@ -0,0 +1,187 @@ + + + + + + Демонстрация редактора изображений в AdminJS + + + + +
+
+
+
+
+

+ + Редактор изображений для AdminJS - Готов к использованию! +

+
+
+
+ + Редактор изображений успешно интегрирован в AdminJS! +
+ +
🎉 Что было реализовано:
+
    +
  • + + Полнофункциональный редактор изображений с галереей +
  • +
  • + + API для загрузки и управления изображениями +
  • +
  • + + Автоматическая интеграция с полями изображений в AdminJS +
  • +
  • + + Превью изображений и организация по папкам +
  • +
  • + + Оптимизация изображений через Sharp +
  • +
+ +
📝 Как использовать в AdminJS:
+
    +
  1. + Войдите в админ-панель: + + + Открыть AdminJS + +
  2. +
  3. + Данные для входа: +
      +
    • Username: admin
    • +
    • Password: Используйте существующий пароль администратора
    • +
    +
  4. +
  5. + Редактируйте маршруты, гидов или статьи +
  6. +
  7. + Для полей изображений появится кнопка "📷 Выбрать" +
  8. +
  9. + Нажмите кнопку для открытия редактора в модальном окне +
  10. +
+ +
🔧 Функции редактора:
+
+
+
+
+ Загрузка +
+
+
    +
  • Drag & Drop
  • +
  • Формат: JPG, PNG, GIF
  • +
  • Макс. размер: 5МБ
  • +
  • Автооптимизация
  • +
+
+
+
+
+
+
+ Галерея +
+
+
    +
  • Все загруженные изображения
  • +
  • Фильтр по папкам
  • +
  • Превью с именами
  • +
  • Поиск по категориям
  • +
+
+
+
+
+
+
+ По URL +
+
+
    +
  • Внешние изображения
  • +
  • Прямые ссылки
  • +
  • Предпросмотр
  • +
  • Быстрая вставка
  • +
+
+
+
+
+ +
📂 Организация файлов:
+
+
+
+ +
/uploads/routes/
+ Изображения маршрутов +
+
+
+
+ +
/uploads/guides/
+ Фотографии гидов +
+
+
+
+ +
/uploads/articles/
+ Изображения статей +
+
+
+
+ +
/uploads/general/
+ Общие изображения +
+
+
+ +
+
+ + Техническая информация: +
+ Редактор автоматически определяет поля изображений по именам (image_url, photo, avatar) и добавляет к ним кнопку выбора. + Изображения оптимизируются до разрешения 1200x800 с качеством 85% для оптимальной производительности. +
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/public/css/admin-custom.css b/public/css/admin-custom.css index 79f6825..150bc2b 100644 --- a/public/css/admin-custom.css +++ b/public/css/admin-custom.css @@ -32,6 +32,35 @@ margin: 0.125rem; } +/* Image Editor Button Styles */ +.image-editor-btn { + background: #007bff !important; + color: white !important; + border: none !important; + border-radius: 4px !important; + padding: 6px 12px !important; + font-size: 13px !important; + cursor: pointer !important; + white-space: nowrap !important; + transition: background 0.2s ease !important; +} + +.image-editor-btn:hover { + background: #0056b3 !important; + color: white !important; +} + +/* Image Preview Styles */ +.image-preview { + max-width: 180px !important; + max-height: 120px !important; + object-fit: cover !important; + border: 1px solid #ddd !important; + border-radius: 4px !important; + margin-top: 8px !important; + margin-bottom: 8px !important; +} + /* Cards Enhancement */ .card { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); @@ -215,4 +244,61 @@ 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } +} + +/* Стили для редактора изображений в AdminJS */ +.image-editor-btn { + transition: all 0.2s ease; + font-size: 13px !important; + padding: 6px 12px !important; +} + +.image-editor-btn:hover { + background: #0056b3 !important; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +/* Стили для превью изображений */ +.image-preview { + border: 2px solid #e9ecef !important; + transition: border-color 0.2s ease; + margin-top: 8px !important; +} + +.image-preview:hover { + border-color: #007bff; +} + +/* Улучшенные стили для полей изображений */ +input[name*="image_url"]:focus, +input[name*="photo"]:focus, +input[name*="avatar"]:focus { + border-left: 3px solid #007bff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +/* Анимация для успешного выбора изображения */ +@keyframes imageSelected { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +.image-selected { + animation: imageSelected 0.3s ease; +} + +/* Responsive стили для редактора */ +@media (max-width: 768px) { + .image-editor-btn { + margin-left: 0 !important; + margin-top: 10px !important; + display: block !important; + width: 100% !important; + } + + .image-preview { + max-width: 100% !important; + } } \ No newline at end of file diff --git a/public/image-editor-compact.html b/public/image-editor-compact.html new file mode 100644 index 0000000..6cfeea6 --- /dev/null +++ b/public/image-editor-compact.html @@ -0,0 +1,543 @@ + + + + + + Выбор изображения + + + +
+
+ + + +
+ + +
+
+
📁
+

Выберите файл или перетащите сюда

+

JPG, PNG, GIF (макс. 5МБ)

+
+ +
+ + + + + +
+ + +
+ + + + + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/public/image-editor.html b/public/image-editor.html new file mode 100644 index 0000000..79e3439 --- /dev/null +++ b/public/image-editor.html @@ -0,0 +1,448 @@ + + + + + + Редактор изображений - Korea Tourism + + + + + +
+
+
+
+
+
+ + Редактор изображений +
+ +
+
+ + + +
+ +
+
+
+
+ +

Перетащите файлы сюда или нажмите для выбора

+ Поддерживаются: JPG, PNG, GIF (макс. 5МБ) + +
+ + + +
+
+
+
Предварительный просмотр
+ Предварительный просмотр +
+ +
+
+
+
+
+ + + + + +
+
+
+ +
+ + +
+ Введите прямую ссылку на изображение +
+
+
+
Предварительный просмотр
+ Предварительный просмотр +
+
+
+
+
+
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/public/image-system-docs.html b/public/image-system-docs.html new file mode 100644 index 0000000..dcb42ae --- /dev/null +++ b/public/image-system-docs.html @@ -0,0 +1,275 @@ + + + + + + Система управления изображениями - Korea Tourism + + + + + + + + + +
+
+

🖼️ Система управления изображениями

+

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

+
+
+ + +
+
+
+
+
+
+
+ +
+
Загрузка и обработка
+

Drag & Drop загрузка с автоматической оптимизацией и конвертацией в JPEG

+
+
+
+
+
+
+
+ +
+
Редактирование
+

Обрезка, поворот на 90°, отражение горизонтально и вертикально

+
+
+
+
+
+
+
+ +
+
API интеграция
+

REST API для интеграции с любыми формами и компонентами

+
+
+
+
+
+
+ + +
+
+

📋 Инструкции по использованию

+ +
+
+

Через AdminJS

+
+ 1 + Зайдите в админ-панель +
+
+ 2 + Выберите "Туры" или "Гиды" +
+
+ 3 + В поле "Image URL" укажите путь к изображению +
+
+ Например: /uploads/routes/my-image.jpg +
+
+ +
+

Через JavaScript

+
+ 1 + Подключите скрипт редактора +
+
+ <script src="/js/image-editor.js"></script> +
+
+ 2 + Откройте редактор +
+
+window.openImageEditor({ + targetFolder: 'routes', // routes, guides, articles +}).then(url => { + console.log('Сохранено:', url); +}); +
+
+
+
+
+ + +
+
+

🔌 API Эндпоинты

+ +
+
+
Загрузка изображения
+
+ POST /api/images/upload-image +
+

Загружает изображение во временную папку

+
+const formData = new FormData(); +formData.append('image', file); + +fetch('/api/images/upload-image', { + method: 'POST', + body: formData +}).then(r => r.json()); +
+
+ +
+
Обработка изображения
+
+ POST /api/images/process-image +
+

Применяет трансформации и сохраняет финальный файл

+
+fetch('/api/images/process-image', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tempId: 'temp-123', + rotation: 90, + flipHorizontal: false, + cropData: { x: 0, y: 0, width: 300, height: 200 }, + targetFolder: 'routes' + }) +}); +
+
+ +
+
Список изображений
+
+ GET /api/images/images/{folder} +
+

Возвращает список всех изображений в папке

+
+// Получить изображения туров +fetch('/api/images/images/routes') + .then(r => r.json()) + .then(data => console.log(data.images)); +
+
+ +
+
Удаление изображения
+
+ DELETE /api/images/image +
+

Удаляет изображение с сервера

+
+fetch('/api/images/image', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: '/uploads/routes/image.jpg' + }) +}); +
+
+
+
+
+ + +
+
+

🧪 Тестирование системы

+

Попробуйте все возможности редактора изображений

+ +
+
+ + + + + + + \ No newline at end of file diff --git a/public/images/placeholders/no-image.svg b/public/images/placeholders/no-image.svg new file mode 100644 index 0000000..436c694 --- /dev/null +++ b/public/images/placeholders/no-image.svg @@ -0,0 +1,13 @@ + + + + + Нет изображения + + + No Image + + + + + \ No newline at end of file diff --git a/public/js/admin-custom.js b/public/js/admin-custom.js index 3b876d9..37036ff 100644 --- a/public/js/admin-custom.js +++ b/public/js/admin-custom.js @@ -1,5 +1,96 @@ /* Korea Tourism Agency Admin Panel Custom Scripts */ +// Функция для открытия редактора изображений +function openImageEditor(fieldName, currentValue) { + const editorUrl = `/image-editor.html?field=${fieldName}¤t=${encodeURIComponent(currentValue || '')}`; + const editorWindow = window.open(editorUrl, 'imageEditor', 'width=1200,height=800,scrollbars=yes,resizable=yes'); + + // Слушаем сообщения от редактора + const messageHandler = (event) => { + if (event.origin !== window.location.origin) return; + + if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) { + const field = document.querySelector(`input[name="${fieldName}"], input[id="${fieldName}"]`); + if (field) { + field.value = event.data.path; + field.dispatchEvent(new Event('change', { bubbles: true })); + + // Обновляем превью если есть + updateImagePreview(fieldName, event.data.path); + } + + window.removeEventListener('message', messageHandler); + editorWindow.close(); + } + }; + + window.addEventListener('message', messageHandler); + + // Очистка обработчика при закрытии окна + const checkClosed = setInterval(() => { + if (editorWindow.closed) { + window.removeEventListener('message', messageHandler); + clearInterval(checkClosed); + } + }, 1000); +} + +// Функция обновления превью изображения +function updateImagePreview(fieldName, imagePath) { + const previewId = `${fieldName}_preview`; + let preview = document.getElementById(previewId); + + if (!preview) { + // Создаем превью если его нет + const field = document.querySelector(`input[name="${fieldName}"], input[id="${fieldName}"]`); + if (field) { + preview = document.createElement('img'); + preview.id = previewId; + preview.className = 'img-thumbnail mt-2'; + preview.style.maxWidth = '200px'; + preview.style.maxHeight = '200px'; + field.parentNode.appendChild(preview); + } + } + + if (preview) { + preview.src = imagePath || '/images/placeholders/no-image.png'; + preview.alt = 'Preview'; + } +} + +// Функция добавления кнопки редактора к полю +function addImageEditorButton(field) { + const fieldName = field.name || field.id; + if (!fieldName) return; + + // Проверяем, не добавлена ли уже кнопка + if (field.parentNode.querySelector('.image-editor-btn')) return; + + const wrapper = document.createElement('div'); + wrapper.className = 'input-group'; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'btn btn-outline-secondary image-editor-btn'; + button.innerHTML = ' Выбрать'; + button.onclick = () => openImageEditor(fieldName, field.value); + + const buttonWrapper = document.createElement('div'); + buttonWrapper.className = 'input-group-append'; + buttonWrapper.appendChild(button); + + // Перестраиваем структуру + field.parentNode.insertBefore(wrapper, field); + wrapper.appendChild(field); + wrapper.appendChild(buttonWrapper); + + // Добавляем превью если есть значение + if (field.value) { + updateImagePreview(fieldName, field.value); + } +} + $(document).ready(function() { // Initialize tooltips $('[data-toggle="tooltip"]').tooltip(); @@ -11,6 +102,33 @@ $(document).ready(function() { setTimeout(function() { $('.alert').fadeOut('slow'); }, 5000); + + // Добавляем кнопки редактора к полям изображений + $('input[type="text"], input[type="url"]').each(function() { + const field = this; + const fieldName = field.name || field.id || ''; + + // Проверяем, относится ли поле к изображениям + if (fieldName.includes('image') || fieldName.includes('photo') || fieldName.includes('avatar') || + fieldName.includes('picture') || fieldName.includes('thumbnail') || fieldName.includes('banner') || + $(field).closest('label').text().toLowerCase().includes('изображение') || + $(field).closest('label').text().toLowerCase().includes('картинка') || + $(field).closest('label').text().toLowerCase().includes('фото')) { + addImageEditorButton(field); + } + }); + + // Обработчик для динамически добавляемых полей + $(document).on('focus', 'input[type="text"], input[type="url"]', function() { + const field = this; + const fieldName = field.name || field.id || ''; + + if ((fieldName.includes('image') || fieldName.includes('photo') || fieldName.includes('avatar') || + fieldName.includes('picture') || fieldName.includes('thumbnail') || fieldName.includes('banner')) && + !field.parentNode.querySelector('.image-editor-btn')) { + addImageEditorButton(field); + } + }); // Confirm delete actions $('.btn-delete').on('click', function(e) { diff --git a/public/js/admin-image-loader.js b/public/js/admin-image-loader.js new file mode 100644 index 0000000..0fe3449 --- /dev/null +++ b/public/js/admin-image-loader.js @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/public/js/admin-image-selector-fixed.js b/public/js/admin-image-selector-fixed.js new file mode 100644 index 0000000..9e39b5a --- /dev/null +++ b/public/js/admin-image-selector-fixed.js @@ -0,0 +1,314 @@ +// JavaScript для интеграции редактора изображений в AdminJS +(function() { + 'use strict'; + + // Функция для открытия редактора изображений + function openImageEditor(inputField, fieldName) { + const currentValue = inputField.value || ''; + const editorUrl = `/image-editor-compact.html?field=${fieldName}¤t=${encodeURIComponent(currentValue)}`; + + // Убираем предыдущие модальные окна + document.querySelectorAll('.image-editor-modal').forEach(modal => modal.remove()); + + // Создаем модальное окно + const modal = document.createElement('div'); + modal.className = 'image-editor-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.7); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + `; + + const content = document.createElement('div'); + content.style.cssText = ` + background: white; + border-radius: 8px; + width: 90%; + max-width: 700px; + height: 80%; + max-height: 600px; + position: relative; + box-shadow: 0 8px 32px rgba(0,0,0,0.3); + `; + + const closeBtn = document.createElement('button'); + closeBtn.innerHTML = '✕'; + closeBtn.style.cssText = ` + position: absolute; + top: 10px; + right: 10px; + background: #ff4757; + color: white; + border: none; + border-radius: 50%; + width: 30px; + height: 30px; + cursor: pointer; + z-index: 1; + font-size: 16px; + font-weight: bold; + `; + + const iframe = document.createElement('iframe'); + iframe.src = editorUrl; + iframe.style.cssText = ` + width: 100%; + height: 100%; + border: none; + border-radius: 8px; + `; + + // Обработчик сообщений от iframe + const messageHandler = function(event) { + if (event.data.type === 'imageSelected' && event.data.field === fieldName) { + console.log('🖼️ Изображение выбрано:', event.data.url); + inputField.value = event.data.url; + updateImagePreview(inputField, event.data.url); + + // Триггерим событие change для обновления формы + const changeEvent = new Event('change', { bubbles: true }); + inputField.dispatchEvent(changeEvent); + + // Триггерим input событие + const inputEvent = new Event('input', { bubbles: true }); + inputField.dispatchEvent(inputEvent); + + modal.remove(); + window.removeEventListener('message', messageHandler); + } else if (event.data.type === 'editorClosed') { + modal.remove(); + window.removeEventListener('message', messageHandler); + } + }; + + window.addEventListener('message', messageHandler); + + closeBtn.onclick = function() { + modal.remove(); + window.removeEventListener('message', messageHandler); + }; + + modal.onclick = function(e) { + if (e.target === modal) { + modal.remove(); + window.removeEventListener('message', messageHandler); + } + }; + + content.appendChild(closeBtn); + content.appendChild(iframe); + modal.appendChild(content); + document.body.appendChild(modal); + } + + // Функция обновления превью изображения + function updateImagePreview(inputField, imagePath) { + const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode; + if (!fieldContainer) return; + + // Находим или создаем превью + let preview = fieldContainer.querySelector('.image-preview'); + + if (!preview) { + preview = document.createElement('img'); + preview.className = 'image-preview'; + preview.style.cssText = ` + display: block; + max-width: 180px; + max-height: 120px; + object-fit: cover; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 8px; + margin-bottom: 8px; + `; + + // Вставляем превью после кнопки + const button = fieldContainer.querySelector('.image-editor-btn'); + if (button) { + const buttonContainer = button.parentNode; + buttonContainer.parentNode.insertBefore(preview, buttonContainer.nextSibling); + } else { + fieldContainer.appendChild(preview); + } + } + + if (imagePath && imagePath.trim()) { + preview.src = imagePath + '?t=' + Date.now(); // Добавляем timestamp для обновления + preview.style.display = 'block'; + preview.onerror = () => { + preview.style.display = 'none'; + }; + } else { + preview.style.display = 'none'; + } + } + + // Функция добавления кнопки редактора к полю + function addImageEditorButton(inputField) { + const fieldName = inputField.name || inputField.id || 'image'; + + // Проверяем, не добавлена ли уже кнопка + const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode; + if (fieldContainer.querySelector('.image-editor-btn')) { + return; + } + + // Создаем контейнер для кнопки + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = ` + margin-top: 8px; + display: flex; + align-items: flex-start; + gap: 10px; + `; + + // Создаем кнопку + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'image-editor-btn'; + button.innerHTML = '📷 Выбрать'; + button.style.cssText = ` + padding: 6px 12px; + background: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + white-space: nowrap; + `; + + button.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + openImageEditor(inputField, fieldName); + }; + + buttonContainer.appendChild(button); + + // Добавляем контейнер после поля ввода + if (inputField.nextSibling) { + inputField.parentNode.insertBefore(buttonContainer, inputField.nextSibling); + } else { + inputField.parentNode.appendChild(buttonContainer); + } + + // Добавляем превью если есть значение + if (inputField.value && inputField.value.trim()) { + updateImagePreview(inputField, inputField.value); + } + + // Слушаем изменения в поле для обновления превью + inputField.addEventListener('input', () => { + updateImagePreview(inputField, inputField.value); + }); + + inputField.addEventListener('change', () => { + updateImagePreview(inputField, inputField.value); + }); + } + + // Функция проверки, является ли поле полем изображения + function isImageField(inputField) { + const fieldName = (inputField.name || inputField.id || '').toLowerCase(); + let labelText = ''; + + // Ищем label для поля + const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]'); + if (fieldContainer) { + const label = fieldContainer.querySelector('label, .property-label, h3'); + if (label) { + labelText = label.textContent.toLowerCase(); + } + } + + // Проверяем по имени поля или тексту label + return fieldName.includes('image') || + fieldName.includes('photo') || + fieldName.includes('avatar') || + fieldName.includes('picture') || + fieldName.includes('banner') || + fieldName.includes('thumbnail') || + (fieldName.includes('url') && (labelText.includes('image') || labelText.includes('изображение'))) || + labelText.includes('изображение') || + labelText.includes('картинка') || + labelText.includes('фото') || + labelText.includes('image') || + labelText.includes('picture'); + } + + // Функция сканирования и добавления кнопок к полям изображений + function scanAndAddImageButtons() { + console.log('🔍 Сканирование полей для добавления кнопок редактора изображений...'); + + // Более широкий поиск полей ввода + const inputFields = document.querySelectorAll('input[type="text"], input[type="url"], input:not([type="hidden"]):not([type="submit"]):not([type="button"])'); + + console.log(`📋 Найдено ${inputFields.length} полей ввода`); + + inputFields.forEach((inputField, index) => { + const fieldName = inputField.name || inputField.id || `field_${index}`; + const isImage = isImageField(inputField); + const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode; + const hasButton = fieldContainer.querySelector('.image-editor-btn'); + + console.log(`🔸 Поле "${fieldName}": isImage=${isImage}, hasButton=${!!hasButton}`); + + if (isImage && !hasButton) { + console.log(`➕ Добавляем кнопку для поля "${fieldName}"`); + addImageEditorButton(inputField); + } + }); + } + + // Инициализация при загрузке DOM + function initialize() { + console.log('🚀 Инициализация селектора изображений AdminJS'); + + scanAndAddImageButtons(); + + // Наблюдаем за изменениями в DOM для динамически добавляемых полей + const observer = new MutationObserver((mutations) => { + let shouldScan = false; + mutations.forEach(mutation => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach(node => { + if (node.nodeType === 1 && (node.tagName === 'INPUT' || node.querySelector('input'))) { + shouldScan = true; + } + }); + } + }); + + if (shouldScan) { + setTimeout(scanAndAddImageButtons, 100); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + } + + // Ждем загрузки DOM + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initialize); + } else { + initialize(); + } + + // Также запускаем через задержки для AdminJS + setTimeout(initialize, 1000); + setTimeout(initialize, 3000); + setTimeout(initialize, 5000); + +})(); \ No newline at end of file diff --git a/public/js/admin-image-selector.js b/public/js/admin-image-selector.js new file mode 100644 index 0000000..1e8ab34 --- /dev/null +++ b/public/js/admin-image-selector.js @@ -0,0 +1,300 @@ +// JavaScript для интеграции редактора изображений в AdminJS +(function() { + 'use strict'; + + // Функция для открытия редактора изображений + function openImageEditor(inputField, fieldName) { + const currentValue = inputField.value || ''; + const editorUrl = `/image-editor-compact.html?field=${fieldName}¤t=${encodeURIComponent(currentValue)}`; + + // Убираем предыдущие модальные окна + document.querySelectorAll('.image-editor-modal').forEach(modal => modal.remove()); + + // Создаем модальное окно + const modal = document.createElement('div'); + modal.className = 'image-editor-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.7); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + `; + + const content = document.createElement('div'); + content.style.cssText = ` + background: white; + border-radius: 8px; + width: 90%; + max-width: 700px; + height: 80%; + max-height: 600px; + position: relative; + box-shadow: 0 8px 32px rgba(0,0,0,0.3); + `; + + const closeBtn = document.createElement('button'); + closeBtn.innerHTML = '✕'; + closeBtn.style.cssText = ` + position: absolute; + top: 15px; + right: 15px; + background: #ff4757; + color: white; + border: none; + border-radius: 50%; + width: 35px; + height: 35px; + cursor: pointer; + z-index: 1; + font-size: 18px; + font-weight: bold; + `; + + const iframe = document.createElement('iframe'); + iframe.src = editorUrl; + iframe.style.cssText = ` + width: 100%; + height: 100%; + border: none; + border-radius: 8px; + `; + + // Обработчик закрытия + const closeModal = () => { + document.body.removeChild(modal); + window.removeEventListener('message', handleMessage); + }; + + closeBtn.onclick = closeModal; + + // Обработчик сообщений от редактора + const handleMessage = (event) => { + if (event.origin !== window.location.origin) return; + + if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) { + inputField.value = event.data.path; + + // Триггерим события изменения + inputField.dispatchEvent(new Event('input', { bubbles: true })); + inputField.dispatchEvent(new Event('change', { bubbles: true })); + + // Обновляем превью если есть + updateImagePreview(inputField, event.data.path); + + closeModal(); + } + }; + + window.addEventListener('message', handleMessage); + + content.appendChild(closeBtn); + content.appendChild(iframe); + modal.appendChild(content); + document.body.appendChild(modal); + + // Закрытие по клику на фон + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeModal(); + } + }); + } + + // Функция обновления превью изображения + function updateImagePreview(inputField, imagePath) { + const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]'); + if (!fieldContainer) return; + + // Находим или создаем превью + let preview = fieldContainer.querySelector('.image-preview'); + + if (!preview) { + preview = document.createElement('img'); + preview.className = 'image-preview'; + preview.style.cssText = ` + display: block; + max-width: 180px; + max-height: 120px; + object-fit: cover; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 8px; + margin-bottom: 8px; + `; + + // Вставляем превью после кнопки + const button = fieldContainer.querySelector('.image-editor-btn'); + if (button && button.nextSibling) { + button.parentNode.insertBefore(preview, button.nextSibling); + } else { + inputField.parentNode.appendChild(preview); + } + } + + if (imagePath && imagePath.trim()) { + preview.src = imagePath + '?t=' + Date.now(); // Добавляем timestamp для обновления + preview.style.display = 'block'; + preview.onerror = () => { + preview.style.display = 'none'; + }; + } else { + preview.style.display = 'none'; + } + } + + // Функция добавления кнопки редактора к полю + function addImageEditorButton(inputField) { + const fieldName = inputField.name || inputField.id || 'image'; + + // Проверяем, не добавлена ли уже кнопка + const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode; + if (fieldContainer.querySelector('.image-editor-btn')) { + return; + } + + // Создаем контейнер для кнопки и превью + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = ` + margin-top: 8px; + display: flex; + align-items: flex-start; + gap: 10px; + `; + + // Создаем кнопку + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'image-editor-btn'; + button.innerHTML = '📷 Выбрать'; + button.style.cssText = ` + padding: 6px 12px; + background: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + white-space: nowrap; + `; + + button.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + openImageEditor(inputField, fieldName); + }; + + buttonContainer.appendChild(button); + + // Добавляем контейнер после поля ввода + if (inputField.nextSibling) { + inputField.parentNode.insertBefore(buttonContainer, inputField.nextSibling); + } else { + inputField.parentNode.appendChild(buttonContainer); + } + + // Добавляем превью если есть значение + if (inputField.value && inputField.value.trim()) { + updateImagePreview(inputField, inputField.value); + } + + // Слушаем изменения в поле для обновления превью + inputField.addEventListener('input', () => { + updateImagePreview(inputField, inputField.value); + }); + + inputField.addEventListener('change', () => { + updateImagePreview(inputField, inputField.value); + }); + } + + // Функция проверки, является ли поле полем изображения + function isImageField(inputField) { + const fieldName = (inputField.name || inputField.id || '').toLowerCase(); + let labelText = ''; + + // Ищем label для поля + const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]'); + if (fieldContainer) { + const label = fieldContainer.querySelector('label, .property-label, h3'); + if (label) { + labelText = label.textContent.toLowerCase(); + } + } + + // Проверяем по имени поля или тексту label + return fieldName.includes('image') || + fieldName.includes('photo') || + fieldName.includes('avatar') || + fieldName.includes('picture') || + fieldName.includes('banner') || + fieldName.includes('thumbnail') || + fieldName.includes('url') && (labelText.includes('image') || labelText.includes('изображение')) || + labelText.includes('изображение') || + labelText.includes('картинка') || + labelText.includes('фото') || + labelText.includes('image') || + labelText.includes('picture'); + } + + // Функция сканирования и добавления кнопок к полям изображений + function scanAndAddImageButtons() { + console.log('🔍 Сканирование полей для добавления кнопок редактора изображений...'); + + // Более широкий поиск полей ввода + const inputFields = document.querySelectorAll('input[type="text"], input[type="url"], input:not([type="hidden"]):not([type="submit"]):not([type="button"])'); + + console.log(`📋 Найдено ${inputFields.length} полей ввода`); + + inputFields.forEach((inputField, index) => { + const fieldName = inputField.name || inputField.id || `field_${index}`; + const isImage = isImageField(inputField); + const hasButton = inputField.parentNode.querySelector('.image-editor-btn'); + + console.log(`🔸 Поле "${fieldName}": isImage=${isImage}, hasButton=${hasButton}`); + + if (isImage && !hasButton) { + console.log(`➕ Добавляем кнопку для поля "${fieldName}"`); + addImageEditorButton(inputField); + } + }); + } + + // Инициализация при загрузке DOM + function initialize() { + console.log('🚀 Инициализация селектора изображений AdminJS'); + + scanAndAddImageButtons(); + + // Наблюдаем за изменениями в DOM для динамически добавляемых полей + const observer = new MutationObserver(() => { + scanAndAddImageButtons(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + // Периодическое сканирование для надежности + setInterval(scanAndAddImageButtons, 2000); + } + + // Ждем загрузки DOM + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initialize); + } else { + initialize(); + } + + // Также запускаем через задержки для AdminJS + setTimeout(initialize, 1000); + setTimeout(initialize, 3000); + setTimeout(initialize, 5000); + +})(); \ No newline at end of file diff --git a/public/js/image-editor.js b/public/js/image-editor.js new file mode 100644 index 0000000..502bd48 --- /dev/null +++ b/public/js/image-editor.js @@ -0,0 +1,690 @@ +/** + * Image Editor Modal Component + * Предоставляет интерфейс для загрузки, обрезки и редактирования изображений + */ + +class ImageEditor { + constructor(options = {}) { + this.options = { + targetFolder: 'routes', + aspectRatio: null, // null = свободная обрезка, или например 16/9 + maxWidth: 1200, + maxHeight: 800, + ...options + }; + + this.modal = null; + this.canvas = null; + this.ctx = null; + this.image = null; + this.imageData = null; + this.cropBox = null; + this.isDragging = false; + this.lastMousePos = { x: 0, y: 0 }; + this.rotation = 0; + this.flipHorizontal = false; + this.flipVertical = false; + + this.onSave = options.onSave || (() => {}); + this.onCancel = options.onCancel || (() => {}); + + this.createModal(); + } + + createModal() { + // Создаем модальное окно + const modalHTML = ` +
+
+
+

Редактор изображений

+ +
+ +
+ +
+
+
📷
+

Перетащите изображение сюда или

+ +
+
+ + + +
+ + +
+
+ `; + + // Добавляем стили + if (!document.getElementById('image-editor-styles')) { + const styles = document.createElement('style'); + styles.id = 'image-editor-styles'; + styles.textContent = ` + .image-editor-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + } + + .image-editor-modal { + background: white; + border-radius: 8px; + width: 90vw; + max-width: 900px; + max-height: 90vh; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .image-editor-header { + padding: 20px; + border-bottom: 1px solid #ddd; + display: flex; + justify-content: space-between; + align-items: center; + } + + .image-editor-header h3 { + margin: 0; + color: #333; + } + + .close-btn { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #999; + } + + .close-btn:hover { + color: #333; + } + + .image-editor-body { + flex: 1; + padding: 20px; + overflow: auto; + } + + .upload-area { + border: 2px dashed #ccc; + border-radius: 8px; + padding: 40px; + text-align: center; + background: #f9f9f9; + transition: all 0.3s ease; + } + + .upload-area.dragover { + border-color: #007bff; + background: #e3f2fd; + } + + .upload-content .upload-icon { + font-size: 48px; + margin-bottom: 16px; + } + + .upload-content p { + margin: 0; + color: #666; + } + + .btn-link { + background: none; + border: none; + color: #007bff; + text-decoration: underline; + cursor: pointer; + } + + .editor-area { + display: flex; + flex-direction: column; + gap: 16px; + } + + .editor-toolbar { + display: flex; + gap: 8px; + justify-content: center; + padding: 12px; + background: #f8f9fa; + border-radius: 4px; + } + + .tool-btn { + background: white; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px 12px; + cursor: pointer; + transition: all 0.2s ease; + } + + .tool-btn:hover { + background: #007bff; + color: white; + border-color: #007bff; + } + + .canvas-container { + position: relative; + display: flex; + justify-content: center; + background: #f0f0f0; + border-radius: 4px; + overflow: hidden; + } + + #editorCanvas { + max-width: 100%; + max-height: 400px; + border: 1px solid #ddd; + } + + .crop-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + } + + .crop-box { + position: absolute; + border: 2px solid #007bff; + background: rgba(0, 123, 255, 0.1); + pointer-events: all; + cursor: move; + } + + .crop-handle { + position: absolute; + width: 10px; + height: 10px; + background: #007bff; + border: 1px solid white; + border-radius: 2px; + } + + .crop-handle.nw { top: -5px; left: -5px; cursor: nw-resize; } + .crop-handle.ne { top: -5px; right: -5px; cursor: ne-resize; } + .crop-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; } + .crop-handle.se { bottom: -5px; right: -5px; cursor: se-resize; } + .crop-handle.n { top: -5px; left: 50%; margin-left: -5px; cursor: n-resize; } + .crop-handle.s { bottom: -5px; left: 50%; margin-left: -5px; cursor: s-resize; } + .crop-handle.e { top: 50%; right: -5px; margin-top: -5px; cursor: e-resize; } + .crop-handle.w { top: 50%; left: -5px; margin-top: -5px; cursor: w-resize; } + + .image-info { + display: flex; + justify-content: space-between; + padding: 8px; + background: #f8f9fa; + border-radius: 4px; + font-size: 12px; + color: #666; + } + + .image-editor-footer { + padding: 20px; + border-top: 1px solid #ddd; + display: flex; + gap: 12px; + justify-content: flex-end; + } + + .btn { + padding: 8px 16px; + border-radius: 4px; + border: 1px solid; + cursor: pointer; + font-size: 14px; + } + + .btn-secondary { + background: #f8f9fa; + color: #333; + border-color: #ddd; + } + + .btn-primary { + background: #007bff; + color: white; + border-color: #007bff; + } + + .btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + `; + document.head.appendChild(styles); + } + + // Добавляем модальное окно в DOM + document.body.insertAdjacentHTML('beforeend', modalHTML); + this.modal = document.querySelector('.image-editor-overlay'); + + this.bindEvents(); + } + + bindEvents() { + // Закрытие модального окна + this.modal.querySelector('.close-btn').addEventListener('click', () => this.close()); + this.modal.querySelector('#cancelBtn').addEventListener('click', () => this.close()); + + // Клик по overlay для закрытия + this.modal.addEventListener('click', (e) => { + if (e.target === this.modal) this.close(); + }); + + // Загрузка файла + const fileInput = this.modal.querySelector('#fileInput'); + const selectFileBtn = this.modal.querySelector('#selectFileBtn'); + const uploadArea = this.modal.querySelector('#uploadArea'); + + selectFileBtn.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', (e) => this.handleFileSelect(e)); + + // Drag & Drop + uploadArea.addEventListener('dragover', (e) => { + e.preventDefault(); + uploadArea.classList.add('dragover'); + }); + + uploadArea.addEventListener('dragleave', () => { + uploadArea.classList.remove('dragover'); + }); + + uploadArea.addEventListener('drop', (e) => { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + const files = e.dataTransfer.files; + if (files.length > 0) { + this.loadImage(files[0]); + } + }); + + // Инструменты редактирования + this.modal.querySelector('#rotateLeftBtn').addEventListener('click', () => this.rotate(-90)); + this.modal.querySelector('#rotateRightBtn').addEventListener('click', () => this.rotate(90)); + this.modal.querySelector('#flipHorizontalBtn').addEventListener('click', () => this.flipHorizontal = !this.flipHorizontal, this.redraw()); + this.modal.querySelector('#flipVerticalBtn').addEventListener('click', () => this.flipVertical = !this.flipVertical, this.redraw()); + this.modal.querySelector('#resetCropBtn').addEventListener('click', () => this.resetCrop()); + + // Сохранение + this.modal.querySelector('#saveBtn').addEventListener('click', () => this.save()); + } + + handleFileSelect(e) { + const file = e.target.files[0]; + if (file) { + this.loadImage(file); + } + } + + async loadImage(file) { + if (!file.type.startsWith('image/')) { + alert('Пожалуйста, выберите изображение'); + return; + } + + const formData = new FormData(); + formData.append('image', file); + + try { + const response = await fetch('/api/images/upload-image', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.success) { + this.imageData = result; + this.showEditor(); + this.loadImageToCanvas(result.tempUrl); + } else { + alert(result.error || 'Ошибка загрузки изображения'); + } + } catch (error) { + console.error('Upload error:', error); + alert('Ошибка загрузки изображения'); + } + } + + loadImageToCanvas(imageUrl) { + this.image = new Image(); + this.image.onload = () => { + this.initCanvas(); + this.initCropBox(); + this.redraw(); + this.updateInfo(); + this.modal.querySelector('#saveBtn').disabled = false; + }; + this.image.src = imageUrl; + } + + showEditor() { + this.modal.querySelector('#uploadArea').style.display = 'none'; + this.modal.querySelector('#editorArea').style.display = 'block'; + } + + initCanvas() { + this.canvas = this.modal.querySelector('#editorCanvas'); + this.ctx = this.canvas.getContext('2d'); + + // Вычисляем размеры canvas + const containerWidth = 600; // максимальная ширина + const containerHeight = 400; // максимальная высота + + const imageRatio = this.image.width / this.image.height; + const containerRatio = containerWidth / containerHeight; + + if (imageRatio > containerRatio) { + this.canvas.width = containerWidth; + this.canvas.height = containerWidth / imageRatio; + } else { + this.canvas.width = containerHeight * imageRatio; + this.canvas.height = containerHeight; + } + + this.scaleX = this.canvas.width / this.image.width; + this.scaleY = this.canvas.height / this.image.height; + } + + initCropBox() { + const overlay = this.modal.querySelector('#cropOverlay'); + const cropBox = this.modal.querySelector('#cropBox'); + + // Устанавливаем размеры overlay как у canvas + const canvasRect = this.canvas.getBoundingClientRect(); + const containerRect = this.canvas.parentElement.getBoundingClientRect(); + + overlay.style.width = `${canvasRect.width}px`; + overlay.style.height = `${canvasRect.height}px`; + overlay.style.left = `${canvasRect.left - containerRect.left}px`; + overlay.style.top = `${canvasRect.top - containerRect.top}px`; + + // Инициализируем crop box (50% от центра) + const boxWidth = canvasRect.width * 0.6; + const boxHeight = canvasRect.height * 0.6; + const boxLeft = (canvasRect.width - boxWidth) / 2; + const boxTop = (canvasRect.height - boxHeight) / 2; + + cropBox.style.width = `${boxWidth}px`; + cropBox.style.height = `${boxHeight}px`; + cropBox.style.left = `${boxLeft}px`; + cropBox.style.top = `${boxTop}px`; + + this.cropBox = { + x: boxLeft / canvasRect.width, + y: boxTop / canvasRect.height, + width: boxWidth / canvasRect.width, + height: boxHeight / canvasRect.height + }; + + this.bindCropEvents(cropBox, overlay); + } + + bindCropEvents(cropBox, overlay) { + let resizing = false; + let moving = false; + let startPos = { x: 0, y: 0 }; + let startBox = {}; + + // Обработчик для перемещения + cropBox.addEventListener('mousedown', (e) => { + if (e.target === cropBox) { + moving = true; + startPos = { x: e.clientX, y: e.clientY }; + startBox = { ...this.cropBox }; + e.preventDefault(); + } + }); + + // Обработчик для изменения размера + const handles = cropBox.querySelectorAll('.crop-handle'); + handles.forEach(handle => { + handle.addEventListener('mousedown', (e) => { + resizing = handle.className.replace('crop-handle ', ''); + startPos = { x: e.clientX, y: e.clientY }; + startBox = { ...this.cropBox }; + e.preventDefault(); + e.stopPropagation(); + }); + }); + + // Обработчики движения и отпускания мыши + document.addEventListener('mousemove', (e) => { + if (moving || resizing) { + this.updateCropBox(e, startPos, startBox, moving ? 'move' : resizing, overlay); + } + }); + + document.addEventListener('mouseup', () => { + moving = false; + resizing = false; + }); + } + + updateCropBox(e, startPos, startBox, action, overlay) { + const overlayRect = overlay.getBoundingClientRect(); + const deltaX = (e.clientX - startPos.x) / overlayRect.width; + const deltaY = (e.clientY - startPos.y) / overlayRect.height; + + let newBox = { ...startBox }; + + if (action === 'move') { + newBox.x = Math.max(0, Math.min(1 - startBox.width, startBox.x + deltaX)); + newBox.y = Math.max(0, Math.min(1 - startBox.height, startBox.y + deltaY)); + } else { + // Изменение размера в зависимости от handle + if (action.includes('n')) newBox.y += deltaY, newBox.height -= deltaY; + if (action.includes('s')) newBox.height += deltaY; + if (action.includes('w')) newBox.x += deltaX, newBox.width -= deltaX; + if (action.includes('e')) newBox.width += deltaX; + + // Ограничиваем минимальные размеры + if (newBox.width < 0.1) newBox.width = 0.1; + if (newBox.height < 0.1) newBox.height = 0.1; + + // Ограничиваем границы + if (newBox.x < 0) newBox.x = 0; + if (newBox.y < 0) newBox.y = 0; + if (newBox.x + newBox.width > 1) newBox.width = 1 - newBox.x; + if (newBox.y + newBox.height > 1) newBox.height = 1 - newBox.y; + } + + this.cropBox = newBox; + this.updateCropBoxDisplay(overlay); + this.updateInfo(); + } + + updateCropBoxDisplay(overlay) { + const cropBoxElement = this.modal.querySelector('#cropBox'); + const overlayRect = overlay.getBoundingClientRect(); + + cropBoxElement.style.left = `${this.cropBox.x * overlayRect.width}px`; + cropBoxElement.style.top = `${this.cropBox.y * overlayRect.height}px`; + cropBoxElement.style.width = `${this.cropBox.width * overlayRect.width}px`; + cropBoxElement.style.height = `${this.cropBox.height * overlayRect.height}px`; + } + + rotate(degrees) { + this.rotation = (this.rotation + degrees) % 360; + this.redraw(); + this.updateInfo(); + } + + redraw() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + this.ctx.save(); + + // Перемещаем к центру + this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); + + // Поворот + this.ctx.rotate(this.rotation * Math.PI / 180); + + // Отражение + this.ctx.scale(this.flipHorizontal ? -1 : 1, this.flipVertical ? -1 : 1); + + // Рисуем изображение + this.ctx.drawImage( + this.image, + -this.canvas.width / 2, + -this.canvas.height / 2, + this.canvas.width, + this.canvas.height + ); + + this.ctx.restore(); + } + + resetCrop() { + // Сброс crop box к исходному состоянию + this.cropBox = { x: 0.2, y: 0.2, width: 0.6, height: 0.6 }; + this.updateCropBoxDisplay(this.modal.querySelector('#cropOverlay')); + this.updateInfo(); + } + + updateInfo() { + const imageInfo = this.modal.querySelector('#imageInfo'); + const cropInfo = this.modal.querySelector('#cropInfo'); + + imageInfo.textContent = `Размер: ${this.image.width}x${this.image.height}`; + + const cropWidth = Math.round(this.cropBox.width * this.image.width); + const cropHeight = Math.round(this.cropBox.height * this.image.height); + cropInfo.textContent = `Обрезка: ${cropWidth}x${cropHeight}`; + } + + async save() { + const saveBtn = this.modal.querySelector('#saveBtn'); + saveBtn.disabled = true; + saveBtn.textContent = 'Сохранение...'; + + try { + const cropData = { + x: this.cropBox.x * this.image.width, + y: this.cropBox.y * this.image.height, + width: this.cropBox.width * this.image.width, + height: this.cropBox.height * this.image.height + }; + + const response = await fetch('/api/images/process-image', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + tempId: this.imageData.tempId, + rotation: this.rotation, + flipHorizontal: this.flipHorizontal, + flipVertical: this.flipVertical, + cropData, + targetFolder: this.options.targetFolder + }) + }); + + const result = await response.json(); + + if (result.success) { + this.onSave(result.url); + this.close(); + } else { + alert(result.error || 'Ошибка сохранения'); + saveBtn.disabled = false; + saveBtn.textContent = 'Сохранить'; + } + } catch (error) { + console.error('Save error:', error); + alert('Ошибка сохранения изображения'); + saveBtn.disabled = false; + saveBtn.textContent = 'Сохранить'; + } + } + + close() { + // Удаляем временный файл + if (this.imageData && this.imageData.tempId) { + fetch(`/api/images/temp-image/${this.imageData.tempId}`, { + method: 'DELETE' + }).catch(console.error); + } + + this.onCancel(); + if (this.modal) { + this.modal.remove(); + } + } + + show() { + if (this.modal) { + this.modal.style.display = 'flex'; + } + } +} + +// Глобально доступная функция для открытия редактора +window.openImageEditor = function(options = {}) { + return new Promise((resolve, reject) => { + const editor = new ImageEditor({ + ...options, + onSave: (url) => resolve(url), + onCancel: () => reject(new Error('Canceled')) + }); + editor.show(); + }); +}; \ No newline at end of file diff --git a/public/test-editor.html b/public/test-editor.html new file mode 100644 index 0000000..bfbdf33 --- /dev/null +++ b/public/test-editor.html @@ -0,0 +1,103 @@ + + + + + + Тест редактора изображений + + + +

Тестирование редактора изображений

+ +
+

Тест для туров (routes)

+ +
+
+ +
+

Тест для гидов (guides)

+ +
+
+ +
+

Тест для статей (articles)

+ +
+
+ + + + + + + \ No newline at end of file diff --git a/public/test-image-editor.html b/public/test-image-editor.html new file mode 100644 index 0000000..5643fc8 --- /dev/null +++ b/public/test-image-editor.html @@ -0,0 +1,191 @@ + + + + + + Тест редактора изображений + + + +
+

Тест интеграции редактора изображений

+

Этот файл демонстрирует, как редактор изображений будет работать с AdminJS.

+ +
+
+
+
+
Поле изображения маршрута
+
+
+ +
+ + +
+ +
+
+
+ +
+
+
+
Поле изображения гида
+
+
+ +
+ + +
+ +
+
+
+
+ +
+
+
+
+
Доступные изображения
+
+
+
+ +
+
+
+
+
+ +
+
+
+
Как использовать:
+
    +
  1. Нажмите кнопку "Выбрать" рядом с полем изображения
  2. +
  3. Откроется редактор изображений в новом окне
  4. +
  5. Выберите изображение из галереи, загрузите новое или укажите URL
  6. +
  7. Нажмите "Выбрать" в редакторе
  8. +
  9. Поле автоматически обновится с выбранным путем
  10. +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/app.js b/src/app.js index 8a0818a..df65d48 100644 --- a/src/app.js +++ b/src/app.js @@ -43,7 +43,7 @@ app.use(helmet({ defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"], fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"], - scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://code.jquery.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://code.jquery.com"], imgSrc: ["'self'", "data:", "https:", "blob:"], connectSrc: ["'self'"], }, @@ -159,6 +159,8 @@ const articlesRouter = (await import('./routes/articles.js')).default; const apiRouter = (await import('./routes/api.js')).default; const settingsRouter = (await import('./routes/settings.js')).default; const ratingsRouter = (await import('./routes/ratings.js')).default; +const imagesRouter = (await import('./routes/images.js')).default; +const crudRouter = (await import('./routes/crud.js')).default; app.use('/', indexRouter); app.use('/routes', toursRouter); @@ -167,6 +169,8 @@ app.use('/articles', articlesRouter); app.use('/api', apiRouter); app.use('/api', ratingsRouter); app.use('/', settingsRouter); // Settings routes (CSS and API) +app.use('/api/images', imagesRouter); // Image management routes +app.use('/api/crud', crudRouter); // CRUD API routes // Health check endpoint app.get('/health', (req, res) => { @@ -177,6 +181,16 @@ app.get('/health', (req, res) => { }); }); +// Test image editor endpoint +app.get('/test-editor', (req, res) => { + res.sendFile(path.join(__dirname, '../public/test-editor.html')); +}); + +// Image system documentation +app.get('/image-docs', (req, res) => { + res.sendFile(path.join(__dirname, '../public/image-system-docs.html')); +}); + // Error handling app.use((req, res) => { res.status(404).render('error', { diff --git a/src/components/ImageEditor.jsx b/src/components/ImageEditor.jsx new file mode 100644 index 0000000..4028393 --- /dev/null +++ b/src/components/ImageEditor.jsx @@ -0,0 +1,271 @@ +import React, { useState, useEffect } from 'react'; + +const ImageEditor = ({ record, property, onChange }) => { + const [currentValue, setCurrentValue] = useState(record.params[property.name] || ''); + const [showEditor, setShowEditor] = useState(false); + const [images, setImages] = useState([]); + const [loading, setLoading] = useState(false); + const [activeTab, setActiveTab] = useState('gallery'); + const [uploadFile, setUploadFile] = useState(null); + + // Загрузка галереи изображений + useEffect(() => { + if (showEditor) { + loadGallery(); + } + }, [showEditor]); + + const loadGallery = async () => { + setLoading(true); + try { + const response = await fetch('/api/images/gallery?folder=all'); + const result = await response.json(); + if (result.success) { + setImages(result.data); + } + } catch (error) { + console.error('Error loading gallery:', error); + } finally { + setLoading(false); + } + }; + + const handleImageSelect = (imagePath) => { + setCurrentValue(imagePath); + onChange(property.name, imagePath); + setShowEditor(false); + }; + + const handleFileUpload = async () => { + if (!uploadFile) return; + + const formData = new FormData(); + formData.append('image', uploadFile); + + setLoading(true); + try { + const response = await fetch('/api/images/upload', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + if (result.success) { + handleImageSelect(result.data.path); + await loadGallery(); // Обновляем галерею + } else { + alert('Ошибка загрузки: ' + result.error); + } + } catch (error) { + alert('Ошибка загрузки: ' + error.message); + } finally { + setLoading(false); + } + }; + + const getFolderFromPropertyName = () => { + const name = property.name.toLowerCase(); + if (name.includes('route')) return 'routes'; + if (name.includes('guide')) return 'guides'; + if (name.includes('article')) return 'articles'; + return 'general'; + }; + + return ( +
+ + + {/* Поле ввода и кнопка */} +
+ { + setCurrentValue(e.target.value); + onChange(property.name, e.target.value); + }} + placeholder="Путь к изображению" + style={{ + flex: 1, + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + marginRight: '8px' + }} + /> + +
+ + {/* Предварительный просмотр */} + {currentValue && ( +
+ Preview { + e.target.style.display = 'none'; + }} + /> +
+ )} + + {/* Редактор изображений */} + {showEditor && ( +
+ {/* Вкладки */} +
+ + +
+ + {/* Контент вкладки Галерея */} + {activeTab === 'gallery' && ( +
+ {loading ? ( +
+ Загрузка... +
+ ) : ( +
+ {images.map((image) => ( +
handleImageSelect(image.path)} + style={{ + cursor: 'pointer', + border: currentValue === image.path ? '2px solid #007bff' : '1px solid #ddd', + borderRadius: '4px', + overflow: 'hidden', + backgroundColor: 'white' + }} + > + {image.name} +
+
+ {image.name} +
+
+ {image.folder} +
+
+
+ ))} +
+ )} +
+ )} + + {/* Контент вкладки Загрузить */} + {activeTab === 'upload' && ( +
+
+ setUploadFile(e.target.files[0])} + style={{ marginBottom: '12px' }} + /> +
+ Поддерживаются: JPG, PNG, GIF (макс. 5МБ) +
+
+ + {uploadFile && ( +
+
+ Выбран файл: {uploadFile.name} +
+ +
+ )} +
+ )} +
+ )} +
+ ); +}; + +export default ImageEditor; \ No newline at end of file diff --git a/src/components/ImageSelector.jsx b/src/components/ImageSelector.jsx new file mode 100644 index 0000000..baa5bd5 --- /dev/null +++ b/src/components/ImageSelector.jsx @@ -0,0 +1,130 @@ +// Простой компонент для выбора изображений, который работает с AdminJS +import React from 'react'; + +const ImageSelector = ({ record, property, onChange }) => { + const currentValue = record.params[property.name] || ''; + + const openImagePicker = () => { + // Создаем модальное окно с iframe для редактора изображений + const modal = document.createElement('div'); + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + `; + + const content = document.createElement('div'); + content.style.cssText = ` + background: white; + border-radius: 8px; + width: 90%; + height: 90%; + position: relative; + `; + + const closeBtn = document.createElement('button'); + closeBtn.textContent = '✕'; + closeBtn.style.cssText = ` + position: absolute; + top: 10px; + right: 10px; + background: #ff4757; + color: white; + border: none; + border-radius: 50%; + width: 30px; + height: 30px; + cursor: pointer; + z-index: 1; + `; + + const iframe = document.createElement('iframe'); + iframe.src = `/image-editor.html?field=${property.name}¤t=${encodeURIComponent(currentValue)}`; + iframe.style.cssText = ` + width: 100%; + height: 100%; + border: none; + border-radius: 8px; + `; + + closeBtn.onclick = () => document.body.removeChild(modal); + + // Слушаем сообщения от iframe + const handleMessage = (event) => { + if (event.origin !== window.location.origin) return; + + if (event.data.type === 'imageSelected' && event.data.targetField === property.name) { + onChange(property.name, event.data.path); + document.body.removeChild(modal); + window.removeEventListener('message', handleMessage); + } + }; + + window.addEventListener('message', handleMessage); + + content.appendChild(closeBtn); + content.appendChild(iframe); + modal.appendChild(content); + document.body.appendChild(modal); + }; + + return React.createElement('div', null, + React.createElement('label', { + style: { fontWeight: 'bold', marginBottom: '8px', display: 'block' } + }, property.label || property.name), + + React.createElement('div', { + style: { display: 'flex', alignItems: 'center', marginBottom: '12px' } + }, + React.createElement('input', { + type: 'text', + value: currentValue, + onChange: (e) => onChange(property.name, e.target.value), + placeholder: 'Путь к изображению', + style: { + flex: 1, + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + marginRight: '8px' + } + }), + React.createElement('button', { + type: 'button', + onClick: openImagePicker, + style: { + padding: '8px 16px', + backgroundColor: '#007bff', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer' + } + }, 'Выбрать') + ), + + currentValue && React.createElement('img', { + src: currentValue, + alt: 'Preview', + style: { + maxWidth: '200px', + maxHeight: '200px', + objectFit: 'cover', + border: '1px solid #ddd', + borderRadius: '4px' + }, + onError: (e) => { + e.target.style.display = 'none'; + } + }) + ); +}; + +export default ImageSelector; \ No newline at end of file diff --git a/src/config/adminjs-simple.js b/src/config/adminjs-simple.js index 812e805..5d0668a 100644 --- a/src/config/adminjs-simple.js +++ b/src/config/adminjs-simple.js @@ -1,9 +1,16 @@ import AdminJS from 'adminjs'; import AdminJSExpress from '@adminjs/express'; import AdminJSSequelize from '@adminjs/sequelize'; +import uploadFeature from '@adminjs/upload'; import bcrypt from 'bcryptjs'; import pkg from 'pg'; import { Sequelize, DataTypes } from 'sequelize'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); const { Pool } = pkg; // Регистрируем адаптер Sequelize @@ -61,6 +68,7 @@ const Guides = sequelize.define('guides', { specialization: { type: DataTypes.ENUM('city', 'mountain', 'fishing', 'general') }, bio: { type: DataTypes.TEXT }, experience: { type: DataTypes.INTEGER }, + image_url: { type: DataTypes.STRING }, hourly_rate: { type: DataTypes.DECIMAL(10, 2) }, is_active: { type: DataTypes.BOOLEAN, defaultValue: true }, created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW } @@ -280,7 +288,7 @@ const adminJsOptions = { }, image_url: { type: 'string', - description: 'URL изображения тура (например: /images/tours/seoul-1.jpg)' + description: 'Изображение тура. Кнопка "Выбрать" будет добавлена автоматически' }, is_featured: { type: 'boolean' }, is_active: { type: 'boolean' }, @@ -298,8 +306,8 @@ const adminJsOptions = { options: { parent: { name: 'Персонал', icon: 'Users' }, listProperties: ['id', 'name', 'email', 'specialization', 'experience', 'hourly_rate', 'is_active'], - editProperties: ['name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'hourly_rate', 'is_active'], - showProperties: ['id', 'name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'hourly_rate', 'is_active', 'created_at'], + editProperties: ['name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'image_url', 'hourly_rate', 'is_active'], + showProperties: ['id', 'name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'image_url', 'hourly_rate', 'is_active', 'created_at'], filterProperties: ['name', 'specialization', 'is_active'], properties: { name: { @@ -328,6 +336,10 @@ const adminJsOptions = { type: 'number', description: 'Опыт работы в годах', }, + image_url: { + type: 'string', + description: 'Фотография гида. Кнопка "Выбрать" будет добавлена автоматически' + }, hourly_rate: { type: 'number', description: 'Ставка за час в вонах', @@ -344,7 +356,7 @@ const adminJsOptions = { options: { parent: { name: 'Контент', icon: 'DocumentText' }, listProperties: ['id', 'title', 'category', 'is_published', 'views', 'created_at'], - editProperties: ['title', 'excerpt', 'content', 'category', 'is_published'], + editProperties: ['title', 'excerpt', 'content', 'category', 'image_url', 'is_published'], showProperties: ['id', 'title', 'excerpt', 'content', 'category', 'is_published', 'views', 'created_at', 'updated_at'], filterProperties: ['title', 'category', 'is_published'], properties: { @@ -369,6 +381,10 @@ const adminJsOptions = { { value: 'history', label: 'История' } ], }, + image_url: { + type: 'string', + description: 'Изображение статьи. Кнопка "Выбрать" будет добавлена автоматически' + }, is_published: { type: 'boolean' }, views: { type: 'number', @@ -730,10 +746,15 @@ const adminJsOptions = { }, dashboard: { component: false + }, + assets: { + styles: ['/css/admin-custom.css'], + scripts: ['/js/admin-image-selector-fixed.js'] } }; -// Создаем экземпляр AdminJS +// Создаем экземпляр AdminJS с componentLoader +// Создание AdminJS с конфигурацией const adminJs = new AdminJS(adminJsOptions); // Настраиваем аутентификацию diff --git a/src/routes/crud.js b/src/routes/crud.js new file mode 100644 index 0000000..745dab9 --- /dev/null +++ b/src/routes/crud.js @@ -0,0 +1,653 @@ +import express from 'express'; +import { Op } from 'sequelize'; +import db from '../config/database.js'; + +const router = express.Router(); + +// Middleware для логирования +const logRequest = (entity) => (req, res, next) => { + console.log(`${new Date().toISOString()} - ${req.method} /api/crud/${entity} - ${JSON.stringify(req.body || req.query).slice(0, 100)}`); + next(); +}; + +// Общая функция для обработки ошибок +const handleError = (res, error, operation = 'operation') => { + console.error(`CRUD ${operation} error:`, error); + res.status(500).json({ + success: false, + error: `Ошибка ${operation}: ${error.message}` + }); +}; + +// Общая функция для валидации ID +const validateId = (id) => { + const numId = parseInt(id); + if (isNaN(numId) || numId < 1) { + throw new Error('Некорректный ID'); + } + return numId; +}; + +/** + * ROUTES CRUD + */ + +// GET /api/crud/routes - получить все маршруты +router.get('/routes', logRequest('routes'), async (req, res) => { + try { + const { + page = 1, + limit = 10, + type, + difficulty_level, + is_active = 'all', + search, + sort_by = 'id', + sort_order = 'ASC' + } = req.query; + + const offset = (parseInt(page) - 1) * parseInt(limit); + const where = {}; + + // Фильтры + if (type) where.type = type; + if (difficulty_level) where.difficulty_level = difficulty_level; + if (is_active !== 'all') where.is_active = is_active === 'true'; + if (search) { + where[Op.or] = [ + { title: { [Op.iLike]: `%${search}%` } }, + { description: { [Op.iLike]: `%${search}%` } } + ]; + } + + const order = [[sort_by, sort_order.toUpperCase()]]; + + const result = await db.query(` + SELECT + id, title, description, type, difficulty_level, + price, duration, max_group_size, image_url, + is_featured, is_active, created_at, updated_at + FROM routes + WHERE ($1::text IS NULL OR type = $1) + AND ($2::text IS NULL OR difficulty_level = $2) + AND ($3::boolean IS NULL OR is_active = $3) + AND ($4::text IS NULL OR (title ILIKE $4 OR description ILIKE $4)) + ORDER BY ${sort_by} ${sort_order.toUpperCase()} + LIMIT $5 OFFSET $6 + `, [ + type || null, + difficulty_level || null, + is_active === 'all' ? null : (is_active === 'true'), + search ? `%${search}%` : null, + parseInt(limit), + offset + ]); + + const countResult = await db.query(` + SELECT COUNT(*) + FROM routes + WHERE ($1::text IS NULL OR type = $1) + AND ($2::text IS NULL OR difficulty_level = $2) + AND ($3::boolean IS NULL OR is_active = $3) + AND ($4::text IS NULL OR (title ILIKE $4 OR description ILIKE $4)) + `, [ + type || null, + difficulty_level || null, + is_active === 'all' ? null : (is_active === 'true'), + search ? `%${search}%` : null + ]); + + const total = parseInt(countResult.rows[0].count); + const totalPages = Math.ceil(total / parseInt(limit)); + + res.json({ + success: true, + data: result.rows, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + }); + } catch (error) { + handleError(res, error, 'получения маршрутов'); + } +}); + +// GET /api/crud/routes/:id - получить маршрут по ID +router.get('/routes/:id', logRequest('routes'), async (req, res) => { + try { + const id = validateId(req.params.id); + + const result = await db.query(` + SELECT * FROM routes WHERE id = $1 + `, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Маршрут не найден' }); + } + + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'получения маршрута'); + } +}); + +// POST /api/crud/routes - создать новый маршрут +router.post('/routes', logRequest('routes'), async (req, res) => { + try { + const { + title, description, content, type, difficulty_level, + price, duration, max_group_size, image_url, + is_featured = false, is_active = true + } = req.body; + + // Валидация обязательных полей + if (!title || !type || !price || !duration || !max_group_size) { + return res.status(400).json({ + success: false, + error: 'Отсутствуют обязательные поля: title, type, price, duration, max_group_size' + }); + } + + const result = await db.query(` + INSERT INTO routes ( + title, description, content, type, difficulty_level, + price, duration, max_group_size, image_url, + is_featured, is_active + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + RETURNING * + `, [ + title, description, content, type, difficulty_level, + price, duration, max_group_size, image_url, + is_featured, is_active + ]); + + res.status(201).json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'создания маршрута'); + } +}); + +// PUT /api/crud/routes/:id - обновить маршрут +router.put('/routes/:id', logRequest('routes'), async (req, res) => { + try { + const id = validateId(req.params.id); + const { + title, description, content, type, difficulty_level, + price, duration, max_group_size, image_url, + is_featured, is_active + } = req.body; + + const result = await db.query(` + UPDATE routes SET + title = COALESCE($1, title), + description = COALESCE($2, description), + content = COALESCE($3, content), + type = COALESCE($4, type), + difficulty_level = COALESCE($5, difficulty_level), + price = COALESCE($6, price), + duration = COALESCE($7, duration), + max_group_size = COALESCE($8, max_group_size), + image_url = COALESCE($9, image_url), + is_featured = COALESCE($10, is_featured), + is_active = COALESCE($11, is_active), + updated_at = CURRENT_TIMESTAMP + WHERE id = $12 + RETURNING * + `, [ + title, description, content, type, difficulty_level, + price, duration, max_group_size, image_url, + is_featured, is_active, id + ]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Маршрут не найден' }); + } + + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'обновления маршрута'); + } +}); + +// DELETE /api/crud/routes/:id - удалить маршрут +router.delete('/routes/:id', logRequest('routes'), async (req, res) => { + try { + const id = validateId(req.params.id); + + const result = await db.query(` + DELETE FROM routes WHERE id = $1 RETURNING id, title + `, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Маршрут не найден' }); + } + + res.json({ + success: true, + message: `Маршрут "${result.rows[0].title}" успешно удален`, + data: { id: result.rows[0].id } + }); + } catch (error) { + handleError(res, error, 'удаления маршрута'); + } +}); + +/** + * GUIDES CRUD + */ + +// GET /api/crud/guides - получить всех гидов +router.get('/guides', logRequest('guides'), async (req, res) => { + try { + const { + page = 1, + limit = 10, + specialization, + is_active = 'all', + search, + sort_by = 'id', + sort_order = 'ASC' + } = req.query; + + const offset = (parseInt(page) - 1) * parseInt(limit); + const where = {}; + + if (specialization) where.specialization = specialization; + if (is_active !== 'all') where.is_active = is_active === 'true'; + + const result = await db.query(` + SELECT + id, name, email, phone, languages, specialization, + bio, experience, image_url, hourly_rate, + is_active, created_at, updated_at + FROM guides + WHERE ($1::text IS NULL OR specialization = $1) + AND ($2::boolean IS NULL OR is_active = $2) + AND ($3::text IS NULL OR (name ILIKE $3 OR email ILIKE $3 OR bio ILIKE $3)) + ORDER BY ${sort_by} ${sort_order.toUpperCase()} + LIMIT $4 OFFSET $5 + `, [ + specialization || null, + is_active === 'all' ? null : (is_active === 'true'), + search ? `%${search}%` : null, + parseInt(limit), + offset + ]); + + const countResult = await db.query(` + SELECT COUNT(*) + FROM guides + WHERE ($1::text IS NULL OR specialization = $1) + AND ($2::boolean IS NULL OR is_active = $2) + AND ($3::text IS NULL OR (name ILIKE $3 OR email ILIKE $3 OR bio ILIKE $3)) + `, [ + specialization || null, + is_active === 'all' ? null : (is_active === 'true'), + search ? `%${search}%` : null + ]); + + const total = parseInt(countResult.rows[0].count); + const totalPages = Math.ceil(total / parseInt(limit)); + + res.json({ + success: true, + data: result.rows, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + }); + } catch (error) { + handleError(res, error, 'получения гидов'); + } +}); + +// GET /api/crud/guides/:id - получить гида по ID +router.get('/guides/:id', logRequest('guides'), async (req, res) => { + try { + const id = validateId(req.params.id); + + const result = await db.query(` + SELECT * FROM guides WHERE id = $1 + `, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Гид не найден' }); + } + + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'получения гида'); + } +}); + +// POST /api/crud/guides - создать нового гида +router.post('/guides', logRequest('guides'), async (req, res) => { + try { + const { + name, email, phone, languages, specialization, + bio, experience, image_url, hourly_rate, + is_active = true + } = req.body; + + // Валидация обязательных полей + if (!name || !email || !specialization) { + return res.status(400).json({ + success: false, + error: 'Отсутствуют обязательные поля: name, email, specialization' + }); + } + + // Преобразовываем languages в массив если передана строка + let processedLanguages = languages; + if (typeof languages === 'string') { + processedLanguages = languages.split(',').map(lang => lang.trim()); + } + + const result = await db.query(` + INSERT INTO guides ( + name, email, phone, languages, specialization, + bio, experience, image_url, hourly_rate, is_active + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + RETURNING * + `, [ + name, email, phone, processedLanguages, specialization, + bio, experience, image_url, hourly_rate, is_active + ]); + + res.status(201).json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'создания гида'); + } +}); + +// PUT /api/crud/guides/:id - обновить гида +router.put('/guides/:id', logRequest('guides'), async (req, res) => { + try { + const id = validateId(req.params.id); + const { + name, email, phone, languages, specialization, + bio, experience, image_url, hourly_rate, is_active + } = req.body; + + // Преобразовываем languages в массив если передана строка + let processedLanguages = languages; + if (typeof languages === 'string') { + processedLanguages = languages.split(',').map(lang => lang.trim()); + } + + const result = await db.query(` + UPDATE guides SET + name = COALESCE($1, name), + email = COALESCE($2, email), + phone = COALESCE($3, phone), + languages = COALESCE($4, languages), + specialization = COALESCE($5, specialization), + bio = COALESCE($6, bio), + experience = COALESCE($7, experience), + image_url = COALESCE($8, image_url), + hourly_rate = COALESCE($9, hourly_rate), + is_active = COALESCE($10, is_active), + updated_at = CURRENT_TIMESTAMP + WHERE id = $11 + RETURNING * + `, [ + name, email, phone, processedLanguages, specialization, + bio, experience, image_url, hourly_rate, is_active, id + ]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Гид не найден' }); + } + + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'обновления гида'); + } +}); + +// DELETE /api/crud/guides/:id - удалить гида +router.delete('/guides/:id', logRequest('guides'), async (req, res) => { + try { + const id = validateId(req.params.id); + + const result = await db.query(` + DELETE FROM guides WHERE id = $1 RETURNING id, name + `, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Гид не найден' }); + } + + res.json({ + success: true, + message: `Гид "${result.rows[0].name}" успешно удален`, + data: { id: result.rows[0].id } + }); + } catch (error) { + handleError(res, error, 'удаления гида'); + } +}); + +/** + * ARTICLES CRUD + */ + +// GET /api/crud/articles - получить все статьи +router.get('/articles', logRequest('articles'), async (req, res) => { + try { + const { + page = 1, + limit = 10, + category, + is_published = 'all', + search, + sort_by = 'id', + sort_order = 'DESC' + } = req.query; + + const offset = (parseInt(page) - 1) * parseInt(limit); + + const result = await db.query(` + SELECT + id, title, excerpt, content, category, + image_url, author_id, is_published, + created_at, updated_at + FROM articles + WHERE ($1::text IS NULL OR category = $1) + AND ($2::boolean IS NULL OR is_published = $2) + AND ($3::text IS NULL OR (title ILIKE $3 OR excerpt ILIKE $3 OR content ILIKE $3)) + ORDER BY ${sort_by} ${sort_order.toUpperCase()} + LIMIT $4 OFFSET $5 + `, [ + category || null, + is_published === 'all' ? null : (is_published === 'true'), + search ? `%${search}%` : null, + parseInt(limit), + offset + ]); + + const countResult = await db.query(` + SELECT COUNT(*) + FROM articles + WHERE ($1::text IS NULL OR category = $1) + AND ($2::boolean IS NULL OR is_published = $2) + AND ($3::text IS NULL OR (title ILIKE $3 OR excerpt ILIKE $3 OR content ILIKE $3)) + `, [ + category || null, + is_published === 'all' ? null : (is_published === 'true'), + search ? `%${search}%` : null + ]); + + const total = parseInt(countResult.rows[0].count); + const totalPages = Math.ceil(total / parseInt(limit)); + + res.json({ + success: true, + data: result.rows, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + }); + } catch (error) { + handleError(res, error, 'получения статей'); + } +}); + +// GET /api/crud/articles/:id - получить статью по ID +router.get('/articles/:id', logRequest('articles'), async (req, res) => { + try { + const id = validateId(req.params.id); + + const result = await db.query(` + SELECT + id, title, excerpt, content, category, image_url, + author_id, is_published, created_at, updated_at + FROM articles + WHERE id = $1 + `, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + error: 'Статья не найдена' + }); + } + + res.json({ + success: true, + data: result.rows[0] + }); + } catch (error) { + handleError(res, error, 'получения статьи'); + } +}); + +// POST /api/crud/articles - создать новую статью +router.post('/articles', logRequest('articles'), async (req, res) => { + try { + const { + title, excerpt, content, category, image_url, + author_id, is_published = false + } = req.body; + + // Валидация обязательных полей + if (!title || !content) { + return res.status(400).json({ + success: false, + error: 'Отсутствуют обязательные поля: title, content' + }); + } + + const result = await db.query(` + INSERT INTO articles ( + title, excerpt, content, category, image_url, + author_id, is_published + ) VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, [ + title, excerpt, content, category, image_url, + author_id, is_published + ]); + + res.status(201).json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'создания статьи'); + } +}); + +// PUT /api/crud/articles/:id - обновить статью +router.put('/articles/:id', logRequest('articles'), async (req, res) => { + try { + const id = validateId(req.params.id); + const { + title, excerpt, content, category, image_url, + author_id, is_published + } = req.body; + + const result = await db.query(` + UPDATE articles SET + title = COALESCE($1, title), + excerpt = COALESCE($2, excerpt), + content = COALESCE($3, content), + category = COALESCE($4, category), + image_url = COALESCE($5, image_url), + author_id = COALESCE($6, author_id), + is_published = COALESCE($7, is_published), + updated_at = CURRENT_TIMESTAMP + WHERE id = $8 + RETURNING * + `, [ + title, excerpt, content, category, image_url, + author_id, is_published, id + ]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Статья не найдена' }); + } + + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + handleError(res, error, 'обновления статьи'); + } +}); + +// DELETE /api/crud/articles/:id - удалить статью +router.delete('/articles/:id', logRequest('articles'), async (req, res) => { + try { + const id = validateId(req.params.id); + + const result = await db.query(` + DELETE FROM articles WHERE id = $1 RETURNING id, title + `, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, error: 'Статья не найдена' }); + } + + res.json({ + success: true, + message: `Статья "${result.rows[0].title}" успешно удалена`, + data: { id: result.rows[0].id } + }); + } catch (error) { + handleError(res, error, 'удаления статьи'); + } +}); + +/** + * ОБЩИЕ ЭНДПОИНТЫ + */ + +// GET /api/crud/stats - общая статистика +router.get('/stats', async (req, res) => { + try { + const [routesCount, guidesCount, articlesCount] = await Promise.all([ + db.query('SELECT COUNT(*) FROM routes WHERE is_active = true'), + db.query('SELECT COUNT(*) FROM guides WHERE is_active = true'), + db.query('SELECT COUNT(*) FROM articles WHERE is_published = true') + ]); + + res.json({ + success: true, + data: { + routes: parseInt(routesCount.rows[0].count), + guides: parseInt(guidesCount.rows[0].count), + articles: parseInt(articlesCount.rows[0].count), + timestamp: new Date().toISOString() + } + }); + } catch (error) { + handleError(res, error, 'получения статистики'); + } +}); + +export default router; \ No newline at end of file diff --git a/src/routes/image-upload.js b/src/routes/image-upload.js new file mode 100644 index 0000000..3ffe88f --- /dev/null +++ b/src/routes/image-upload.js @@ -0,0 +1,246 @@ +import express from 'express'; +import multer from 'multer'; +import sharp from 'sharp'; +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const router = express.Router(); + +// Настройка multer для загрузки файлов +const storage = multer.diskStorage({ + destination: async (req, file, cb) => { + const uploadPath = path.join(__dirname, '../../public/uploads/temp'); + try { + await fs.mkdir(uploadPath, { recursive: true }); + } catch (error) { + console.error('Error creating upload directory:', error); + } + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, `temp-${uniqueSuffix}${path.extname(file.originalname)}`); + } +}); + +const fileFilter = (req, file, cb) => { + // Проверяем, что это изображение + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Файл должен быть изображением'), false); + } +}; + +const upload = multer({ + storage, + fileFilter, + limits: { + fileSize: 10 * 1024 * 1024 // 10MB + } +}); + +/** + * Загрузка изображения во временную папку + */ +router.post('/upload-image', upload.single('image'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'Файл не загружен' }); + } + + // Получаем информацию об изображении + const metadata = await sharp(req.file.path).metadata(); + + res.json({ + success: true, + tempId: path.basename(req.file.filename, path.extname(req.file.filename)), + filename: req.file.filename, + originalName: req.file.originalname, + size: req.file.size, + width: metadata.width, + height: metadata.height, + format: metadata.format, + tempUrl: `/uploads/temp/${req.file.filename}` + }); + } catch (error) { + console.error('Upload error:', error); + res.status(500).json({ error: 'Ошибка загрузки файла' }); + } +}); + +/** + * Обработка изображения (поворот, отражение, обрезка) + */ +router.post('/process-image', async (req, res) => { + try { + const { + tempId, + rotation = 0, + flipHorizontal = false, + flipVertical = false, + cropData, + targetFolder = 'routes' // routes, guides, articles, etc. + } = req.body; + + if (!tempId) { + return res.status(400).json({ error: 'Отсутствует ID временного файла' }); + } + + const tempPath = path.join(__dirname, '../../public/uploads/temp'); + const targetPath = path.join(__dirname, `../../public/uploads/${targetFolder}`); + + // Создаем целевую папку если её нет + await fs.mkdir(targetPath, { recursive: true }); + + // Ищем временный файл + const tempFiles = await fs.readdir(tempPath); + const tempFile = tempFiles.find(file => file.includes(tempId)); + + if (!tempFile) { + return res.status(404).json({ error: 'Временный файл не найден' }); + } + + const tempFilePath = path.join(tempPath, tempFile); + + // Генерируем имя для финального файла + const timestamp = Date.now(); + const finalFileName = `${targetFolder}-${timestamp}.jpg`; + const finalFilePath = path.join(targetPath, finalFileName); + + // Обрабатываем изображение + let sharpInstance = sharp(tempFilePath); + + // Поворот + if (rotation !== 0) { + sharpInstance = sharpInstance.rotate(rotation); + } + + // Отражение + if (flipHorizontal || flipVertical) { + sharpInstance = sharpInstance.flip(flipVertical).flop(flipHorizontal); + } + + // Обрезка + if (cropData && cropData.width > 0 && cropData.height > 0) { + sharpInstance = sharpInstance.extract({ + left: Math.round(cropData.x), + top: Math.round(cropData.y), + width: Math.round(cropData.width), + height: Math.round(cropData.height) + }); + } + + // Сжимаем и сохраняем + await sharpInstance + .jpeg({ quality: 85 }) + .toFile(finalFilePath); + + // Удаляем временный файл + try { + await fs.unlink(tempFilePath); + } catch (error) { + console.warn('Не удалось удалить временный файл:', error.message); + } + + res.json({ + success: true, + url: `/uploads/${targetFolder}/${finalFileName}`, + filename: finalFileName + }); + + } catch (error) { + console.error('Image processing error:', error); + res.status(500).json({ error: 'Ошибка обработки изображения' }); + } +}); + +/** + * Удаление временного файла + */ +router.delete('/temp-image/:tempId', async (req, res) => { + try { + const { tempId } = req.params; + const tempPath = path.join(__dirname, '../../public/uploads/temp'); + + const tempFiles = await fs.readdir(tempPath); + const tempFile = tempFiles.find(file => file.includes(tempId)); + + if (tempFile) { + await fs.unlink(path.join(tempPath, tempFile)); + } + + res.json({ success: true }); + } catch (error) { + console.error('Delete temp image error:', error); + res.status(500).json({ error: 'Ошибка удаления временного файла' }); + } +}); + +/** + * Получение списка изображений в папке + */ +router.get('/images/:folder', async (req, res) => { + try { + const { folder } = req.params; + const imagesPath = path.join(__dirname, `../../public/uploads/${folder}`); + + try { + const files = await fs.readdir(imagesPath); + const images = files + .filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file)) + .map(file => ({ + filename: file, + url: `/uploads/${folder}/${file}`, + path: path.join(imagesPath, file) + })); + + res.json({ success: true, images }); + } catch (error) { + if (error.code === 'ENOENT') { + res.json({ success: true, images: [] }); + } else { + throw error; + } + } + } catch (error) { + console.error('List images error:', error); + res.status(500).json({ error: 'Ошибка получения списка изображений' }); + } +}); + +/** + * Удаление изображения + */ +router.delete('/image', async (req, res) => { + try { + const { url } = req.body; + + if (!url || !url.startsWith('/uploads/')) { + return res.status(400).json({ error: 'Некорректный URL изображения' }); + } + + const imagePath = path.join(__dirname, '../../public', url); + + try { + await fs.unlink(imagePath); + res.json({ success: true }); + } catch (error) { + if (error.code === 'ENOENT') { + res.json({ success: true, message: 'Файл уже удален' }); + } else { + throw error; + } + } + } catch (error) { + console.error('Delete image error:', error); + res.status(500).json({ error: 'Ошибка удаления изображения' }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/routes/images.js b/src/routes/images.js new file mode 100644 index 0000000..9b606b5 --- /dev/null +++ b/src/routes/images.js @@ -0,0 +1,259 @@ +import express from 'express'; +import multer from 'multer'; +import sharp from 'sharp'; +import path from 'path'; +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const router = express.Router(); + +// Настройка multer для загрузки файлов +const storage = multer.diskStorage({ + destination: async (req, file, cb) => { + const uploadDir = path.join(__dirname, '../../public/uploads'); + try { + await fs.mkdir(uploadDir, { recursive: true }); + cb(null, uploadDir); + } catch (error) { + cb(error); + } + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + cb(null, `image-${uniqueSuffix}${ext}`); + } +}); + +const upload = multer({ + storage, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + }, + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|gif/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + + if (mimetype && extname) { + return cb(null, true); + } else { + cb(new Error('Недопустимый тип файла')); + } + } +}); + +// POST /api/images/upload - загрузка изображения +router.post('/upload', upload.single('image'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + error: 'Файл не был загружен' + }); + } + + const { folder = 'general' } = req.body; + const originalPath = req.file.path; + const targetDir = path.join(__dirname, '../../public/uploads', folder); + + // Создаем целевую папку если она не существует + await fs.mkdir(targetDir, { recursive: true }); + + // Генерируем имя файла + const ext = path.extname(req.file.originalname); + const filename = `${Date.now()}-${Math.round(Math.random() * 1E9)}${ext}`; + const targetPath = path.join(targetDir, filename); + + // Оптимизируем изображение с помощью Sharp + await sharp(originalPath) + .resize(1200, 800, { + fit: 'inside', + withoutEnlargement: true + }) + .jpeg({ quality: 85 }) + .toFile(targetPath); + + // Удаляем временный файл + await fs.unlink(originalPath); + + const relativePath = `/uploads/${folder}/${filename}`; + + res.json({ + success: true, + data: { + path: relativePath, + originalName: req.file.originalname, + size: req.file.size, + folder + }, + message: 'Изображение успешно загружено' + }); + + } catch (error) { + console.error('Upload error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Ошибка загрузки изображения' + }); + } +}); + +// GET /api/images/gallery - получение списка изображений +router.get('/gallery', async (req, res) => { + try { + const { folder = 'all' } = req.query; + const uploadsDir = path.join(__dirname, '../../public/uploads'); + + let images = []; + + if (folder === 'all') { + // Получаем изображения из всех папок + const folders = ['routes', 'guides', 'articles', 'general']; + + for (const folderName of folders) { + const folderPath = path.join(uploadsDir, folderName); + try { + const files = await fs.readdir(folderPath); + const folderImages = files + .filter(file => /\.(jpg|jpeg|png|gif)$/i.test(file)) + .map(file => ({ + name: file, + path: `/uploads/${folderName}/${file}`, + folder: folderName, + fullPath: path.join(folderPath, file) + })); + images.push(...folderImages); + } catch (err) { + // Папка может не существовать - это нормально + } + } + } else { + // Получаем изображения из конкретной папки + const folderPath = path.join(uploadsDir, folder); + try { + const files = await fs.readdir(folderPath); + images = files + .filter(file => /\.(jpg|jpeg|png|gif)$/i.test(file)) + .map(file => ({ + name: file, + path: `/uploads/${folder}/${file}`, + folder, + fullPath: path.join(folderPath, file) + })); + } catch (err) { + // Папка может не существовать + } + } + + // Получаем статистику файлов + const imagesWithStats = await Promise.all( + images.map(async (img) => { + try { + const stats = await fs.stat(img.fullPath); + return { + ...img, + size: stats.size, + created: stats.birthtime, + modified: stats.mtime + }; + } catch (err) { + return img; + } + }) + ); + + // Сортируем по дате создания (новые сначала) + imagesWithStats.sort((a, b) => { + if (a.created && b.created) { + return new Date(b.created) - new Date(a.created); + } + return 0; + }); + + res.json({ + success: true, + data: imagesWithStats, + total: imagesWithStats.length + }); + + } catch (error) { + console.error('Gallery error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Ошибка получения галереи' + }); + } +}); + +// DELETE /api/images/:folder/:filename - удаление изображения +router.delete('/:folder/:filename', async (req, res) => { + try { + const { folder, filename } = req.params; + const filePath = path.join(__dirname, '../../public/uploads', folder, filename); + + // Проверяем существование файла + try { + await fs.access(filePath); + } catch (err) { + return res.status(404).json({ + success: false, + error: 'Файл не найден' + }); + } + + // Удаляем файл + await fs.unlink(filePath); + + res.json({ + success: true, + message: 'Изображение успешно удалено' + }); + + } catch (error) { + console.error('Delete error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Ошибка удаления изображения' + }); + } +}); + +// GET /api/images/info/:folder/:filename - получение информации об изображении +router.get('/info/:folder/:filename', async (req, res) => { + try { + const { folder, filename } = req.params; + const filePath = path.join(__dirname, '../../public/uploads', folder, filename); + + const stats = await fs.stat(filePath); + const metadata = await sharp(filePath).metadata(); + + res.json({ + success: true, + data: { + filename, + folder, + path: `/uploads/${folder}/${filename}`, + size: stats.size, + width: metadata.width, + height: metadata.height, + format: metadata.format, + created: stats.birthtime, + modified: stats.mtime + } + }); + + } catch (error) { + console.error('Info error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Ошибка получения информации об изображении' + }); + } +}); + +export default router; \ No newline at end of file diff --git a/test-crud.js b/test-crud.js new file mode 100644 index 0000000..f7d070b --- /dev/null +++ b/test-crud.js @@ -0,0 +1,370 @@ +#!/usr/bin/env node + +/** + * Скрипт для тестирования всех CRUD операций + * Проверяет создание, чтение, обновление и удаление для Routes, Guides и Articles + */ + +const BASE_URL = 'http://localhost:3000/api/crud'; + +// Цветной вывод в консоль +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m' +}; + +const log = (color, message) => { + console.log(`${colors[color]}${message}${colors.reset}`); +}; + +// Утилита для HTTP запросов +async function request(method, url, data = null) { + const options = { + method, + headers: { + 'Content-Type': 'application/json', + } + }; + + if (data) { + options.body = JSON.stringify(data); + } + + const response = await fetch(url, options); + const result = await response.json(); + + return { + status: response.status, + data: result + }; +} + +// Тестовые данные +const testData = { + route: { + title: 'Тестовый маршрут CRUD', + description: 'Описание тестового маршрута для проверки CRUD операций', + content: 'Подробное описание маршрута с инструкциями', + type: 'city', + difficulty_level: 'easy', + price: 25000, + duration: 4, + max_group_size: 15, + image_url: '/uploads/routes/seoul-city-tour.jpg', + is_featured: false, + is_active: true + }, + guide: { + name: 'Тестовый Гид CRUD', + email: 'test-guide-crud@example.com', + phone: '+82-10-1234-5678', + languages: 'Корейский, Английский, Русский', + specialization: 'city', + bio: 'Опытный гид для тестирования CRUD операций', + experience: 3, + image_url: '/uploads/guides/guide-profile.jpg', + hourly_rate: 30000, + is_active: true + }, + article: { + title: 'Тестовая статья CRUD', + excerpt: 'Краткое описание тестовой статьи', + content: 'Полный текст тестовой статьи для проверки CRUD операций', + category: 'travel-tips', + image_url: '/images/articles/test-article.jpg', + author_id: 1, + is_published: true + } +}; + +// Основная функция тестирования +async function runCRUDTests() { + log('cyan', '🚀 Запуск тестирования CRUD операций...\n'); + + const results = { + routes: await testEntityCRUD('routes', testData.route), + guides: await testEntityCRUD('guides', testData.guide), + articles: await testEntityCRUD('articles', testData.article) + }; + + // Тестируем общие эндпоинты + log('blue', '📊 Тестирование общей статистики...'); + try { + const statsResponse = await request('GET', `${BASE_URL}/stats`); + if (statsResponse.status === 200 && statsResponse.data.success) { + log('green', `✅ Статистика: ${JSON.stringify(statsResponse.data.data)}`); + } else { + log('red', `❌ Ошибка получения статистики: ${JSON.stringify(statsResponse.data)}`); + } + } catch (error) { + log('red', `❌ Ошибка получения статистики: ${error.message}`); + } + + // Итоговый отчет + log('cyan', '\n📋 Итоговый отчет тестирования:'); + Object.entries(results).forEach(([entity, result]) => { + const status = result.success ? '✅' : '❌'; + log(result.success ? 'green' : 'red', `${status} ${entity.toUpperCase()}: ${result.message}`); + }); + + const totalTests = Object.values(results).length; + const passedTests = Object.values(results).filter(r => r.success).length; + + log('cyan', `\n🎯 Результат: ${passedTests}/${totalTests} тестов прошли успешно`); + + if (passedTests === totalTests) { + log('green', '🎉 Все CRUD операции работают корректно!'); + } else { + log('red', '⚠️ Некоторые операции требуют внимания'); + } +} + +// Тестирование CRUD для конкретной сущности +async function testEntityCRUD(entity, testData) { + log('magenta', `\n🔍 Тестирование ${entity.toUpperCase()}...`); + + let createdId = null; + const steps = []; + + try { + // 1. CREATE - Создание записи + log('yellow', '1. CREATE - Создание записи...'); + const createResponse = await request('POST', `${BASE_URL}/${entity}`, testData); + + if (createResponse.status === 201 && createResponse.data.success) { + createdId = createResponse.data.data.id; + log('green', `✅ Создание успешно. ID: ${createdId}`); + steps.push('CREATE: ✅'); + } else { + log('red', `❌ Ошибка создания: ${JSON.stringify(createResponse.data)}`); + steps.push('CREATE: ❌'); + return { success: false, message: 'Ошибка создания записи', steps }; + } + + // 2. READ - Чтение записи по ID + log('yellow', '2. READ - Чтение записи по ID...'); + const readResponse = await request('GET', `${BASE_URL}/${entity}/${createdId}`); + + if (readResponse.status === 200 && readResponse.data.success) { + log('green', `✅ Чтение успешно. Заголовок: "${readResponse.data.data.title || readResponse.data.data.name}"`); + steps.push('READ: ✅'); + } else { + log('red', `❌ Ошибка чтения: ${JSON.stringify(readResponse.data)}`); + steps.push('READ: ❌'); + } + + // 3. READ ALL - Чтение всех записей + log('yellow', '3. READ ALL - Чтение всех записей...'); + const readAllResponse = await request('GET', `${BASE_URL}/${entity}?page=1&limit=5`); + + if (readAllResponse.status === 200 && readAllResponse.data.success) { + const count = readAllResponse.data.data.length; + log('green', `✅ Получено записей: ${count}`); + steps.push('READ ALL: ✅'); + } else { + log('red', `❌ Ошибка чтения списка: ${JSON.stringify(readAllResponse.data)}`); + steps.push('READ ALL: ❌'); + } + + // 4. UPDATE - Обновление записи + log('yellow', '4. UPDATE - Обновление записи...'); + const updateData = entity === 'guides' + ? { name: testData.name + ' (ОБНОВЛЕНО)' } + : { title: testData.title + ' (ОБНОВЛЕНО)' }; + + const updateResponse = await request('PUT', `${BASE_URL}/${entity}/${createdId}`, updateData); + + if (updateResponse.status === 200 && updateResponse.data.success) { + log('green', '✅ Обновление успешно'); + steps.push('UPDATE: ✅'); + } else { + log('red', `❌ Ошибка обновления: ${JSON.stringify(updateResponse.data)}`); + steps.push('UPDATE: ❌'); + } + + // 5. DELETE - Удаление записи + log('yellow', '5. DELETE - Удаление записи...'); + const deleteResponse = await request('DELETE', `${BASE_URL}/${entity}/${createdId}`); + + if (deleteResponse.status === 200 && deleteResponse.data.success) { + log('green', `✅ Удаление успешно: ${deleteResponse.data.message}`); + steps.push('DELETE: ✅'); + } else { + log('red', `❌ Ошибка удаления: ${JSON.stringify(deleteResponse.data)}`); + steps.push('DELETE: ❌'); + } + + // 6. Проверка удаления + log('yellow', '6. VERIFY DELETE - Проверка удаления...'); + const verifyResponse = await request('GET', `${BASE_URL}/${entity}/${createdId}`); + + if (verifyResponse.status === 404) { + log('green', '✅ Запись действительно удалена'); + steps.push('VERIFY: ✅'); + } else { + log('red', '❌ Запись не была удалена'); + steps.push('VERIFY: ❌'); + } + + const successCount = steps.filter(s => s.includes('✅')).length; + const isSuccess = successCount === steps.length; + + return { + success: isSuccess, + message: `${successCount}/6 операций успешно`, + steps + }; + + } catch (error) { + log('red', `❌ Критическая ошибка: ${error.message}`); + return { + success: false, + message: `Критическая ошибка: ${error.message}`, + steps: [...steps, 'ERROR: ❌'] + }; + } +} + +// Дополнительные тесты для специфических функций +async function testAdvancedFeatures() { + log('cyan', '\n🔬 Тестирование расширенных функций...'); + + // Тест поиска + log('yellow', 'Тест поиска по routes...'); + try { + const searchResponse = await request('GET', `${BASE_URL}/routes?search=seoul&limit=3`); + if (searchResponse.status === 200) { + log('green', `✅ Поиск работает. Найдено: ${searchResponse.data.data.length} записей`); + } else { + log('red', '❌ Ошибка поиска'); + } + } catch (error) { + log('red', `❌ Ошибка поиска: ${error.message}`); + } + + // Тест фильтрации + log('yellow', 'Тест фильтрации guides по специализации...'); + try { + const filterResponse = await request('GET', `${BASE_URL}/guides?specialization=city&limit=3`); + if (filterResponse.status === 200) { + log('green', `✅ Фильтрация работает. Найдено: ${filterResponse.data.data.length} гидов`); + } else { + log('red', '❌ Ошибка фильтрации'); + } + } catch (error) { + log('red', `❌ Ошибка фильтрации: ${error.message}`); + } + + // Тест пагинации + log('yellow', 'Тест пагинации для articles...'); + try { + const paginationResponse = await request('GET', `${BASE_URL}/articles?page=1&limit=2`); + if (paginationResponse.status === 200) { + const pagination = paginationResponse.data.pagination; + log('green', `✅ Пагинация работает. Страница ${pagination.page}, всего ${pagination.total} записей`); + } else { + log('red', '❌ Ошибка пагинации'); + } + } catch (error) { + log('red', `❌ Ошибка пагинации: ${error.message}`); + } +} + +// Тест валидации +async function testValidation() { + log('cyan', '\n🛡️ Тестирование валидации...'); + + // Тест создания без обязательных полей + log('yellow', 'Тест создания маршрута без обязательных полей...'); + try { + const invalidResponse = await request('POST', `${BASE_URL}/routes`, { + description: 'Только описание, без заголовка' + }); + + if (invalidResponse.status === 400) { + log('green', '✅ Валидация работает - отклонены невалидные данные'); + } else { + log('red', '❌ Валидация не работает - приняты невалидные данные'); + } + } catch (error) { + log('red', `❌ Ошибка тестирования валидации: ${error.message}`); + } + + // Тест обновления несуществующей записи + log('yellow', 'Тест обновления несуществующей записи...'); + try { + const notFoundResponse = await request('PUT', `${BASE_URL}/guides/99999`, { + name: 'Не существует' + }); + + if (notFoundResponse.status === 404) { + log('green', '✅ Корректно обрабатывается отсутствующая запись'); + } else { + log('red', '❌ Неправильная обработка отсутствующей записи'); + } + } catch (error) { + log('red', `❌ Ошибка тестирования несуществующей записи: ${error.message}`); + } +} + +// Запуск всех тестов +async function runAllTests() { + try { + await runCRUDTests(); + await testAdvancedFeatures(); + await testValidation(); + + log('cyan', '\n🏁 Тестирование завершено!'); + } catch (error) { + log('red', `💥 Критическая ошибка тестирования: ${error.message}`); + process.exit(1); + } +} + +// Проверка доступности сервера +async function checkServer() { + try { + const response = await fetch('http://localhost:3000/health'); + if (response.ok) { + log('green', '✅ Сервер доступен'); + return true; + } else { + log('red', '❌ Сервер недоступен'); + return false; + } + } catch (error) { + log('red', `❌ Сервер недоступен: ${error.message}`); + return false; + } +} + +// Главная функция +async function main() { + log('cyan', '🧪 Система тестирования CRUD API'); + log('cyan', '====================================\n'); + + // Проверяем доступность сервера + const serverAvailable = await checkServer(); + if (!serverAvailable) { + log('red', 'Запустите сервер командой: docker-compose up -d'); + process.exit(1); + } + + // Запускаем тесты + await runAllTests(); +} + +// Запуск если файл выполняется напрямую +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch(error => { + log('red', `💥 Неожиданная ошибка: ${error.message}`); + process.exit(1); + }); +} + +export { runCRUDTests, testAdvancedFeatures, testValidation }; \ No newline at end of file diff --git a/views/layout.ejs b/views/layout.ejs index 03c16a4..6a8ecd2 100644 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -195,6 +195,7 @@ +