diff --git a/.gitignore b/.gitignore index 21d061f..6703040 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ npm-debug.log* coverage/ .cache/ tmp/ +docker-compose.yaml diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts index c513509..8650e1c 100644 --- a/backend/src/db/migrate.ts +++ b/backend/src/db/migrate.ts @@ -16,18 +16,40 @@ export async function runMigrations(): Promise { return; } + // Создаём таблицу учёта миграций если её нет + await pool.query(` + CREATE TABLE IF NOT EXISTS _migrations ( + filename TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ DEFAULT now() + ) + `); + const files = fs .readdirSync(MIGRATIONS_DIR) .filter((f) => f.endsWith('.sql')) .sort(); for (const file of files) { + // Проверяем — уже применялась? + const { rowCount } = await pool.query( + 'SELECT 1 FROM _migrations WHERE filename = $1', + [file] + ); + if (rowCount && rowCount > 0) { + console.log(`[migrate] ✓ ${file} (уже применена)`); + continue; + } + const fullPath = path.join(MIGRATIONS_DIR, file); const sql = fs.readFileSync(fullPath, 'utf-8'); console.log(`[migrate] → ${file}`); try { + await pool.query('BEGIN'); await pool.query(sql); + await pool.query('INSERT INTO _migrations (filename) VALUES ($1)', [file]); + await pool.query('COMMIT'); } catch (err) { + await pool.query('ROLLBACK'); console.error(`[migrate] Ошибка в ${file}:`, err); throw err; } diff --git a/backend/src/index.ts b/backend/src/index.ts index 817e9de..6ac0b89 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -12,7 +12,7 @@ dotenv.config(); const app = express(); const PORT = parseInt(process.env.PORT || '3000', 10); -app.use(express.json({ limit: '1mb' })); +app.use(express.json({ limit: '6mb' })); app.use(cookieParser()); app.use( diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index e5223a1..a4ad370 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -111,7 +111,7 @@ adminRouter.post('/products', requireAdmin, async (req, res) => { }); adminRouter.put('/products/:id', requireAdmin, async (req, res) => { - const id = parseInt(req.params.id, 10); + const id = parseInt(String(req.params.id), 10); if (Number.isNaN(id)) { res.status(400).json({ error: 'invalid_id' }); return; @@ -152,7 +152,7 @@ adminRouter.put('/products/:id', requireAdmin, async (req, res) => { }); adminRouter.delete('/products/:id', requireAdmin, async (req, res) => { - const id = parseInt(req.params.id, 10); + const id = parseInt(String(req.params.id), 10); await query(`DELETE FROM products WHERE id = $1`, [id]); res.json({ ok: true }); }); @@ -192,7 +192,7 @@ adminRouter.get('/approach', requireAdmin, async (_req, res) => { }); adminRouter.put('/approach/:id', requireAdmin, async (req, res) => { - const id = parseInt(req.params.id, 10); + const id = parseInt(String(req.params.id), 10); const { title, description, icon_key, sort_order, is_active } = req.body; const updated = await queryOne( `UPDATE approach_items SET @@ -231,3 +231,106 @@ adminRouter.put('/settings/:key', requireAdmin, async (req, res) => { ); res.json({ ok: true }); }); + +// ============ Электронные визитки ============ +const cardSchema = z.object({ + slug: z.string().min(1).max(64).regex(/^[a-z0-9-]+$/, 'slug_format'), + name: z.string().min(1).max(128), + role: z.string().max(128).nullable().optional(), + company: z.string().max(128).nullable().optional(), + phone: z.string().max(64).nullable().optional(), + email: z.string().max(128).nullable().optional(), + telegram: z.string().max(128).nullable().optional(), + photo: z.string().max(5_000_000).nullable().optional(), + product_title: z.string().max(128).nullable().optional(), + product_subtitle: z.string().max(256).nullable().optional(), + product_url: z.string().max(256).nullable().optional(), + site_url: z.string().max(256).nullable().optional(), + is_published: z.boolean().optional(), +}); + +adminRouter.get('/cards', requireAdmin, async (_req, res) => { + const cards = await query(`SELECT * FROM business_cards ORDER BY created_at ASC`); + res.json({ cards }); +}); + +adminRouter.post('/cards', requireAdmin, async (req, res) => { + const parsed = cardSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: 'invalid_payload' }); + return; + } + const c = parsed.data; + try { + const created = await queryOne( + `INSERT INTO business_cards + (slug, name, role, company, phone, email, telegram, photo, + product_title, product_subtitle, product_url, site_url, is_published) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) + RETURNING *`, + [ + c.slug, c.name, c.role ?? null, c.company ?? null, c.phone ?? null, + c.email ?? null, c.telegram ?? null, c.photo ?? null, + c.product_title ?? null, c.product_subtitle ?? null, + c.product_url ?? null, c.site_url ?? null, c.is_published ?? true, + ] + ); + res.status(201).json({ card: created }); + } catch (err: any) { + if (err?.code === '23505') { + res.status(409).json({ error: 'slug_exists' }); + return; + } + console.error('[POST /cards]', err); + res.status(500).json({ error: 'internal_error' }); + } +}); + +adminRouter.put('/cards/:id', requireAdmin, async (req, res) => { + const id = parseInt(String(req.params.id), 10); + if (Number.isNaN(id)) { + res.status(400).json({ error: 'invalid_id' }); + return; + } + const parsed = cardSchema.partial().safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: 'invalid_payload' }); + return; + } + const c = parsed.data; + const updated = await queryOne( + `UPDATE business_cards SET + slug = COALESCE($2, slug), + name = COALESCE($3, name), + role = COALESCE($4, role), + company = COALESCE($5, company), + phone = COALESCE($6, phone), + email = COALESCE($7, email), + telegram = COALESCE($8, telegram), + photo = COALESCE($9, photo), + product_title = COALESCE($10, product_title), + product_subtitle = COALESCE($11, product_subtitle), + product_url = COALESCE($12, product_url), + site_url = COALESCE($13, site_url), + is_published = COALESCE($14, is_published), + updated_at = now() + WHERE id = $1 + RETURNING *`, + [ + id, c.slug, c.name, c.role, c.company, c.phone, c.email, c.telegram, + c.photo, c.product_title, c.product_subtitle, c.product_url, + c.site_url, c.is_published, + ] + ); + if (!updated) { + res.status(404).json({ error: 'not_found' }); + return; + } + res.json({ card: updated }); +}); + +adminRouter.delete('/cards/:id', requireAdmin, async (req, res) => { + const id = parseInt(String(req.params.id), 10); + await query(`DELETE FROM business_cards WHERE id = $1`, [id]); + res.json({ ok: true }); +}); diff --git a/backend/src/routes/public.ts b/backend/src/routes/public.ts index e773706..e25a37b 100644 --- a/backend/src/routes/public.ts +++ b/backend/src/routes/public.ts @@ -58,3 +58,23 @@ publicRouter.get('/content', async (_req, res) => { res.status(500).json({ error: 'internal_error' }); } }); + +publicRouter.get('/cards/:slug', async (req, res) => { + try { + const card = await query( + `SELECT slug, name, role, company, phone, email, telegram, photo, + product_title, product_subtitle, product_url, site_url + FROM business_cards + WHERE slug = $1 AND is_published = true`, + [req.params.slug] + ); + if (!card[0]) { + res.status(404).json({ error: 'not_found' }); + return; + } + res.json({ card: card[0] }); + } catch (err) { + console.error('[GET /cards/:slug]', err); + res.status(500).json({ error: 'internal_error' }); + } +}); diff --git a/db/003_business_cards.sql b/db/003_business_cards.sql new file mode 100644 index 0000000..d228156 --- /dev/null +++ b/db/003_business_cards.sql @@ -0,0 +1,27 @@ +-- Электронные визитки (управляются из админки) +CREATE TABLE IF NOT EXISTS business_cards ( + id SERIAL PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + role TEXT, + company TEXT, + phone TEXT, + email TEXT, + telegram TEXT, + photo TEXT, -- data URI (base64), сжимается на клиенте + product_title TEXT, + product_subtitle TEXT, + product_url TEXT, + site_url TEXT, + is_published BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +INSERT INTO business_cards + (slug, name, role, company, phone, email, telegram, product_title, product_subtitle, product_url, site_url) +VALUES + ('alexey', 'Алексей Павлов', 'Основатель · разработчик', 'ООО «Умный Байт»', + '+7 960 011 0007', 'info@umbyte.ru', NULL, + 'АгроТО', 'CMMS для агропромышленности', 'https://agroto.ru', 'https://umbyte.ru') +ON CONFLICT (slug) DO NOTHING; diff --git a/db/003_product_url_and_requisites.sql b/db/004_product_url_and_requisites.sql similarity index 100% rename from db/003_product_url_and_requisites.sql rename to db/004_product_url_and_requisites.sql diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 773e1f1..115ebb8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,11 +8,13 @@ "name": "umbyte-frontend", "version": "0.1.0", "dependencies": { + "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.28.0" }, "devDependencies": { + "@types/qrcode": "^1.5.5", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.7.0", @@ -93,14 +95,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -223,13 +225,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.8" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -286,18 +288,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", + "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", + "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -305,9 +307,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { @@ -759,23 +761,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -815,9 +800,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.4", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz", - "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -831,9 +816,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", - "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -845,9 +830,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", - "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -859,9 +844,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", - "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -873,9 +858,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", - "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -887,9 +872,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", - "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -901,9 +886,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", - "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -915,9 +900,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", - "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], @@ -929,9 +914,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", - "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], @@ -943,9 +928,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", - "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -957,9 +942,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", - "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], @@ -971,9 +956,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", - "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], @@ -985,9 +970,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", - "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], @@ -999,9 +984,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", - "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -1013,9 +998,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", - "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], @@ -1027,9 +1012,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", - "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], @@ -1041,9 +1026,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", - "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], @@ -1055,9 +1040,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", - "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], @@ -1069,9 +1054,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", - "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], @@ -1083,9 +1068,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", - "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -1097,9 +1082,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", - "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -1111,9 +1096,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", - "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1125,9 +1110,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", - "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -1139,9 +1124,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", - "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -1153,9 +1138,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", - "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1167,9 +1152,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", - "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1232,6 +1217,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1239,6 +1234,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", @@ -1281,6 +1286,30 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1310,9 +1339,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", - "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", "dev": true, "funding": [ { @@ -1330,8 +1359,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.6", - "caniuse-lite": "^1.0.30001806", + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -1347,9 +1376,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.17", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.17.tgz", - "integrity": "sha512-KAUDn1OSS0fmPlGO+NOUMRcOQ/b/shUBH3OgkG73mPgdf+JD/BQ6fHboGxNOxnUmlwcq+lLq3dTkayRPuSfXwg==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1386,9 +1415,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", "dev": true, "funding": [ { @@ -1406,11 +1435,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -1419,6 +1448,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1430,9 +1468,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001809", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", - "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", "dev": true, "funding": [ { @@ -1488,6 +1526,35 @@ "node": ">= 6" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1543,6 +1610,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1550,6 +1626,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -1558,12 +1640,18 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.412", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", - "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", + "version": "1.5.388", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.388.tgz", + "integrity": "sha512-Pl/aJaqOOxYxda3vcx1IKSJimwYXHDkEnGn0F+kG2EE68dDtx2uCinaS+Vih8Z91B9t8CSAbiF/HKyWcnXjhzw==", "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -1676,6 +1764,19 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1725,6 +1826,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1790,6 +1900,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1875,6 +1994,18 @@ "dev": true, "license": "MIT" }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1941,9 +2072,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -1960,9 +2091,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", "dev": true, "license": "MIT", "engines": { @@ -1999,6 +2130,51 @@ "node": ">= 6" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2026,6 +2202,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -2036,10 +2222,19 @@ "node": ">= 6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -2057,7 +2252,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2199,6 +2394,23 @@ "dev": true, "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2256,12 +2468,12 @@ } }, "node_modules/react-router": { - "version": "6.30.6", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz", - "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.4" + "@remix-run/router": "1.23.3" }, "engines": { "node": ">=14.0.0" @@ -2271,13 +2483,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.6", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz", - "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.4", - "react-router": "6.30.6" + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" }, "engines": { "node": ">=14.0.0" @@ -2288,11 +2500,14 @@ } }, "node_modules/read-cache": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", - "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } }, "node_modules/readdirp": { "version": "3.6.0", @@ -2307,6 +2522,21 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -2341,9 +2571,9 @@ } }, "node_modules/rollup": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", - "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { @@ -2357,32 +2587,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.62.5", - "@rollup/rollup-android-arm64": "4.62.5", - "@rollup/rollup-darwin-arm64": "4.62.5", - "@rollup/rollup-darwin-x64": "4.62.5", - "@rollup/rollup-freebsd-arm64": "4.62.5", - "@rollup/rollup-freebsd-x64": "4.62.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", - "@rollup/rollup-linux-arm-musleabihf": "4.62.5", - "@rollup/rollup-linux-arm64-gnu": "4.62.5", - "@rollup/rollup-linux-arm64-musl": "4.62.5", - "@rollup/rollup-linux-loong64-gnu": "4.62.5", - "@rollup/rollup-linux-loong64-musl": "4.62.5", - "@rollup/rollup-linux-ppc64-gnu": "4.62.5", - "@rollup/rollup-linux-ppc64-musl": "4.62.5", - "@rollup/rollup-linux-riscv64-gnu": "4.62.5", - "@rollup/rollup-linux-riscv64-musl": "4.62.5", - "@rollup/rollup-linux-s390x-gnu": "4.62.5", - "@rollup/rollup-linux-x64-gnu": "4.62.5", - "@rollup/rollup-linux-x64-musl": "4.62.5", - "@rollup/rollup-openbsd-x64": "4.62.5", - "@rollup/rollup-openharmony-arm64": "4.62.5", - "@rollup/rollup-win32-arm64-msvc": "4.62.5", - "@rollup/rollup-win32-ia32-msvc": "4.62.5", - "@rollup/rollup-win32-x64-gnu": "4.62.5", - "@rollup/rollup-win32-x64-msvc": "4.62.5", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -2429,6 +2658,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2439,6 +2674,32 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2618,10 +2879,17 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", - "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -2716,12 +2984,73 @@ } } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 5f8c9dd..5d1a4eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,13 @@ "preview": "vite preview" }, "dependencies": { + "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.28.0" }, "devDependencies": { + "@types/qrcode": "^1.5.5", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.7.0", diff --git a/frontend/src/admin/AdminLayout.tsx b/frontend/src/admin/AdminLayout.tsx index 4e5fb77..528e410 100644 --- a/frontend/src/admin/AdminLayout.tsx +++ b/frontend/src/admin/AdminLayout.tsx @@ -5,6 +5,7 @@ import { Logo } from '../components/Icons'; import { ProductsAdmin } from './ProductsAdmin'; import { SectionsAdmin } from './SectionsAdmin'; import { SettingsAdmin } from './SettingsAdmin'; +import { CardsAdmin } from './CardsAdmin'; export function AdminLayout() { const navigate = useNavigate(); @@ -53,6 +54,7 @@ export function AdminLayout() { { to: '/admin/products', label: 'Продукты' }, { to: '/admin/sections', label: 'Секции' }, { to: '/admin/settings', label: 'Настройки' }, + { to: '/admin/cards', label: 'Визитки' }, ].map((item) => ( } /> } /> } /> + } /> diff --git a/frontend/src/admin/CardsAdmin.tsx b/frontend/src/admin/CardsAdmin.tsx new file mode 100644 index 0000000..705a1ce --- /dev/null +++ b/frontend/src/admin/CardsAdmin.tsx @@ -0,0 +1,205 @@ +import { useEffect, useState } from 'react'; +import { api, type BusinessCard } from '../api/client'; + +type Draft = Partial; + +const EMPTY: Draft = { + slug: '', name: '', role: 'Основатель · разработчик', company: 'ООО «Умный Байт»', + phone: '', email: '', telegram: '', photo: null, + product_title: '', product_subtitle: '', product_url: '', site_url: 'https://umbyte.ru', +}; + +export function CardsAdmin() { + const [cards, setCards] = useState([]); + const [editing, setEditing] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [err, setErr] = useState(null); + + async function load() { + const { cards } = await api.listCards(); + setCards(cards); + setLoading(false); + } + useEffect(() => { load(); }, []); + + function patch(p: Partial) { setEditing((e) => ({ ...(e || {}), ...p })); } + + function onPhoto(file: File | undefined) { + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const img = new Image(); + img.onload = () => { + const max = 420; + let { width, height } = img; + if (width > height && width > max) { height = Math.round(height * max / width); width = max; } + else if (height > max) { width = Math.round(width * max / height); height = max; } + const canvas = document.createElement('canvas'); + canvas.width = width; canvas.height = height; + canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); + patch({ photo: canvas.toDataURL('image/jpeg', 0.85) }); + }; + img.src = String(ev.target!.result); + }; + reader.readAsDataURL(file); + } + + async function handleSave() { + if (!editing || !editing.slug || !editing.name) { setErr('Адрес и имя обязательны'); return; } + setSaving(true); setErr(null); + try { + if (editing.id) await api.updateCard(editing.id, editing); + else await api.createCard(editing); + setEditing(null); + await load(); + } catch (e: any) { + setErr(String(e.message).includes('slug_exists') ? 'Такой адрес уже занят' : 'Не удалось сохранить'); + } finally { setSaving(false); } + } + + async function handleDelete(c: BusinessCard) { + if (!confirm(`Удалить визитку «${c.name}»?`)) return; + await api.deleteCard(c.id); + await load(); + } + + if (loading) return
загрузка…
; + + return ( +
+
+

Визитки

+ +
+ +
+ + + + + + + + + + + {cards.map((c) => ( + + + + + + + ))} + {cards.length === 0 && ( + + )} + +
ИмяАдресСтатус
+ {c.name} +
{c.role}
+
+ + /card/{c.slug} ↗ + + {c.is_published === false ? 'скрыта' : 'опубликована'} + + +
Пока нет визиток
+
+ + {editing && ( +
setEditing(null)}> +
e.stopPropagation()}> +

{editing.id ? 'Редактировать визитку' : 'Новая визитка'}

+ +
+
+
+ {editing.photo + ? + : нет фото} +
+
+ + {editing.photo && ( + + )} +
+
+ +
+ + patch({ slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') })} placeholder="alexey" className="input" /> + + + patch({ name: e.target.value })} className="input" /> + +
+ + patch({ role: e.target.value })} className="input" /> + + + patch({ company: e.target.value })} className="input" /> + +
+ + patch({ phone: e.target.value })} placeholder="+7 900 000 0000" className="input" /> + + + patch({ email: e.target.value })} className="input" /> + +
+ + patch({ telegram: e.target.value })} placeholder="@username" className="input" /> + +
+ + patch({ product_title: e.target.value })} placeholder="АгроТО" className="input" /> + + + patch({ product_subtitle: e.target.value })} className="input" /> + +
+
+ + patch({ product_url: e.target.value })} placeholder="https://agroto.ru" className="input" /> + + + patch({ site_url: e.target.value })} className="input" /> + +
+
+ + {err &&
{err}
} + +
+ + +
+
+
+ )} + + +
+ ); +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 722475c..6a52ea5 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -29,6 +29,23 @@ export interface ApproachItem { description_en?: string | null; } +export interface BusinessCard { + id: number; + slug: string; + name: string; + role: string | null; + company: string | null; + phone: string | null; + email: string | null; + telegram: string | null; + photo: string | null; + product_title: string | null; + product_subtitle: string | null; + product_url: string | null; + site_url: string | null; + is_published?: boolean; +} + export interface LandingContent { sections: Record>; sections_en?: Record>; @@ -106,4 +123,21 @@ export const api = { method: 'PUT', body: JSON.stringify({ value }), }), + + getCard: (slug: string) => + request<{ card: BusinessCard }>(`/api/cards/${slug}`), + listCards: () => + request<{ cards: BusinessCard[] }>('/api/admin/cards'), + createCard: (data: Partial) => + request<{ card: BusinessCard }>('/api/admin/cards', { + method: 'POST', + body: JSON.stringify(data), + }), + updateCard: (id: number, data: Partial) => + request<{ card: BusinessCard }>(`/api/admin/cards/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }), + deleteCard: (id: number) => + request<{ ok: true }>(`/api/admin/cards/${id}`, { method: 'DELETE' }), }; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 3a0d677..42133f9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { Landing } from './pages/Landing'; +import { CardPage } from './pages/CardPage'; import { AdminLogin } from './admin/AdminLogin'; import { AdminLayout } from './admin/AdminLayout'; import './styles/index.css'; @@ -11,6 +12,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render( } /> + } /> } /> } /> diff --git a/frontend/src/pages/CardPage.tsx b/frontend/src/pages/CardPage.tsx new file mode 100644 index 0000000..427da80 --- /dev/null +++ b/frontend/src/pages/CardPage.tsx @@ -0,0 +1,235 @@ +import { useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import QRCode from 'qrcode'; +import { api, type BusinessCard } from '../api/client'; + +export function CardPage() { + const { slug } = useParams<{ slug: string }>(); + const [card, setCard] = useState(null); + const [error, setError] = useState(false); + const [qrOpen, setQrOpen] = useState(false); + const cardRef = useRef(null); + const glowRef = useRef(null); + const qrCanvasRef = useRef(null); + + useEffect(() => { + if (!slug) return; + api.getCard(slug).then(({ card }) => setCard(card)).catch(() => setError(true)); + }, [slug]); + + useEffect(() => { + if (!card) return; + if (!window.matchMedia('(hover:hover)').matches) return; + const el = cardRef.current, glow = glowRef.current; + function onMove(e: MouseEvent) { + if (!el) return; + const r = el.getBoundingClientRect(); + const dx = (e.clientX - (r.left + r.width / 2)) / r.width; + const dy = (e.clientY - (r.top + r.height / 2)) / r.height; + el.style.transform = `rotateY(${(dx * 5).toFixed(2)}deg) rotateX(${(-dy * 5).toFixed(2)}deg)`; + if (glow) { glow.style.left = e.clientX + 'px'; glow.style.top = e.clientY + 'px'; } + } + function onLeave() { if (el) el.style.transform = ''; } + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseleave', onLeave); + return () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseleave', onLeave); }; + }, [card]); + + if (error) return
Визитка не найдена
; + if (!card) return
загрузка…
; + + const initials = card.name.split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase(); + const telHref = card.phone ? 'tel:' + card.phone.replace(/[^+\d]/g, '') : ''; + + function saveContact() { + if (!card) return; + const parts = card.name.split(/\s+/); + const lines = ['BEGIN:VCARD', 'VERSION:3.0', + `N:${parts[1] || ''};${parts[0] || ''};;;`, `FN:${card.name}`]; + if (card.company) lines.push(`ORG:${card.company}`); + if (card.role) lines.push(`TITLE:${card.role.replace(/·/g, ',')}`); + if (card.phone) lines.push(`TEL;TYPE=CELL,VOICE:${card.phone.replace(/[^+\d]/g, '')}`); + if (card.email) lines.push(`EMAIL;TYPE=INTERNET:${card.email}`); + if (card.product_url) lines.push(`URL:${card.product_url}`); + if (card.site_url) lines.push(`URL:${card.site_url}`); + if (card.photo && card.photo.startsWith('data:image')) { + const b64 = card.photo.split(',')[1]; + const type = (card.photo.match(/data:image\/(\w+)/) || [])[1]?.toUpperCase() || 'JPEG'; + if (b64 && b64.length < 60000) lines.push(`PHOTO;ENCODING=b;TYPE=${type}:${b64}`); + } + lines.push('END:VCARD'); + const vcard = lines.join('\r\n'); + try { + const isIOS = /iP(hone|ad|od)/.test(navigator.userAgent); + if (isIOS) { + window.location.href = 'data:text/vcard;charset=utf-8,' + encodeURIComponent(vcard); + } else { + const blob = new Blob([vcard], { type: 'text/vcard;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = `${card.slug}.vcf`; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } + toast('Открываю карточку контакта…'); + } catch { toast('Не удалось открыть'); } + } + useEffect(() => { + if (!qrOpen || !qrCanvasRef.current) return; + QRCode.toCanvas(qrCanvasRef.current, window.location.href, { + width: 240, + margin: 1, + color: { dark: '#15132E', light: '#FFFFFF' }, + }).catch(() => toast('Не удалось построить QR')); + }, [qrOpen]); + + function toast(msg: string) { + const t = document.getElementById('cp-toast'); + if (!t) return; + t.textContent = msg; t.classList.add('on'); + setTimeout(() => t.classList.remove('on'), 2200); + } + + return ( +
+
+
+ +
+
+
+
+ {card.photo + ? {card.name} + : {initials}} +
+
+

{card.name}

+ {card.role &&
{card.role}
} + {card.company && ( +
+ + {card.company} +
+ )} + +
+ {card.phone && ( + + )} + {card.email && ( + + )} + {card.telegram && ( + + )} + {card.site_url && ( + + )} + +
+ + + + {card.product_title && ( + + + {card.product_title}{card.product_subtitle && {card.product_subtitle}} + + + )} + +
+ + Умный Байт · umbyte.ru +
+
+ +
Контакт сохранён ✓
+ + {qrOpen && ( +
setQrOpen(false)}> +
e.stopPropagation()}> + +
Отсканируйте, чтобы открыть визитку
+ +
+
+ )} + + +
+ ); +} + +const cardCss = ` +@import url('https://fonts.googleapis.com/css2?family=Unbounded:wght@400;500;600;700&family=Onest:wght@400;500;600&display=swap'); +.cardpage{--bg:#E9EDF7;--bg2:#DDE3F2;--card:#FFFFFF;--card2:#FBFCFF;--ink:#15132E;--ink2:#5C5984;--line:#E7E9F4;--indigo:#312E81;--indigo2:#4F4BC4;--cyan:#06B6D4;--cyan-glow:#22D3EE;--violet:#7C5CFF;--shadow:0 30px 70px -24px rgba(49,46,129,.32),0 10px 30px -18px rgba(6,182,212,.28); + font-family:'Onest',sans-serif;color:var(--ink);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:30px 18px;position:relative;overflow:hidden;perspective:1200px;background:linear-gradient(165deg,var(--bg),var(--bg2))} +@media (prefers-color-scheme:dark){.cardpage{--bg:#0C0B22;--bg2:#141233;--card:#17163a;--card2:#1c1a44;--ink:#F4F4FF;--ink2:#A6A3D9;--line:rgba(255,255,255,.12);--indigo:#4F4BC4;--indigo2:#6A66E0;--cyan:#22D3EE;--cyan-glow:#67E8F9;--shadow:0 30px 80px -24px rgba(0,0,0,.7),0 0 40px -10px rgba(34,211,238,.25)}} +.cardpage-msg{min-height:100vh;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-family:'Onest',sans-serif;background:#E9EDF7} +@media (prefers-color-scheme:dark){.cardpage-msg{background:#0C0B22}} +.cardpage *{margin:0;padding:0;box-sizing:border-box} +.cardpage .blob{position:fixed;border-radius:50%;filter:blur(70px);opacity:.6;z-index:0;pointer-events:none;will-change:transform} +.cardpage .b1{width:380px;height:380px;background:radial-gradient(circle,rgba(34,211,238,.5),transparent 70%);top:-80px;left:-60px;animation:cpf1 14s ease-in-out infinite} +.cardpage .b2{width:420px;height:420px;background:radial-gradient(circle,rgba(124,92,255,.4),transparent 70%);bottom:-120px;right:-80px;animation:cpf2 18s ease-in-out infinite} +.cardpage .b3{width:300px;height:300px;background:radial-gradient(circle,rgba(79,75,196,.4),transparent 70%);top:40%;left:60%;animation:cpf3 16s ease-in-out infinite} +@keyframes cpf1{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(60px,40px) scale(1.12)}} +@keyframes cpf2{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-50px,-40px) scale(1.1)}} +@keyframes cpf3{0%,100%{transform:translate(0,0) scale(1)}50%{transform:translate(-40px,50px) scale(1.15)}} +.cardpage .glow{position:fixed;width:300px;height:300px;border-radius:50%;pointer-events:none;z-index:1;background:radial-gradient(circle,rgba(34,211,238,.18),transparent 65%);transform:translate(-50%,-50%);left:-999px;top:-999px;mix-blend-mode:multiply} +@media (prefers-color-scheme:dark){.cardpage .glow{mix-blend-mode:screen}} +.cp-card{position:relative;z-index:2;width:100%;max-width:404px;background:linear-gradient(180deg,var(--card),var(--card2));border-radius:30px;padding:38px 28px 26px;box-shadow:var(--shadow);display:flex;flex-direction:column;align-items:center;text-align:center;transform-style:preserve-3d;transition:transform .15s ease-out;will-change:transform} +.cp-card::before{content:"";position:absolute;inset:0;border-radius:30px;padding:1.5px;pointer-events:none;background:linear-gradient(135deg,var(--cyan-glow),var(--violet) 35%,var(--indigo2) 60%,transparent 80%);background-size:200% 200%;animation:cpholo 6s linear infinite;-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude} +@keyframes cpholo{0%{background-position:0% 50%}100%{background-position:200% 50%}} +.cardpage .reveal{opacity:0;transform:translateY(14px);animation:cprise .7s cubic-bezier(.2,.7,.2,1) forwards} +@keyframes cprise{to{opacity:1;transform:none}} +.cardpage .d1{animation-delay:.05s}.cardpage .d2{animation-delay:.14s}.cardpage .d3{animation-delay:.22s}.cardpage .d4{animation-delay:.32s}.cardpage .d5{animation-delay:.42s}.cardpage .d6{animation-delay:.52s} +.cp-avatar{width:112px;height:112px;border-radius:50%;position:relative;margin-bottom:22px;display:flex;align-items:center;justify-content:center} +.cp-avatar .ring{position:absolute;inset:0;border-radius:50%;background:conic-gradient(from 0deg,var(--cyan-glow),var(--violet),var(--indigo),var(--cyan-glow));animation:cpspin 7s linear infinite} +@keyframes cpspin{to{transform:rotate(360deg)}} +.cp-avatar .inner{position:absolute;inset:5px;border-radius:50%;background:linear-gradient(180deg,var(--card),var(--card2));display:flex;align-items:center;justify-content:center;overflow:hidden;z-index:1} +.cp-avatar .inner img{width:100%;height:100%;object-fit:cover} +.cp-avatar .inner span{font-family:'Unbounded';font-weight:600;font-size:38px;letter-spacing:-1px;background:linear-gradient(150deg,var(--indigo2),var(--indigo));-webkit-background-clip:text;background-clip:text;color:transparent} +@media (prefers-color-scheme:dark){.cp-avatar .inner span{background:linear-gradient(150deg,#fff,#cfe9ff);-webkit-background-clip:text;background-clip:text;color:transparent}} +.cp-card h1{font-family:'Unbounded';font-weight:600;font-size:26px;letter-spacing:-.6px;line-height:1.1} +.cp-card .role{color:var(--cyan);font-weight:600;font-size:14.5px;margin-top:9px} +@media (prefers-color-scheme:dark){.cp-card .role{color:var(--cyan-glow)}} +.cp-card .company{color:var(--ink2);font-size:13px;margin-top:7px;display:flex;align-items:center;gap:7px;justify-content:center} +.cp-card .actions{display:flex;gap:12px;margin:26px 0 6px} +.cp-card .act{width:56px;height:56px;border-radius:18px;border:1px solid var(--line);background:var(--card);display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--indigo);box-shadow:0 6px 16px -8px rgba(49,46,129,.4);transition:transform .18s,box-shadow .18s,color .18s,border-color .18s;-webkit-tap-highlight-color:transparent} +@media (prefers-color-scheme:dark){.cp-card .act{color:var(--cyan-glow)}} +.cp-card .act:hover{transform:translateY(-4px);color:var(--cyan);border-color:rgba(6,182,212,.5);box-shadow:0 12px 22px -10px rgba(6,182,212,.5)} +.cp-card .act svg{width:23px;height:23px} +.cp-card .save{margin-top:18px;width:100%;border:none;cursor:pointer;padding:17px 20px;border-radius:18px;font-family:'Onest';font-weight:600;font-size:16px;color:#fff;background:linear-gradient(110deg,var(--indigo),var(--violet) 35%,var(--cyan) 70%,var(--indigo));background-size:220% 100%;animation:cphb 5s linear infinite;box-shadow:0 16px 34px -12px rgba(49,46,129,.6);display:flex;align-items:center;justify-content:center;gap:10px;transition:transform .18s,box-shadow .18s} +@keyframes cphb{0%{background-position:0 0}100%{background-position:220% 0}} +.cp-card .save:hover{transform:translateY(-2px);box-shadow:0 20px 42px -12px rgba(124,92,255,.6)} +.cp-card .product{margin-top:13px;width:100%;text-decoration:none;color:var(--ink);border:1px solid var(--line);background:var(--card);border-radius:18px;padding:14px 16px;display:flex;align-items:center;gap:13px;text-align:left;transition:transform .18s,border-color .18s,box-shadow .18s} +.cp-card .product:hover{transform:translateY(-2px);border-color:rgba(6,182,212,.5);box-shadow:0 12px 24px -14px rgba(6,182,212,.5)} +.cp-card .product .ic{width:42px;height:42px;border-radius:12px;background:linear-gradient(145deg,var(--indigo2),var(--indigo));display:flex;align-items:center;justify-content:center;flex-shrink:0} +.cp-card .product .t{font-weight:600;font-size:15px;display:block} +.cp-card .product .s{color:var(--ink2);font-size:12px;margin-top:2px;display:block} +.cp-card .product .go{margin-left:auto;color:var(--cyan)} +.cp-card .foot{margin-top:24px;display:flex;align-items:center;gap:8px;color:var(--ink2);font-size:12px} +.cp-card .foot b{color:var(--ink);font-weight:600} +.cardpage .toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(20px);background:var(--ink);color:var(--bg);padding:11px 18px;border-radius:12px;font-size:14px;opacity:0;pointer-events:none;transition:all .3s;z-index:6;box-shadow:0 12px 30px -8px rgba(0,0,0,.4)} +.cardpage .toast.on{opacity:1;transform:translateX(-50%) translateY(0)} +.qr-overlay{position:fixed;inset:0;z-index:10;background:rgba(21,19,46,.55);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:20px;animation:cprise .2s ease-out} +.qr-box{background:var(--card);border-radius:24px;padding:28px 26px 22px;display:flex;flex-direction:column;align-items:center;gap:14px;box-shadow:var(--shadow);max-width:300px} +.qr-box canvas{border-radius:14px;display:block} +.qr-hint{font-size:13px;color:var(--ink2);text-align:center} +.qr-close{border:none;cursor:pointer;padding:12px 22px;border-radius:14px;font-family:'Onest';font-weight:600;font-size:14px;color:#fff;background:linear-gradient(110deg,var(--indigo),var(--violet) 60%,var(--cyan));width:100%} +`; diff --git a/frontend/src/pages/Landing.tsx b/frontend/src/pages/Landing.tsx index 1d3db9f..4991b03 100644 --- a/frontend/src/pages/Landing.tsx +++ b/frontend/src/pages/Landing.tsx @@ -82,6 +82,15 @@ export function Landing() { const cta = sec('cta'); const settings = content.settings; + // Реальное количество продуктов из БД + русское склонение + const productCount = content.products.length; + const pluralProducts = (n: number) => { + const m10 = n % 10, m100 = n % 100; + if (m10 === 1 && m100 !== 11) return 'ФЛАГМАНСКИЙ ПРОДУКТ'; + if (m10 >= 2 && m10 <= 4 && !(m100 >= 12 && m100 <= 14)) return 'ФЛАГМАНСКИХ ПРОДУКТА'; + return 'ФЛАГМАНСКИХ ПРОДУКТОВ'; + }; + return (
@@ -298,14 +312,22 @@ export function Landing() { почта, она одна и та же на обоих сайтах компании. */} {settings.contact_email && (