Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
340a1c3e61 | ||
|
|
6281a0d467 | ||
|
|
3e164fb687 | ||
|
|
1c6e06bd6d | ||
|
|
f6857712a8 | ||
|
|
22fade091d | ||
|
|
b8e20ab510 | ||
|
|
6071cd5228 | ||
| 98a17011a2 | |||
| b066a53ffa |
2
.gitignore
vendored
@@ -25,3 +25,5 @@ pnpm-debug.log*
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
.env
|
||||||
|
|||||||
@@ -4,19 +4,32 @@ import tailwind from '@astrojs/tailwind';
|
|||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://siliconpin.cs1.hz.siliconpin.com',
|
site: 'https://sp-dev-h1-astgsbchdykdvtf.siliconpin.com',
|
||||||
|
integrations: [tailwind(), react()],
|
||||||
|
|
||||||
|
// Vite-specific settings (including proxy)
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
|
allowedHosts: ['sp-dev-h1-astgsbchdykdvtf.siliconpin.com'],
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080', // Your backend server
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
secure: false, // Only needed if using self-signed HTTPS
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
|
allowedHosts: ['sp-dev-h1-astgsbchdykdvtf.siliconpin.com'],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
integrations: [tailwind(), react()],
|
|
||||||
|
// Astro server settings
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0', // Accessible on all network interfaces
|
||||||
port: 4000,
|
port: 4000,
|
||||||
},
|
},
|
||||||
|
|
||||||
output: 'static',
|
output: 'static',
|
||||||
});
|
});
|
||||||
24
env_sample.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
PUBLIC_USER_API_URL=https://host-api-sxashuasysagibx.siliconpin.com/v1/users/
|
||||||
|
|
||||||
|
PUBLIC_SERVICE_API_URL=https://host-api-sxashuasysagibx.siliconpin.com/v1/services/
|
||||||
|
|
||||||
|
PUBLIC_INVOICE_API_URL=https://host-api-sxashuasysagibx.siliconpin.com/v1/invoice/
|
||||||
|
|
||||||
|
PUBLIC_MINIO_UPLOAD_URL=https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload
|
||||||
|
|
||||||
|
PUBLIC_HOST_API2_USERS=https://hostapi2.cs1.hz.siliconpin.com/api/users
|
||||||
|
|
||||||
|
PUBLIC_TOPIC_API_URL=https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/
|
||||||
|
|
||||||
|
PUBLIC_TICKET_API_URL=https://host-api-sxashuasysagibx.siliconpin.com/v1/ticket/index.php
|
||||||
|
|
||||||
|
PUBLIC_HETZNER_API_KEY=uMSBR9nxdtbuvazsVM8YMMDd0PvuynpgJbmzFIO47HblMlh7tlHT8wV05sQ28Squ
|
||||||
|
|
||||||
|
PUBLIC_UTHO_API_KEY=IoNXhkRJsQPyOEqFMceSfzuKaDLrpxUCATgZjiVdvYlBHbwWmGtn
|
||||||
|
|
||||||
|
PUBLIC_DIGITALOCEAN_API_KEY=dop_v1_b6a075ece5786faf7c58d21761dbf95d47af372da062d68a870ce2a0bae51adf
|
||||||
|
|
||||||
|
PUBLIC_POCKETBASE_URL=https://tst-pb.s38.siliconpin.com
|
||||||
|
|
||||||
|
|
||||||
|
ENV_TYPE=DEV
|
||||||
59
fix-php-tags-and-decode.cjs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const buildDir = path.join(__dirname, 'dist');
|
||||||
|
|
||||||
|
// Basic HTML entity decode
|
||||||
|
function decodeHtmlEntities(str) {
|
||||||
|
return str
|
||||||
|
.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(dec))
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode both URI and HTML entities inside <?php ... ?>
|
||||||
|
function decodePhpBlocks(content) {
|
||||||
|
return content.replace(/<\?php([\s\S]*?)\?>/g, (match, code) => {
|
||||||
|
try {
|
||||||
|
let decoded = decodeURIComponent(code); // %XX to characters
|
||||||
|
decoded = decodeHtmlEntities(decoded); // ' to '
|
||||||
|
return `<?php${decoded}?>`;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('⚠️ Decode failed for block:', code.trim().slice(0, 50), '...');
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixPhpTagsInFile(filePath) {
|
||||||
|
let content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// Step 1: Replace escaped tags
|
||||||
|
content = content
|
||||||
|
.replace(/<\?php/g, '<?php')
|
||||||
|
.replace(/\?>/g, '?>');
|
||||||
|
|
||||||
|
// Step 2: Decode content inside <?php ... ?>
|
||||||
|
content = decodePhpBlocks(content);
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content, 'utf-8');
|
||||||
|
console.log(`✅ Processed: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkDir(dir) {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkDir(fullPath);
|
||||||
|
} else if (fullPath.endsWith('.html') || fullPath.endsWith('.php')) {
|
||||||
|
fixPhpTagsInFile(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walkDir(buildDir);
|
||||||
|
console.log('🎉 All PHP tag fixes, URL decodes, and HTML entity decodes complete.');
|
||||||
736
package-lock.json
generated
@@ -25,6 +25,9 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"framer-motion": "^12.18.1",
|
||||||
|
"image-resize-compress": "^2.1.1",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.484.0",
|
"lucide-react": "^0.484.0",
|
||||||
"marked": "^15.0.8",
|
"marked": "^15.0.8",
|
||||||
"menubar": "^9.5.1",
|
"menubar": "^9.5.1",
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
"react-simplemde-editor": "^5.2.0",
|
"react-simplemde-editor": "^5.2.0",
|
||||||
"react-to-print": "^3.0.5",
|
"react-to-print": "^3.0.5",
|
||||||
"rehype-rewrite": "^4.0.2",
|
"rehype-rewrite": "^4.0.2",
|
||||||
|
"serve": "^14.2.4",
|
||||||
"shadcn": "^2.5.0",
|
"shadcn": "^2.5.0",
|
||||||
"simplemde": "^1.11.2",
|
"simplemde": "^1.11.2",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
@@ -4499,6 +4503,12 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@zeit/schemas": {
|
||||||
|
"version": "2.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
|
||||||
|
"integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@zxing/text-encoding": {
|
"node_modules/@zxing/text-encoding": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||||
@@ -4512,6 +4522,19 @@
|
|||||||
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==",
|
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
@@ -4548,6 +4571,22 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||||
|
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-align": {
|
"node_modules/ansi-align": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
||||||
@@ -4680,6 +4719,26 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/arch": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
@@ -5206,6 +5265,15 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cacheable-lookup": {
|
"node_modules/cacheable-lookup": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||||
@@ -5383,6 +5451,52 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chalk-template": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk-template?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chalk-template/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/chalk-template/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/character-entities-html4": {
|
"node_modules/character-entities-html4": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
||||||
@@ -5524,6 +5638,139 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clipboardy": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"arch": "^2.2.0",
|
||||||
|
"execa": "^5.1.1",
|
||||||
|
"is-wsl": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/execa": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"get-stream": "^6.0.0",
|
||||||
|
"human-signals": "^2.1.0",
|
||||||
|
"is-stream": "^2.0.0",
|
||||||
|
"merge-stream": "^2.0.0",
|
||||||
|
"npm-run-path": "^4.0.1",
|
||||||
|
"onetime": "^5.1.2",
|
||||||
|
"signal-exit": "^3.0.3",
|
||||||
|
"strip-final-newline": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/human-signals": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/is-docker": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"is-docker": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/is-stream": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/is-wsl": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-docker": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/mimic-fn": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/npm-run-path": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/onetime": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-fn": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clipboardy/node_modules/strip-final-newline": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -5739,6 +5986,72 @@
|
|||||||
"integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==",
|
"integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.5",
|
||||||
|
"bytes": "3.0.0",
|
||||||
|
"compressible": "~2.0.16",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"on-headers": "~1.0.2",
|
||||||
|
"safe-buffer": "5.1.2",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compression/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-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -5958,6 +6271,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
@@ -6746,6 +7068,33 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.18.1.tgz",
|
||||||
|
"integrity": "sha512-6o4EDuRPLk4LSZ1kRnnEOurbQ86MklVk+Y1rFBUKiF+d2pCdvMjWVu0ZkyMVCTwl5UyTH2n/zJEJx+jvTYuxow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.18.1",
|
||||||
|
"motion-utils": "^12.18.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "11.3.0",
|
"version": "11.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||||
@@ -7051,6 +7400,15 @@
|
|||||||
"uncrypto": "^0.1.3"
|
"uncrypto": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-property-descriptors": {
|
"node_modules/has-property-descriptors": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
@@ -7737,6 +8095,15 @@
|
|||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/image-resize-compress": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/image-resize-compress/-/image-resize-compress-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-aF4O1ZzAM0wDziIQZdqlxvDBe163J1Kiu+hSXbYNaGDcD4ggqAgRLLtRmpspnpJPHa4PkhVjwOshzBMHbOWESQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -7769,6 +8136,12 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/inline-style-parser": {
|
"node_modules/inline-style-parser": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
|
||||||
@@ -8053,6 +8426,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-port-reachable": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-regex": {
|
"node_modules/is-regex": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
@@ -8182,6 +8567,15 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -8225,6 +8619,12 @@
|
|||||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/json-stringify-safe": {
|
"node_modules/json-stringify-safe": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||||
@@ -12900,6 +13300,21 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.18.1.tgz",
|
||||||
|
"integrity": "sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.18.1.tgz",
|
||||||
|
"integrity": "sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
@@ -13006,6 +13421,15 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/neotraverse": {
|
"node_modules/neotraverse": {
|
||||||
"version": "0.6.18",
|
"version": "0.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz",
|
||||||
@@ -13207,6 +13631,15 @@
|
|||||||
"ufo": "^1.5.4"
|
"ufo": "^1.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -13446,6 +13879,12 @@
|
|||||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/path-is-inside": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
|
||||||
|
"license": "(WTFPL OR MIT)"
|
||||||
|
},
|
||||||
"node_modules/path-key": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
@@ -13877,6 +14316,30 @@
|
|||||||
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
|
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.0.0",
|
"version": "19.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||||
@@ -14297,6 +14760,28 @@
|
|||||||
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/registry-auth-token": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"rc": "^1.1.6",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/registry-url": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"rc": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rehype": {
|
"node_modules/rehype": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz",
|
||||||
@@ -16246,6 +16731,208 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/serve": {
|
||||||
|
"version": "14.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz",
|
||||||
|
"integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@zeit/schemas": "2.36.0",
|
||||||
|
"ajv": "8.12.0",
|
||||||
|
"arg": "5.0.2",
|
||||||
|
"boxen": "7.0.0",
|
||||||
|
"chalk": "5.0.1",
|
||||||
|
"chalk-template": "0.4.0",
|
||||||
|
"clipboardy": "3.0.0",
|
||||||
|
"compression": "1.7.4",
|
||||||
|
"is-port-reachable": "4.0.0",
|
||||||
|
"serve-handler": "6.1.6",
|
||||||
|
"update-check": "1.5.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"serve": "build/main.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-handler": {
|
||||||
|
"version": "6.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
|
||||||
|
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.0.0",
|
||||||
|
"content-disposition": "0.5.2",
|
||||||
|
"mime-types": "2.1.18",
|
||||||
|
"minimatch": "3.1.2",
|
||||||
|
"path-is-inside": "1.0.2",
|
||||||
|
"path-to-regexp": "3.3.0",
|
||||||
|
"range-parser": "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-handler/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-handler/node_modules/mime-db": {
|
||||||
|
"version": "1.33.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||||
|
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-handler/node_modules/mime-types": {
|
||||||
|
"version": "2.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||||
|
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "~1.33.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-handler/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-handler/node_modules/path-to-regexp": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/boxen": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-align": "^3.0.1",
|
||||||
|
"camelcase": "^7.0.0",
|
||||||
|
"chalk": "^5.0.1",
|
||||||
|
"cli-boxes": "^3.0.0",
|
||||||
|
"string-width": "^5.1.2",
|
||||||
|
"type-fest": "^2.13.0",
|
||||||
|
"widest-line": "^4.0.1",
|
||||||
|
"wrap-ansi": "^8.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/camelcase": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/chalk": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/emoji-regex": {
|
||||||
|
"version": "9.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/string-width": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eastasianwidth": "^0.2.0",
|
||||||
|
"emoji-regex": "^9.2.2",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/type-fest": {
|
||||||
|
"version": "2.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||||
|
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/widest-line": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve/node_modules/wrap-ansi": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.1.0",
|
||||||
|
"string-width": "^5.0.1",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
@@ -16705,6 +17392,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strnum": {
|
"node_modules/strnum": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
|
||||||
@@ -16779,6 +17475,18 @@
|
|||||||
"node": ">= 8.0"
|
"node": ">= 8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
@@ -17558,6 +18266,25 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/update-check": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"registry-auth-token": "3.3.2",
|
||||||
|
"registry-url": "3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uri-js": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/url-parse": {
|
"node_modules/url-parse": {
|
||||||
"version": "1.5.10",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
@@ -17648,6 +18375,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview --port 4000",
|
"preview": "astro preview --port 4000",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"push-prod": "rsync -rv --exclude .hta_config/conf.php dist/ u2@siliconpin.com:~/web/siliconpin.com/public_html/"
|
"push-prod": "rsync -rv --exclude .hta_config/conf.php dist/ sp@siliconpin.com:~/web/siliconpin.com/public_html/",
|
||||||
|
"proxy": "http://localhost:8080"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^4.2.1",
|
"@astrojs/react": "^4.2.1",
|
||||||
@@ -27,6 +28,9 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"framer-motion": "^12.18.1",
|
||||||
|
"image-resize-compress": "^2.1.1",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.484.0",
|
"lucide-react": "^0.484.0",
|
||||||
"marked": "^15.0.8",
|
"marked": "^15.0.8",
|
||||||
"menubar": "^9.5.1",
|
"menubar": "^9.5.1",
|
||||||
@@ -40,6 +44,7 @@
|
|||||||
"react-simplemde-editor": "^5.2.0",
|
"react-simplemde-editor": "^5.2.0",
|
||||||
"react-to-print": "^3.0.5",
|
"react-to-print": "^3.0.5",
|
||||||
"rehype-rewrite": "^4.0.2",
|
"rehype-rewrite": "^4.0.2",
|
||||||
|
"serve": "^14.2.4",
|
||||||
"shadcn": "^2.5.0",
|
"shadcn": "^2.5.0",
|
||||||
"simplemde": "^1.11.2",
|
"simplemde": "^1.11.2",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
@@ -48,4 +53,4 @@
|
|||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
BIN
public/assets/images/services/kubernetes-edge.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
BIN
public/assets/images/services/stt-streaming.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
12
public/assets/js/index.min.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Skipped minification because the original files appears to be already minified.
|
||||||
|
* Original file: /npm/image-resize-compress@2.1.1/dist/index.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
var h=e=>({png:"image/png",webp:"image/webp",bmp:"image/bmp",gif:"image/gif",jpeg:"image/jpeg"})[e]||"image/jpeg",w=(e,t="auto",r="auto")=>{let o=t==="auto"||t===0,m=r==="auto"||r===0;if(!o&&!m)return{width:t,height:r};if(!o){let n=e.naturalWidth/t;return{width:t,height:Math.round((e.naturalHeight/n+Number.EPSILON)*100)/100}}if(!m){let n=e.naturalHeight/r;return{width:Math.round((e.naturalWidth/n+Number.EPSILON)*100)/100,height:r}}return{width:e.naturalWidth,height:e.naturalHeight}},E=async(e,t=100,r="auto",o="auto",m=null,n=null)=>{if(!(e instanceof Blob))throw new TypeError(`Expected a Blob or File, but got ${typeof e}.`);if(e.size===0)throw new Error("Failed to load the image. The file might be corrupt or empty.");if(t<=0)throw new RangeError("Quality must be greater than 0.");if(typeof r=="number"&&r<0||typeof o=="number"&&o<0)throw new RangeError("Invalid width or height value!");let a=m?h(m):e.type,g=t<1?t:t/100;return new Promise((p,l)=>{let u=new FileReader;u.onload=()=>{let s=new Image;s.src=u.result,s.onload=()=>{let i=document.createElement("canvas"),d=w(s,r,o);i.width=d.width,i.height=d.height;let f=i.getContext("2d");if(!f){l(new Error("Failed to get canvas context."));return}n&&a==="image/png"&&(f.fillStyle=n,f.fillRect(0,0,i.width,i.height)),f.drawImage(s,0,0,i.width,i.height),i.toBlob(b=>{if(b===null)return l(new Error("Failed to generate image blob."));p(new Blob([b],{type:a}))},a,g)},s.onerror=()=>{l(new Error("Failed to load the image. The file might be corrupt or empty."))}},u.onerror=()=>l(new Error("Failed to read the blob as a Data URL.")),u.readAsDataURL(e)})},c=E;var F=async(e,t=100,r="auto",o="auto",m,n)=>{try{let a=await fetch(e,n);if(!a.ok)throw new Error(`Failed to fetch image: ${a.statusText}`);let g=await a.blob();return await c(g,t,r,o,m)}catch(a){throw new Error(`Failed to process the image from URL. Check CORS or network issues. Error: ${a}`)}},y=F;var I=e=>new Promise((t,r)=>{if(e.size===0)return r(new Error("Cannot convert empty Blob."));if(e.size>10485760)return r(new Error("File size exceeds the maximum allowed limit."));let o=new FileReader;o.onloadend=()=>{o.result?t(o.result):r(new Error("Failed to convert blob to DataURL."))},o.onerror=()=>r(new Error("Error reading blob.")),o.readAsDataURL(e)}),R=I;var L=async(e,t)=>{try{let r=await fetch(e,t);if(!r.ok)throw new Error(`Failed to fetch image: ${r.statusText}`);return await r.blob()}catch(r){throw new Error(`Failed to fetch image from URL. Error: ${r}`)}},T=L;export{R as blobToURL,c as fromBlob,y as fromURL,T as urlToBlob};
|
||||||
|
/*!
|
||||||
|
* image-resize-compress
|
||||||
|
* Copyright(c) 2024 Álef Duarte
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
1381
public/assets/md-editor/mdeditor.css
Normal file
@@ -5,8 +5,9 @@ import { Label } from "./ui/label";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
const pb = new PocketBase('https://tst-pb.s38.siliconpin.com');
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
const PUBLIC_POCKETBASE_URL = import.meta.env.PUBLIC_POCKETBASE_URL;
|
||||||
|
const pb = new PocketBase(PUBLIC_POCKETBASE_URL);
|
||||||
|
|
||||||
export function AvatarUpload({ userId }: { userId: string }) {
|
export function AvatarUpload({ userId }: { userId: string }) {
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
@@ -45,7 +46,7 @@ export function AvatarUpload({ userId }: { userId: string }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Update PHP backend session
|
// 2. Update PHP backend session
|
||||||
const response = await fetch(`${INVOICE_API_URL}?query=login`, {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include', // Important for sessions to work
|
credentials: 'include', // Important for sessions to work
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Button } from "./ui/button";
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
export default function UserBillingList() {
|
export default function UserBillingList() {
|
||||||
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
const [billingData, setBillingData] = useState([]);
|
const [billingData, setBillingData] = useState([]);
|
||||||
const [dataLoading, setDataLoading] = useState(true);
|
const [dataLoading, setDataLoading] = useState(true);
|
||||||
@@ -27,7 +28,6 @@ export default function UserBillingList() {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage] = useState(20);
|
const [itemsPerPage] = useState(20);
|
||||||
|
|
||||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
@@ -38,7 +38,7 @@ export default function UserBillingList() {
|
|||||||
|
|
||||||
const fetchBillingData = async () => {
|
const fetchBillingData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${INVOICE_API_URL}?query=invoice-info`, {
|
const res = await fetch(`${PUBLIC_USER_API_URL}?query=invoice-info`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import { Button } from '../ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
|
||||||
export default function BuyVPN() {
|
export default function BuyVPN() {
|
||||||
// API URL - make sure to set this correctly
|
|
||||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
|
||||||
|
|
||||||
// State for VPN configuration
|
// State for VPN configuration
|
||||||
const [vpnContinent, setVpnContinent] = useState("");
|
const [vpnContinent, setVpnContinent] = useState("");
|
||||||
const [selectedCycle, setSelectedCycle] = useState("");
|
const [selectedCycle, setSelectedCycle] = useState("");
|
||||||
@@ -39,7 +37,7 @@ export default function BuyVPN() {
|
|||||||
setError('Please select a billing cycle');
|
setError('Please select a billing cycle');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@@ -51,7 +49,7 @@ export default function BuyVPN() {
|
|||||||
formData.append('amount', selectedPrice.toString());
|
formData.append('amount', selectedPrice.toString());
|
||||||
formData.append('vpn_continent', vpnContinent);
|
formData.append('vpn_continent', vpnContinent);
|
||||||
formData.append('service_type', 'vpn');
|
formData.append('service_type', 'vpn');
|
||||||
const response = await fetch(`${USER_API_URL}?query=initiate_payment`, {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=initiate_payment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData, // Using FormData instead of JSON
|
body: formData, // Using FormData instead of JSON
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -146,7 +144,7 @@ export default function BuyVPN() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* Selection Summary */}
|
{/* Selection Summary */}
|
||||||
{selectedCycle && (
|
{selectedCycle && (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Loader2 } from "lucide-react";
|
|||||||
import { useToast } from "../ui/toast";
|
import { useToast } from "../ui/toast";
|
||||||
|
|
||||||
export default function NewDroplet() {
|
export default function NewDroplet() {
|
||||||
|
const PUBLIC_DIGITALOCEAN_API_KEY = import.meta.env.PUBLIC_DIGITALOCEAN_API_KEY;
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -63,7 +64,7 @@ export default function NewDroplet() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": "Bearer dop_v1_b6a075ece5786faf7c58d21761dbf95d47af372da062d68a870ce2a0bae51adf"
|
"Authorization": `Bearer ${PUBLIC_DIGITALOCEAN_API_KEY}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
...formData,
|
...formData,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function NewHetznerInstance() {
|
|||||||
const [images, setImages] = useState([]);
|
const [images, setImages] = useState([]);
|
||||||
const [sshKeys, setSshKeys] = useState([]);
|
const [sshKeys, setSshKeys] = useState([]);
|
||||||
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
||||||
const HETZNER_API_KEY = "uMSBR9nxdtbuvazsVM8YMMDd0PvuynpgJbmzFIO47HblMlh7tlHT8wV05sQ28Squ"; // Replace with your actual API key
|
const PUBLIC_HETZNER_API_KEY = import.meta.env.PUBLIC_HETZNER_API_KEY;
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -38,16 +38,16 @@ export default function NewHetznerInstance() {
|
|||||||
// Fetch all required data in parallel
|
// Fetch all required data in parallel
|
||||||
const [serverTypesResponse, locationsResponse, imagesResponse, sshKeysResponse] = await Promise.all([
|
const [serverTypesResponse, locationsResponse, imagesResponse, sshKeysResponse] = await Promise.all([
|
||||||
fetch("https://api.hetzner.cloud/v1/server_types", {
|
fetch("https://api.hetzner.cloud/v1/server_types", {
|
||||||
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
|
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
|
||||||
}),
|
}),
|
||||||
fetch("https://api.hetzner.cloud/v1/locations", {
|
fetch("https://api.hetzner.cloud/v1/locations", {
|
||||||
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
|
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
|
||||||
}),
|
}),
|
||||||
fetch("https://api.hetzner.cloud/v1/images", {
|
fetch("https://api.hetzner.cloud/v1/images", {
|
||||||
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
|
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
|
||||||
}),
|
}),
|
||||||
fetch("https://api.hetzner.cloud/v1/ssh_keys", {
|
fetch("https://api.hetzner.cloud/v1/ssh_keys", {
|
||||||
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
|
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ export default function NewHetznerInstance() {
|
|||||||
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
|
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${HETZNER_API_KEY}`,
|
"Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
@@ -149,7 +149,7 @@ export default function NewHetznerInstance() {
|
|||||||
const response = await fetch('https://api.hetzner.cloud/v1/ssh_keys', {
|
const response = await fetch('https://api.hetzner.cloud/v1/ssh_keys', {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${HETZNER_API_KEY}`,
|
"Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -163,7 +163,7 @@ export default function NewHetznerInstance() {
|
|||||||
if (data.ssh_key) {
|
if (data.ssh_key) {
|
||||||
// Refresh SSH keys list
|
// Refresh SSH keys list
|
||||||
const sshResponse = await fetch("https://api.hetzner.cloud/v1/ssh_keys", {
|
const sshResponse = await fetch("https://api.hetzner.cloud/v1/ssh_keys", {
|
||||||
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
|
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
|
||||||
});
|
});
|
||||||
const sshData = await sshResponse.json();
|
const sshData = await sshResponse.json();
|
||||||
setSshKeys(sshData.ssh_keys || []);
|
setSshKeys(sshData.ssh_keys || []);
|
||||||
|
|||||||
486
src/components/BuyServices/NewKubernetisUtho.jsx
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
import Loader from "../ui/loader";
|
||||||
|
import { useToast } from "../ui/toast";
|
||||||
|
import { Switch } from "../ui/switch";
|
||||||
|
|
||||||
|
export default function NewKubernetesService() {
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
||||||
|
const [plans, setPlans] = useState([]);
|
||||||
|
const [vpcs, setVpcs] = useState([]);
|
||||||
|
const [subnets, setSubnets] = useState([]);
|
||||||
|
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
dcslug: "innoida",
|
||||||
|
cluster_version: "1.24",
|
||||||
|
cluster_label: "",
|
||||||
|
nodepools: [{
|
||||||
|
label: "default-pool",
|
||||||
|
size: "",
|
||||||
|
count: 1,
|
||||||
|
maxCount: 1
|
||||||
|
}],
|
||||||
|
firewall: "",
|
||||||
|
vpc: "",
|
||||||
|
subnet: "",
|
||||||
|
network_type: "public",
|
||||||
|
cpumodel: "intel"
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchInitialData = async () => {
|
||||||
|
setIsFetchingData(true);
|
||||||
|
try {
|
||||||
|
const [plansResponse, vpcsResponse] = await Promise.all([
|
||||||
|
fetch("https://api.utho.com/v2/plans", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
fetch("https://api.utho.com/v2/vpc", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const plansData = await plansResponse.json();
|
||||||
|
const vpcsData = await vpcsResponse.json();
|
||||||
|
|
||||||
|
setPlans(plansData.plans || []);
|
||||||
|
setVpcs(vpcsData.vpc || []);
|
||||||
|
|
||||||
|
// Set default VPC if available
|
||||||
|
if (vpcsData.vpc?.length > 0) {
|
||||||
|
const firstVpc = vpcsData.vpc[0];
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
vpc: firstVpc.id
|
||||||
|
}));
|
||||||
|
// Fetch subnets for the default VPC
|
||||||
|
await fetchSubnets(firstVpc.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Initial data fetch error:", error);
|
||||||
|
showToast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load initial configuration data",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchInitialData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchSubnets = async (vpcId) => {
|
||||||
|
try {
|
||||||
|
// First try to get subnets from the VPC data
|
||||||
|
const selectedVpc = vpcs.find(vpc => vpc.id === vpcId);
|
||||||
|
if (selectedVpc?.subnet?.length > 0) {
|
||||||
|
setSubnets(selectedVpc.subnet);
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
subnet: selectedVpc.subnet[0].id
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no subnets in VPC data, try the subnets endpoint
|
||||||
|
const response = await fetch(`https://api.utho.com/v2/vpc/${vpcId}/subnets`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setSubnets(data.subnets || []);
|
||||||
|
if (data.subnets?.length > 0) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
subnet: data.subnets[0].id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching subnets:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVpcChange = async (vpcId) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
vpc: vpcId,
|
||||||
|
subnet: ""
|
||||||
|
}));
|
||||||
|
await fetchSubnets(vpcId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate required fields including subnet
|
||||||
|
if (!formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet) {
|
||||||
|
throw new Error("Please fill all required fields including VPC and Subnet");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
dcslug: formData.dcslug,
|
||||||
|
cluster_version: formData.cluster_version,
|
||||||
|
cluster_label: formData.cluster_label,
|
||||||
|
nodepools: formData.nodepools.map(pool => ({
|
||||||
|
label: pool.label,
|
||||||
|
size: pool.size,
|
||||||
|
count: pool.count.toString(),
|
||||||
|
maxCount: pool.maxCount.toString()
|
||||||
|
})),
|
||||||
|
firewall: formData.firewall || undefined,
|
||||||
|
vpc: formData.vpc,
|
||||||
|
subnet: formData.subnet, // Now required based on API behavior
|
||||||
|
network_type: formData.network_type,
|
||||||
|
cpumodel: formData.cpumodel
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Deployment payload:", payload);
|
||||||
|
|
||||||
|
const response = await fetch("https://api.utho.com/v2/kubernetes/deploy", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === "success") {
|
||||||
|
showToast({
|
||||||
|
title: "Success",
|
||||||
|
description: `Cluster ${data.cluster_id || data.id} is being deployed`,
|
||||||
|
variant: "success"
|
||||||
|
});
|
||||||
|
// Reset form
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
cluster_label: "",
|
||||||
|
nodepools: [{
|
||||||
|
label: "default-pool",
|
||||||
|
size: "",
|
||||||
|
count: 1,
|
||||||
|
maxCount: 1
|
||||||
|
}],
|
||||||
|
vpc: vpcs[0]?.id || "",
|
||||||
|
subnet: subnets[0]?.id || ""
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast({
|
||||||
|
title: "Deployment Failed",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
console.error("Deployment error:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: type === "checkbox" ? checked : value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodePoolChange = (index, field, value) => {
|
||||||
|
const updatedNodePools = [...formData.nodepools];
|
||||||
|
updatedNodePools[index][field] = value;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
nodepools: updatedNodePools
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isFetchingData) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<Loader />
|
||||||
|
<span className="ml-2">Loading configuration...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Deploy New Kubernetes Cluster</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure your Kubernetes cluster with the required parameters
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{/* Cluster Label */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cluster_label">Cluster Label *</Label>
|
||||||
|
<Input
|
||||||
|
id="cluster_label"
|
||||||
|
name="cluster_label"
|
||||||
|
value={formData.cluster_label}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="my-cluster"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data Center and Version */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="dcslug">Data Center *</Label>
|
||||||
|
<Select
|
||||||
|
name="dcslug"
|
||||||
|
value={formData.dcslug}
|
||||||
|
onValueChange={(value) => setFormData({...formData, dcslug: value})}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select location" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="innoida">Noida</SelectItem>
|
||||||
|
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
|
||||||
|
<SelectItem value="indelhi">Delhi</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
|
||||||
|
<Select
|
||||||
|
name="cluster_version"
|
||||||
|
value={formData.cluster_version}
|
||||||
|
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select version" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1.24">1.24</SelectItem>
|
||||||
|
<SelectItem value="1.25">1.25</SelectItem>
|
||||||
|
<SelectItem value="1.26">1.26</SelectItem>
|
||||||
|
<SelectItem value="1.27">1.27</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Network Type and CPU Model */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="network_type">Network Type *</Label>
|
||||||
|
<Select
|
||||||
|
name="network_type"
|
||||||
|
value={formData.network_type}
|
||||||
|
onValueChange={(value) => setFormData({...formData, network_type: value})}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select network type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="public">Public</SelectItem>
|
||||||
|
<SelectItem value="private">Private</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cpumodel">CPU Model *</Label>
|
||||||
|
<Select
|
||||||
|
name="cpumodel"
|
||||||
|
value={formData.cpumodel}
|
||||||
|
onValueChange={(value) => setFormData({...formData, cpumodel: value})}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select CPU model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="intel">Intel</SelectItem>
|
||||||
|
<SelectItem value="amd">AMD</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Node Pool Configuration */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Node Pool Configuration</h3>
|
||||||
|
{formData.nodepools.map((pool, index) => (
|
||||||
|
<div key={index} className="p-4 border rounded-lg space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`pool-label-${index}`}>Pool Label *</Label>
|
||||||
|
<Input
|
||||||
|
id={`pool-label-${index}`}
|
||||||
|
value={pool.label}
|
||||||
|
onChange={(e) => handleNodePoolChange(index, 'label', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`pool-size-${index}`}>Node Size *</Label>
|
||||||
|
<Select
|
||||||
|
value={pool.size}
|
||||||
|
onValueChange={(value) => handleNodePoolChange(index, 'size', value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select node size" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{plans.map(plan => (
|
||||||
|
<SelectItem key={plan.id} value={plan.id}>
|
||||||
|
{`${plan.cpu} vCPU, ${Math.floor(parseInt(plan.ram)/1024)}GB RAM`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`pool-count-${index}`}>Node Count *</Label>
|
||||||
|
<Input
|
||||||
|
id={`pool-count-${index}`}
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={pool.count}
|
||||||
|
onChange={(e) => handleNodePoolChange(index, 'count', parseInt(e.target.value))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`pool-maxCount-${index}`}>Max Nodes</Label>
|
||||||
|
<Input
|
||||||
|
id={`pool-maxCount-${index}`}
|
||||||
|
type="number"
|
||||||
|
min={pool.count}
|
||||||
|
value={pool.maxCount}
|
||||||
|
onChange={(e) => handleNodePoolChange(index, 'maxCount', parseInt(e.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VPC and Subnet */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="vpc">VPC *</Label>
|
||||||
|
<Select
|
||||||
|
name="vpc"
|
||||||
|
value={formData.vpc}
|
||||||
|
onValueChange={handleVpcChange}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select VPC" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{vpcs.map(vpc => (
|
||||||
|
<SelectItem key={vpc.id} value={vpc.id}>
|
||||||
|
{vpc.name || vpc.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="subnet">Subnet *</Label>
|
||||||
|
<Select
|
||||||
|
name="subnet"
|
||||||
|
value={formData.subnet}
|
||||||
|
onValueChange={(value) => setFormData({...formData, subnet: value})}
|
||||||
|
required
|
||||||
|
disabled={!formData.vpc || subnets.length === 0}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={subnets.length === 0 ? "No subnets available" : "Select subnet"}>
|
||||||
|
{formData.subnet || (subnets.length === 0 ? "No subnets available" : "Select subnet")}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{subnets.map(subnet => (
|
||||||
|
<SelectItem key={subnet.id} value={subnet.id}>
|
||||||
|
{subnet.name || subnet.id} ({subnet.network}/{subnet.size})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{subnets.length === 0 && formData.vpc && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
No subnets found in this VPC. Please create a subnet first.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Firewall (Optional) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="firewall">Firewall ID (Optional)</Label>
|
||||||
|
<Input
|
||||||
|
id="firewall"
|
||||||
|
name="firewall"
|
||||||
|
value={formData.firewall}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Firewall ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet}
|
||||||
|
className="w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader className="mr-2 h-4 w-4" />
|
||||||
|
Deploying...
|
||||||
|
</>
|
||||||
|
) : "Deploy Kubernetes Cluster"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,13 +11,13 @@ import { Textarea } from "../ui/textarea";
|
|||||||
import { Switch } from "../ui/switch";
|
import { Switch } from "../ui/switch";
|
||||||
|
|
||||||
export default function NewCloudInstance() {
|
export default function NewCloudInstance() {
|
||||||
|
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isFetchingData, setIsFetchingData] = useState(true);
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
||||||
const [plans, setPlans] = useState([]);
|
const [plans, setPlans] = useState([]);
|
||||||
const [sshKeys, setSshKeys] = useState([]);
|
const [sshKeys, setSshKeys] = useState([]);
|
||||||
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
||||||
const UTHO_API_KEY = "Bearer IoNXhkRJsQPyOEqFMceSfzuKaDLrpxUCATgZjiVdvYlBHbwWmGtn";
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
dcslug: "inmumbaizone2",
|
dcslug: "inmumbaizone2",
|
||||||
@@ -39,10 +39,10 @@ export default function NewCloudInstance() {
|
|||||||
// Fetch plans and SSH keys in parallel
|
// Fetch plans and SSH keys in parallel
|
||||||
const [plansResponse, sshResponse] = await Promise.all([
|
const [plansResponse, sshResponse] = await Promise.all([
|
||||||
fetch("https://api.utho.com/v2/plans", {
|
fetch("https://api.utho.com/v2/plans", {
|
||||||
headers: { "Authorization": UTHO_API_KEY }
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
||||||
}),
|
}),
|
||||||
fetch("https://api.utho.com/v2/key", {
|
fetch("https://api.utho.com/v2/key", {
|
||||||
headers: { "Authorization": UTHO_API_KEY }
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ export default function NewCloudInstance() {
|
|||||||
const response = await fetch("https://api.utho.com/v2/cloud/deploy", {
|
const response = await fetch("https://api.utho.com/v2/cloud/deploy", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": UTHO_API_KEY,
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
@@ -139,7 +139,7 @@ export default function NewCloudInstance() {
|
|||||||
const response = await fetch('https://api.utho.com/v2/key/import', {
|
const response = await fetch('https://api.utho.com/v2/key/import', {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": UTHO_API_KEY,
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -153,7 +153,7 @@ export default function NewCloudInstance() {
|
|||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
// Refresh SSH keys list
|
// Refresh SSH keys list
|
||||||
const sshResponse = await fetch("https://api.utho.com/v2/key", {
|
const sshResponse = await fetch("https://api.utho.com/v2/key", {
|
||||||
headers: { "Authorization": UTHO_API_KEY }
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
||||||
});
|
});
|
||||||
const sshData = await sshResponse.json();
|
const sshData = await sshResponse.json();
|
||||||
setSshKeys(sshData.key || []);
|
setSshKeys(sshData.key || []);
|
||||||
|
|||||||
114
src/components/BuyServices/STTStreaming.jsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
import Loader from "../ui/loader";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||||
|
const PRICE_CONFIG = [
|
||||||
|
{purchaseType: 'loose', price: 2000, minute: 10000},
|
||||||
|
{purchaseType: 'bulk', price: 1000, minute: 10000}
|
||||||
|
];
|
||||||
|
export default function STTStreaming(){
|
||||||
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
const [purchaseType, setPurchaseType] = useState();
|
||||||
|
const [amount, setAmount] = useState();
|
||||||
|
const [dataError, setDataError] = useState();
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const handlePurchaseType = (type) => {
|
||||||
|
const selected = PRICE_CONFIG.find(item => item.purchaseType === type);
|
||||||
|
setAmount(type === 'bulk' ? selected.price : type === 'loose' ? selected.price : '')
|
||||||
|
setPurchaseType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePurchase = async () => {
|
||||||
|
// Validate inputs
|
||||||
|
if (!purchaseType) {
|
||||||
|
setDataError('Please select a Plan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
setDataError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('service', 'STT Streaming API');
|
||||||
|
formData.append('serviceId', 'sttstreamingapi');
|
||||||
|
formData.append('cycle', 'monthly');
|
||||||
|
formData.append('amount', amount.toString());
|
||||||
|
formData.append('service_type', 'streaming_api');
|
||||||
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=initiate_payment`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData, // Using FormData instead of JSON
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Redirect to payment success page with order ID
|
||||||
|
window.location.href = `/success?service=streaming_api&orderId=${data.order_id}`;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Payment initialization failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Purchase error:', error);
|
||||||
|
setDataError(error.message || 'An error occurred during purchase. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if(!purchaseType){
|
||||||
|
<div>
|
||||||
|
<p>{dataError}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<div className='container mx-auto'>
|
||||||
|
<Card className='max-w-2xl mx-auto px-4 mt-8'>
|
||||||
|
<CardContent>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>STT Streaming API</CardTitle>
|
||||||
|
<CardDescription>Real-time Speech-to-Text (STT) streaming that converts spoken words into accurate, readable text</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<div className='flex flex-row justify-between items-center gap-x-6'>
|
||||||
|
<label htmlFor="purchase-type-loose" className={`border ${purchaseType === 'loose' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||||
|
<input onChange={() => handlePurchaseType('loose')} type="radio" name="purchase-type" id="purchase-type-loose" className='hidden' />
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='text-2xl font-bold'>Loose Plan</span>
|
||||||
|
<span className=''>🕐10,000 Min ₹2000</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="purchase-type-bulk" className={`border ${purchaseType === 'bulk' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||||
|
<input onChange={() => handlePurchaseType('bulk')} type="radio" name="purchase-type" id="purchase-type-bulk" className='hidden' />
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<span className='text-2xl font-bold'>Bulk Plan</span>
|
||||||
|
<span className=''>🕐10,000 Min ₹1000</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ul className="grid grid-cols-2 justify-between mt-2 gap-x-2 text-xs">
|
||||||
|
{['Setup Charge 5000', '10000 Min INR 2000 in Loose Plan', '10000 Min INR 1000 in Bulk Plan', 'Real-time transcription with high accuracy', 'Supports multiple languages', 'Custom vocabulary for domain-specific terms', 'Integrates easily with video/audio streams', 'Secure and scalable API access', 'Live subtitle overlay options'].map((feature, index) => (
|
||||||
|
<li key={index} className="flex items-start">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
<span className='text-zinc-400'>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className='flex my-8 w-full'>
|
||||||
|
<Button onClick={handlePurchase} disabled={!purchaseType} className={`w-full ${!purchaseType ? 'cursor-not-allowed' : ''}`}>{isProcessing ? (<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Processing...</>) : 'Proceed'}</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
200
src/components/Comment copy.jsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Card } from "./ui/card";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
|
|
||||||
|
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
|
||||||
|
|
||||||
|
export default function Comment(props) {
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
const [newComment, setNewComment] = useState({ comment: '' });
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState('');
|
||||||
|
const [isLoadingComments, setIsLoadingComments] = useState(true);
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
|
||||||
|
// Load comments when component mounts or when topicId changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.topicId) {
|
||||||
|
fetchComments(props.topicId);
|
||||||
|
}
|
||||||
|
}, [props.topicId]);
|
||||||
|
|
||||||
|
const fetchComments = async (topicId) => {
|
||||||
|
setIsLoadingComments(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || 'Failed to fetch comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success && data.comments) {
|
||||||
|
setComments(data.comments);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching comments:', error);
|
||||||
|
setSubmitError('Failed to load comments');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingComments(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setNewComment(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitComment = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...newComment,
|
||||||
|
topicId: props.topicId
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setNewComment({ comment: '' });
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
setTimeout(() => setSubmitSuccess(false), 3000);
|
||||||
|
|
||||||
|
// Refresh comments after successful submission
|
||||||
|
await fetchComments(props.topicId);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setSubmitError(errorData.message || 'Failed to submit comment');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError('Network error. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserInitials = (name) => {
|
||||||
|
if (!name) return 'U';
|
||||||
|
const words = name.trim().split(' ');
|
||||||
|
return words
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(word => word[0].toUpperCase())
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Comments List */}
|
||||||
|
<div className="space-y-6 border-t">
|
||||||
|
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
|
||||||
|
|
||||||
|
{isLoadingComments ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
{/* <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div> */}
|
||||||
|
</div>
|
||||||
|
) : comments.length === 0 ? (
|
||||||
|
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
|
||||||
|
) : (
|
||||||
|
comments.map(comment => (
|
||||||
|
<div key={comment.id} className="border-b pb-6 last:border-b-0">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
|
||||||
|
{getUserInitials(comment.userName)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-medium text-[#6d9e37]">
|
||||||
|
{comment.userName || 'User'}
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{new Date(comment.created_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="">{comment.comment}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comments Section */}
|
||||||
|
<Card className="mt-16 border-t">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="px-6 pb-6 rounded-lg">
|
||||||
|
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
|
||||||
|
{submitSuccess && (
|
||||||
|
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
|
||||||
|
Thank you for your comment!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{submitError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||||
|
{submitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmitComment}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Label htmlFor="comment">Comment *</Label>
|
||||||
|
<Textarea
|
||||||
|
className="mt-2"
|
||||||
|
id="comment"
|
||||||
|
name="comment"
|
||||||
|
rows="4"
|
||||||
|
value={newComment.comment}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
disabled={!isLoggedIn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Post Comment'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : !isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||||
|
Join the conversation! Log in or sign up to post a comment
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
src/components/CommentSystem/CommentSystem.jsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Card } from '../ui/card';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Textarea } from '../ui/textarea';
|
||||||
|
import CommentThread from './CommentThread';
|
||||||
|
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
||||||
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
const CommentSystem = ({ topicId, pbId }) => {
|
||||||
|
const isLoginCoockie = JSON.parse(isLogin);
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
const [newComment, setNewComment] = useState('');
|
||||||
|
const [commentsLoading, setCommentsLoading] = useState(false);
|
||||||
|
const [commentsError, setCommentsError] = useState('');
|
||||||
|
|
||||||
|
const nestComments = (flatComments) => {
|
||||||
|
const commentMap = {};
|
||||||
|
const rootComments = [];
|
||||||
|
|
||||||
|
flatComments?.forEach(comment => {
|
||||||
|
comment.replies = [];
|
||||||
|
commentMap[comment.comment_id] = comment;
|
||||||
|
});
|
||||||
|
|
||||||
|
flatComments?.forEach(comment => {
|
||||||
|
if (comment.parent_comment_id?.Valid) {
|
||||||
|
const parent = commentMap[comment.parent_comment_id.String];
|
||||||
|
if (parent) parent.replies.push(comment);
|
||||||
|
} else {
|
||||||
|
rootComments.push(comment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rootComments.sort((a, b) =>
|
||||||
|
new Date(b.created_at) - new Date(a.created_at));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchComments = async () => {
|
||||||
|
try {
|
||||||
|
setCommentsLoading(true);
|
||||||
|
setCommentsError('');
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
topic_id: String(topicId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only add silicon_id if it has a value
|
||||||
|
if (siliconId && siliconId !== 'null') {
|
||||||
|
params.append('silicon_id', siliconId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add pb_id if it has a value
|
||||||
|
if (pb_id && pb_id !== 'null') {
|
||||||
|
params.append('pb_id', pb_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/comments?${params.toString()}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flatComments = await response.json();
|
||||||
|
setComments(nestComments(flatComments || []));
|
||||||
|
} catch (err) {
|
||||||
|
setCommentsError(err.message);
|
||||||
|
console.error('Fetch error:', err);
|
||||||
|
} finally {
|
||||||
|
setCommentsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const postComment = async (text, parentId = null) => {
|
||||||
|
try {
|
||||||
|
setCommentsLoading(true);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
topic_id: String(topicId),
|
||||||
|
silicon_id: siliconId,
|
||||||
|
pb_id: pb_id,
|
||||||
|
user_id: siliconId,
|
||||||
|
user_name: user_name,
|
||||||
|
comment_text: text,
|
||||||
|
is_approved: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
payload.parent_comment_id = parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = parentId ? '/api/comments/reply' : '/api/comments';
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || 'Failed to post comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchComments();
|
||||||
|
} catch (err) {
|
||||||
|
setCommentsError(err.message);
|
||||||
|
} finally {
|
||||||
|
setCommentsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchComments();
|
||||||
|
}, [topicId, siliconId, pbId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className=" mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-2xl font-bold text-[#6d9e37] mb-6">Comments</h2>
|
||||||
|
|
||||||
|
{commentsError && (
|
||||||
|
<div className="bg-red-50 text-red-700 p-3 rounded-md mb-4">
|
||||||
|
{commentsError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{commentsLoading && comments.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-[#6d9e37]">Loading comments...</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{comments.length > 0 ? (
|
||||||
|
comments.map(comment => (
|
||||||
|
<CommentThread key={comment.comment_id} comment={comment} onReply={postComment} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center py-8 text-gray-500 italic">
|
||||||
|
No comments yet. Be the first to comment!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='relative'>
|
||||||
|
{
|
||||||
|
isLoginCoockie !== true ? (
|
||||||
|
<div className="absolute bg-black bg-opacity-70 w-full h-full rounded-lg">
|
||||||
|
<div className='absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2'>
|
||||||
|
<p className='italic bg-[#2a2e35] px-2 py-1.5 rounded-md shadow-xl'>To join the conversation, please log in first.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Card className="mt-8 p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Add a Comment</h3>
|
||||||
|
<form onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newComment.trim()) return;
|
||||||
|
postComment(newComment);
|
||||||
|
setNewComment('');
|
||||||
|
}}>
|
||||||
|
<Textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="Share your thoughts..." className="min-h-[100px]" required minLength={1}/>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button type="submit" disabled={commentsLoading || !newComment.trim()} className="w-full sm:w-auto">
|
||||||
|
{commentsLoading ? (
|
||||||
|
<span className="flex items-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Posting...
|
||||||
|
</span>
|
||||||
|
) : 'Post Comment'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentSystem;
|
||||||
64
src/components/CommentSystem/CommentThread.jsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Textarea } from '../ui/textarea';
|
||||||
|
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
||||||
|
const CommentThread = ({ comment, depth = 0, onReply }) => {
|
||||||
|
const isLoginCoockie = JSON.parse(isLogin);
|
||||||
|
const [isReplying, setIsReplying] = useState(false);
|
||||||
|
const [replyText, setReplyText] = useState('');
|
||||||
|
const [replyError, setReplyError] = useState('')
|
||||||
|
|
||||||
|
const handleReplySubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!replyText.trim()) return;
|
||||||
|
onReply(replyText, comment.comment_id);
|
||||||
|
setReplyText('');
|
||||||
|
setIsReplying(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`mb-4 pl-${depth * 4} border-l-2 ${depth > 0 ? 'border-[#6d9e37]' : 'border-transparent'}`}
|
||||||
|
style={{ marginLeft: `${depth * 0.5}rem` }}
|
||||||
|
>
|
||||||
|
<div className="bg-[#25272950] rounded-lg shadow-sm px-4 py-3 mb-2">
|
||||||
|
<div className="flex gap-3 items-center justify-center">
|
||||||
|
{/* User Avatar SVG */}
|
||||||
|
<div className="flex-shrink-0 ">
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" className="rounded-full bg-[#6d9e37] p-1"><path d="M20 20C24.1421 20 27.5 16.6421 27.5 12.5C27.5 8.35786 24.1421 5 20 5C15.8579 5 12.5 8.35786 12.5 12.5C12.5 16.6421 15.8579 20 20 20ZM20 23.75C14.2675 23.75 5 26.4825 5 32.1875V35H35V32.1875C35 26.4825 25.7325 23.75 20 23.75Z" fill="white"/></svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex justify-between items-start mb-1">
|
||||||
|
<span className="font-medium text-[#6d9e37]">{comment.user_name}</span>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{new Date(comment.created_at).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-100 mb-2 whitespace-pre-wrap">{comment.comment_text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
isLoginCoockie === true && (
|
||||||
|
<Button variant="outline" className="border-none text-[#6d9e37] hover:text-[#8bc34a] hover:bg-transparent px-0 py-0 h-auto" onClick={() => setIsReplying(!isReplying)}>Reply</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{isReplying && (
|
||||||
|
<form onSubmit={handleReplySubmit} className="mt-3 pl-11">
|
||||||
|
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="Write your reply..." className="mb-2 min-h-[70px]" required minLength={1} />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button type="submit" size="sm" className="bg-[#6d9e37] hover:bg-[#8bc34a]">Post Reply</Button>
|
||||||
|
<Button type="button" variant="outline" size="sm" className="border-gray-600 text-gray-300 hover:border-[#6d9e37] hover:text-[#8bc34a]" onClick={() => setIsReplying(false)}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{comment.replies?.map((reply) => (
|
||||||
|
<CommentThread key={reply.comment_id} comment={reply} depth={depth + 1} onReply={onReply}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentThread;
|
||||||
5
src/components/CommentSystem/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import CommentSystem from './CommentSystem';
|
||||||
|
import CommentThread from './CommentThread';
|
||||||
|
|
||||||
|
export default CommentSystem;
|
||||||
|
export { CommentThread };
|
||||||
371
src/components/CommentWithMD.jsx
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||||
|
import { Card } from "./ui/card";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
|
|
||||||
|
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
|
||||||
|
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
||||||
|
|
||||||
|
export default function Comment(props) {
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
const [newComment, setNewComment] = useState({ comment: '' });
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState('');
|
||||||
|
const [isLoadingComments, setIsLoadingComments] = useState(true);
|
||||||
|
const [editorMode, setEditorMode] = useState('edit');
|
||||||
|
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
||||||
|
const [imageUrlInput, setImageUrlInput] = useState('');
|
||||||
|
const [imageUploadFile, setImageUploadFile] = useState(null);
|
||||||
|
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
|
||||||
|
// Custom image command for MDEditor
|
||||||
|
const customImageCommand = {
|
||||||
|
name: 'image',
|
||||||
|
keyCommand: 'image',
|
||||||
|
buttonProps: { 'aria-label': 'Insert image' },
|
||||||
|
icon: (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
execute: () => {
|
||||||
|
setImageDialogOpen(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all default commands and replace the image command
|
||||||
|
const allCommands = commands.getCommands().map(cmd => {
|
||||||
|
if (cmd.name === 'image') {
|
||||||
|
return customImageCommand;
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load comments when component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.topicId) {
|
||||||
|
fetchComments(props.topicId);
|
||||||
|
}
|
||||||
|
}, [props.topicId]);
|
||||||
|
|
||||||
|
const fetchComments = async (topicId) => {
|
||||||
|
setIsLoadingComments(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || 'Failed to fetch comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success && data.comments) {
|
||||||
|
setComments(data.comments);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching comments:', error);
|
||||||
|
setSubmitError('Failed to load comments');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingComments(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload file to MinIO
|
||||||
|
const uploadToMinIO = async (file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.publicUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image URL insertion
|
||||||
|
const handleInsertImageUrl = () => {
|
||||||
|
if (imageUrlInput) {
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
setNewComment(prev => ({
|
||||||
|
...prev,
|
||||||
|
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
|
||||||
|
}));
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUrlInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image file selection
|
||||||
|
const handleImageFileSelect = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setImageUploadFile(file);
|
||||||
|
|
||||||
|
// Create preview
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setImageUploadPreview(reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload image file to MinIO and insert into editor
|
||||||
|
const handleImageUpload = async () => {
|
||||||
|
if (!imageUploadFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
const uploadedUrl = await uploadToMinIO(imageUploadFile);
|
||||||
|
|
||||||
|
// Insert markdown for the uploaded image
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
setNewComment(prev => ({
|
||||||
|
...prev,
|
||||||
|
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
|
||||||
|
}));
|
||||||
|
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUploadFile(null);
|
||||||
|
setImageUploadPreview('');
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError('Failed to upload image: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitComment = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...newComment,
|
||||||
|
topicId: props.topicId
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setNewComment({ comment: '' });
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
setTimeout(() => setSubmitSuccess(false), 3000);
|
||||||
|
|
||||||
|
// Refresh comments after successful submission
|
||||||
|
await fetchComments(props.topicId);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setSubmitError(errorData.message || 'Failed to submit comment');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError('Network error. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserInitials = (name) => {
|
||||||
|
if (!name) return 'U';
|
||||||
|
const words = name.trim().split(' ');
|
||||||
|
return words
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(word => word[0].toUpperCase())
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Comments List */}
|
||||||
|
<div className="space-y-6 border-t">
|
||||||
|
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
|
||||||
|
|
||||||
|
{isLoadingComments ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div>
|
||||||
|
</div>
|
||||||
|
) : comments.length === 0 ? (
|
||||||
|
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
|
||||||
|
) : (
|
||||||
|
comments.map(comment => (
|
||||||
|
<div key={comment.id} className="border-b pb-6 last:border-b-0">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
|
||||||
|
{getUserInitials(comment.userName)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-medium text-[#6d9e37]">
|
||||||
|
{comment.userName || 'User'}
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{new Date(comment.created_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div data-color-mode="light" className="markdown-body">
|
||||||
|
<MDEditor.Markdown source={comment.comment} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comments Section with MDEditor */}
|
||||||
|
<Card className="mt-16 border-t">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="px-6 pb-6 rounded-lg">
|
||||||
|
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
|
||||||
|
{submitSuccess && (
|
||||||
|
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
|
||||||
|
Thank you for your comment!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{submitError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||||
|
{submitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmitComment}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Label htmlFor="comment">Comment *</Label>
|
||||||
|
<div data-color-mode="light">
|
||||||
|
<MDEditor
|
||||||
|
placeholder="Write your comment (markdown supported)"
|
||||||
|
value={newComment.comment}
|
||||||
|
onChange={(value) => setNewComment(prev => ({ ...prev, comment: value || '' }))}
|
||||||
|
height={300}
|
||||||
|
preview={editorMode}
|
||||||
|
commands={allCommands}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
|
||||||
|
className={`text-sm ${editorMode !== 'edit' ? 'bg-[#6d9e37] text-white' : 'text-[#6d9e37]'} px-2 py-1 rounded-md border border-[#6d9e37]`}
|
||||||
|
>
|
||||||
|
{editorMode === 'edit' ? 'Preview' : 'Edit'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Post Comment'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image Upload Dialog */}
|
||||||
|
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Insert Image</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image-url">From URL</Label>
|
||||||
|
<Input
|
||||||
|
id="image-url"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter image URL"
|
||||||
|
value={imageUrlInput}
|
||||||
|
onChange={(e) => setImageUrlInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleInsertImageUrl}
|
||||||
|
disabled={!imageUrlInput}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
Insert Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image-upload">Upload Image</Label>
|
||||||
|
<Input
|
||||||
|
id="image-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageFileSelect}
|
||||||
|
/>
|
||||||
|
{imageUploadPreview && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={imageUploadPreview}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-40 rounded-md border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleImageUpload}
|
||||||
|
disabled={!imageUploadFile || isSubmitting}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : !isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||||
|
Join the conversation! Log in or sign up to post a comment
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,58 +4,63 @@ import { Textarea } from './ui/textarea';
|
|||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
|
|
||||||
export function ContactForm() {
|
export function ContactForm() {
|
||||||
const [formState, setFormState] = useState({
|
const [formState, setFormState] = useState({ name: '', email: '', company: '', service: '', message: '' });
|
||||||
name: '',
|
const [formStatus, setFormStatus] = useState('idle');
|
||||||
email: '',
|
|
||||||
company: '',
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
service: '',
|
|
||||||
message: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [formStatus, setFormStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
const handleInputChange = (e) => {
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormState(prev => ({ ...prev, [name]: value }));
|
setFormState(prev => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectChange = (value: string) => {
|
const handleSelectChange = (value) => {
|
||||||
setFormState(prev => ({ ...prev, service: value }));
|
setFormState(prev => ({ ...prev, service: value }));
|
||||||
console.log(formState)
|
// console.log(formState)
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setFormStatus('submitting');
|
setFormStatus('submitting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Simulate API call
|
const payload = {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
name: formState.name,
|
||||||
|
email: formState.email,
|
||||||
console.log('Form submitted:', formState);
|
company: formState.company,
|
||||||
setFormStatus('success');
|
service_intrest: formState.service,
|
||||||
|
message: formState.message
|
||||||
|
};
|
||||||
|
|
||||||
// Reset form
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=contact-form`, {
|
||||||
setFormState({
|
method: "POST",
|
||||||
name: '',
|
credentials: "include",
|
||||||
email: '',
|
headers: {
|
||||||
company: '',
|
'Content-Type': 'application/json',
|
||||||
service: '',
|
},
|
||||||
message: '',
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => setFormStatus('idle'), 3000);
|
const data = await response.json();
|
||||||
|
// console.log('Form Response', data);
|
||||||
|
|
||||||
|
if(data.success) {
|
||||||
|
setFormStatus('success');
|
||||||
|
// Reset form only after successful submission
|
||||||
|
setFormState({ name: '', email: '', company: '', service: '', message: '' });
|
||||||
|
} else {
|
||||||
|
setFormStatus('error');
|
||||||
|
console.error('Backend error:', data.message);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submission error:', error);
|
console.error('Submission error:', error);
|
||||||
setFormStatus('error');
|
setFormStatus('error');
|
||||||
setTimeout(() => setFormStatus('idle'), 3000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6" id='contact-form'>
|
||||||
<div className="space-y-3 sm:space-y-4">
|
<div className="space-y-3 sm:space-y-4">
|
||||||
{/* Name and Email */}
|
{/* Name and Email */}
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
|
||||||
@@ -5,8 +5,9 @@ import { Button } from './ui/button';
|
|||||||
|
|
||||||
export const DomainSetupForm = ({ defaultSubdomain }) => {
|
export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||||
// API URLs
|
// API URLs
|
||||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
|
||||||
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
const PUBLIC_SERVICE_API_URL = import.meta.env.PUBLIC_SERVICE_API_URL;
|
||||||
|
|
||||||
// State for deployment options
|
// State for deployment options
|
||||||
const [deploymentType, setDeploymentType] = useState('app');
|
const [deploymentType, setDeploymentType] = useState('app');
|
||||||
@@ -57,6 +58,14 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
hestia: { monthly: 200, yearly: 2000 }
|
hestia: { monthly: 200, yearly: 2000 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeploymentType = (e) => {
|
||||||
|
console.log('setDeploymentType ',e.target.value);
|
||||||
|
setDeploymentType(e.target.value);
|
||||||
|
if(e.target.value === 'vpn'){
|
||||||
|
window.location.href = '/services/buy-vpn';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show toast notification
|
// Show toast notification
|
||||||
const showToast = useCallback((message) => {
|
const showToast = useCallback((message) => {
|
||||||
setToast({ visible: true, message });
|
setToast({ visible: true, message });
|
||||||
@@ -105,7 +114,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
formData.append('cycle', selectedCycle);
|
formData.append('cycle', selectedCycle);
|
||||||
formData.append('amount', selectedPrice.toString());
|
formData.append('amount', selectedPrice.toString());
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}?query=initiate_payment`, {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=initiate_payment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -158,7 +167,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
setShowDnsConfig(false);
|
setShowDnsConfig(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${SERVICES_API_URL}?query=validate-domain`, {
|
const response = await fetch(`${PUBLIC_SERVICE_API_URL}?query=validate-domain`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ domain })
|
body: JSON.stringify({ domain })
|
||||||
@@ -200,7 +209,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
const checkSubDomainCname = useCallback(() => {
|
const checkSubDomainCname = useCallback(() => {
|
||||||
const domainToCheck = customDomain || customSubdomain;
|
const domainToCheck = customDomain || customSubdomain;
|
||||||
|
|
||||||
fetch(`${SERVICES_API_URL}?query=check-c-name`, {
|
fetch(`${PUBLIC_SERVICE_API_URL}?query=check-c-name`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: `domain=${encodeURIComponent(domainToCheck)}`
|
body: `domain=${encodeURIComponent(domainToCheck)}`
|
||||||
@@ -451,54 +460,54 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'vpn':
|
// case 'vpn':
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
<div className="space-y-2">
|
// <div className="space-y-2">
|
||||||
<label htmlFor="vpn-continent" className="block text-white font-medium">Select Continent</label>
|
// <label htmlFor="vpn-continent" className="block text-white font-medium">Select Continent</label>
|
||||||
<select
|
// <select
|
||||||
id="vpn-continent"
|
// id="vpn-continent"
|
||||||
value={vpnContinent}
|
// value={vpnContinent}
|
||||||
onChange={(e) => setVpnContinent(e.target.value)}
|
// onChange={(e) => setVpnContinent(e.target.value)}
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
// className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
>
|
// >
|
||||||
<option value="">-Select-</option>
|
// <option value="">-Select-</option>
|
||||||
<option value="India">India</option>
|
// <option value="India">India</option>
|
||||||
<option value="America">America</option>
|
// <option value="America">America</option>
|
||||||
<option value="Europe">Europe</option>
|
// <option value="Europe">Europe</option>
|
||||||
</select>
|
// </select>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
<ul className="flex justify-between text-sm">
|
// <ul className="flex justify-between text-sm">
|
||||||
{["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"].map((feature, index) => (
|
// {["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"].map((feature, index) => (
|
||||||
<li key={index} className="flex items-start">
|
// <li key={index} className="flex items-start">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" ><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
// <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" ><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
||||||
<span className='text-zinc-400'>{feature}</span>
|
// <span className='text-zinc-400'>{feature}</span>
|
||||||
</li>
|
// </li>
|
||||||
))}
|
// ))}
|
||||||
</ul>
|
// </ul>
|
||||||
<div className='flex flex-row justify-between items-center gap-x-6'>
|
// <div className='flex flex-row justify-between items-center gap-x-6'>
|
||||||
<label className={`border ${selectedCycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
// <label className={`border ${selectedCycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
||||||
<input type="checkbox" checked={selectedCycle === 'monthly'} onChange={() => handleBillingCycleChange('monthly', PRICE_CONFIG.vpn.monthly)} className="hidden" />
|
// <input type="checkbox" checked={selectedCycle === 'monthly'} onChange={() => handleBillingCycleChange('monthly', PRICE_CONFIG.vpn.monthly)} className="hidden" />
|
||||||
<p className='text-3xl font-bold text-center'>₹{PRICE_CONFIG.vpn.monthly}</p>
|
// <p className='text-3xl font-bold text-center'>₹{PRICE_CONFIG.vpn.monthly}</p>
|
||||||
<span>Monthly</span>
|
// <span>Monthly</span>
|
||||||
</label>
|
// </label>
|
||||||
|
|
||||||
<label className={`border ${selectedCycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
// <label className={`border ${selectedCycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
|
||||||
<input type="checkbox" checked={selectedCycle === 'yearly'} onChange={() => handleBillingCycleChange('yearly', PRICE_CONFIG.vpn.yearly)} className="hidden" />
|
// <input type="checkbox" checked={selectedCycle === 'yearly'} onChange={() => handleBillingCycleChange('yearly', PRICE_CONFIG.vpn.yearly)} className="hidden" />
|
||||||
<p className='text-3xl font-bold text-center'>₹{PRICE_CONFIG.vpn.yearly}</p>
|
// <p className='text-3xl font-bold text-center'>₹{PRICE_CONFIG.vpn.yearly}</p>
|
||||||
<span>Yearly</span>
|
// <span>Yearly</span>
|
||||||
</label>
|
// </label>
|
||||||
</div>
|
// </div>
|
||||||
<div className='text-white'>
|
// <div className='text-white'>
|
||||||
{selectedCycle && (
|
// {selectedCycle && (
|
||||||
<p className={`${selectedCycle === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedCycle}</strong> plan at ₹{selectedPrice}</p>
|
// <p className={`${selectedCycle === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedCycle}</strong> plan at ₹{selectedPrice}</p>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
<Button onClick={() => handlePurchase('vpn')} className='w-full'>Proceed to Pay</Button>
|
// <Button onClick={() => handlePurchase('vpn')} className='w-full'>Proceed to Pay</Button>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@@ -514,7 +523,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
<select
|
<select
|
||||||
id="deployment-type"
|
id="deployment-type"
|
||||||
value={deploymentType}
|
value={deploymentType}
|
||||||
onChange={(e) => setDeploymentType(e.target.value)}
|
onChange={handleDeploymentType}
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
>
|
>
|
||||||
<option value="app">💻 Deploy an App</option>
|
<option value="app">💻 Deploy an App</option>
|
||||||
|
|||||||
@@ -38,9 +38,8 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
|
|
||||||
|
|
||||||
const [vpnContinent, setVpnContinent] = useState('');
|
const [vpnContinent, setVpnContinent] = useState('');
|
||||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
|
const PUBLIC_SERVICE_API_URL = import.meta.env.PUBLIC_SERVICE_API_URL;
|
||||||
// const BILLING_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
|
||||||
|
|
||||||
const [selectedcycle, setSelectedcycle] = useState('');
|
const [selectedcycle, setSelectedcycle] = useState('');
|
||||||
const [selectedPrice, setSelectedPrice] = useState(0);
|
const [selectedPrice, setSelectedPrice] = useState(0);
|
||||||
@@ -70,7 +69,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
formData.append('cycle', selectedcycle);
|
formData.append('cycle', selectedcycle);
|
||||||
formData.append('amount', selectedPrice); //selectedPrice
|
formData.append('amount', selectedPrice); //selectedPrice
|
||||||
|
|
||||||
fetch(`${API_URL}?query=initiate_payment`, {
|
fetch(`${PUBLIC_USER_API_URL}?query=initiate_payment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -105,7 +104,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
formData.append('serviceId', 'vpnservices');
|
formData.append('serviceId', 'vpnservices');
|
||||||
formData.append('cycle', selectedcycle);
|
formData.append('cycle', selectedcycle);
|
||||||
formData.append('amount', selectedPrice);
|
formData.append('amount', selectedPrice);
|
||||||
fetch(`${API_URL}?query=initiate_payment`, {
|
fetch(`${PUBLIC_USER_API_URL}?query=initiate_payment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -295,7 +294,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Make API call to validate domain
|
// Make API call to validate domain
|
||||||
const response = await fetch(`${SERVICES_API_URL}?query=validate-domain`, {
|
const response = await fetch(`${PUBLIC_SERVICE_API_URL}?query=validate-domain`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -331,7 +330,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||||||
const checkSubDomainCname = () => {
|
const checkSubDomainCname = () => {
|
||||||
const domainToCheck = customDomain || customSubdomain;
|
const domainToCheck = customDomain || customSubdomain;
|
||||||
|
|
||||||
fetch(`${SERVICES_API_URL}?query=check-c-name`, {
|
fetch(`${PUBLIC_SERVICE_API_URL}?query=check-c-name`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ const LoginPage = () => {
|
|||||||
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
|
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
const PUBLIC_HOST_API2_USERS = import.meta.env.PUBLIC_HOST_API2_USERS;
|
||||||
|
const PUBLIC_POCKETBASE_URL = import.meta.env.PUBLIC_POCKETBASE_URL;
|
||||||
|
|
||||||
|
const pb = new PocketBase(PUBLIC_POCKETBASE_URL);
|
||||||
|
|
||||||
interface AuthResponse {
|
interface AuthResponse {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -48,6 +52,14 @@ const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set Coockie Function
|
||||||
|
function setCookie(name : string, value : string, daysToExpire : number) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
|
||||||
|
const expires = "expires=" + date.toUTCString();
|
||||||
|
document.cookie = `${name}=${value}; ${expires}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -125,7 +137,7 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
const handleCreateUserInMongo = async (siliconId: string) => {
|
const handleCreateUserInMongo = async (siliconId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users`, {
|
const response = await fetch(`${PUBLIC_HOST_API2_USERS}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -137,22 +149,27 @@ const LoginPage = () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('MongoDB Response:', data);
|
|
||||||
return data;
|
if (response.ok) {
|
||||||
|
if (response.status === 200 && data.message) {
|
||||||
|
console.log('User exists:', data.message);
|
||||||
|
return data.user; // return the existing user
|
||||||
|
}
|
||||||
|
console.log('New user created:', data);
|
||||||
|
return data; // return the new user
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.error || `HTTP error! status: ${response.status}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('MongoDB Creation Error:', error);
|
console.error('MongoDB Creation Error:', error);
|
||||||
throw error; // Return Error
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||||
try {
|
try {
|
||||||
// Step 1: Sync with SiliconPin backend
|
// Step 1: Sync with SiliconPin backend
|
||||||
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -184,7 +201,14 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
// Step 3: Redirect to profile if all are ok
|
// Step 3: Redirect to profile if all are ok
|
||||||
window.location.href = '/profile';
|
window.location.href = '/profile';
|
||||||
|
console.log('data', data)
|
||||||
|
setCookie('token', data.userData.accessToken, 7);
|
||||||
|
setCookie('user_name', authData.record.name, 7);
|
||||||
|
setCookie('pb_id', authData.record.id, 7);
|
||||||
|
setCookie('siliconId', data.userData.userId, 7);
|
||||||
|
setCookie('isLogin', JSON.stringify(true), 7);
|
||||||
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Sync Error:', error);
|
console.error('Sync Error:', error);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function LoginOrProfile({ deviceType }) {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
deviceType === 'mobile' && (
|
deviceType === 'mobile' && (
|
||||||
<svg className="animate-spin flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Download_x5F_25_x25_"> </g> <g id="Download_x5F_50_x25_"> </g> <g id="Download_x5F_75_x25_"> </g> <g id="Download_x5F_100_x25_"> </g> <g id="Upload"> </g> <g id="Next"> </g> <g id="Last"> </g> <g id="OK"> </g> <g id="Fail"> </g> <g id="Add"> </g> <g id="Spinner_x5F_0_x25_"> </g> <g id="Spinner_x5F_25_x25_"> <g> <path fill="none" stroke="#6d9e37" stroke-width="4" stroke-linejoin="round" stroke-miterlimit="10" d="M50.188,26.812 c12.806,0,23.188,10.381,23.188,23.188"></path> <g> <circle cx="73.375" cy="50" r="1.959"></circle> <circle cx="72.029" cy="57.704" r="1.959"></circle> <circle cx="68.237" cy="64.579" r="1.959"></circle> <circle cx="62.324" cy="69.759" r="1.959"></circle> <circle cx="55.013" cy="72.699" r="1.959"></circle> </g> <g> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.0326 2.3121)" cx="27.045" cy="51.843" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.628 2.4912)" cx="28.998" cy="59.416" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.1348 2.8556)" cx="33.325" cy="65.967" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.4877 3.3713)" cx="39.63" cy="70.661" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.6506 3.9762)" cx="47.152" cy="73.012" rx="1.959" ry="1.959"></ellipse> </g> <g> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.713 2.567)" cx="27.71" cy="44.049" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.079 2.8158)" cx="30.892" cy="36.872" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.567 3.266)" cx="36.334" cy="31.199" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.2318 3.8617)" cx="43.363" cy="27.636" rx="1.959" ry="1.959"></ellipse> </g> </g> </g> <g id="Spinner_x5F_50_x25_"> </g> <g id="Spinner_x5F_75_x25_"> </g> <g id="Brightest_x5F_25_x25_"> </g> <g id="Brightest_x5F_50_x25_"> </g> <g id="Brightest_x5F_75_x25_"> </g> <g id="Brightest_x5F_100_x25_"> </g> <g id="Reload"> </g> <g id="Forbidden"> </g> <g id="Clock"> </g> <g id="Compass"> </g> <g id="World"> </g> <g id="Speed"> </g> <g id="Microphone"> </g> <g id="Options"> </g> <g id="Chronometer"> </g> <g id="Lock"> </g> <g id="User"> </g> <g id="Position"> </g> <g id="No_x5F_Signal"> </g> <g id="Low_x5F_Signal"> </g> <g id="Mid_x5F_Signal"> </g> <g id="High_x5F_Signal"> </g> <g id="Options_1_"> </g> <g id="Flash"> </g> <g id="No_x5F_Signal_x5F_02"> </g> <g id="Low_x5F_Signal_x5F_02"> </g> <g id="Mid_x5F_Signal_x5F_02"> </g> <g id="High_x5F_Signal_x5F_02"> </g> <g id="Favorite"> </g> <g id="Search"> </g> <g id="Stats_x5F_01"> </g> <g id="Stats_x5F_02"> </g> <g id="Turn_x5F_On_x5F_Off"> </g> <g id="Full_x5F_Height"> </g> <g id="Full_x5F_Width"> </g> <g id="Full_x5F_Screen"> </g> <g id="Compress_x5F_Screen"> </g> <g id="Chat"> </g> <g id="Bluetooth"> </g> <g id="Share_x5F_iOS"> </g> <g id="Share_x5F_Android"> </g> <g id="Love__x2F__Favorite"> </g> <g id="Hamburguer"> </g> <g id="Flying"> </g> <g id="Take_x5F_Off"> </g> <g id="Land"> </g> <g id="City"> </g> <g id="Nature"> </g> <g id="Pointer"> </g> <g id="Prize"> </g> <g id="Extract"> </g> <g id="Play"> </g> <g id="Pause"> </g> <g id="Stop"> </g> <g id="Forward"> </g> <g id="Reverse"> </g> <g id="Next_1_"> </g> <g id="Last_1_"> </g> <g id="Empty_x5F_Basket"> </g> <g id="Add_x5F_Basket"> </g> <g id="Delete_x5F_Basket"> </g> <g id="Error_x5F_Basket"> </g> <g id="OK_x5F_Basket"> </g> </g></svg>
|
<svg className="animate-spin flex flex-col items-center justify-center w-[45px] h-[45px] text-[#6d9e37] hover:text-white transition-colors" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Download_x5F_25_x25_"> </g> <g id="Download_x5F_50_x25_"> </g> <g id="Download_x5F_75_x25_"> </g> <g id="Download_x5F_100_x25_"> </g> <g id="Upload"> </g> <g id="Next"> </g> <g id="Last"> </g> <g id="OK"> </g> <g id="Fail"> </g> <g id="Add"> </g> <g id="Spinner_x5F_0_x25_"> </g> <g id="Spinner_x5F_25_x25_"> <g> <path fill="none" stroke="#6d9e37" stroke-width="4" stroke-linejoin="round" stroke-miterlimit="10" d="M50.188,26.812 c12.806,0,23.188,10.381,23.188,23.188"></path> <g> <circle cx="73.375" cy="50" r="1.959"></circle> <circle cx="72.029" cy="57.704" r="1.959"></circle> <circle cx="68.237" cy="64.579" r="1.959"></circle> <circle cx="62.324" cy="69.759" r="1.959"></circle> <circle cx="55.013" cy="72.699" r="1.959"></circle> </g> <g> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.0326 2.3121)" cx="27.045" cy="51.843" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.628 2.4912)" cx="28.998" cy="59.416" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.1348 2.8556)" cx="33.325" cy="65.967" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.4877 3.3713)" cx="39.63" cy="70.661" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.6506 3.9762)" cx="47.152" cy="73.012" rx="1.959" ry="1.959"></ellipse> </g> <g> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.713 2.567)" cx="27.71" cy="44.049" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.079 2.8158)" cx="30.892" cy="36.872" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.567 3.266)" cx="36.334" cy="31.199" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.2318 3.8617)" cx="43.363" cy="27.636" rx="1.959" ry="1.959"></ellipse> </g> </g> </g> <g id="Spinner_x5F_50_x25_"> </g> <g id="Spinner_x5F_75_x25_"> </g> <g id="Brightest_x5F_25_x25_"> </g> <g id="Brightest_x5F_50_x25_"> </g> <g id="Brightest_x5F_75_x25_"> </g> <g id="Brightest_x5F_100_x25_"> </g> <g id="Reload"> </g> <g id="Forbidden"> </g> <g id="Clock"> </g> <g id="Compass"> </g> <g id="World"> </g> <g id="Speed"> </g> <g id="Microphone"> </g> <g id="Options"> </g> <g id="Chronometer"> </g> <g id="Lock"> </g> <g id="User"> </g> <g id="Position"> </g> <g id="No_x5F_Signal"> </g> <g id="Low_x5F_Signal"> </g> <g id="Mid_x5F_Signal"> </g> <g id="High_x5F_Signal"> </g> <g id="Options_1_"> </g> <g id="Flash"> </g> <g id="No_x5F_Signal_x5F_02"> </g> <g id="Low_x5F_Signal_x5F_02"> </g> <g id="Mid_x5F_Signal_x5F_02"> </g> <g id="High_x5F_Signal_x5F_02"> </g> <g id="Favorite"> </g> <g id="Search"> </g> <g id="Stats_x5F_01"> </g> <g id="Stats_x5F_02"> </g> <g id="Turn_x5F_On_x5F_Off"> </g> <g id="Full_x5F_Height"> </g> <g id="Full_x5F_Width"> </g> <g id="Full_x5F_Screen"> </g> <g id="Compress_x5F_Screen"> </g> <g id="Chat"> </g> <g id="Bluetooth"> </g> <g id="Share_x5F_iOS"> </g> <g id="Share_x5F_Android"> </g> <g id="Love__x2F__Favorite"> </g> <g id="Hamburguer"> </g> <g id="Flying"> </g> <g id="Take_x5F_Off"> </g> <g id="Land"> </g> <g id="City"> </g> <g id="Nature"> </g> <g id="Pointer"> </g> <g id="Prize"> </g> <g id="Extract"> </g> <g id="Play"> </g> <g id="Pause"> </g> <g id="Stop"> </g> <g id="Forward"> </g> <g id="Reverse"> </g> <g id="Next_1_"> </g> <g id="Last_1_"> </g> <g id="Empty_x5F_Basket"> </g> <g id="Add_x5F_Basket"> </g> <g id="Delete_x5F_Basket"> </g> <g id="Error_x5F_Basket"> </g> <g id="OK_x5F_Basket"> </g> </g></svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { Toast } from './Toast';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||||
import { FileX, Copy, CheckCircle2, AlertCircle, X } from "lucide-react";
|
import { FileX, Copy, CheckCircle2, AlertCircle, X } from "lucide-react";
|
||||||
import Loader from "./ui/loader";
|
import Loader from "./ui/loader";
|
||||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
|
||||||
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
|
||||||
export default function MakePayment(){
|
export default function MakePayment(){
|
||||||
const [initialOrderData, setInitialOrderData] = useState(null);
|
const [initialOrderData, setInitialOrderData] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -36,7 +38,7 @@ export default function MakePayment(){
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('order_id', orderId);
|
formData.append('order_id', orderId);
|
||||||
|
|
||||||
fetch(`${API_URL}?query=get-initiated_payment`, {
|
fetch(`${PUBLIC_USER_API_URL}?query=get-initiated_payment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -154,7 +156,7 @@ export default function MakePayment(){
|
|||||||
|
|
||||||
const handleSaveUPIPayment = async () => {
|
const handleSaveUPIPayment = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}?query=save-upi-payment`, {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=save-upi-payment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ChevronUp, ChevronDown, Eye, Pencil, Trash2, Download, CircleArrowRight
|
|||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||||
export default function AllCustomerList() {
|
export default function AllCustomerList() {
|
||||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
const [usersData, setUsersData] = useState([]);
|
const [usersData, setUsersData] = useState([]);
|
||||||
const [dataLoading, setDataLoading] = useState(true);
|
const [dataLoading, setDataLoading] = useState(true);
|
||||||
@@ -28,7 +28,7 @@ export default function AllCustomerList() {
|
|||||||
const fetchUsersData = async () => {
|
const fetchUsersData = async () => {
|
||||||
setDataLoading(true);
|
setDataLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}?query=get-all-users`, {
|
const res = await fetch(`${PUBLIC_USER_API_URL}?query=get-all-users`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -76,7 +76,7 @@ export default function AllCustomerList() {
|
|||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}${currentUser.id}`, {
|
const res = await fetch(`${PUBLIC_USER_API_URL}${currentUser.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -96,7 +96,7 @@ export default function AllCustomerList() {
|
|||||||
|
|
||||||
const saveUserChanges = async (updatedUser) => {
|
const saveUserChanges = async (updatedUser) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}${updatedUser.id}`, {
|
const res = await fetch(`${PUBLIC_USER_API_URL}${updatedUser.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function AllSellingList() {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage] = useState(10);
|
const [itemsPerPage] = useState(10);
|
||||||
|
|
||||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn && sessionData?.user_type === 'admin') {
|
if (isLoggedIn && sessionData?.user_type === 'admin') {
|
||||||
@@ -49,7 +49,7 @@ export default function AllSellingList() {
|
|||||||
const fetchBillingData = async () => {
|
const fetchBillingData = async () => {
|
||||||
try {
|
try {
|
||||||
setDataLoading(true);
|
setDataLoading(true);
|
||||||
const res = await fetch(`${INVOICE_API_URL}?query=all-selling-list`, {
|
const res = await fetch(`${PUBLIC_USER_API_URL}?query=all-selling-list`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -69,7 +69,7 @@ export default function AllSellingList() {
|
|||||||
|
|
||||||
const fetchUsersData = async () => {
|
const fetchUsersData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${INVOICE_API_URL}?query=get-all-users`, {
|
const res = await fetch(`${PUBLIC_USER_API_URL}?query=get-all-users`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -122,7 +122,7 @@ export default function AllSellingList() {
|
|||||||
if (!window.confirm('Are you sure you want to delete this billing record?')) return;
|
if (!window.confirm('Are you sure you want to delete this billing record?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${INVOICE_API_URL}?query=delete-billing&billingId=${billingId}&serviceType=${serviceType}`, {
|
const res = await fetch(`${PUBLIC_USER_API_URL}?query=delete-billing&billingId=${billingId}&serviceType=${serviceType}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -150,7 +150,7 @@ export default function AllSellingList() {
|
|||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const url = selectedItem ? `${INVOICE_API_URL}?query=update-billing&id=${selectedItem.id}` : `${INVOICE_API_URL}?query=create-billing`;
|
const url = selectedItem ? `${PUBLIC_USER_API_URL}?query=update-billing&id=${selectedItem.id}` : `${PUBLIC_USER_API_URL}?query=create-billing`;
|
||||||
|
|
||||||
const method = selectedItem ? 'PUT' : 'POST';
|
const method = selectedItem ? 'PUT' : 'POST';
|
||||||
|
|
||||||
@@ -774,7 +774,7 @@ export default function AllSellingList() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">Remarks</label>
|
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">Remarks</label>
|
||||||
<input type="text" name="remarks" value={formData.remarks} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" />
|
<textarea name="remarks" value={formData.remarks} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ export default function HetznerServicesList() {
|
|||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const HETZNER_API_KEY = "uMSBR9nxdtbuvazsVM8YMMDd0PvuynpgJbmzFIO47HblMlh7tlHT8wV05sQ28Squ"; // Replace with your actual API key
|
const PUBLIC_HETZNER_API_KEY = import.meta.env.PUBLIC_HETZNER_API_KEY;
|
||||||
|
|
||||||
// Define fetchServers outside useEffect so it can be reused
|
// Define fetchServers outside useEffect so it can be reused
|
||||||
const fetchServers = async () => {
|
const fetchServers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
|
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
|
||||||
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
|
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setServers(data.servers || []);
|
setServers(data.servers || []);
|
||||||
@@ -42,7 +42,7 @@ export default function HetznerServicesList() {
|
|||||||
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}/actions/${action}`, {
|
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}/actions/${action}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${HETZNER_API_KEY}`,
|
"Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -77,7 +77,7 @@ export default function HetznerServicesList() {
|
|||||||
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}`, {
|
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${HETZNER_API_KEY}`,
|
"Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ export default function ViewAsUser() {
|
|||||||
const [dataError, setDataError] = useState(null);
|
const [dataError, setDataError] = useState(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [selectedInvoice, setSelectedInvoice] = useState(null);
|
const [selectedInvoice, setSelectedInvoice] = useState(null);
|
||||||
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
|
||||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const id = urlParams.get('siliconId');
|
const id = urlParams.get('siliconId');
|
||||||
@@ -36,7 +35,7 @@ export default function ViewAsUser() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${USER_API_URL}?query=login-from-admin&siliconId=${id}`,
|
`${PUBLIC_USER_API_URL}?query=login-from-admin&siliconId=${id}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Loader from "./ui/loader";
|
|||||||
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
||||||
|
|
||||||
export default function TopicCreation(props) {
|
export default function TopicCreation(props) {
|
||||||
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
||||||
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
|
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
|
||||||
const [topics, setTopics] = useState([]);
|
const [topics, setTopics] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -13,7 +14,7 @@ export default function TopicCreation(props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTopics = async () => {
|
const fetchTopics = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=my-topics', {
|
const res = await fetch(`${PUBLIC_TOPIC_API_URL}?query=my-topics`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
485
src/components/NewTopic copy.jsx
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Label } from './ui/label';
|
||||||
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||||
|
import { CustomTabs } from './ui/tabs';
|
||||||
|
|
||||||
|
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
||||||
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
||||||
|
|
||||||
|
const NewTopic = () => {
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({ status: 'draft', category: '', title: '', content: '', imageUrl: '' });
|
||||||
|
// UI state
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [imageFile, setImageFile] = useState(null);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
||||||
|
const [imageUrlInput, setImageUrlInput] = useState('');
|
||||||
|
const [imageUploadFile, setImageUploadFile] = useState(null);
|
||||||
|
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||||
|
const [editorMode, setEditorMode] = useState('edit');
|
||||||
|
|
||||||
|
// Upload file to MinIO
|
||||||
|
const uploadToMinIO = async (file, onProgress) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data)
|
||||||
|
return data.url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// // Generate slug from title
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (formData.title) {
|
||||||
|
// const slug = formData.title
|
||||||
|
// .toLowerCase()
|
||||||
|
// .replace(/[^\w\s]/g, '')
|
||||||
|
// .replace(/\s+/g, '-');
|
||||||
|
// setFormData(prev => ({ ...prev, slug }));
|
||||||
|
// }
|
||||||
|
// }, [formData.title]);
|
||||||
|
|
||||||
|
const customImageCommand = {
|
||||||
|
name: 'image',
|
||||||
|
keyCommand: 'image',
|
||||||
|
buttonProps: { 'aria-label': 'Insert image' },
|
||||||
|
icon: (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
execute: () => {
|
||||||
|
setImageDialogOpen(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all default commands and replace the image command
|
||||||
|
const allCommands = commands.getCommands().map(cmd => {
|
||||||
|
if (cmd.name === 'image') {
|
||||||
|
return customImageCommand;
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Handle image URL insertion
|
||||||
|
const handleInsertImageUrl = () => {
|
||||||
|
if (imageUrlInput) {
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
|
if (textarea) {
|
||||||
|
const startPos = textarea.selectionStart;
|
||||||
|
const endPos = textarea.selectionEnd;
|
||||||
|
const currentValue = formData.content;
|
||||||
|
|
||||||
|
const newValue =
|
||||||
|
currentValue.substring(0, startPos) +
|
||||||
|
imgMarkdown +
|
||||||
|
currentValue.substring(endPos);
|
||||||
|
|
||||||
|
setFormData(prev => ({ ...prev, content: newValue }));
|
||||||
|
}
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUrlInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image file selection
|
||||||
|
const handleImageFileSelect = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setImageUploadFile(file);
|
||||||
|
|
||||||
|
// Create preview
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setImageUploadPreview(reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload image file to MinIO and insert into editor
|
||||||
|
const handleImageUpload = async () => {
|
||||||
|
if (!imageUploadFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
|
||||||
|
setUploadProgress(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert markdown for the uploaded image
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
const textarea = document.querySelector('.w-md-editor-text-input');
|
||||||
|
if (textarea) {
|
||||||
|
const startPos = textarea.selectionStart;
|
||||||
|
const endPos = textarea.selectionEnd;
|
||||||
|
const currentValue = formData.content;
|
||||||
|
|
||||||
|
const newValue =
|
||||||
|
currentValue.substring(0, startPos) +
|
||||||
|
imgMarkdown +
|
||||||
|
currentValue.substring(endPos);
|
||||||
|
|
||||||
|
setFormData(prev => ({ ...prev, content: newValue }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUploadFile(null);
|
||||||
|
setImageUploadPreview('');
|
||||||
|
} catch (error) {
|
||||||
|
setError('Failed to upload image: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form submission
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upload featured image if selected
|
||||||
|
let imageUrl = formData.imageUrl;
|
||||||
|
if (imageFile) {
|
||||||
|
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
||||||
|
setUploadProgress(progress);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
imageUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit to API
|
||||||
|
const response = await fetch(`${PUBLIC_TOPIC_API_URL}?query=create-new-topic`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || 'Failed to create topic');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setUploadProgress(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle form field changes
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle featured image file selection
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setImageFile(file);
|
||||||
|
// Preview image
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
status: 'draft',
|
||||||
|
category: '',
|
||||||
|
title: '',
|
||||||
|
slug: '',
|
||||||
|
content: '',
|
||||||
|
imageUrl: ''
|
||||||
|
});
|
||||||
|
setImageFile(null);
|
||||||
|
setSuccess(false);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Success state
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 text-center py-8">
|
||||||
|
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic created successfully!</h3>
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<Button onClick={resetForm}>Create Another</Button>
|
||||||
|
<Button variant="outline" onClick={() => window.location.href = '/'}>Return Home</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
|
||||||
|
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Create New Topic</h2>
|
||||||
|
<p className="text-gray-600 mb-6">Start a new discussion in the SiliconPin community</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="status">Status</Label>
|
||||||
|
<Select
|
||||||
|
name="status"
|
||||||
|
required
|
||||||
|
value={formData.status}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="draft">Draft</SelectItem>
|
||||||
|
<SelectItem value="published">Published</SelectItem>
|
||||||
|
<SelectItem value="archived">Archived</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="category">Category *</Label>
|
||||||
|
<Select
|
||||||
|
name="category"
|
||||||
|
required
|
||||||
|
value={formData.category}
|
||||||
|
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
|
||||||
|
<SelectValue placeholder="Select a category" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent position="popper" className="z-50">
|
||||||
|
<SelectItem value="php">PHP Hosting</SelectItem>
|
||||||
|
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title and Slug */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Title *</Label>
|
||||||
|
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<Label htmlFor="slug">URL Slug</Label>
|
||||||
|
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
|
||||||
|
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Content Editor with Preview Toggle */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label htmlFor="content">Content *</Label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
|
||||||
|
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
|
||||||
|
>
|
||||||
|
{editorMode === 'edit' ? 'Preview' : 'Edit'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div data-color-mode="light">
|
||||||
|
<MDEditor
|
||||||
|
placeholder="Write your content"
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
|
||||||
|
height={400}
|
||||||
|
preview={editorMode}
|
||||||
|
commands={allCommands}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image Upload Dialog */}
|
||||||
|
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Insert Image</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<CustomTabs
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
label: "From URL",
|
||||||
|
value: "url",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter image URL"
|
||||||
|
value={imageUrlInput}
|
||||||
|
onChange={(e) => setImageUrlInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleInsertImageUrl}
|
||||||
|
disabled={!imageUrlInput}
|
||||||
|
>
|
||||||
|
Insert Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Upload",
|
||||||
|
value: "upload",
|
||||||
|
content: (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageFileSelect}
|
||||||
|
/>
|
||||||
|
{imageUploadPreview && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={imageUploadPreview}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-40 rounded-md border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2.5 rounded-full"
|
||||||
|
style={{ width: `${uploadProgress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleImageUpload}
|
||||||
|
disabled={!imageUploadFile || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Featured Image Upload */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image">Featured Image</Label>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
id="image"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2.5 rounded-full"
|
||||||
|
style={{ width: `${uploadProgress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formData.imageUrl && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={formData.imageUrl}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-40 rounded-md border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Form Actions */}
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={resetForm}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="min-w-32"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span className="animate-spin">↻</span>
|
||||||
|
Creating...
|
||||||
|
</span>
|
||||||
|
) : 'Create Topic'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-600 rounded-md">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewTopic;
|
||||||
@@ -8,8 +8,8 @@ import { Separator } from './ui/separator';
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||||
import { CustomTabs } from './ui/tabs';
|
import { CustomTabs } from './ui/tabs';
|
||||||
|
|
||||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
||||||
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
||||||
|
|
||||||
const NewTopic = () => {
|
const NewTopic = () => {
|
||||||
|
|
||||||
@@ -31,14 +31,13 @@ const NewTopic = () => {
|
|||||||
const uploadToMinIO = async (file, onProgress) => {
|
const uploadToMinIO = async (file, onProgress) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('bucket', 'siliconpin-uploads');
|
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
|
||||||
formData.append('folder', 'topic-images');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(MINIO_UPLOAD_URL, {
|
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include',
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -46,7 +45,8 @@ const NewTopic = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.url;
|
console.log(data.publicUrl)
|
||||||
|
return data.publicUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -85,7 +85,7 @@ const NewTopic = () => {
|
|||||||
}
|
}
|
||||||
return cmd;
|
return cmd;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Handle image URL insertion
|
// Handle image URL insertion
|
||||||
const handleInsertImageUrl = () => {
|
const handleInsertImageUrl = () => {
|
||||||
@@ -127,7 +127,7 @@ const NewTopic = () => {
|
|||||||
// Upload image file to MinIO and insert into editor
|
// Upload image file to MinIO and insert into editor
|
||||||
const handleImageUpload = async () => {
|
const handleImageUpload = async () => {
|
||||||
if (!imageUploadFile) return;
|
if (!imageUploadFile) return;
|
||||||
|
console.log(imageUploadFile)
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
@@ -174,10 +174,11 @@ const NewTopic = () => {
|
|||||||
let imageUrl = formData.imageUrl;
|
let imageUrl = formData.imageUrl;
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
imageUrl = await uploadToMinIO(imageFile, (progress) => {
|
||||||
|
console.log('imageUrl', imageUrl)
|
||||||
setUploadProgress(progress);
|
setUploadProgress(progress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// let finalImageUrl = imageUrl.publicUrl;
|
||||||
// Prepare payload
|
// Prepare payload
|
||||||
const payload = {
|
const payload = {
|
||||||
...formData,
|
...formData,
|
||||||
@@ -185,7 +186,7 @@ const NewTopic = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Submit to API
|
// Submit to API
|
||||||
const response = await fetch(`${TOPIC_API_URL}?query=create-new-topic`, {
|
const response = await fetch(`${PUBLIC_TOPIC_API_URL}?query=create-new-topic`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -328,14 +329,14 @@ const NewTopic = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div data-color-mode="light">
|
<div data-color-mode="light">
|
||||||
<MDEditor
|
<MDEditor
|
||||||
placeholder="Write your content"
|
placeholder="Write your content"
|
||||||
value={formData.content}
|
value={formData.content}
|
||||||
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
|
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
|
||||||
height={400}
|
height={400}
|
||||||
preview={editorMode}
|
preview={editorMode}
|
||||||
commands={allCommands}
|
commands={allCommands}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/
|
|||||||
import { Eye, EyeOff, Loader2 } from "lucide-react";
|
import { Eye, EyeOff, Loader2 } from "lucide-react";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
// import type { RecordModel } from 'pocketbase';
|
// import type { RecordModel } from 'pocketbase';
|
||||||
const pb = new PocketBase('https://tst-pb.s38.siliconpin.com');
|
const PUBLIC_POCKETBASE_URL = import.meta.env.PUBLIC_POCKETBASE_URL;
|
||||||
|
const pb = new PocketBase(PUBLIC_POCKETBASE_URL);
|
||||||
|
|
||||||
interface RecordModel {
|
interface RecordModel {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { FileDown } from "lucide-react";
|
import { FileDown } from "lucide-react";
|
||||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
@@ -11,9 +12,8 @@ export default function HestiaCredentialsFetcher() {
|
|||||||
const [serviceName, setServiceName] = useState(null);
|
const [serviceName, setServiceName] = useState(null);
|
||||||
const [serviceOrderId, setServiceOrderId] = useState(null);
|
const [serviceOrderId, setServiceOrderId] = useState(null);
|
||||||
const [userSiliconId, setUserSiliconId] = useState();
|
const [userSiliconId, setUserSiliconId] = useState();
|
||||||
|
const PUBLIC_SERVICE_API_URL = import.meta.env.PUBLIC_SERVICE_API_URL;
|
||||||
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
|
const PUBLIC_HOST_API2_USERS = import.meta.env.PUBLIC_HOST_API2_USERS;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -32,7 +32,7 @@ export default function HestiaCredentialsFetcher() {
|
|||||||
}, [sessionData]);
|
}, [sessionData]);
|
||||||
|
|
||||||
const handleSaveDataInMongoDB = async (servicesData, currentBillingId, serviceToEndpoint, siliconId) => {
|
const handleSaveDataInMongoDB = async (servicesData, currentBillingId, serviceToEndpoint, siliconId) => {
|
||||||
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? hostings : '';
|
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? 'hostings' : '';
|
||||||
try {
|
try {
|
||||||
const serviceDataPayload = {
|
const serviceDataPayload = {
|
||||||
success: servicesData.success,
|
success: servicesData.success,
|
||||||
@@ -46,8 +46,8 @@ export default function HestiaCredentialsFetcher() {
|
|||||||
billingId: currentBillingId,
|
billingId: currentBillingId,
|
||||||
status: "active"
|
status: "active"
|
||||||
}
|
}
|
||||||
console.log('serviceDataPayload', serviceDataPayload)
|
// console.log('serviceDataPayload', serviceDataPayload)
|
||||||
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users/${siliconId}/${query}`, {
|
const response = await fetch(`${PUBLIC_HOST_API2_USERS}/${siliconId}/${query}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -73,7 +73,7 @@ export default function HestiaCredentialsFetcher() {
|
|||||||
// Use the passed orderIdParam or fall back to state
|
// Use the passed orderIdParam or fall back to state
|
||||||
const effectiveOrderId = orderIdParam || serviceOrderId;
|
const effectiveOrderId = orderIdParam || serviceOrderId;
|
||||||
const query = serviceToCall === 'vpn' ? 'get-vpn-cred' : 'get-hestia-cred';
|
const query = serviceToCall === 'vpn' ? 'get-vpn-cred' : 'get-hestia-cred';
|
||||||
const response = await fetch(`${SERVICES_API_URL}?query=${query}&orderId=${effectiveOrderId}`, {
|
const response = await fetch(`${PUBLIC_SERVICE_API_URL}?query=${query}&orderId=${effectiveOrderId}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
@@ -108,7 +108,7 @@ export default function HestiaCredentialsFetcher() {
|
|||||||
element.click();
|
element.click();
|
||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
};
|
};
|
||||||
|
console.log('serviceName', serviceName)
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ export default function HestiaCredentialsFetcher() {
|
|||||||
|
|
||||||
{error && <p className="text-red-600 mt-2">{error}</p>}
|
{error && <p className="text-red-600 mt-2">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
) : creds && creds.service === 'hestia' && (
|
) : creds && creds.service === 'hestia' ? (
|
||||||
<div className="p-4 bg-gray-100 rounded shadow max-w-md mx-auto mt-10">
|
<div className="p-4 bg-gray-100 rounded shadow max-w-md mx-auto mt-10">
|
||||||
<h2 className="text-xl font-bold mb-4">Hestia Credentials</h2>
|
<h2 className="text-xl font-bold mb-4">Hestia Credentials</h2>
|
||||||
|
|
||||||
@@ -186,7 +186,16 @@ export default function HestiaCredentialsFetcher() {
|
|||||||
|
|
||||||
{error && <p className="text-red-600 mt-2">{error}</p>}
|
{error && <p className="text-red-600 mt-2">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : serviceName === 'streaming_api' ? (
|
||||||
|
<Card className='max-w-2xl mx-auto px-4 mt-8'>
|
||||||
|
<CardContent>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>STT Streaming API Purchase Succesfully</CardTitle>
|
||||||
|
<CardDescription>You can See Info on my Billing Section in Profile</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,24 @@ import React, { useState } from 'react';
|
|||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
import Loader from "./ui/loader";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||||
|
|
||||||
export interface ServiceCardProps {
|
export interface ServiceCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
features: string[];
|
features: [];
|
||||||
learnMoreUrl: string;
|
learnMoreUrl: string;
|
||||||
buyButtonText: string;
|
buyButtonText: string;
|
||||||
buyButtonUrl: string;
|
buyButtonUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl, buyButtonText, buyButtonUrl }: ServiceCardProps) {
|
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl, buyButtonText, buyButtonUrl }: ServiceCardProps) {
|
||||||
|
// console.log('checkType', typeof features)
|
||||||
|
// console.log('checkType', features)
|
||||||
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
|
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
|
||||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
|
const parsedFeatures = typeof features === 'string' ? JSON.parse(features) : features;
|
||||||
|
|
||||||
const handleBuyClick = () => {
|
const handleBuyClick = () => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
@@ -25,6 +27,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.location.href = buyButtonUrl;
|
window.location.href = buyButtonUrl;
|
||||||
|
console.log(buyButtonUrl)
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,16 +42,20 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
|||||||
</div>
|
</div>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
|
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
|
||||||
<CardDescription className="text-neutral-400">{description}</CardDescription>
|
<CardDescription className="text-neutral-400 text-justify">{description}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-grow">
|
<CardContent className="flex-grow">
|
||||||
<ul className="space-y-2 text-sm">
|
<ul className="space-y-2 text-sm">
|
||||||
{features.map((feature, index) => (
|
{
|
||||||
<li key={index} className="flex items-start">
|
parsedFeatures && (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
parsedFeatures.map((feature : string, index: number) => (
|
||||||
<span className='text-zinc-600'>{feature}</span>
|
<li key={index} className="flex items-start">
|
||||||
</li>
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
||||||
))}
|
<span className='text-zinc-600'>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
@@ -60,7 +67,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
|||||||
href={learnMoreUrl}
|
href={learnMoreUrl}
|
||||||
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
|
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
|
||||||
>
|
>
|
||||||
Learn More
|
Details
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ const SignupPage = () => {
|
|||||||
const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
|
const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
|
||||||
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
|
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
const PUBLIC_POCKETBASE_URL = import.meta.env.PUBLIC_POCKETBASE_URL;
|
||||||
|
|
||||||
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
const pb = new PocketBase(PUBLIC_POCKETBASE_URL);
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -135,7 +137,7 @@ const SignupPage = () => {
|
|||||||
|
|
||||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ interface Message {
|
|||||||
user_type: string;
|
user_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/ticket/index.php';
|
const PUBLIC_TICKET_API_URL = import.meta.env.PUBLIC_TICKET_API_URL;
|
||||||
|
|
||||||
function Ticketing() {
|
function Ticketing() {
|
||||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||||
@@ -47,7 +47,7 @@ function Ticketing() {
|
|||||||
|
|
||||||
const fetchTickets = async () => {
|
const fetchTickets = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}?action=get_tickets&status=${activeTab}`);
|
const response = await fetch(`${PUBLIC_TICKET_API_URL}?action=get_tickets&status=${activeTab}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setTickets(data.tickets);
|
setTickets(data.tickets);
|
||||||
@@ -59,7 +59,7 @@ function Ticketing() {
|
|||||||
|
|
||||||
const fetchMessages = async (ticketId: number) => {
|
const fetchMessages = async (ticketId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}?action=get_messages&ticket_id=${ticketId}`);
|
const response = await fetch(`${PUBLIC_TICKET_API_URL}?action=get_messages&ticket_id=${ticketId}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Messages response:', data); // Debug log
|
console.log('Messages response:', data); // Debug log
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -78,7 +78,7 @@ function Ticketing() {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_URL, {
|
const response = await fetch(PUBLIC_TICKET_API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -99,7 +99,7 @@ function Ticketing() {
|
|||||||
|
|
||||||
const handleStatusChange = async (ticketId: number, newStatus: 'open' | 'in-progress' | 'resolved' | 'closed') => {
|
const handleStatusChange = async (ticketId: number, newStatus: 'open' | 'in-progress' | 'resolved' | 'closed') => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_URL, {
|
const response = await fetch(PUBLIC_TICKET_API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -124,7 +124,7 @@ function Ticketing() {
|
|||||||
if (!selectedTicket || !newMessage.trim()) return;
|
if (!selectedTicket || !newMessage.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_URL, {
|
const response = await fetch(PUBLIC_TICKET_API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -146,7 +146,7 @@ function Ticketing() {
|
|||||||
|
|
||||||
const handleDeleteTicket = async (ticketId: number) => {
|
const handleDeleteTicket = async (ticketId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_URL, {
|
const response = await fetch(PUBLIC_TICKET_API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
773
src/components/Tools/AudioToText.jsx
Normal file
@@ -0,0 +1,773 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
|
||||||
|
const API_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: 'whisper',
|
||||||
|
name: 'Whisper (GPU)',
|
||||||
|
endpoint: 'https://stt-41.siliconpin.com/stt',
|
||||||
|
description: '2 Req / Min, 10 / Day is free'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vosk',
|
||||||
|
name: 'Vosk (CPU)',
|
||||||
|
endpoint: 'https://api.vosk.ai/stt',
|
||||||
|
description: '10 Req / Min, 100 / Day is free'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE_MB = 5;
|
||||||
|
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||||
|
|
||||||
|
export default function AudioUploader() {
|
||||||
|
// State management
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [status, setStatus] = useState('No file uploaded yet');
|
||||||
|
const [response, setResponse] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [selectedApi, setSelectedApi] = useState('whisper');
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [debugLogs, setDebugLogs] = useState([]);
|
||||||
|
const [recordingTime, setRecordingTime] = useState(0);
|
||||||
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
|
const [audioBlob, setAudioBlob] = useState(null);
|
||||||
|
const [audioUrl, setAudioUrl] = useState(null);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
const mediaRecorderRef = useRef(null);
|
||||||
|
const audioChunksRef = useRef([]);
|
||||||
|
const timerRef = useRef(null);
|
||||||
|
const audioContextRef = useRef(null);
|
||||||
|
const analyserRef = useRef(null);
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
const animationRef = useRef(null);
|
||||||
|
const streamRef = useRef(null);
|
||||||
|
|
||||||
|
// Debug logging with useCallback to prevent infinite loops
|
||||||
|
const addDebugLog = useCallback((message) => {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
||||||
|
const logMessage = `${timestamp}: ${message}`;
|
||||||
|
setDebugLogs(prev => [...prev.slice(-100), logMessage]);
|
||||||
|
console.debug(logMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Timer effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRecording) {
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
setRecordingTime(prev => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
}, [isRecording]);
|
||||||
|
|
||||||
|
// Clean up on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
addDebugLog('Component unmounting - cleaning up resources');
|
||||||
|
stopRecording();
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current);
|
||||||
|
}
|
||||||
|
if (audioContextRef.current?.state !== 'closed') {
|
||||||
|
audioContextRef.current?.close().catch(err => {
|
||||||
|
addDebugLog(`Error closing AudioContext: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
};
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
// Handle file change - completely stable implementation
|
||||||
|
const handleFileChange = useCallback((e) => {
|
||||||
|
const selectedFile = e.target.files[0];
|
||||||
|
if (!selectedFile) {
|
||||||
|
setFile(null);
|
||||||
|
setStatus('No file selected');
|
||||||
|
addDebugLog('No file selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/webm'].includes(selectedFile.type) &&
|
||||||
|
!selectedFile.name.match(/\.(wav|mp3|ogg|webm)$/i)) {
|
||||||
|
const errorMsg = 'Unsupported file format. Please use WAV, MP3, or OGG';
|
||||||
|
setError(errorMsg);
|
||||||
|
setStatus('Invalid file type');
|
||||||
|
setFile(null);
|
||||||
|
e.target.value = '';
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFile.size > MAX_FILE_SIZE_BYTES) {
|
||||||
|
const errorMsg = `File size exceeds ${MAX_FILE_SIZE_MB}MB limit`;
|
||||||
|
setError(errorMsg);
|
||||||
|
setStatus('File too large');
|
||||||
|
setFile(null);
|
||||||
|
e.target.value = '';
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFile(selectedFile);
|
||||||
|
setStatus(`File selected: ${selectedFile.name}`);
|
||||||
|
setResponse(null);
|
||||||
|
setError(null);
|
||||||
|
setAudioBlob(null);
|
||||||
|
setAudioUrl(null);
|
||||||
|
addDebugLog(`File selected: ${selectedFile.name} (${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB)`);
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
// Create WAV blob - stable implementation
|
||||||
|
const createWavBlob = useCallback(async (audioBlob) => {
|
||||||
|
try {
|
||||||
|
addDebugLog('Starting WAV blob creation');
|
||||||
|
const arrayBuffer = await audioBlob.arrayBuffer();
|
||||||
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: 16000
|
||||||
|
});
|
||||||
|
|
||||||
|
const decodedData = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
|
addDebugLog(`Decoded audio data: ${decodedData.length} samples, ${decodedData.numberOfChannels} channels`);
|
||||||
|
|
||||||
|
let audioData;
|
||||||
|
if (decodedData.numberOfChannels > 1) {
|
||||||
|
audioData = new Float32Array(decodedData.length);
|
||||||
|
for (let i = 0; i < decodedData.length; i++) {
|
||||||
|
audioData[i] = (decodedData.getChannelData(0)[i] + decodedData.getChannelData(1)[i]) / 2;
|
||||||
|
}
|
||||||
|
addDebugLog('Converted stereo to mono');
|
||||||
|
} else {
|
||||||
|
audioData = decodedData.getChannelData(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pcmData = new Int16Array(audioData.length);
|
||||||
|
for (let i = 0; i < audioData.length; i++) {
|
||||||
|
const s = Math.max(-1, Math.min(1, audioData[i]));
|
||||||
|
pcmData[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
||||||
|
}
|
||||||
|
addDebugLog(`Converted to 16-bit PCM: ${pcmData.length} samples`);
|
||||||
|
|
||||||
|
const wavHeader = createWaveHeader(pcmData.length * 2, {
|
||||||
|
sampleRate: 16000,
|
||||||
|
numChannels: 1,
|
||||||
|
bitDepth: 16
|
||||||
|
});
|
||||||
|
|
||||||
|
const wavBlob = new Blob([wavHeader, pcmData], { type: 'audio/wav' });
|
||||||
|
addDebugLog(`Created WAV blob: ${(wavBlob.size / 1024).toFixed(2)} KB`);
|
||||||
|
return wavBlob;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = `Error creating WAV blob: ${err.message}`;
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
throw new Error('Failed to process audio recording');
|
||||||
|
}
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
// Handle submit - stable implementation
|
||||||
|
const handleSubmit = useCallback(async () => {
|
||||||
|
let fileToSubmit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const apiName = API_OPTIONS.find(api => api.id === selectedApi)?.name;
|
||||||
|
setStatus(`Processing with ${apiName}...`);
|
||||||
|
setError(null);
|
||||||
|
addDebugLog(`Starting submission with ${apiName}`);
|
||||||
|
|
||||||
|
if (audioBlob) {
|
||||||
|
addDebugLog('Processing recorded audio blob');
|
||||||
|
fileToSubmit = await createWavBlob(audioBlob);
|
||||||
|
} else if (file) {
|
||||||
|
addDebugLog('Processing uploaded file');
|
||||||
|
fileToSubmit = file;
|
||||||
|
} else {
|
||||||
|
const errorMsg = 'No audio file selected';
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileToSubmit.size > MAX_FILE_SIZE_BYTES) {
|
||||||
|
const errorMsg = `File size exceeds ${MAX_FILE_SIZE_MB}MB limit`;
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', fileToSubmit, 'audio.wav');
|
||||||
|
addDebugLog(`Created FormData with ${(fileToSubmit.size / 1024).toFixed(2)} KB file`);
|
||||||
|
|
||||||
|
const apiConfig = API_OPTIONS.find(api => api.id === selectedApi);
|
||||||
|
if (!apiConfig) {
|
||||||
|
const errorMsg = 'Selected API not found';
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
addDebugLog(`Sending request to ${apiConfig.endpoint}`);
|
||||||
|
const apiResponse = await fetch(apiConfig.endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
let errorMessage = `API returned error status: ${apiResponse.status}`;
|
||||||
|
try {
|
||||||
|
const errorData = await apiResponse.json();
|
||||||
|
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||||
|
} catch (e) {
|
||||||
|
addDebugLog('Failed to parse error response');
|
||||||
|
}
|
||||||
|
addDebugLog(`API error: ${errorMessage}`);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiResponse.json();
|
||||||
|
addDebugLog('Received successful response from API');
|
||||||
|
setResponse({
|
||||||
|
api: selectedApi,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
setStatus('Processing complete');
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err.message.includes('Failed to fetch')
|
||||||
|
? 'Network error: Could not connect to the API server'
|
||||||
|
: err.message;
|
||||||
|
addDebugLog(`Error during submission: ${errorMsg}`);
|
||||||
|
setError(errorMsg);
|
||||||
|
setStatus('Processing failed');
|
||||||
|
setResponse(null);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
addDebugLog('Submission process completed');
|
||||||
|
}
|
||||||
|
}, [selectedApi, audioBlob, file, createWavBlob, addDebugLog]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const createWaveHeader = useCallback((dataLength, config) => {
|
||||||
|
const byteRate = config.sampleRate * config.numChannels * (config.bitDepth / 8);
|
||||||
|
const blockAlign = config.numChannels * (config.bitDepth / 8);
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(44);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
writeString(view, 0, 'RIFF');
|
||||||
|
view.setUint32(4, 36 + dataLength, true);
|
||||||
|
writeString(view, 8, 'WAVE');
|
||||||
|
writeString(view, 12, 'fmt ');
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
view.setUint16(22, config.numChannels, true);
|
||||||
|
view.setUint32(24, config.sampleRate, true);
|
||||||
|
view.setUint32(28, byteRate, true);
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
view.setUint16(34, config.bitDepth, true);
|
||||||
|
writeString(view, 36, 'data');
|
||||||
|
view.setUint32(40, dataLength, true);
|
||||||
|
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const writeString = useCallback((view, offset, string) => {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback((text) => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
addDebugLog('Text copied to clipboard');
|
||||||
|
}).catch(err => {
|
||||||
|
const errorMsg = 'Failed to copy text to clipboard';
|
||||||
|
addDebugLog(`${errorMsg}: ${err.message}`);
|
||||||
|
setError(errorMsg);
|
||||||
|
});
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
// Fixed getDisplayText to prevent infinite loops
|
||||||
|
const displayText = useMemo(() => {
|
||||||
|
if (!response?.data) {
|
||||||
|
addDebugLog('No response data to display');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof response.data === 'string') {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
if (response.data.text) {
|
||||||
|
return response.data.text;
|
||||||
|
}
|
||||||
|
if (response.data.transcript) {
|
||||||
|
return response.data.transcript;
|
||||||
|
}
|
||||||
|
if (response.data.results?.[0]?.alternatives?.[0]?.transcript) {
|
||||||
|
return response.data.results[0].alternatives[0].transcript;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Received response but couldn't extract text. View full response for details.";
|
||||||
|
}, [response, addDebugLog]);
|
||||||
|
|
||||||
|
// Recording functions
|
||||||
|
const startRecording = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
addDebugLog('Attempting to start recording');
|
||||||
|
setStatus("Requesting microphone access...");
|
||||||
|
|
||||||
|
if (audioBlob) {
|
||||||
|
addDebugLog('Clearing previous recording');
|
||||||
|
setAudioBlob(null);
|
||||||
|
setAudioUrl(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize audio context
|
||||||
|
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: 16000
|
||||||
|
});
|
||||||
|
addDebugLog(`AudioContext created with sample rate: ${audioContextRef.current.sampleRate}`);
|
||||||
|
|
||||||
|
// Get user media
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
streamRef.current = stream;
|
||||||
|
addDebugLog('Microphone access granted, stream created');
|
||||||
|
|
||||||
|
// Setup visualization
|
||||||
|
setupVisualizer(stream);
|
||||||
|
|
||||||
|
// Initialize MediaRecorder
|
||||||
|
mediaRecorderRef.current = new MediaRecorder(stream);
|
||||||
|
audioChunksRef.current = [];
|
||||||
|
|
||||||
|
mediaRecorderRef.current.ondataavailable = (e) => {
|
||||||
|
audioChunksRef.current.push(e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorderRef.current.onstop = () => {
|
||||||
|
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
||||||
|
setAudioBlob(audioBlob);
|
||||||
|
setAudioUrl(URL.createObjectURL(audioBlob));
|
||||||
|
setStatus("Recording stopped. Ready to process.");
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorderRef.current.start(100); // Collect data every 100ms
|
||||||
|
setIsRecording(true);
|
||||||
|
setStatus("Recording (16kHz, 16-bit mono)...");
|
||||||
|
addDebugLog('Recording started');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = `Error starting recording: ${err.message}`;
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
setError(errorMsg);
|
||||||
|
setStatus("Recording failed");
|
||||||
|
setIsRecording(false);
|
||||||
|
}
|
||||||
|
}, [audioBlob, addDebugLog]);
|
||||||
|
|
||||||
|
const stopRecording = useCallback(() => {
|
||||||
|
addDebugLog('Stop recording initiated');
|
||||||
|
if (!isRecording) {
|
||||||
|
addDebugLog('Not currently recording, ignoring stop request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsRecording(false);
|
||||||
|
addDebugLog('Recording state updated to false');
|
||||||
|
|
||||||
|
if (mediaRecorderRef.current?.state === 'recording') {
|
||||||
|
mediaRecorderRef.current.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamRef.current) {
|
||||||
|
streamRef.current.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
addDebugLog(`Stopped track: ${track.kind}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current);
|
||||||
|
animationRef.current = null;
|
||||||
|
addDebugLog('Visualization animation stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
addDebugLog('Recording successfully stopped');
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = `Error stopping recording: ${err.message}`;
|
||||||
|
addDebugLog(errorMsg);
|
||||||
|
setError(errorMsg);
|
||||||
|
setStatus("Recording stop failed");
|
||||||
|
}
|
||||||
|
}, [isRecording, addDebugLog]);
|
||||||
|
|
||||||
|
const playRecording = useCallback(() => {
|
||||||
|
if (audioUrl) {
|
||||||
|
addDebugLog('Playing recording');
|
||||||
|
const audio = new Audio(audioUrl);
|
||||||
|
audio.play();
|
||||||
|
setStatus("Playing recording...");
|
||||||
|
|
||||||
|
audio.onended = () => {
|
||||||
|
addDebugLog('Playback finished');
|
||||||
|
setStatus("Playback finished");
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
addDebugLog('No audio URL available for playback');
|
||||||
|
}
|
||||||
|
}, [audioUrl, addDebugLog]);
|
||||||
|
|
||||||
|
const setupVisualizer = useCallback((stream) => {
|
||||||
|
if (!audioContextRef.current) {
|
||||||
|
addDebugLog('AudioContext not available for visualization');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const source = audioContextRef.current.createMediaStreamSource(stream);
|
||||||
|
analyserRef.current = audioContextRef.current.createAnalyser();
|
||||||
|
analyserRef.current.fftSize = 64;
|
||||||
|
source.connect(analyserRef.current);
|
||||||
|
addDebugLog('Visualizer audio nodes connected');
|
||||||
|
|
||||||
|
const bufferLength = analyserRef.current.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
animationRef.current = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
analyserRef.current.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const barWidth = (canvas.width / bufferLength) * 2.5;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < bufferLength; i++) {
|
||||||
|
const barHeight = dataArray[i] / 2;
|
||||||
|
|
||||||
|
ctx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
|
||||||
|
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
||||||
|
|
||||||
|
x += barWidth + 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
draw();
|
||||||
|
addDebugLog('Visualizer animation started');
|
||||||
|
} catch (err) {
|
||||||
|
addDebugLog(`Error setting up visualizer: ${err.message}`);
|
||||||
|
}
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const formatTime = useCallback((seconds) => {
|
||||||
|
const mins = Math.floor(seconds / 60).toString().padStart(2, '0');
|
||||||
|
const secs = (seconds % 60).toString().padStart(2, '0');
|
||||||
|
return `${mins}:${secs}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearDebugLogs = useCallback(() => {
|
||||||
|
setDebugLogs([]);
|
||||||
|
addDebugLog('Debug logs cleared');
|
||||||
|
}, [addDebugLog]);
|
||||||
|
|
||||||
|
const toggleDebug = useCallback(() => {
|
||||||
|
setShowDebug(!showDebug);
|
||||||
|
addDebugLog(`Debug panel ${showDebug ? 'hidden' : 'shown'}`);
|
||||||
|
}, [showDebug, addDebugLog]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl my-6">
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800">Speech to Text Converter</h1>
|
||||||
|
<Button
|
||||||
|
onClick={toggleDebug}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{showDebug ? 'Hide Debug' : 'Show Debug'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recording Section */}
|
||||||
|
<div className="mb-8 p-4 border border-dashed border-[#6d9e37] rounded-lg bg-gray-50">
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-gray-700">Record Audio (16kHz, 16-bit mono)</h2>
|
||||||
|
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="w-full h-20 bg-gray-200 rounded mb-3"
|
||||||
|
style={{ display: isRecording ? 'block' : 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
<Button
|
||||||
|
onClick={startRecording}
|
||||||
|
disabled={isRecording || isLoading}
|
||||||
|
variant="outline"
|
||||||
|
className="px-4 py-2"
|
||||||
|
>
|
||||||
|
Start Recording
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={stopRecording}
|
||||||
|
disabled={!isRecording || isLoading}
|
||||||
|
variant="outline"
|
||||||
|
className="px-4 py-2"
|
||||||
|
>
|
||||||
|
Stop Recording
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={playRecording}
|
||||||
|
disabled={!audioUrl || isRecording || isLoading}
|
||||||
|
variant="outline"
|
||||||
|
className="px-4 py-2"
|
||||||
|
>
|
||||||
|
Play Recording
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isRecording && (
|
||||||
|
<div className="text-center text-[#6d9e37] font-medium">
|
||||||
|
Recording: {formatTime(recordingTime)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{audioUrl && !isRecording && (
|
||||||
|
<div className="flex items-center justify-between bg-white p-3 rounded border mt-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="w-5 h-5 text-[#6d9e37] mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium text-[#6d9e37]">recording.wav</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[#6d9e37]">
|
||||||
|
{(audioBlob?.size / (1024 * 1024)).toFixed(2)} MB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-full mb-3 -mt-2">
|
||||||
|
<span className="flex-grow border-b-2 border-[#6d9e37]"></span>
|
||||||
|
<h2 className="mx-2 text-[#6d9e37] font-bold">OR</h2>
|
||||||
|
<span className="flex-grow border-b-2 border-[#6d9e37]"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Upload Section */}
|
||||||
|
<div className="mb-8 p-4 border border-dashed border-[#6d9e37] rounded-lg bg-gray-50">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Upload Audio File
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-500">Supports WAV, MP3, OGG formats (max {MAX_FILE_SIZE_MB}MB)</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
variant="outline"
|
||||||
|
className="px-4 py-2"
|
||||||
|
disabled={isLoading || isRecording}
|
||||||
|
>
|
||||||
|
Select File
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
accept="audio/*,.wav,.mp3,.ogg"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
disabled={isLoading || isRecording}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{file && (
|
||||||
|
<div className="flex items-center justify-between bg-white p-3 rounded border">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="w-5 h-5 text-[#6d9e37] mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium text-[#6d9e37]">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[#6d9e37]">
|
||||||
|
{(file.size / (1024 * 1024)).toFixed(2)} MB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Selection Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-lg font-semibold mb-3 text-gray-700">Select Speech Recognition API</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{API_OPTIONS.map(api => (
|
||||||
|
<div
|
||||||
|
key={api.id}
|
||||||
|
onClick={() => !isLoading && !isRecording && setSelectedApi(api.id)}
|
||||||
|
className={`p-4 border-[1.5px] rounded-lg cursor-pointer transition-all ${
|
||||||
|
selectedApi === api.id
|
||||||
|
? 'border-[1.5px] border-[#6d9e37] bg-[#6d9e3720]'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
} ${isLoading || isRecording ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={selectedApi === api.id}
|
||||||
|
onChange={() => {}}
|
||||||
|
className="mr-2 h-4 w-4 accent-[#6d9e37]"
|
||||||
|
disabled={isLoading || isRecording}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-[#6d9e37]">{api.name}</h3>
|
||||||
|
<p className="text-xs text-gray-500">{api.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={(!file && !audioBlob) || isLoading || isRecording ||
|
||||||
|
(file && file.size > MAX_FILE_SIZE_BYTES)}
|
||||||
|
className="px-6 py-3 text-lg w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Processing...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
`Convert with ${API_OPTIONS.find(api => api.id === selectedApi)?.name}`
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="mb-6 text-center">
|
||||||
|
<p className={`text-sm ${
|
||||||
|
error ? 'text-red-600' :
|
||||||
|
isLoading ? 'text-[#6d9e37]' :
|
||||||
|
isRecording ? 'text-[#6d9e37]' :
|
||||||
|
'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{status}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<div className="flex items-center text-red-600">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h3 className="font-medium">Error</h3>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-red-600">{error}</p>
|
||||||
|
{error.includes(`${MAX_FILE_SIZE_MB}MB`) && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">
|
||||||
|
Please select a smaller audio file or record a shorter audio
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Results Display */}
|
||||||
|
{response && (
|
||||||
|
<div className="mt-6 border rounded-lg overflow-hidden">
|
||||||
|
<div className="bg-gray-50 px-4 py-3 border-b flex justify-between items-center">
|
||||||
|
<h3 className="font-medium text-[#6d9e37]">{API_OPTIONS.find(api => api.id === response.api)?.name} Results</h3>
|
||||||
|
{displayText && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => copyToClipboard(displayText)}
|
||||||
|
className="text-sm">
|
||||||
|
{copied ? (<span className="flex items-center">Copied!</span>) : 'Copy Text'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-white">
|
||||||
|
{displayText ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">Transcription:</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<p className="p-3 bg-gray-50 rounded text-sm text-gray-600 font-medium">{displayText}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
No text transcription found in the response.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Debug Section */}
|
||||||
|
{showDebug && (
|
||||||
|
<div className="mt-8 border rounded-lg overflow-hidden">
|
||||||
|
<div className="bg-gray-50 px-4 py-3 border-b flex justify-between items-center">
|
||||||
|
<h3 className="font-medium text-gray-700">Debug Logs</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearDebugLogs}
|
||||||
|
className="text-sm">
|
||||||
|
Clear Logs
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const blob = new Blob([debugLogs.join('\n')], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `audio-recorder-debug-${new Date().toISOString()}.log`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}}
|
||||||
|
className="text-sm">
|
||||||
|
Export Logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-white max-h-60 overflow-y-auto">
|
||||||
|
{debugLogs.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{debugLogs.map((log, index) => (
|
||||||
|
<div key={index} className="text-xs font-mono text-gray-600 border-b border-gray-100 pb-1">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">No debug logs yet. Interactions will appear here.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
295
src/components/Tools/ImageLabeling.jsx
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [images, setImages] = useState(Array(9).fill({ url: null, secretKey: null }));
|
||||||
|
const [loading, setLoading] = useState(Array(9).fill(true));
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
|
||||||
|
// Fetch initial images with staggered loading
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchInitialImages = async () => {
|
||||||
|
try {
|
||||||
|
const newImages = [];
|
||||||
|
const newLoading = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const imageData = await fetchImage();
|
||||||
|
setImages(prev => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[i] = imageData;
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
setLoading(prev => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[i] = false;
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, i * 150); // Staggered loading effect
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
setLoading(Array(9).fill(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchInitialImages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchImage = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api-img-lbeling-jugtfrvbghysvfr.siliconpin.com/get-image');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch image');
|
||||||
|
return await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching image:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitScore = async (imageData, score, index) => {
|
||||||
|
try {
|
||||||
|
setLoading(prev => {
|
||||||
|
const newLoading = [...prev];
|
||||||
|
newLoading[index] = true;
|
||||||
|
return newLoading;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileName = imageData.url.split('/').pop();
|
||||||
|
const response = await fetch('https://api-img-lbeling-jugtfrvbghysvfr.siliconpin.com/save-score', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
fileName: fileName,
|
||||||
|
score: score,
|
||||||
|
secretKey: imageData.secretKey
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to submit score');
|
||||||
|
|
||||||
|
// Animate out the current image
|
||||||
|
setImages(prev => {
|
||||||
|
const newImages = [...prev];
|
||||||
|
newImages[index] = { ...newImages[index], exiting: true };
|
||||||
|
return newImages;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for animation to complete
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
// Get new image and animate it in
|
||||||
|
const newImage = await fetchImage();
|
||||||
|
setImages(prev => {
|
||||||
|
const newImages = [...prev];
|
||||||
|
newImages[index] = { ...newImage, entering: true };
|
||||||
|
return newImages;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove animation flags after entrance
|
||||||
|
setTimeout(() => {
|
||||||
|
setImages(prev => {
|
||||||
|
const newImages = [...prev];
|
||||||
|
newImages[index] = { ...newImages[index], entering: false };
|
||||||
|
return newImages;
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error submitting score:', err);
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(prev => {
|
||||||
|
const newLoading = [...prev];
|
||||||
|
newLoading[index] = false;
|
||||||
|
return newLoading;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStarButtons = (imageData, index) => {
|
||||||
|
if (!imageData.url) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="flex justify-center gap-2 mt-4"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4, 5].map(star => (
|
||||||
|
<motion.button
|
||||||
|
key={star}
|
||||||
|
onClick={() => submitScore(imageData, star, index)}
|
||||||
|
disabled={loading[index]}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className={`
|
||||||
|
px-4 py-2 rounded-full border-2 font-medium text-sm
|
||||||
|
transition-colors duration-200
|
||||||
|
${loading[index] ?
|
||||||
|
'bg-gray-100 border-gray-200 text-gray-400 cursor-not-allowed' :
|
||||||
|
`bg-white ${star >= 3 ?
|
||||||
|
'border-emerald-300 text-emerald-600 hover:bg-emerald-50' :
|
||||||
|
'border-amber-300 text-amber-600 hover:bg-amber-50'}`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{star} ★
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-50 to-purple-50"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="max-w-md p-8 bg-white rounded-2xl shadow-xl text-center"
|
||||||
|
initial={{ scale: 0.9 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
>
|
||||||
|
<div className="text-red-500 text-2xl font-bold mb-4">Oops!</div>
|
||||||
|
<div className="text-gray-600 mb-6">{error}</div>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl hover:shadow-md transition-all"
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen py-12 px-4 sm:px-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="text-center mb-12">
|
||||||
|
<h1 className="text-4xl font-bold mb-3 text-[#6d9e37]">Image Rating App</h1>
|
||||||
|
<p className="text-lg max-w-2xl mx-auto">Rate these beautiful images from 1 to 5 stars. Your feedback helps improve our collection!</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{images.map((imageData, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{
|
||||||
|
opacity: loading[index] && !imageData.url ? 0.7 : 1,
|
||||||
|
y: 0
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 15,
|
||||||
|
delay: index * 0.05
|
||||||
|
}}
|
||||||
|
className={`relative bg-white rounded-2xl shadow-lg overflow-hidden
|
||||||
|
transition-all hover:shadow-xl ${loading[index] ? 'animate-pulse' : ''}`}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{imageData.url ? (
|
||||||
|
<motion.div
|
||||||
|
key={`image-${index}`}
|
||||||
|
initial={{ opacity: imageData.exiting ? 1 : 0, scale: imageData.exiting ? 1 : 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="h-72 flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 p-4 relative">
|
||||||
|
<motion.img
|
||||||
|
src={imageData.url}
|
||||||
|
alt={`Image ${index}`}
|
||||||
|
className="max-h-full max-w-full object-contain cursor-pointer"
|
||||||
|
onClick={() => setSelectedImage(imageData.url)}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.onerror = null;
|
||||||
|
e.target.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QxZDFkMSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0zIDNoMTh2MThIM3oiLz48cGF0aCBkPSJNMTkgOUw5IDE5Ii8+PHBhdGggZD0iTTkgOWwxMCAxMCIvPjwvc3ZnPg==';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
{renderStarButtons(imageData, index)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key={`skeleton-${index}`}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 0.7 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="h-72 bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="text-gray-400">Loading image...</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{loading[index] && imageData.url && (
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ repeat: Infinity, duration: 1, ease: "linear" }}
|
||||||
|
className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedImage && (
|
||||||
|
<motion.div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
exit={{ scale: 0.9 }}
|
||||||
|
className="relative max-w-4xl w-full"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="absolute -top-10 right-0 text-white text-3xl hover:text-gray-300 transition"
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<img
|
||||||
|
src={selectedImage}
|
||||||
|
alt="Enlarged view"
|
||||||
|
className="max-h-[80vh] w-full object-contain"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
132
src/components/Tools/ImageResize.jsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { fromBlob, blobToURL } from "image-resize-compress";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import {
|
||||||
|
ImageIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
SlidersHorizontal,
|
||||||
|
FileImage,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export default function ImageResize() {
|
||||||
|
const [previewUrl, setPreviewUrl] = useState(null);
|
||||||
|
const [resizedBlob, setResizedBlob] = useState(null);
|
||||||
|
const [imgQuality, setImgQuality] = useState(80);
|
||||||
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
const [originalSize, setOriginalSize] = useState(0);
|
||||||
|
const [resizedSize, setResizedSize] = useState(0);
|
||||||
|
const [originalExtension, setOriginalExtension] = useState("webp");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resizeImage = async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const width = "auto";
|
||||||
|
const height = "auto";
|
||||||
|
const ext = selectedFile.name.split(".").pop().toLowerCase();
|
||||||
|
|
||||||
|
setOriginalExtension(ext);
|
||||||
|
setOriginalSize((selectedFile.size / 1024).toFixed(2)); // KB
|
||||||
|
|
||||||
|
// Only apply quality change for JPEG or WebP
|
||||||
|
const format = ["jpg", "jpeg", "webp"].includes(ext)
|
||||||
|
? ext
|
||||||
|
: "jpeg"; // fallback to jpeg for better quality control
|
||||||
|
|
||||||
|
const resized = await fromBlob(selectedFile, imgQuality, width, height, format);
|
||||||
|
const url = await blobToURL(resized);
|
||||||
|
|
||||||
|
setResizedBlob(resized);
|
||||||
|
setResizedSize((resized.size / 1024).toFixed(2)); // KB
|
||||||
|
setPreviewUrl(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Image resizing failed:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resizeImage();
|
||||||
|
}, [selectedFile, imgQuality]);
|
||||||
|
|
||||||
|
const handleFileChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setSelectedFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (!resizedBlob) return;
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = URL.createObjectURL(resizedBlob);
|
||||||
|
link.download = `resized-image.${originalExtension}`;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen text-white p-6 flex items-center justify-center">
|
||||||
|
<div className="bg-gray-800 rounded-2xl shadow-lg w-full max-w-2xl p-6 space-y-6">
|
||||||
|
<h2 className="text-2xl font-bold flex items-center gap-2 text-[#6d9e37]">
|
||||||
|
<ImageIcon className="w-6 h-6" />
|
||||||
|
Image Resizer
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2 text-sm text-gray-300 font-medium">
|
||||||
|
<FileImage className="w-4 h-4" />
|
||||||
|
Choose an image
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="w-full p-2 bg-gray-700 border border-gray-600 rounded-lg text-sm file:bg-[#6d9e37] file:text-white file:border-0 file:px-3 file:py-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium flex items-center gap-1">
|
||||||
|
<SlidersHorizontal className="w-4 h-4" />
|
||||||
|
Quality: <span className="text-[#6d9e37]">{imgQuality}%</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={imgQuality}
|
||||||
|
onChange={(e) => setImgQuality(e.target.value)}
|
||||||
|
className="w-full accent-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{previewUrl && (
|
||||||
|
<div className="border-t border-gray-700 pt-4 space-y-3">
|
||||||
|
<div className="text-sm text-gray-300 grid grid-cols-2 gap-2">
|
||||||
|
<p>
|
||||||
|
<strong>Original Size:</strong> {originalSize} KB
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Resized Size:</strong> {resizedSize} KB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-400 mb-2">Preview:</p>
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="Resized Preview"
|
||||||
|
className="w-full h-auto max-h-96 object-contain rounded-lg border border-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="mt-4 w-full gap-2"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="w-5 h-5" />
|
||||||
|
Download Resized Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
|
import { token, user_name, pb_id } from '../lib/CookieValues';
|
||||||
|
import CommentSystem from './CommentSystem/CommentSystem';
|
||||||
|
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
|
||||||
export default function TopicDetail(props) {
|
export default function TopicDetail(props) {
|
||||||
|
// console.log('coockie data', user_name)
|
||||||
const [showCopied, setShowCopied] = useState(false);
|
const [showCopied, setShowCopied] = useState(false);
|
||||||
|
|
||||||
if (!props.topic) {
|
if (!props.topic) {
|
||||||
return <div>Topic not found</div>;
|
return <div>Topic not found</div>;
|
||||||
}
|
}
|
||||||
@@ -12,31 +20,6 @@ export default function TopicDetail(props) {
|
|||||||
const title = props.topic.title;
|
const title = props.topic.title;
|
||||||
const text = `Check out this article: ${title}`;
|
const text = `Check out this article: ${title}`;
|
||||||
|
|
||||||
// const shareOnFacebook = () => {
|
|
||||||
// window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnTwitter = () => {
|
|
||||||
// window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnLinkedIn = () => {
|
|
||||||
// window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnReddit = () => {
|
|
||||||
// window.open(`https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnWhatsApp = () => {
|
|
||||||
// window.open(`https://wa.me/?text=${encodeURIComponent(`${text} ${shareUrl}`)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareViaEmail = () => {
|
|
||||||
// window.open(`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(`${text}\n\n${shareUrl}`)}`);
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
const shareOnSocialMedia = (platform) => {
|
const shareOnSocialMedia = (platform) => {
|
||||||
switch (platform){
|
switch (platform){
|
||||||
case 'facebook':
|
case 'facebook':
|
||||||
@@ -125,12 +108,12 @@ export default function TopicDetail(props) {
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12">
|
<div className="container mx-auto px-4 py-12">
|
||||||
<article className="max-w-4xl mx-auto">
|
<article className="max-w-4xl mx-auto">
|
||||||
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
|
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
|
||||||
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
|
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
|
||||||
|
|
||||||
{/* Enhanced Social Share Buttons */}
|
{/* Enhanced Social Share Buttons */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<p className="text-sm text-gray-500 mb-3 font-medium">Share this article:</p>
|
<p className="text-sm text-gray-500 mb-3 font-medium">Share this Topic:</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => shareOnSocialMedia('facebook')}
|
onClick={() => shareOnSocialMedia('facebook')}
|
||||||
@@ -211,11 +194,11 @@ export default function TopicDetail(props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
|
||||||
className="font-light mb-8 text-justify prose max-w-none"
|
<CommentSystem topicId={props.topic.id}/>
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }}
|
|
||||||
></div>
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bg-[#6d9e37]
|
||||||
@@ -8,9 +8,8 @@ import { Separator } from './ui/separator';
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||||
import { CustomTabs } from './ui/tabs';
|
import { CustomTabs } from './ui/tabs';
|
||||||
import Loader from "./ui/loader";
|
import Loader from "./ui/loader";
|
||||||
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
||||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
const PUBLIC_MINIO_UPLOAD_URL = import.meta.env.PUBLIC_MINIO_UPLOAD_URL;
|
||||||
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const slug = urlParams.get('slug');
|
const slug = urlParams.get('slug');
|
||||||
// console.log('find slug from url', slug);
|
// console.log('find slug from url', slug);
|
||||||
@@ -36,7 +35,7 @@ export default function EditTopic (){
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTopic = async () => {
|
const fetchTopic = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${TOPIC_API_URL}?query=get-single-topic&slug=${slug}`, {
|
const response = await fetch(`${PUBLIC_TOPIC_API_URL}?query=get-single-topic&slug=${slug}`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ export default function EditTopic (){
|
|||||||
formData.append('folder', 'topic-images');
|
formData.append('folder', 'topic-images');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(MINIO_UPLOAD_URL, {
|
const response = await fetch(PUBLIC_MINIO_UPLOAD_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@@ -221,7 +220,7 @@ export default function EditTopic (){
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Submit to API (PUT request for update)
|
// Submit to API (PUT request for update)
|
||||||
const response = await fetch(`${TOPIC_API_URL}?query=update-topic&slug=${formData.slug}`, {
|
const response = await fetch(`${PUBLIC_TOPIC_API_URL}?query=update-topic&slug=${formData.slug}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useIsLoggedIn } from '../lib/isLoggedIn';
|
|||||||
import { Pencil, Trash2, Search } from "lucide-react";
|
import { Pencil, Trash2, Search } from "lucide-react";
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
export default function TopicItems(props) {
|
export default function TopicItems(props) {
|
||||||
console.table(props.topics)
|
// console.table(props.topics)
|
||||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
const [localSearchTerm, setLocalSearchTerm] = React.useState(props.searchTerm || '');
|
const [localSearchTerm, setLocalSearchTerm] = React.useState(props.searchTerm || '');
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function TopicCreation() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`,
|
`https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Loader from "./ui/loader";
|
|||||||
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
|
||||||
|
|
||||||
export default function TopicCreation() {
|
export default function TopicCreation() {
|
||||||
|
const PUBLIC_TOPIC_API_URL = import.meta.env.PUBLIC_TOPIC_API_URL;
|
||||||
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
|
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
|
||||||
const [topics, setTopics] = useState([]);
|
const [topics, setTopics] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -16,7 +17,7 @@ export default function TopicCreation() {
|
|||||||
last_page: 1,
|
last_page: 1,
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
total: 0
|
total: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get current page from URL or default to 1
|
// Get current page from URL or default to 1
|
||||||
const getCurrentPage = () => {
|
const getCurrentPage = () => {
|
||||||
@@ -28,8 +29,8 @@ export default function TopicCreation() {
|
|||||||
// Fetch topics data
|
// Fetch topics data
|
||||||
const fetchTopics = async (page, search = '') => {
|
const fetchTopics = async (page, search = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
let url = `https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`;
|
let url = `${PUBLIC_TOPIC_API_URL}?query=get-all-topics&page=${page}`;
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
url += `&search=${encodeURIComponent(search)}`;
|
url += `&search=${encodeURIComponent(search)}`;
|
||||||
@@ -112,10 +113,18 @@ export default function TopicCreation() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
|
<>
|
||||||
<div className="container mx-auto flex justify-end gap-x-4 my-4">
|
<div className="container mx-auto flex justify-end gap-x-4 my-4">
|
||||||
<Button onClick={() => window.location.href = '/topic/new'} variant="outline">Create New</Button>
|
<Button className="hidden lg:block" onClick={() => window.location.href = '/topic/new'} variant="outline">Create New</Button>
|
||||||
<Button onClick={() => window.location.href = '/topic/my-topic'} variant="outline">My Creation</Button>
|
<Button onClick={() => window.location.href = '/topic/my-topic'} variant="outline">My Creation</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<button onClick={() => window.location.href = '/topic/new'} className="fixed z-10 bottom-20 lg:bottom-10 right-0 lg:right-10 rounded-full w-16 h-16 bg-[#6d9e37]">
|
||||||
|
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#6d9e37] opacity-75 duration-[1s,15s]"></span>
|
||||||
|
<span class="relative inline-flex w-16 h-16 rounded-full bg-[#6d9e37]">
|
||||||
|
<svg className="border-[2px] border-[#fff] rounded-full" fill="#FFFFFF" viewBox="-5.76 -5.76 35.52 35.52" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" d="M12.3023235,7.94519388 L4.69610276,15.549589 C4.29095108,15.9079238 4.04030835,16.4092335 4,16.8678295 L4,20.0029438 L7.06398288,20.004826 C7.5982069,19.9670062 8.09548693,19.7183782 8.49479322,19.2616227 L16.0567001,11.6997158 L12.3023235,7.94519388 Z M13.7167068,6.53115006 L17.4709137,10.2855022 L19.8647941,7.89162181 C19.9513987,7.80501747 20.0000526,7.68755666 20.0000526,7.56507948 C20.0000526,7.4426023 19.9513987,7.32514149 19.8647932,7.23853626 L16.7611243,4.13485646 C16.6754884,4.04854589 16.5589355,4 16.43735,4 C16.3157645,4 16.1992116,4.04854589 16.1135757,4.13485646 L13.7167068,6.53115006 Z M16.43735,2 C17.0920882,2 17.7197259,2.26141978 18.1781068,2.7234227 L21.2790059,5.82432181 C21.7406843,6.28599904 22.0000526,6.91216845 22.0000526,7.56507948 C22.0000526,8.21799052 21.7406843,8.84415992 21.2790068,9.30583626 L9.95750718,20.6237545 C9.25902448,21.4294925 8.26890003,21.9245308 7.1346,22.0023295 L2,22.0023295 L2,21.0023295 L2.00324765,16.7873015 C2.08843822,15.7328366 2.57866679,14.7523321 3.32649633,14.0934196 L14.6953877,2.72462818 C15.1563921,2.2608295 15.7833514,2 16.43735,2 Z"></path> </g></svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && !topics.length ? (
|
{loading && !topics.length ? (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "./ui/select";
|
|
||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import { Textarea } from "./ui/textarea";
|
import { Textarea } from "./ui/textarea";
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
@@ -53,13 +52,14 @@ export default function ProfilePage() {
|
|||||||
const [toast, setToast] = useState<ToastState>({ visible: false, message: '' });
|
const [toast, setToast] = useState<ToastState>({ visible: false, message: '' });
|
||||||
const [txnId, setTxnId] = useState<string>('');
|
const [txnId, setTxnId] = useState<string>('');
|
||||||
const [userEmail, setUserEmail] = useState<string>('');
|
const [userEmail, setUserEmail] = useState<string>('');
|
||||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
|
||||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/invoice/';
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
const PUBLIC_INVOICE_API_URL = import.meta.env.PUBLIC_INVOICE_API_URL;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSessionData = async () => {
|
const fetchSessionData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${USER_API_URL}?query=get-user`, {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=get-user`, {
|
||||||
credentials: 'include', // Crucial for cookies
|
credentials: 'include', // Crucial for cookies
|
||||||
headers: { 'Accept': 'application/json' }
|
headers: { 'Accept': 'application/json' }
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export default function ProfilePage() {
|
|||||||
};
|
};
|
||||||
const getInvoiceListData = async () => {
|
const getInvoiceListData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${USER_API_URL}?query=invoice-info`, {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=invoice-info`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include', // Crucial for cookies
|
credentials: 'include', // Crucial for cookies
|
||||||
headers: { 'Accept': 'application/json' }
|
headers: { 'Accept': 'application/json' }
|
||||||
@@ -128,7 +128,7 @@ export default function ProfilePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sessionLogOut = () => {
|
const sessionLogOut = () => {
|
||||||
fetch(`${USER_API_URL}?query=logout`, {
|
fetch(`${PUBLIC_USER_API_URL}?query=logout`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
})
|
})
|
||||||
@@ -188,7 +188,7 @@ export default function ProfilePage() {
|
|||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="firstName">Full Name</Label>
|
<Label htmlFor="firstName">Full Name</Label>
|
||||||
<Input id="firstName" defaultValue={userData.session_data?.user_name || 'Jhon'} />
|
<Input id="firstName" defaultValue={userData.session_data?.user_name || ''} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="phone">Phone</Label>
|
<Label htmlFor="phone">Phone</Label>
|
||||||
@@ -266,7 +266,42 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 space-y-6 lg:max-w-xl">
|
<div className="flex-1 space-y-6 lg:max-w-xl">
|
||||||
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut}/>
|
<Card className="">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<CardTitle>My Services</CardTitle>
|
||||||
|
{/* <a href="/profile/billing-info" className="hover:bg-[#6d9e37] hover:text-white transtion duration-500 py-1 px-2 rounded">View All</a> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
View your all Services.
|
||||||
|
</CardDescription>
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-left">Order ID</th>
|
||||||
|
<th className="text-center">Date</th>
|
||||||
|
<th className="text-left">Description</th>
|
||||||
|
<th className="text-center">Status</th>
|
||||||
|
{/* <th className="text-center">Action</th> */}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
invoiceList.slice(0, 5).map((invoice, index) => (
|
||||||
|
<tr key={index} className="">
|
||||||
|
<td>{invoice.billing_id}</td>
|
||||||
|
<td className="text-center">{invoice?.created_at.split(' ')[0]}</td>
|
||||||
|
<td className=""><p className="line-clamp-1">{invoice.service}</p></td>
|
||||||
|
<td className={`text-center text-sm rounded-full h-fit ${invoice.service_status === '0' ? 'text-yellow-500' : invoice.service_status === '1' ? 'text-green-500' : 'text-red-500'}`}>{invoice.service_status === '1' ? 'Active' : invoice.service_status === '0' ? 'Unactive' : ''}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut} />
|
||||||
<Card className="mt-6">
|
<Card className="mt-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Danger Zone</CardTitle>
|
<CardTitle>Danger Zone</CardTitle>
|
||||||
@@ -301,10 +336,10 @@ export default function ProfilePage() {
|
|||||||
<span className="font-bold">Amount:</span>
|
<span className="font-bold">Amount:</span>
|
||||||
<span>₹{selectedData?.amount}</span>
|
<span>₹{selectedData?.amount}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
{/* <div className="flex justify-between">
|
||||||
<span className="font-bold">User:</span>
|
<span className="font-bold">User:</span>
|
||||||
<span>{selectedData?.user}</span>
|
<span>{selectedData?.user}</span>
|
||||||
</div>
|
</div> */}
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="font-bold">Status:</span>
|
<span className="font-bold">Status:</span>
|
||||||
<span className={`px-2 py-0.5 rounded text-white text-xs ${selectedData?.status === 'pending' ? 'bg-yellow-500' : selectedData?.status === 'completed' ? 'bg-green-500' : 'bg-red-500'}`}>
|
<span className={`px-2 py-0.5 rounded text-white text-xs ${selectedData?.status === 'pending' ? 'bg-yellow-500' : selectedData?.status === 'completed' ? 'bg-green-500' : 'bg-red-500'}`}>
|
||||||
|
|||||||
@@ -2,50 +2,11 @@
|
|||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import LoginProfile from '../components/LoginOrProfile';
|
import LoginProfile from '../components/LoginOrProfile';
|
||||||
|
|
||||||
interface Props {
|
interface Props { title: string; description?: string; ogImage?: string; canonicalURL?: string; type?: 'website' | 'article'; keyWords?: string; newSchema1 : string; newSchema2 : string; newSchema3 : string; newSchema4 : string; newSchema5 : string}
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
ogImage?: string;
|
|
||||||
canonicalURL?: string;
|
|
||||||
type?: 'website' | 'article';
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
description = "SiliconPin offers high-performance hosting solutions for PHP, Node.js, Python, K8s, and K3s applications with 24/7 support.",
|
|
||||||
ogImage,
|
|
||||||
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
|
|
||||||
type = "website",
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
|
const { title, description, ogImage, canonicalURL = new URL(Astro.url.pathname, Astro.site).href, type = "website", keyWords, newSchema1, newSchema2, newSchema3, newSchema4, newSchema5} = Astro.props;
|
||||||
// Organization schema
|
// Organization schema
|
||||||
const organizationSchema = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "SiliconPin",
|
|
||||||
"url": "https://siliconpin.com",
|
|
||||||
"logo": "https://siliconpin.com/assets/logo.svg",
|
|
||||||
"contactPoint": {
|
|
||||||
"@type": "ContactPoint",
|
|
||||||
"telephone": "+91-700-160-1485",
|
|
||||||
"contactType": "customer service",
|
|
||||||
"availableLanguage": ["English", "Hindi", "Bengali"]
|
|
||||||
},
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"streetAddress": "121 Lalbari, GourBongo Road",
|
|
||||||
"addressLocality": "Habra",
|
|
||||||
"addressRegion": "W.B.",
|
|
||||||
"postalCode": "743271",
|
|
||||||
"addressCountry": "India"
|
|
||||||
},
|
|
||||||
"sameAs": [
|
|
||||||
"https://www.linkedin.com/company/siliconpin",
|
|
||||||
"https://x.com/dwd_consultancy",
|
|
||||||
"https://www.facebook.com/profile.php?id=100088549643337",
|
|
||||||
"https://instagram.com/siliconpin.com_"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -62,6 +23,7 @@ const organizationSchema = {
|
|||||||
<meta name="keywords" content="hosting, PHP hosting, Node.js hosting, Python hosting, Kubernetes, K8s, K3s, cloud services, web hosting, application hosting, server hosting, India hosting" />
|
<meta name="keywords" content="hosting, PHP hosting, Node.js hosting, Python hosting, Kubernetes, K8s, K3s, cloud services, web hosting, application hosting, server hosting, India hosting" />
|
||||||
<meta name="author" content="SiliconPin" />
|
<meta name="author" content="SiliconPin" />
|
||||||
<link rel="canonical" href={canonicalURL} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
<meta name="keywords" content={keyWords} />
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content={type} />
|
<meta property="og:type" content={type} />
|
||||||
@@ -83,7 +45,14 @@ const organizationSchema = {
|
|||||||
<link rel="apple-touch-icon" href="https://siliconpin.com/assets/logo.svg" />
|
<link rel="apple-touch-icon" href="https://siliconpin.com/assets/logo.svg" />
|
||||||
|
|
||||||
<!-- Structured Data -->
|
<!-- Structured Data -->
|
||||||
<script type="application/ld+json" set:html={JSON.stringify(organizationSchema)}></script>
|
<script is:inline type="application/ld+json" set:html={newSchema1}></script>
|
||||||
|
<script is:inline type="application/ld+json" set:html={newSchema2}></script>
|
||||||
|
<script is:inline type="application/ld+json" set:html={newSchema3}></script>
|
||||||
|
<script is:inline type="application/ld+json" set:html={newSchema4}></script>
|
||||||
|
<script is:inline type="application/ld+json" set:html={newSchema5}></script>
|
||||||
|
|
||||||
|
<!-- For markdown editor css -->
|
||||||
|
<link rel="stylesheet" href="/assets/md-editor/mdeditor.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex flex-col">
|
<body class="min-h-screen flex flex-col">
|
||||||
<!-- Top header - visible on all devices -->
|
<!-- Top header - visible on all devices -->
|
||||||
@@ -96,12 +65,12 @@ const organizationSchema = {
|
|||||||
<!-- Desktop navigation -->
|
<!-- Desktop navigation -->
|
||||||
<nav class="hidden md:flex items-center gap-6" aria-label="Main Navigation">
|
<nav class="hidden md:flex items-center gap-6" aria-label="Main Navigation">
|
||||||
<a href="/" class="hover:text-[#6d9e37] transition-colors">Home</a>
|
<a href="/" class="hover:text-[#6d9e37] transition-colors">Home</a>
|
||||||
<a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a>
|
<!-- <a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a> -->
|
||||||
<a href="/topic" class="hover:text-[#6d9e37] transition-colors">Topic</a>
|
<a href="/topic" class="hover:text-[#6d9e37] transition-colors">Topic</a>
|
||||||
<a href="/contact" class="hover:text-[#6d9e37] transition-colors">Contact</a>
|
<a href="/contact" class="hover:text-[#6d9e37] transition-colors">Contact</a>
|
||||||
<LoginProfile deviceType="desktop" client:load />
|
<LoginProfile deviceType="desktop" client:load />
|
||||||
<!-- <a href="/profile" class="hover:text-[#6d9e37] transition-colors"></a> -->
|
<!-- <a href="/profile" class="hover:text-[#6d9e37] transition-colors"></a> -->
|
||||||
<a href="/get-started" class="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors">Get Started</a>
|
<a href="/services" class="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors">Services</a>
|
||||||
</nav>
|
</nav>
|
||||||
<!-- Mobile menu button -->
|
<!-- Mobile menu button -->
|
||||||
<div class="md:hidden relative">
|
<div class="md:hidden relative">
|
||||||
@@ -112,11 +81,11 @@ const organizationSchema = {
|
|||||||
<!-- Mobile dropdown menu -->
|
<!-- Mobile dropdown menu -->
|
||||||
<div id="mobileMenu" class="absolute right-0 mt-2 w-48 bg-neutral-800 rounded-md shadow-lg py-1 z-50 hidden border border-neutral-700">
|
<div id="mobileMenu" class="absolute right-0 mt-2 w-48 bg-neutral-800 rounded-md shadow-lg py-1 z-50 hidden border border-neutral-700">
|
||||||
<a href="/" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Home</a>
|
<a href="/" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Home</a>
|
||||||
<a href="/services" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Services</a>
|
<!-- <a href="/services" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Services</a> -->
|
||||||
<a href="/topic" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Topic</a>
|
<a href="/topic" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Topic</a>
|
||||||
<a href="/contact" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Contact</a>
|
<a href="/contact" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Contact</a>
|
||||||
<a href="/about-us" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">About Us</a>
|
<a href="/about-us" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">About Us</a>
|
||||||
<a href="/get-started" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700 font-medium text-[#6d9e37]">Get Started</a>
|
<a href="/services" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700 font-medium text-[#6d9e37]">Services</a>
|
||||||
<a href="/suggestion-or-report" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Suggestion or Report</a>
|
<a href="/suggestion-or-report" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Suggestion or Report</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,8 +139,8 @@ const organizationSchema = {
|
|||||||
<div class="bg-neutral-800 p-5 rounded-lg border border-neutral-700 shadow-lg">
|
<div class="bg-neutral-800 p-5 rounded-lg border border-neutral-700 shadow-lg">
|
||||||
<h3 class="font-medium text-lg text-[#6d9e37] mb-4 pb-2 border-b border-[#6d9e37]">Services</h3>
|
<h3 class="font-medium text-lg text-[#6d9e37] mb-4 pb-2 border-b border-[#6d9e37]">Services</h3>
|
||||||
<ul class="space-y-2 text-neutral-400">
|
<ul class="space-y-2 text-neutral-400">
|
||||||
<li class="flex items-center gap-2">📦 <a href="/services" class="hover:text-[#6d9e37] transition-colors">All Services</a></li>
|
<!-- <li class="flex items-center gap-2">📦 <a href="/services" class="hover:text-[#6d9e37] transition-colors">All Services</a></li> -->
|
||||||
<li class="flex items-center gap-2">🚀 <a href="/get-started" class="hover:text-[#6d9e37] transition-colors">Get Started</a></li>
|
<li class="flex items-center gap-2">🚀 <a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a></li>
|
||||||
<li class="flex items-center gap-2">👨💻 <a href="/hire-developer" class="hover:text-[#6d9e37] transition-colors">Hire a Human Developer</a></li>
|
<li class="flex items-center gap-2">👨💻 <a href="/hire-developer" class="hover:text-[#6d9e37] transition-colors">Hire a Human Developer</a></li>
|
||||||
<li class="flex items-center gap-2">🤖 <a href="/hire-ai-agent" class="hover:text-[#6d9e37] transition-colors">Hire an AI Agent</a></li>
|
<li class="flex items-center gap-2">🤖 <a href="/hire-ai-agent" class="hover:text-[#6d9e37] transition-colors">Hire an AI Agent</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -222,21 +191,24 @@ const organizationSchema = {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
||||||
<span class="text-xs mt-1">Home</span>
|
<span class="text-xs mt-1">Home</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/services" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
<a href="/topic" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
<!-- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> -->
|
||||||
<span class="text-xs mt-1">Services</span>
|
<svg fill="#6d9e37" width="20" height="20" viewBox="-4 -2 24 24" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin" class="jam jam-document-f"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M3 0h10a3 3 0 0 1 3 3v14a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm1 7a1 1 0 1 0 0 2h8a1 1 0 0 0 0-2H4zm0 8a1 1 0 0 0 0 2h5a1 1 0 0 0 0-2H4zM4 3a1 1 0 1 0 0 2h8a1 1 0 0 0 0-2H4zm0 8a1 1 0 0 0 0 2h8a1 1 0 0 0 0-2H4z"></path></g></svg>
|
||||||
|
<span class="text-xs mt-1">Topic</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/get-started" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
<a href="/services" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||||
<div class="w-10 h-10 rounded-full bg-[#6d9e37] flex items-center justify-center -mt-5 shadow-lg">
|
<div class="w-10 h-10 rounded-full bg-[#6d9e37] flex items-center justify-center -mt-5 shadow-lg">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs mt-1">Start</span>
|
<span class="text-xs mt-1">Services</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/about-us" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
<a href="/about-us" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||||
<svg width="20px" height="20px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>about</title> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="about-white" fill="#6d9e37" transform="translate(42.666667, 42.666667)"> <path d="M213.333333,3.55271368e-14 C95.51296,3.55271368e-14 3.55271368e-14,95.51168 3.55271368e-14,213.333333 C3.55271368e-14,331.153707 95.51296,426.666667 213.333333,426.666667 C331.154987,426.666667 426.666667,331.153707 426.666667,213.333333 C426.666667,95.51168 331.154987,3.55271368e-14 213.333333,3.55271368e-14 Z M213.333333,384 C119.227947,384 42.6666667,307.43872 42.6666667,213.333333 C42.6666667,119.227947 119.227947,42.6666667 213.333333,42.6666667 C307.44,42.6666667 384,119.227947 384,213.333333 C384,307.43872 307.44,384 213.333333,384 Z M240.04672,128 C240.04672,143.46752 228.785067,154.666667 213.55008,154.666667 C197.698773,154.666667 186.713387,143.46752 186.713387,127.704107 C186.713387,112.5536 197.99616,101.333333 213.55008,101.333333 C228.785067,101.333333 240.04672,112.5536 240.04672,128 Z M192.04672,192 L234.713387,192 L234.713387,320 L192.04672,320 L192.04672,192 Z" id="Shape"> </path> </g> </g> </g></svg>
|
<svg width="20px" height="20px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>about</title> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="about-white" fill="#6d9e37" transform="translate(42.666667, 42.666667)"> <path d="M213.333333,3.55271368e-14 C95.51296,3.55271368e-14 3.55271368e-14,95.51168 3.55271368e-14,213.333333 C3.55271368e-14,331.153707 95.51296,426.666667 213.333333,426.666667 C331.154987,426.666667 426.666667,331.153707 426.666667,213.333333 C426.666667,95.51168 331.154987,3.55271368e-14 213.333333,3.55271368e-14 Z M213.333333,384 C119.227947,384 42.6666667,307.43872 42.6666667,213.333333 C42.6666667,119.227947 119.227947,42.6666667 213.333333,42.6666667 C307.44,42.6666667 384,119.227947 384,213.333333 C384,307.43872 307.44,384 213.333333,384 Z M240.04672,128 C240.04672,143.46752 228.785067,154.666667 213.55008,154.666667 C197.698773,154.666667 186.713387,143.46752 186.713387,127.704107 C186.713387,112.5536 197.99616,101.333333 213.55008,101.333333 C228.785067,101.333333 240.04672,112.5536 240.04672,128 Z M192.04672,192 L234.713387,192 L234.713387,320 L192.04672,320 L192.04672,192 Z" id="Shape"> </path> </g> </g> </g></svg>
|
||||||
<span class="text-xs mt-1">About</span>
|
<span class="text-xs mt-1">About</span>
|
||||||
</a>
|
</a>
|
||||||
<LoginProfile deviceType="mobile" client:load />
|
<div class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||||
|
<LoginProfile deviceType="mobile" client:load />
|
||||||
|
</div>
|
||||||
<!-- <a href="/profile" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
<!-- <a href="/profile" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="10" r="3"></circle><path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="10" r="3"></circle><path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path></svg>
|
||||||
<span class="text-xs mt-1">Profile</span>
|
<span class="text-xs mt-1">Profile</span>
|
||||||
@@ -264,6 +236,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style is:global>
|
<style is:global>
|
||||||
@@ -309,4 +282,91 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.animate-fade-in-up {
|
.animate-fade-in-up {
|
||||||
animation: fadeInUp 0.3s ease-out forwards;
|
animation: fadeInUp 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* comment-system.css */
|
||||||
|
.comment-system {
|
||||||
|
/* max-width: 800px; */
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-thread {
|
||||||
|
/* margin-bottom: 1.5rem; */
|
||||||
|
/* padding: 1rem; */
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-thread.reply {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-left-color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-author {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-date {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-text {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #0066cc;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-form {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #d32f2f;
|
||||||
|
background: #fde0e0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-comment-card {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-comments {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
8
src/lib/CookieValues.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// CookieValues.js
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
export const token = Cookies.get('token') || null;
|
||||||
|
export const user_name = Cookies.get('user_name') || null;
|
||||||
|
export const pb_id = Cookies.get('pb_id') || null;
|
||||||
|
export const siliconId = Cookies.get('siliconId') || null;
|
||||||
|
export const isLogin = Cookies.get('isLogin') || null;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
const PUBLIC_USER_API_URL = import.meta.env.PUBLIC_USER_API_URL;
|
||||||
|
|
||||||
export const useIsLoggedIn = () => {
|
export const useIsLoggedIn = () => {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(null); // null means "unknown"
|
const [isLoggedIn, setIsLoggedIn] = useState(null); // null means "unknown"
|
||||||
const [sessionData, setSessionData] = useState(null);
|
const [sessionData, setSessionData] = useState(null);
|
||||||
@@ -11,7 +10,7 @@ export const useIsLoggedIn = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkLoginStatus = async () => {
|
const checkLoginStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${USER_API_URL}?query=isLoggedIn`, {
|
const response = await fetch(`${PUBLIC_USER_API_URL}?query=isLoggedIn`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
|||||||
<div class="text-center mb-8 sm:mb-12">
|
<div class="text-center mb-8 sm:mb-12">
|
||||||
<h1 class="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">About SiliconPin</h1>
|
<h1 class="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">About SiliconPin</h1>
|
||||||
<p class="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">
|
<p class="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">
|
||||||
Empowering businesses with reliable, high-performance hosting solutions
|
We promote awareness about digital freedom by provoding software and hardware development and research
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,55 +3,116 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
import { ContactForm } from '../components/ContactForm';
|
import { ContactForm } from '../components/ContactForm';
|
||||||
|
|
||||||
// Page-specific SEO metadata
|
// Page-specific SEO metadata
|
||||||
const pageTitle = "Contact Us | SiliconPin";
|
const metaTitle = "Contact Us | SiliconPin";
|
||||||
const pageDescription = "Contact SiliconPin for reliable hosting solutions. Our team is available 24/7 to help with your PHP, Node.js, Python, Kubernetes, and K3s hosting needs.";
|
const metaDescription = "Contact SiliconPin for reliable hosting solutions. Our team is available 24/7 to help with your PHP, Node.js, Python, Kubernetes, and K3s hosting needs.";
|
||||||
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
const ogImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
||||||
|
|
||||||
// Contact schema for structured data
|
// Contact schema for structured data
|
||||||
const contactSchema = {
|
const contactSchema1 = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "ContactPage",
|
"@type": "ContactPage",
|
||||||
"name": "SiliconPin Contact Page",
|
"name": "Contact SiliconPin",
|
||||||
"description": "Contact SiliconPin for reliable hosting solutions",
|
"url": "https://siliconpin.com/contact/",
|
||||||
"url": "https://siliconpin.com/contact",
|
"description": "Contact page for SiliconPin – Reach out for cloud hosting, VPN, DevOps, and AI services.",
|
||||||
"mainEntityOfPage": {
|
"inLanguage": ["en", "hi", "bn"],
|
||||||
|
"contactPoint": {
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
"telephone": "+91-700-160-1485",
|
||||||
|
"contactType": "customer service",
|
||||||
|
"availableLanguage": ["English", "Hindi", "Bengali"]
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "121 Lalbari, GourBongo Road",
|
||||||
|
"addressLocality": "Habra",
|
||||||
|
"addressRegion": "W.B.",
|
||||||
|
"postalCode": "743271",
|
||||||
|
"addressCountry": "India"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactSchema2 = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "SiliconPin",
|
||||||
|
"url": "https://siliconpin.com",
|
||||||
|
"logo": "https://siliconpin.com/images/logo.png",
|
||||||
|
"contactPoint": {
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
"telephone": "+91-700-160-1485",
|
||||||
|
"contactType": "Customer Support",
|
||||||
|
"areaServed": "IN",
|
||||||
|
"availableLanguage": ["English", "Hindi", "Bengali"]
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://www.linkedin.com/company/siliconpin",
|
||||||
|
"https://x.com/dwd_consultancy",
|
||||||
|
"https://www.facebook.com/dwdsiliconpin",
|
||||||
|
"https://instagram.com/siliconpin.com_"
|
||||||
|
],
|
||||||
|
"openingHoursSpecification": [
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Monday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": [
|
||||||
|
"Tuesday",
|
||||||
|
"Wednesday",
|
||||||
|
"Thursday",
|
||||||
|
"Friday"
|
||||||
|
],
|
||||||
|
"opens": "09:00",
|
||||||
|
"closes": "17:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Saturday",
|
||||||
|
"opens": "10:00",
|
||||||
|
"closes": "16:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Sunday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closecs": "00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactSchema3 = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"name": "Contact Us - SiliconPin",
|
||||||
|
"url": "https://siliconpin.com/contact/",
|
||||||
|
"description": "Contact the SiliconPin team for cloud hosting, VPN, AI agents, and DevOps services. We’re here to help developers, startups, and businesses.",
|
||||||
|
"inLanguage": ["en", "hi", "bn"],
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "SiliconPin",
|
||||||
|
"url": "https://siliconpin.com"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": "SiliconPin",
|
"name": "SiliconPin",
|
||||||
"telephone": "+91-700-160-1485",
|
"url": "https://siliconpin.com"
|
||||||
"email": "contact@siliconpin.com",
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"streetAddress": "121 Lalbari, GourBongo Road",
|
|
||||||
"addressLocality": "Habra",
|
|
||||||
"addressRegion": "W.B.",
|
|
||||||
"postalCode": "743271",
|
|
||||||
"addressCountry": "India"
|
|
||||||
},
|
|
||||||
"openingHoursSpecification": [
|
|
||||||
{
|
|
||||||
"@type": "OpeningHoursSpecification",
|
|
||||||
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
|
||||||
"opens": "09:00",
|
|
||||||
"closes": "18:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@type": "OpeningHoursSpecification",
|
|
||||||
"dayOfWeek": "Saturday",
|
|
||||||
"opens": "10:00",
|
|
||||||
"closes": "16:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={pageTitle}
|
title={metaTitle}
|
||||||
description={pageDescription}
|
description={metaDescription}
|
||||||
ogImage={pageImage}
|
ogImage={ogImage}
|
||||||
type="website"
|
type="website"
|
||||||
>
|
newSchema1={JSON.stringify(contactSchema1)}
|
||||||
<script type="application/ld+json" set:html={JSON.stringify(contactSchema)}></script>
|
newSchema2={JSON.stringify(contactSchema2)}
|
||||||
|
newSchema3={JSON.stringify(contactSchema3)}
|
||||||
|
>
|
||||||
|
|
||||||
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||||
<div class="text-center mb-8 sm:mb-12">
|
<div class="text-center mb-8 sm:mb-12">
|
||||||
@@ -62,7 +123,7 @@ const contactSchema = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Full width contact form section -->
|
<!-- Full width contact form section -->
|
||||||
<section aria-labelledby="contact-form-heading" class="mb-8 sm:mb-12 bg-neutral-800 rounded-lg p-4 sm:p-6 lg:p-8 border border-neutral-700">
|
<section aria-labelledby="contact-form-heading" class="mb-8 sm:mb-12 mx-auto bg-neutral-800 rounded-lg p-4 sm:p-6 lg:p-8 border border-neutral-700">
|
||||||
<h2 id="contact-form-heading" class="text-xl sm:text-2xl font-bold text-white mb-4 sm:mb-6">Send us a message</h2>
|
<h2 id="contact-form-heading" class="text-xl sm:text-2xl font-bold text-white mb-4 sm:mb-6">Send us a message</h2>
|
||||||
<ContactForm client:load />
|
<ContactForm client:load />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,16 +1,113 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
|
||||||
|
const schema1 ={
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Cloud Hosting, VPN, DevOps & AI Services",
|
||||||
|
"description": "SiliconPin provides cloud hosting, secure VPN, AI tools, and DevOps services tailored for developers, startups, and small businesses.",
|
||||||
|
"serviceType": "Cloud Hosting, VPN, DevOps, AI Tools",
|
||||||
|
"provider": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "SiliconPin",
|
||||||
|
"url": "https://siliconpin.com",
|
||||||
|
"logo": "https://siliconpin.com/images/logo.png",
|
||||||
|
"contactPoint": {
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
"telephone": "+91-700-160-1485",
|
||||||
|
"contactType": "customer service",
|
||||||
|
"availableLanguage": ["English", "Hindi", "Bengali"]
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "121 Lalbari, GourBongo Road",
|
||||||
|
"addressLocality": "Habra",
|
||||||
|
"addressRegion": "W.B.",
|
||||||
|
"postalCode": "743271",
|
||||||
|
"addressCountry": "India"
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://www.linkedin.com/company/siliconpin",
|
||||||
|
"https://x.com/dwd_consultancy",
|
||||||
|
"https://www.facebook.com/dwdsiliconpin",
|
||||||
|
"https://instagram.com/siliconpin.com_"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"areaServed": {
|
||||||
|
"@type": "Place",
|
||||||
|
"name": "Worldwide"
|
||||||
|
},
|
||||||
|
"url": "https://siliconpin.com/services/"
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema2 ={
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "SiliconPin",
|
||||||
|
"url": "https://siliconpin.com",
|
||||||
|
"logo": "https://siliconpin.com/images/logo.png",
|
||||||
|
"description": "SiliconPin delivers affordable and scalable cloud infrastructure, VPN services, AI tools, and DevOps for modern developers and businesses.",
|
||||||
|
"contactPoint": {
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
"telephone": "+91-700-160-1485",
|
||||||
|
"contactType": "customer service",
|
||||||
|
"availableLanguage": ["English", "Hindi", "Bengali"]
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "121 Lalbari, GourBongo Road",
|
||||||
|
"addressLocality": "Habra",
|
||||||
|
"addressRegion": "W.B.",
|
||||||
|
"postalCode": "743271",
|
||||||
|
"addressCountry": "India"
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://www.linkedin.com/company/siliconpin",
|
||||||
|
"https://x.com/dwd_consultancy",
|
||||||
|
"https://www.facebook.com/dwdsiliconpin",
|
||||||
|
"https://instagram.com/siliconpin.com_"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema3 ={
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"name": "Our Services",
|
||||||
|
"url": "https://siliconpin.com/services/",
|
||||||
|
"description": "Explore SiliconPin’s cloud hosting, VPN, AI tools, and DevOps services built for developers, startups, and small businesses.",
|
||||||
|
"inLanguage": ["en", "hi", "bn"],
|
||||||
|
"isPartOf": {
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "SiliconPin",
|
||||||
|
"url": "https://siliconpin.com"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "SiliconPin",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://siliconpin.com/images/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Page-specific SEO metadata
|
// Page-specific SEO metadata
|
||||||
const pageTitle = "SiliconPin - Lets create some digital freedom";
|
const pageTitle = "SiliconPin - Lets create some digital freedom";
|
||||||
const pageDescription = "SiliconPin - easy to deploy apps and tools, freedom oriented apps and tools, high-performance, hosting solutions for PHP, Node.js, Python, Kubernetes (K8s), and K3s, and technical support.";
|
const pageDescription = "SiliconPin - easy to deploy apps and tools, freedom oriented apps and tools, high-performance, hosting solutions for PHP, Node.js, Python, Kubernetes (K8s), and K3s, and technical support.";
|
||||||
const pageImage = "/assets/logo.svg";
|
const ogImage = "/assets/logo.svg";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={pageTitle}
|
title={pageTitle}
|
||||||
description={pageDescription}
|
description={pageDescription}
|
||||||
ogImage={pageImage}
|
ogImage={ogImage}
|
||||||
|
keyWords={'here is keywords content'}
|
||||||
|
newSchema1={JSON.stringify(schema1)}
|
||||||
|
newSchema2={JSON.stringify(schema2)}
|
||||||
|
newSchema3={JSON.stringify(schema3)}
|
||||||
|
newSchema4={'here is another schema content'}
|
||||||
|
newSchema5={'here is another schema content'}
|
||||||
|
|
||||||
>
|
>
|
||||||
<!-- Canvas background animation -->
|
<!-- Canvas background animation -->
|
||||||
<div class="fixed inset-0 z-0 opacity-20 pointer-events-none">
|
<div class="fixed inset-0 z-0 opacity-20 pointer-events-none">
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<script is:inline type="module">
|
<script is:inline type="module">
|
||||||
import PocketBase from 'https://cdn.jsdelivr.net/npm/pocketbase@0.19.0/+esm';
|
import PocketBase from 'https://cdn.jsdelivr.net/npm/pocketbase@0.19.0/+esm';
|
||||||
|
|
||||||
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
||||||
let isAuthenticated = false;
|
let isAuthenticated = false;
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ import BuyVPN from "../../components/BuyServices/BuyVPN"
|
|||||||
---
|
---
|
||||||
<Layout title="Buy VPN | WireGuard | SiliconPin">
|
<Layout title="Buy VPN | WireGuard | SiliconPin">
|
||||||
<BuyVPN client:load />
|
<BuyVPN client:load />
|
||||||
</Layout>
|
</Layout>
|
||||||
@@ -1,111 +1,230 @@
|
|||||||
---
|
---
|
||||||
|
// Astro script block
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import { ServiceCard } from '../../components/ServiceCard';
|
import { ServiceCard } from '../../components/ServiceCard';
|
||||||
|
const PUBLIC_SERVICE_API_URL = import.meta.env.PUBLIC_SERVICE_API_URL;
|
||||||
// Page-specific SEO metadata
|
// console.log(PUBLIC_SERVICE_API_URL)
|
||||||
const pageTitle = "Hosting Services | SiliconPin";
|
|
||||||
const pageDescription = "Explore SiliconPin's reliable hosting services for PHP, Node.js, Python, Kubernetes (K8s), and K3s. Scalable solutions for businesses of all sizes with 24/7 support.";
|
|
||||||
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
|
||||||
|
|
||||||
// Service
|
|
||||||
const services = [
|
const services = [
|
||||||
{
|
{
|
||||||
title: 'VPN (WireGuard)',
|
imageUrl: '/assets/images/services/wiregurd-thumb.jpg',
|
||||||
description: 'WireGuard is much better than other solutions like open VPN Free VPN. as WireGuard is the best we provide WireGuard VPN',
|
|
||||||
imageUrl: '/assets/images/wiregurd-thumb.jpg',
|
|
||||||
features: ["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"],
|
|
||||||
learnMoreUrl: '/services/vpn',
|
learnMoreUrl: '/services/vpn',
|
||||||
buyButtonText: 'Buy VPN',
|
buyButtonText: 'Buy VPN',
|
||||||
buyButtonUrl: '/services/buy-vpn'
|
buyButtonUrl: '/services/buy-vpn'
|
||||||
},{
|
},{
|
||||||
title: 'Deploy an App',
|
imageUrl: '/assets/images/services/deployapp-thumb.jpg',
|
||||||
description: 'WordPress, Joomla, Drupal, PrestaShop, Wiki, Moodle, Directus, PocketBase, StarAPI and more.',
|
|
||||||
imageUrl: '/assets/images/deployapp-thumb.jpg',
|
|
||||||
features: [ 'WordPress', 'Joomla', 'Drupal', 'PrestaShop', 'Wiki', 'Moodle', 'Directus', 'PocketBase', 'StarAPI' ],
|
|
||||||
learnMoreUrl: '/services/deploy-an-app',
|
learnMoreUrl: '/services/deploy-an-app',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Deploy From Source Code',
|
|
||||||
description: 'Node.js, Python, Ruby, Go, Rust, and more. Deploy your custom applications with ease.',
|
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1599507593499-a3f7d7d97667?q=80&w=2000&auto=format&fit=crop',
|
imageUrl: 'https://images.unsplash.com/photo-1599507593499-a3f7d7d97667?q=80&w=2000&auto=format&fit=crop',
|
||||||
features: ['Node.js', 'Python', 'Ruby', 'Go', 'Rust', 'Docker', 'Kubernetes', 'JAMstack', 'Serverless'],
|
|
||||||
learnMoreUrl: '/services/deploy-from-source-code',
|
learnMoreUrl: '/services/deploy-from-source-code',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Static Site Hosting',
|
|
||||||
description: 'Secure and scalable hosting for static websites and JAMstack applications.',
|
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1526379879527-8559ecfcaec0?q=80&w=2000&auto=format&fit=crop',
|
imageUrl: 'https://images.unsplash.com/photo-1526379879527-8559ecfcaec0?q=80&w=2000&auto=format&fit=crop',
|
||||||
features: ['JAMstack', 'Gatsby', 'Hugo', 'Next.js', 'Nuxt.js', 'VuePress', 'Eleventy', 'SvelteKit', 'Astro'],
|
|
||||||
learnMoreUrl: '/services/static-site-hosting',
|
learnMoreUrl: '/services/static-site-hosting',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Python Hosting",
|
|
||||||
description: "High-performance hosting for Python applications with support for all major frameworks.",
|
|
||||||
imageUrl: "https://images.unsplash.com/photo-1526379095098-d400fd0bf935?q=80&w=2000&auto=format&fit=crop",
|
imageUrl: "https://images.unsplash.com/photo-1526379095098-d400fd0bf935?q=80&w=2000&auto=format&fit=crop",
|
||||||
features: ["Django", "Flask", "FastAPI", "Pyramid", "Bottle", "WSGI Support", "ASGI Support", "Celery", "Redis"],
|
|
||||||
learnMoreUrl: "/services/python-hosting",
|
learnMoreUrl: "/services/python-hosting",
|
||||||
buyButtonText: "",
|
buyButtonText: "",
|
||||||
buyButtonUrl: ""
|
buyButtonUrl: ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Node.js Hosting",
|
imageUrl: "/assets/images/services/node-js.jpg",
|
||||||
description: "Optimized cloud hosting for Node.js applications with automatic scaling.",
|
|
||||||
imageUrl: "/assets/node-js.jpg",
|
|
||||||
features: ["Express", "NestJS", "Koa", "Socket.io", "PM2", "WebSockets", "Serverless", "TypeScript", "GraphQL"],
|
|
||||||
learnMoreUrl: "/services/nodejs-hosting",
|
learnMoreUrl: "/services/nodejs-hosting",
|
||||||
buyButtonText: "",
|
buyButtonText: "",
|
||||||
buyButtonUrl: ""
|
buyButtonUrl: ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Kubernetes (K8s)',
|
imageUrl: '/assets/images/services/stt-streaming.png',
|
||||||
description: 'Enterprise-grade Kubernetes clusters for container orchestration at scale.',
|
learnMoreUrl: '',
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop',
|
buyButtonText: 'Purchase',
|
||||||
features: [ 'Fully managed K8s clusters', 'Auto-scaling and load balancing', 'Advanced networking options', 'Persistent storage solutions', 'Enterprise support available'],
|
buyButtonUrl: '/services/stt-streaming'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imageUrl: '/assets/images/services/kubernetes-edge.png',
|
||||||
learnMoreUrl: '/services/kubernetes',
|
learnMoreUrl: '/services/kubernetes',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'K3s Lightweight Kubernetes',
|
|
||||||
description: 'Lightweight Kubernetes for edge, IoT, and resource-constrained environments.',
|
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
||||||
features: [ 'Minimal resource requirements', 'Single binary < 100MB', 'Edge computing optimized', 'ARM support (Raspberry Pi compatible)', 'Simplified management'],
|
|
||||||
learnMoreUrl: '/services/k3s',
|
learnMoreUrl: '/services/k3s',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Hire a Human Developer',
|
|
||||||
description: 'Need a custom solution? Our experts can design a tailored App or WebApp for your specific needs.',
|
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
||||||
features: ['Node.js', 'Python', 'Ruby', 'Go', 'Rust', 'Docker', 'Kubernetes', 'JAMstack', 'Serverless'],
|
|
||||||
learnMoreUrl: '/services/hire-a-human-developer',
|
learnMoreUrl: '/services/hire-a-human-developer',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Hire an AI Agent',
|
|
||||||
description: 'Need a custom solution? Our experts can design a tailored AI Agent for your specific needs.',
|
|
||||||
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
|
||||||
features: ['Reactive Agents (Stateless, Rule-Based)', 'Proactive Agents (Stateful, Machine Learning)', 'Hybrid Agents (Reactive + Proactive)', 'Model-Based Agents (Stateful, Uses Memory)', 'Goal-Based Agents (Optimizes for an Objective)', 'Utility-Based Agents', 'Self Learning Agents', 'Autonomous AI Agents'],
|
|
||||||
learnMoreUrl: '/services/hire-an-ai-agent',
|
learnMoreUrl: '/services/hire-an-ai-agent',
|
||||||
buyButtonText: '',
|
buyButtonText: '',
|
||||||
buyButtonUrl: ''
|
buyButtonUrl: ''
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
let data = [];
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${PUBLIC_SERVICE_API_URL}?query=all-services-list`);
|
||||||
|
const json = await response.json();
|
||||||
|
if (json.success) {
|
||||||
|
// data = [...json.data, ...services];
|
||||||
|
data = json.data;
|
||||||
|
} else {
|
||||||
|
console.error("API error:", json.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fetch failed:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const servicesData = services.map((item, index) => {
|
||||||
|
return { ...item, ...data[index] };
|
||||||
|
});
|
||||||
|
|
||||||
|
// SEO
|
||||||
|
let schema1 = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "SiliconPin Hosting & Development Services",
|
||||||
|
"description": "Reliable, scalable, and secure hosting solutions for all your applications, plus custom developer and AI agent hiring.",
|
||||||
|
"provider": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "SiliconPin",
|
||||||
|
"url": "https://siliconpin.com",
|
||||||
|
"telephone": "+91-700-160-1485",
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "121 Lalbari, GourBongo Road",
|
||||||
|
"addressLocality": "Habra",
|
||||||
|
"addressRegion": "West Bengal",
|
||||||
|
"postalCode": "743271",
|
||||||
|
"addressCountry": "IN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"areaServed": "Worldwide",
|
||||||
|
"hasOfferCatalog": {
|
||||||
|
"@type": "OfferCatalog",
|
||||||
|
"name": "SiliconPin Services",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "VPN (WireGuard)",
|
||||||
|
"description": "WireGuard VPN with 600 GB bandwidth, no logs, global servers."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "App Deployment",
|
||||||
|
"description": "Deploy WordPress, Joomla, Drupal, Moodle, Directus, PocketBase, StarAPI and more."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Deploy From Source Code",
|
||||||
|
"description": "Deploy custom Node.js, Python, Ruby, Go, Rust, Docker, Kubernetes, JAMstack & serverless apps."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Static Site Hosting",
|
||||||
|
"description": "Hosting for JAMstack static sites via Gatsby, Hugo, Next.js, Nuxt.js, VuePress, SvelteKit, Astro."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Python Hosting",
|
||||||
|
"description": "High‑performance hosting for Django, Flask, FastAPI, Pyramid, Bottle with WSGI/ASGI, Celery, Redis."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Node.js Hosting",
|
||||||
|
"description": "Optimized cloud hosting of Node.js apps with auto‑scaling, WebSockets, PM2, GraphQL support."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "STT Streaming (API)",
|
||||||
|
"description": "Real‑time speech‑to‑text streaming with multi‑language support, custom vocabulary, live subtitle overlay."
|
||||||
|
},
|
||||||
|
"priceCurrency": "INR",
|
||||||
|
"priceSpecification": {
|
||||||
|
"@type": "UnitPriceSpecification",
|
||||||
|
"price": "2000",
|
||||||
|
"billingIncrement": 10000,
|
||||||
|
"description": "Loose plan per 10000 minutes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Kubernetes (K8s)",
|
||||||
|
"description": "Fully managed enterprise‑grade Kubernetes with auto‑scaling, load balancing, persistent storage."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "K3s Lightweight Kubernetes",
|
||||||
|
"description": "Lightweight K3s clusters for edge/IoT—<100 MB binary, ARM support, simplified management."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Hire a Human Developer",
|
||||||
|
"description": "Custom app/webapp development using Node.js, Python, Ruby, Go, Rust, Docker, Kubernetes, JAMstack, serverless."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"itemOffered": {
|
||||||
|
"@type": "Service",
|
||||||
|
"name": "Hire an AI Agent",
|
||||||
|
"description": "Build reactive, proactive, hybrid, self‑learning or goal‑based AI agents tailored to your needs."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pageTitle = "Hosting Services | SiliconPin";
|
||||||
|
const pageDescription = "Explore SiliconPin's reliable hosting services for PHP, Node.js, Python, Kubernetes (K8s), and K3s. Scalable solutions for businesses of all sizes with 24/7 support.";
|
||||||
|
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={pageTitle}
|
title={pageTitle}
|
||||||
description={pageDescription}
|
description={pageDescription}
|
||||||
ogImage={pageImage}
|
ogImage={pageImage}
|
||||||
type="website"
|
type="website"
|
||||||
|
newSchema1={JSON.stringify(schema1)}
|
||||||
>
|
>
|
||||||
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||||
<div class="text-center mb-8 sm:mb-12">
|
<div class="text-center mb-8 sm:mb-12">
|
||||||
@@ -118,17 +237,17 @@ const services = [
|
|||||||
<section aria-labelledby="services-heading">
|
<section aria-labelledby="services-heading">
|
||||||
<h2 id="services-heading" class="sr-only">Our hosting services</h2>
|
<h2 id="services-heading" class="sr-only">Our hosting services</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
|
||||||
{services.map((service, index) => (
|
{servicesData.map((service : any, index: number) => (
|
||||||
<ServiceCard
|
<ServiceCard
|
||||||
client:load
|
client:load
|
||||||
title={service.title}
|
title={service.title}
|
||||||
description={service.description}
|
description={service.description}
|
||||||
imageUrl={service.imageUrl}
|
imageUrl={service.imageUrl}
|
||||||
features={service.features}
|
features={service.features}
|
||||||
learnMoreUrl={service.learnMoreUrl}
|
learnMoreUrl={service.learnMoreUrl}
|
||||||
buyButtonText={service.buyButtonText}
|
buyButtonText={service.buyButtonText}
|
||||||
buyButtonUrl={service.buyButtonUrl}
|
buyButtonUrl={service.buyButtonUrl}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
7
src/pages/services/kubernetis.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
|
||||||
|
---
|
||||||
|
<Layout title="Buy VPN | WireGuard | SiliconPin">
|
||||||
|
<NewKubernetisUtho client:load />
|
||||||
|
</Layout>
|
||||||
7
src/pages/services/stt-streaming.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
import STTStreaming from "../../components/BuyServices/STTStreaming"
|
||||||
|
---
|
||||||
|
<Layout title="STT Streaming | SiliconPin">
|
||||||
|
<STTStreaming client:load />
|
||||||
|
</Layout>
|
||||||
9
src/pages/test.astro
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="">
|
||||||
|
<div class="text-center text-2xl font-bold my-4">Test to Include PHP under Astro file</div>
|
||||||
|
<div set:html="<?php echo 'Hello! I am from PHP'; ?>"></div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
100
src/pages/tmp.astro
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div>
|
||||||
|
<button id="startBtn">Start Speaking</button>
|
||||||
|
<button id="stopBtn">Stop</button>
|
||||||
|
<div id="outputText" style="min-height: 100px; border: 1px solid #ccc; padding: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
/**
|
||||||
|
* Speech-to-Text function that listens to microphone input and displays transcribed text
|
||||||
|
* @param {HTMLElement} outputElement - Element where the transcribed text will be displayed
|
||||||
|
* @param {function} onResult - Optional callback function that receives the transcribed text
|
||||||
|
* @returns {object} An object with start and stop methods to control the recognition
|
||||||
|
*/
|
||||||
|
function speechToText(outputElement, onResult) {
|
||||||
|
// Check if browser supports the Web Speech API
|
||||||
|
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
||||||
|
alert('Your browser does not support speech recognition. Try Chrome or Edge.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create speech recognition object
|
||||||
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
const recognition = new SpeechRecognition();
|
||||||
|
|
||||||
|
// Configure recognition settings
|
||||||
|
recognition.continuous = true;
|
||||||
|
recognition.interimResults = true;
|
||||||
|
recognition.lang = 'en-US'; // Set language - change as needed
|
||||||
|
|
||||||
|
let finalTranscript = '';
|
||||||
|
|
||||||
|
// Event handler for recognition results
|
||||||
|
recognition.onresult = (event) => {
|
||||||
|
let interimTranscript = '';
|
||||||
|
|
||||||
|
// Clear final transcript at the start of new session
|
||||||
|
if (event.resultIndex === 0) {
|
||||||
|
finalTranscript = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
|
const transcript = event.results[i][0].transcript;
|
||||||
|
|
||||||
|
if (event.results[i].isFinal) {
|
||||||
|
// Replace rather than append to avoid duplicates
|
||||||
|
finalTranscript = transcript;
|
||||||
|
if (onResult) onResult(finalTranscript);
|
||||||
|
} else {
|
||||||
|
interimTranscript = transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the output element
|
||||||
|
if (outputElement) {
|
||||||
|
outputElement.innerHTML = finalTranscript + (interimTranscript ? '<span style="color:#999">' + interimTranscript + '</span>' : '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = (event) => {
|
||||||
|
console.error('Speech recognition error', event.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = () => {
|
||||||
|
console.log('Speech recognition ended');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return control methods
|
||||||
|
return {
|
||||||
|
start: () => recognition.start(),
|
||||||
|
stop: () => recognition.stop(),
|
||||||
|
abort: () => recognition.abort(),
|
||||||
|
getFinalTranscript: () => finalTranscript
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DOM elements
|
||||||
|
const outputElement = document.getElementById('outputText');
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
|
||||||
|
// Initialize speech recognition
|
||||||
|
const recognizer = speechToText(outputElement, (finalText) => {
|
||||||
|
console.log('Final text:', finalText);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add button event listeners
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
outputElement.textContent = 'Listening...';
|
||||||
|
recognizer.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
recognizer.stop();
|
||||||
|
console.log('Final transcript:', recognizer.getFinalTranscript());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import TopicDetail from "../../components/TopicDetail";
|
import TopicDetail from "../../components/TopicDetail";
|
||||||
|
|
||||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||||
const { id } = Astro.params;
|
const { id } = Astro.params;
|
||||||
|
|
||||||
let topic = null;
|
let topic = null;
|
||||||
@@ -25,7 +25,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${TOPIC_API_URL}?query=get-all-topics-for-slug`, {
|
const response = await fetch(`${TOPIC_API_URL}?query=get-all-topics-for-slug`, {
|
||||||
@@ -48,8 +48,11 @@ export async function getStaticPaths() {
|
|||||||
// console.log(topic)
|
// console.log(topic)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={topic?.title ?? 'Topic | SiliconPin'} ogImage={topic.img? topic.img : '/assets/images/thumb-place.jpg'} >
|
<Layout title={`${topic.title ? topic.title + '- Silicon Topic' : 'Silicon Topic'}`} ogImage={topic.img? topic.img : '/assets/images/thumb-place.jpg'} >
|
||||||
<TopicDetail client:load topic={topic} />
|
<TopicDetail client:load topic={topic} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- topic?.title ?? 'Silicon Topic | SiliconPin' -->
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import TopicsList from "../../components/Topics";
|
import TopicsList from "../../components/Topics";
|
||||||
|
|
||||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||||
let topics = [];
|
let topics = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -19,7 +19,7 @@ try {
|
|||||||
// console.log('Topic Data', data)
|
// console.log('Topic Data', data)
|
||||||
topics = data.data || [];
|
topics = data.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('An error occurred', error);
|
console.error('An error occurred', error);
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
200
src/pages/web-tools/annyang-js.astro
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div class=" min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<div class="bg-blue-600 p-4 text-white">
|
||||||
|
<h1 class="text-2xl font-bold">Real-Time Speech Transcription</h1>
|
||||||
|
<p class="text-blue-100">Using Annyang.js for speech recognition</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800">Transcription</h2>
|
||||||
|
<div id="status" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-800">
|
||||||
|
Not Listening
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="transcript" class="h-64 p-4 border border-gray-300 rounded-lg overflow-y-auto bg-gray-50">
|
||||||
|
<p class="text-gray-500 italic">Your transcribed text will appear here...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
|
<button id="startBtn" class="flex-1 bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-4 rounded-lg transition">
|
||||||
|
Start Listening
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition" disabled>
|
||||||
|
Stop Listening
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-4 rounded-lg transition">
|
||||||
|
Clear Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Available Commands</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||||
|
<ul class="list-disc pl-5 text-sm text-gray-600">
|
||||||
|
<li>"clear transcript" - Clears the transcription</li>
|
||||||
|
<li>"new paragraph" - Inserts a new paragraph</li>
|
||||||
|
<li>"period", "comma", "question mark" - Adds punctuation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline src="https://cdnjs.cloudflare.com/ajax/libs/annyang/2.6.1/annyang.min.js"></script>
|
||||||
|
<script is:inline>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const transcript = document.getElementById('transcript');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
|
||||||
|
// Check if browser supports speech recognition
|
||||||
|
if (!annyang) {
|
||||||
|
status.textContent = "Speech recognition not supported";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add commands
|
||||||
|
const commands = {
|
||||||
|
'*text': function(phrase) {
|
||||||
|
appendToTranscript(phrase);
|
||||||
|
},
|
||||||
|
'clear transcript': function() {
|
||||||
|
clearTranscript();
|
||||||
|
},
|
||||||
|
'new paragraph': function() {
|
||||||
|
appendToTranscript("\n\n");
|
||||||
|
},
|
||||||
|
'period': function() {
|
||||||
|
appendToTranscript(". ");
|
||||||
|
},
|
||||||
|
'comma': function() {
|
||||||
|
appendToTranscript(", ");
|
||||||
|
},
|
||||||
|
'question mark': function() {
|
||||||
|
appendToTranscript("? ");
|
||||||
|
},
|
||||||
|
'exclamation mark': function() {
|
||||||
|
appendToTranscript("! ");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
annyang.addCommands(commands);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
startBtn.addEventListener('click', function() {
|
||||||
|
annyang.start({
|
||||||
|
autoRestart: true,
|
||||||
|
continuous: true
|
||||||
|
});
|
||||||
|
status.textContent = "Listening...";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-green-200 text-green-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', function() {
|
||||||
|
try {
|
||||||
|
annyang.abort();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Stopping recognition", e);
|
||||||
|
}
|
||||||
|
status.textContent = "Not Listening";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
startBtn.disabled = false;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
clearBtn.addEventListener('click', clearTranscript);
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
annyang.addCallback('start', function() {
|
||||||
|
console.log("Speech recognition started");
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('soundstart', function() {
|
||||||
|
console.log("User is speaking");
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('end', function() {
|
||||||
|
console.log("Speech recognition ended");
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('error', function(err) {
|
||||||
|
if (err.error === 'aborted') {
|
||||||
|
console.log("Recognition stopped manually");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Error:", err);
|
||||||
|
status.textContent = "Error occurred";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('errorNetwork', function(err) {
|
||||||
|
console.error("Network error:", err);
|
||||||
|
status.textContent = "Network error";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('errorPermissionBlocked', function() {
|
||||||
|
console.error("Permission blocked");
|
||||||
|
status.textContent = "Permission blocked";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
annyang.addCallback('errorPermissionDenied', function() {
|
||||||
|
console.error("Permission denied");
|
||||||
|
status.textContent = "Permission denied";
|
||||||
|
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function appendToTranscript(text) {
|
||||||
|
if (transcript.innerHTML.includes("Your transcribed text will appear here...")) {
|
||||||
|
transcript.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text === "\n\n") {
|
||||||
|
transcript.innerHTML += '<p class="mt-4"></p>';
|
||||||
|
} else {
|
||||||
|
let paragraphs = transcript.querySelectorAll('p');
|
||||||
|
let lastParagraph = paragraphs.length > 0 ? paragraphs[paragraphs.length - 1] : null;
|
||||||
|
|
||||||
|
if (!lastParagraph) {
|
||||||
|
lastParagraph = document.createElement('p');
|
||||||
|
transcript.appendChild(lastParagraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastParagraph.textContent += text;
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTranscript() {
|
||||||
|
transcript.innerHTML = '<p class="text-gray-500 italic">Your transcribed text will appear here...</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
168
src/pages/web-tools/articulate.astro
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="">
|
||||||
|
<div class="min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-blue-600 text-white p-4">
|
||||||
|
<h1 class="text-2xl font-bold">Real-Time Transcription</h1>
|
||||||
|
<p class="text-blue-100">Using Articulate.js</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Status Indicator -->
|
||||||
|
<div id="status" class="mb-4 flex items-center">
|
||||||
|
<div id="statusDot" class="w-3 h-3 rounded-full bg-gray-400 mr-2"></div>
|
||||||
|
<span id="statusText" class="text-sm">Ready to start</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transcription Output -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-gray-700 font-medium mb-2">Transcription</label>
|
||||||
|
<div id="transcriptOutput" class="min-h-32 border border-gray-300 rounded-lg p-4 bg-gray-50 overflow-y-auto max-h-64">
|
||||||
|
<p id="interimText" class="text-gray-500 italic">Your transcription will appear here...</p>
|
||||||
|
<p id="finalText" class="text-gray-800"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button id="startBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" disabled class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg flex items-center opacity-50 cursor-not-allowed">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg flex items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Language Selector -->
|
||||||
|
<select id="languageSelect" class="border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="en-US">English (US)</option>
|
||||||
|
<option value="en-GB">English (UK)</option>
|
||||||
|
<option value="es-ES">Spanish</option>
|
||||||
|
<option value="fr-FR">French</option>
|
||||||
|
<option value="de-DE">German</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="bg-gray-100 p-3 text-center text-sm text-gray-600">
|
||||||
|
<p>Click "Start" and begin speaking. Transcription will appear in real-time.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline src="https://cdn.jsdelivr.net/npm/@articulate/chromeless@1.3.0-beta5/dist/src/index.js"></script>
|
||||||
|
<script is:inline>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// DOM Elements
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const languageSelect = document.getElementById('languageSelect');
|
||||||
|
const statusDot = document.getElementById('statusDot');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
const interimText = document.getElementById('interimText');
|
||||||
|
const finalText = document.getElementById('finalText');
|
||||||
|
|
||||||
|
// Initialize Articulate.js
|
||||||
|
const articulate = new Articulate();
|
||||||
|
let isListening = false;
|
||||||
|
|
||||||
|
// Configure Articulate
|
||||||
|
function configureArticulate() {
|
||||||
|
articulate.configure({
|
||||||
|
continuous: true,
|
||||||
|
interimResults: true,
|
||||||
|
language: languageSelect.value,
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Recognition error:', error);
|
||||||
|
updateStatus('Error: ' + error.message, 'red');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status UI
|
||||||
|
function updateStatus(text, color = 'gray') {
|
||||||
|
statusText.textContent = text;
|
||||||
|
statusDot.className = `w-3 h-3 rounded-full mr-2 bg-${color}-500`;
|
||||||
|
|
||||||
|
// Handle special colors
|
||||||
|
if (color === 'green') {
|
||||||
|
statusDot.classList.add('animate-pulse');
|
||||||
|
} else {
|
||||||
|
statusDot.classList.remove('animate-pulse');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transcription
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
configureArticulate();
|
||||||
|
|
||||||
|
articulate.start()
|
||||||
|
.on('transcript', (text, isFinal) => {
|
||||||
|
if (isFinal) {
|
||||||
|
finalText.textContent += text + ' ';
|
||||||
|
interimText.textContent = '';
|
||||||
|
} else {
|
||||||
|
interimText.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
document.getElementById('transcriptOutput').scrollTop =
|
||||||
|
document.getElementById('transcriptOutput').scrollHeight;
|
||||||
|
})
|
||||||
|
.on('start', () => {
|
||||||
|
isListening = true;
|
||||||
|
updateStatus('Listening...', 'green');
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop transcription
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
articulate.stop();
|
||||||
|
isListening = false;
|
||||||
|
updateStatus('Ready to continue', 'blue');
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
stopBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear transcription
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
finalText.textContent = '';
|
||||||
|
interimText.textContent = 'Your transcription will appear here...';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change language
|
||||||
|
languageSelect.addEventListener('change', () => {
|
||||||
|
if (isListening) {
|
||||||
|
articulate.stop();
|
||||||
|
configureArticulate();
|
||||||
|
articulate.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial configuration
|
||||||
|
configureArticulate();
|
||||||
|
updateStatus('Ready to start', 'blue');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
292
src/pages/web-tools/audio-recorder-wav-16-bit-mono-16khz.astro
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="">
|
||||||
|
<div class="container mx-auto px-4" id="stt-container">
|
||||||
|
<h1>WAV Recorder (16-bit Mono 16kHz)</h1>
|
||||||
|
|
||||||
|
<div id="audioVisualizer"></div>
|
||||||
|
|
||||||
|
<button id="startBtn">Start Recording</button>
|
||||||
|
<button id="stopBtn" disabled>Stop Recording</button>
|
||||||
|
<button id="playBtn" disabled>Play Recording</button>
|
||||||
|
<button id="downloadBtn" disabled>Download WAV</button>
|
||||||
|
|
||||||
|
<div id="status">Ready to record</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
// DOM elements
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const playBtn = document.getElementById('playBtn');
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
const audioVisualizer = document.getElementById('audioVisualizer');
|
||||||
|
const statusDisplay = document.getElementById('status');
|
||||||
|
|
||||||
|
// Audio variables
|
||||||
|
let audioContext;
|
||||||
|
let mediaStream;
|
||||||
|
let processor;
|
||||||
|
let recording = false;
|
||||||
|
let audioChunks = [];
|
||||||
|
let audioBlob;
|
||||||
|
let audioUrl;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const config = {
|
||||||
|
sampleRate: 16000, // 16kHz sample rate
|
||||||
|
numChannels: 1, // Mono
|
||||||
|
bitDepth: 16 // 16-bit
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
statusDisplay.textContent = "Requesting microphone access...";
|
||||||
|
|
||||||
|
// Get microphone access
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// Create audio context with our desired sample rate
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: config.sampleRate
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a script processor node to process the audio
|
||||||
|
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
processor.onaudioprocess = processAudio;
|
||||||
|
|
||||||
|
// Create a media stream source
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
|
||||||
|
// Connect the source to the processor and to the destination
|
||||||
|
source.connect(processor);
|
||||||
|
processor.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Setup visualization
|
||||||
|
setupVisualizer(source);
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
recording = true;
|
||||||
|
audioChunks = [];
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
statusDisplay.textContent = "Recording...";
|
||||||
|
|
||||||
|
console.log('Recording started at ' + config.sampleRate + 'Hz');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
statusDisplay.textContent = "Error: " + error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process audio data
|
||||||
|
function processAudio(event) {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
// Get audio data (already mono because we set numChannels to 1)
|
||||||
|
const inputData = event.inputBuffer.getChannelData(0);
|
||||||
|
|
||||||
|
// Convert float32 to 16-bit PCM
|
||||||
|
const buffer = new ArrayBuffer(inputData.length * 2);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
for (let i = 0, offset = 0; i < inputData.length; i++, offset += 2) {
|
||||||
|
const s = Math.max(-1, Math.min(1, inputData[i]));
|
||||||
|
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the chunk
|
||||||
|
audioChunks.push(new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop recording
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
// Stop recording
|
||||||
|
recording = false;
|
||||||
|
|
||||||
|
// Disconnect processor
|
||||||
|
if (processor) {
|
||||||
|
processor.disconnect();
|
||||||
|
processor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop media stream tracks
|
||||||
|
if (mediaStream) {
|
||||||
|
mediaStream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV file from collected chunks
|
||||||
|
createWavFile();
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
playBtn.disabled = false;
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
statusDisplay.textContent = "Recording stopped. Ready to play or download.";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create WAV file from collected PCM data
|
||||||
|
function createWavFile() {
|
||||||
|
// Combine all chunks into a single buffer
|
||||||
|
const length = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||||
|
const result = new Uint8Array(length);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
audioChunks.forEach(chunk => {
|
||||||
|
result.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create WAV header
|
||||||
|
const wavHeader = createWaveHeader(result.length, config);
|
||||||
|
|
||||||
|
// Combine header and PCM data
|
||||||
|
const wavData = new Uint8Array(wavHeader.length + result.length);
|
||||||
|
wavData.set(wavHeader, 0);
|
||||||
|
wavData.set(result, wavHeader.length);
|
||||||
|
|
||||||
|
// Create blob
|
||||||
|
audioBlob = new Blob([wavData], { type: 'audio/wav' });
|
||||||
|
audioUrl = URL.createObjectURL(audioBlob);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV header
|
||||||
|
function createWaveHeader(dataLength, config) {
|
||||||
|
const byteRate = config.sampleRate * config.numChannels * (config.bitDepth / 8);
|
||||||
|
const blockAlign = config.numChannels * (config.bitDepth / 8);
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(44);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// RIFF identifier
|
||||||
|
writeString(view, 0, 'RIFF');
|
||||||
|
// RIFF chunk length
|
||||||
|
view.setUint32(4, 36 + dataLength, true);
|
||||||
|
// RIFF type
|
||||||
|
writeString(view, 8, 'WAVE');
|
||||||
|
// Format chunk identifier
|
||||||
|
writeString(view, 12, 'fmt ');
|
||||||
|
// Format chunk length
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
// Sample format (raw)
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
// Channel count
|
||||||
|
view.setUint16(22, config.numChannels, true);
|
||||||
|
// Sample rate
|
||||||
|
view.setUint32(24, config.sampleRate, true);
|
||||||
|
// Byte rate (sample rate * block align)
|
||||||
|
view.setUint32(28, byteRate, true);
|
||||||
|
// Block align (channel count * bytes per sample)
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
// Bits per sample
|
||||||
|
view.setUint16(34, config.bitDepth, true);
|
||||||
|
// Data chunk identifier
|
||||||
|
writeString(view, 36, 'data');
|
||||||
|
// Data chunk length
|
||||||
|
view.setUint32(40, dataLength, true);
|
||||||
|
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to write strings to DataView
|
||||||
|
function writeString(view, offset, string) {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play recorded audio
|
||||||
|
playBtn.addEventListener('click', () => {
|
||||||
|
if (!audioUrl) return;
|
||||||
|
|
||||||
|
const audio = new Audio(audioUrl);
|
||||||
|
audio.play();
|
||||||
|
statusDisplay.textContent = "Playing recording...";
|
||||||
|
|
||||||
|
audio.onended = () => {
|
||||||
|
statusDisplay.textContent = "Playback finished";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download recorded audio as WAV file
|
||||||
|
downloadBtn.addEventListener('click', () => {
|
||||||
|
if (!audioBlob) return;
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = audioUrl;
|
||||||
|
a.download = `recording_${config.sampleRate}Hz_${config.bitDepth}bit.wav`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
statusDisplay.textContent = "Download started";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup audio visualization
|
||||||
|
function setupVisualizer(source) {
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
analyser.fftSize = 64;
|
||||||
|
source.connect(analyser);
|
||||||
|
|
||||||
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = audioVisualizer.offsetWidth;
|
||||||
|
canvas.height = audioVisualizer.offsetHeight;
|
||||||
|
audioVisualizer.innerHTML = '';
|
||||||
|
audioVisualizer.appendChild(canvas);
|
||||||
|
|
||||||
|
const canvasCtx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
analyser.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
|
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
|
||||||
|
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const barWidth = (canvas.width / bufferLength) * 2.5;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < bufferLength; i++) {
|
||||||
|
const barHeight = dataArray[i] / 2;
|
||||||
|
|
||||||
|
canvasCtx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
|
||||||
|
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
||||||
|
|
||||||
|
x += barWidth + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#stt-container > button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#audioVisualizer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
src/pages/web-tools/deep-speech.astro
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
<Layout title="DeepSpeech Streaming STT">
|
||||||
|
<h1>DeepSpeech Real-Time Transcription</h1>
|
||||||
|
<button id="startBtn">Start Listening</button>
|
||||||
|
<button id="stopBtn" disabled>Stop</button>
|
||||||
|
<div id="transcript"></div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const transcriptDiv = document.getElementById('transcript');
|
||||||
|
|
||||||
|
let mediaStream;
|
||||||
|
let audioContext;
|
||||||
|
let processor;
|
||||||
|
let websocket;
|
||||||
|
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
// Initialize WebSocket
|
||||||
|
websocket = new WebSocket('ws://localhost:3000');
|
||||||
|
|
||||||
|
websocket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
transcriptDiv.innerHTML += data.transcript + (data.isFinal ? '<br>' : ' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get microphone
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// Set up audio processing (16kHz, mono for DeepSpeech)
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
|
||||||
|
// Resample to 16kHz (DeepSpeech requirement)
|
||||||
|
const downsampler = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
downsampler.onaudioprocess = (e) => {
|
||||||
|
const inputData = e.inputBuffer.getChannelData(0);
|
||||||
|
const pcm16 = new Int16Array(inputData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < inputData.length; i++) {
|
||||||
|
pcm16[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (websocket.readyState === WebSocket.OPEN) {
|
||||||
|
websocket.send(pcm16.buffer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
source.connect(downsampler);
|
||||||
|
downsampler.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Notify server to start processing
|
||||||
|
websocket.onopen = () => {
|
||||||
|
websocket.send('start');
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
if (mediaStream) mediaStream.getTracks().forEach(track => track.stop());
|
||||||
|
if (websocket) websocket.send('end');
|
||||||
|
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
8
src/pages/web-tools/image-labeling.astro
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import ImageLabelings from "../../components/Tools/ImageLabeling";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="">
|
||||||
|
<ImageLabelings client:load />
|
||||||
|
</Layout>
|
||||||
40
src/pages/web-tools/image-resize.astro
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import ImageResizes from "../../components/Tools/ImageResize"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div>
|
||||||
|
<ImageResizes client:load />
|
||||||
|
<!-- <input type="file" id="fileInput" accept="image/*" /> -->
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<!-- <script is:inline type="module">
|
||||||
|
// Import the library directly from CDN (as ES module)
|
||||||
|
import ImageResizeCompress from 'https://cdn.jsdelivr.net/npm/image-resize-compress/dist/index.min.js';
|
||||||
|
</script>
|
||||||
|
<script is:inline>
|
||||||
|
|
||||||
|
document.getElementById('fileInput').addEventListener('change', async function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
console.error('Selected file is not an image');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resizedBlob = await ImageResizeCompress.fromBlob(
|
||||||
|
file,
|
||||||
|
75,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'webp'
|
||||||
|
);
|
||||||
|
console.log('Resized image:', resizedBlob);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error resizing image:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script> -->
|
||||||
128
src/pages/web-tools/speech-to-text.astro
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import AudioToText from "../../components/Tools/AudioToText"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Whisper CPP Light">
|
||||||
|
<AudioToText client:load />
|
||||||
|
</Layout>
|
||||||
|
<!-- <div class="container mx-auto px-4" id="stt-container">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Audio File to STT</h1>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2" for="audioFile">
|
||||||
|
Upload Audio File (WAV format recommended)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="audioFile"
|
||||||
|
accept="audio/*,.wav"
|
||||||
|
class="block w-full text-sm text-gray-500
|
||||||
|
file:mr-4 file:py-2 file:px-4
|
||||||
|
file:rounded-md file:border-0
|
||||||
|
file:text-sm file:font-semibold
|
||||||
|
file:bg-blue-50 file:text-blue-700
|
||||||
|
hover:file:bg-blue-100"
|
||||||
|
>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Supported formats: WAV, MP3, OGG, etc.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="sendBtn"
|
||||||
|
disabled
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Send to STT API
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">API Response</h2>
|
||||||
|
<div id="responseContainer" class="p-4 bg-gray-100 rounded-md min-h-20">
|
||||||
|
<p id="statusMessage" class="text-gray-600">No file uploaded yet</p>
|
||||||
|
<pre id="apiResponse" class="hidden mt-2 p-2 bg-white rounded overflow-auto"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<!-- <script is:inline>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const audioFileInput = document.getElementById('audioFile');
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const statusMessage = document.getElementById('statusMessage');
|
||||||
|
const apiResponse = document.getElementById('apiResponse');
|
||||||
|
const responseContainer = document.getElementById('responseContainer');
|
||||||
|
|
||||||
|
// Enable send button when a file is selected
|
||||||
|
audioFileInput.addEventListener('change', () => {
|
||||||
|
if (audioFileInput.files.length > 0) {
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
statusMessage.textContent = `File selected: ${audioFileInput.files[0].name}`;
|
||||||
|
} else {
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
statusMessage.textContent = 'No file selected';
|
||||||
|
}
|
||||||
|
apiResponse.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle sending to STT API
|
||||||
|
sendBtn.addEventListener('click', async () => {
|
||||||
|
if (audioFileInput.files.length === 0) return;
|
||||||
|
|
||||||
|
const file = audioFileInput.files[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
sendBtn.textContent = 'Processing...';
|
||||||
|
statusMessage.textContent = `Sending ${file.name} to STT API...`;
|
||||||
|
apiResponse.classList.add('hidden');
|
||||||
|
responseContainer.classList.remove('bg-red-50', 'bg-green-50');
|
||||||
|
responseContainer.classList.add('bg-blue-50');
|
||||||
|
|
||||||
|
// Create FormData and append the file
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', file);
|
||||||
|
|
||||||
|
// Send to STT API
|
||||||
|
const response = await fetch('https://stt-41.siliconpin.com/stt', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
apiResponse.textContent = JSON.stringify(result, null, 2);
|
||||||
|
apiResponse.classList.remove('hidden');
|
||||||
|
statusMessage.textContent = 'STT API Response:';
|
||||||
|
responseContainer.classList.remove('bg-blue-50');
|
||||||
|
responseContainer.classList.add('bg-green-50');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
statusMessage.textContent = `Error: ${error.message}`;
|
||||||
|
responseContainer.classList.remove('bg-blue-50');
|
||||||
|
responseContainer.classList.add('bg-red-50');
|
||||||
|
} finally {
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.textContent = 'Send to STT API';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Additional styling if needed */
|
||||||
|
#stt-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#apiResponse {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
</style> -->
|
||||||
300
src/pages/web-tools/stt2.html
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WAV Recorder (16-bit Mono 16kHz)</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#audioVisualizer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WAV Recorder (16-bit Mono 16kHz)</h1>
|
||||||
|
|
||||||
|
<div id="audioVisualizer"></div>
|
||||||
|
|
||||||
|
<button id="startBtn">Start Recording</button>
|
||||||
|
<button id="stopBtn" disabled>Stop Recording</button>
|
||||||
|
<button id="playBtn" disabled>Play Recording</button>
|
||||||
|
<button id="downloadBtn" disabled>Download WAV</button>
|
||||||
|
|
||||||
|
<div id="status">Ready to record</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// DOM elements
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const playBtn = document.getElementById('playBtn');
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
const audioVisualizer = document.getElementById('audioVisualizer');
|
||||||
|
const statusDisplay = document.getElementById('status');
|
||||||
|
|
||||||
|
// Audio variables
|
||||||
|
let audioContext;
|
||||||
|
let mediaStream;
|
||||||
|
let processor;
|
||||||
|
let recording = false;
|
||||||
|
let audioChunks = [];
|
||||||
|
let audioBlob;
|
||||||
|
let audioUrl;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const config = {
|
||||||
|
sampleRate: 16000, // 16kHz sample rate
|
||||||
|
numChannels: 1, // Mono
|
||||||
|
bitDepth: 16 // 16-bit
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
statusDisplay.textContent = "Requesting microphone access...";
|
||||||
|
|
||||||
|
// Get microphone access
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// Create audio context with our desired sample rate
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: config.sampleRate
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a script processor node to process the audio
|
||||||
|
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
processor.onaudioprocess = processAudio;
|
||||||
|
|
||||||
|
// Create a media stream source
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
|
||||||
|
// Connect the source to the processor and to the destination
|
||||||
|
source.connect(processor);
|
||||||
|
processor.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Setup visualization
|
||||||
|
setupVisualizer(source);
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
recording = true;
|
||||||
|
audioChunks = [];
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
statusDisplay.textContent = "Recording...";
|
||||||
|
|
||||||
|
console.log('Recording started at ' + config.sampleRate + 'Hz');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
statusDisplay.textContent = "Error: " + error.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process audio data
|
||||||
|
function processAudio(event) {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
// Get audio data (already mono because we set numChannels to 1)
|
||||||
|
const inputData = event.inputBuffer.getChannelData(0);
|
||||||
|
|
||||||
|
// Convert float32 to 16-bit PCM
|
||||||
|
const buffer = new ArrayBuffer(inputData.length * 2);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
for (let i = 0, offset = 0; i < inputData.length; i++, offset += 2) {
|
||||||
|
const s = Math.max(-1, Math.min(1, inputData[i]));
|
||||||
|
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the chunk
|
||||||
|
audioChunks.push(new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop recording
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
if (!recording) return;
|
||||||
|
|
||||||
|
// Stop recording
|
||||||
|
recording = false;
|
||||||
|
|
||||||
|
// Disconnect processor
|
||||||
|
if (processor) {
|
||||||
|
processor.disconnect();
|
||||||
|
processor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop media stream tracks
|
||||||
|
if (mediaStream) {
|
||||||
|
mediaStream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV file from collected chunks
|
||||||
|
createWavFile();
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
playBtn.disabled = false;
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
statusDisplay.textContent = "Recording stopped. Ready to play or download.";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create WAV file from collected PCM data
|
||||||
|
function createWavFile() {
|
||||||
|
// Combine all chunks into a single buffer
|
||||||
|
const length = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||||
|
const result = new Uint8Array(length);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
audioChunks.forEach(chunk => {
|
||||||
|
result.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create WAV header
|
||||||
|
const wavHeader = createWaveHeader(result.length, config);
|
||||||
|
|
||||||
|
// Combine header and PCM data
|
||||||
|
const wavData = new Uint8Array(wavHeader.length + result.length);
|
||||||
|
wavData.set(wavHeader, 0);
|
||||||
|
wavData.set(result, wavHeader.length);
|
||||||
|
|
||||||
|
// Create blob
|
||||||
|
audioBlob = new Blob([wavData], { type: 'audio/wav' });
|
||||||
|
audioUrl = URL.createObjectURL(audioBlob);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV header
|
||||||
|
function createWaveHeader(dataLength, config) {
|
||||||
|
const byteRate = config.sampleRate * config.numChannels * (config.bitDepth / 8);
|
||||||
|
const blockAlign = config.numChannels * (config.bitDepth / 8);
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(44);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// RIFF identifier
|
||||||
|
writeString(view, 0, 'RIFF');
|
||||||
|
// RIFF chunk length
|
||||||
|
view.setUint32(4, 36 + dataLength, true);
|
||||||
|
// RIFF type
|
||||||
|
writeString(view, 8, 'WAVE');
|
||||||
|
// Format chunk identifier
|
||||||
|
writeString(view, 12, 'fmt ');
|
||||||
|
// Format chunk length
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
// Sample format (raw)
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
// Channel count
|
||||||
|
view.setUint16(22, config.numChannels, true);
|
||||||
|
// Sample rate
|
||||||
|
view.setUint32(24, config.sampleRate, true);
|
||||||
|
// Byte rate (sample rate * block align)
|
||||||
|
view.setUint32(28, byteRate, true);
|
||||||
|
// Block align (channel count * bytes per sample)
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
// Bits per sample
|
||||||
|
view.setUint16(34, config.bitDepth, true);
|
||||||
|
// Data chunk identifier
|
||||||
|
writeString(view, 36, 'data');
|
||||||
|
// Data chunk length
|
||||||
|
view.setUint32(40, dataLength, true);
|
||||||
|
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to write strings to DataView
|
||||||
|
function writeString(view, offset, string) {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play recorded audio
|
||||||
|
playBtn.addEventListener('click', () => {
|
||||||
|
if (!audioUrl) return;
|
||||||
|
|
||||||
|
const audio = new Audio(audioUrl);
|
||||||
|
audio.play();
|
||||||
|
statusDisplay.textContent = "Playing recording...";
|
||||||
|
|
||||||
|
audio.onended = () => {
|
||||||
|
statusDisplay.textContent = "Playback finished";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download recorded audio as WAV file
|
||||||
|
downloadBtn.addEventListener('click', () => {
|
||||||
|
if (!audioBlob) return;
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = audioUrl;
|
||||||
|
a.download = `recording_${config.sampleRate}Hz_${config.bitDepth}bit.wav`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
statusDisplay.textContent = "Download started";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup audio visualization
|
||||||
|
function setupVisualizer(source) {
|
||||||
|
const analyser = audioContext.createAnalyser();
|
||||||
|
analyser.fftSize = 64;
|
||||||
|
source.connect(analyser);
|
||||||
|
|
||||||
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = audioVisualizer.offsetWidth;
|
||||||
|
canvas.height = audioVisualizer.offsetHeight;
|
||||||
|
audioVisualizer.innerHTML = '';
|
||||||
|
audioVisualizer.appendChild(canvas);
|
||||||
|
|
||||||
|
const canvasCtx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
analyser.getByteFrequencyData(dataArray);
|
||||||
|
|
||||||
|
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
|
||||||
|
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const barWidth = (canvas.width / bufferLength) * 2.5;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < bufferLength; i++) {
|
||||||
|
const barHeight = dataArray[i] / 2;
|
||||||
|
|
||||||
|
canvasCtx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
|
||||||
|
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
||||||
|
|
||||||
|
x += barWidth + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
276
src/pages/web-tools/text-to-audio.astro
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div class="bg-gradient-to-br from-indigo-50 to-blue-100 min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-12">
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-10">
|
||||||
|
<h1 class="text-4xl font-bold text-indigo-800 mb-2">VoiceCraft Pro</h1>
|
||||||
|
<p class="text-lg text-indigo-600">Convert text to natural sounding speech</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Card -->
|
||||||
|
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
|
||||||
|
<!-- Text Area -->
|
||||||
|
<div class="p-6 border-b border-gray-200">
|
||||||
|
<label for="text-to-speak" class="block text-sm font-medium text-gray-700 mb-2">Enter your text</label>
|
||||||
|
<textarea id="text-to-speak" rows="6" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" placeholder="Type or paste your text here..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="p-6 border-b border-gray-200">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Voice Selection -->
|
||||||
|
<div>
|
||||||
|
<label for="voice-select" class="block text-sm font-medium text-gray-700 mb-2">Select Voice</label>
|
||||||
|
<select id="voice-select" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
|
||||||
|
<option value="">Loading voices...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pitch Control -->
|
||||||
|
<div>
|
||||||
|
<label for="pitch" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<svg class="icon inline mr-1" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.15 2.34 1.87 0 .53-.39 1.39-2.1 1.39-1.6 0-2.23-.72-2.32-1.64H8.04c.1 1.7 1.36 2.66 2.86 2.97V19h2.34v-1.67c1.52-.29 2.72-1.16 2.73-2.77-.01-2.2-1.9-2.96-3.66-3.42z"/>
|
||||||
|
</svg>
|
||||||
|
Pitch: <span id="pitch-value">1</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="pitch" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rate Control -->
|
||||||
|
<div>
|
||||||
|
<label for="rate" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<svg class="icon inline mr-1" viewBox="0 0 24 24">
|
||||||
|
<path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM9.8 8.9L7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7"/>
|
||||||
|
</svg>
|
||||||
|
Speed: <span id="rate-value">1</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="rate" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Volume Control -->
|
||||||
|
<div>
|
||||||
|
<label for="volume" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<svg class="icon inline mr-1" viewBox="0 0 24 24">
|
||||||
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||||
|
</svg>
|
||||||
|
Volume: <span id="volume-value">1</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="volume" min="0" max="1" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="p-6 flex flex-wrap justify-between items-center gap-4">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button id="speak-btn" class="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||||
|
▶ <!-- Play symbol --> Speak
|
||||||
|
</button>
|
||||||
|
<button id="pause-btn" class="px-6 py-3 bg-amber-500 hover:bg-amber-600 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||||
|
❚❚ <!-- Pause symbol --> Pause
|
||||||
|
</button>
|
||||||
|
<button id="resume-btn" class="px-6 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||||
|
▶ <!-- Play symbol --> Resume
|
||||||
|
</button>
|
||||||
|
<button id="stop-btn" class="px-6 py-3 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||||
|
■ <!-- Stop symbol --> Stop
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="download-btn" class="px-6 py-3 bg-gray-800 hover:bg-gray-900 text-white rounded-lg font-medium transition flex items-center gap-2">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||||
|
</svg>
|
||||||
|
Download Audio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div id="status" class="mt-4 text-center text-gray-500 text-sm"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// DOM Elements
|
||||||
|
const textInput = document.getElementById('text-to-speak');
|
||||||
|
const voiceSelect = document.getElementById('voice-select');
|
||||||
|
const pitchInput = document.getElementById('pitch');
|
||||||
|
const rateInput = document.getElementById('rate');
|
||||||
|
const volumeInput = document.getElementById('volume');
|
||||||
|
const pitchValue = document.getElementById('pitch-value');
|
||||||
|
const rateValue = document.getElementById('rate-value');
|
||||||
|
const volumeValue = document.getElementById('volume-value');
|
||||||
|
const speakBtn = document.getElementById('speak-btn');
|
||||||
|
const pauseBtn = document.getElementById('pause-btn');
|
||||||
|
const resumeBtn = document.getElementById('resume-btn');
|
||||||
|
const stopBtn = document.getElementById('stop-btn');
|
||||||
|
const downloadBtn = document.getElementById('download-btn');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
|
||||||
|
// Speech synthesis
|
||||||
|
const synth = window.speechSynthesis;
|
||||||
|
let voices = [];
|
||||||
|
let utterance = null;
|
||||||
|
let audioChunks = [];
|
||||||
|
let mediaRecorder;
|
||||||
|
let audioBlob;
|
||||||
|
|
||||||
|
// Update display values
|
||||||
|
pitchInput.addEventListener('input', () => pitchValue.textContent = pitchInput.value);
|
||||||
|
rateInput.addEventListener('input', () => rateValue.textContent = rateInput.value);
|
||||||
|
volumeInput.addEventListener('input', () => volumeValue.textContent = volumeInput.value);
|
||||||
|
|
||||||
|
// Load voices
|
||||||
|
function loadVoices() {
|
||||||
|
voices = synth.getVoices();
|
||||||
|
voiceSelect.innerHTML = '';
|
||||||
|
|
||||||
|
voices.forEach(voice => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.textContent = `${voice.name} (${voice.lang})${voice.default ? ' — DEFAULT' : ''}`;
|
||||||
|
option.setAttribute('data-name', voice.name);
|
||||||
|
option.setAttribute('data-lang', voice.lang);
|
||||||
|
voiceSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (voices.length === 0) {
|
||||||
|
statusEl.textContent = 'No voices available. Please try again later.';
|
||||||
|
} else {
|
||||||
|
statusEl.textContent = `${voices.length} voices loaded.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chrome needs this
|
||||||
|
if (speechSynthesis.onvoiceschanged !== undefined) {
|
||||||
|
speechSynthesis.onvoiceschanged = loadVoices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
loadVoices();
|
||||||
|
|
||||||
|
// Speak function
|
||||||
|
function speak() {
|
||||||
|
if (synth.speaking) {
|
||||||
|
synth.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textInput.value.trim() === '') {
|
||||||
|
statusEl.textContent = 'Please enter some text first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
utterance = new SpeechSynthesisUtterance(textInput.value);
|
||||||
|
|
||||||
|
const selectedOption = voiceSelect.selectedOptions[0];
|
||||||
|
if (selectedOption) {
|
||||||
|
const selectedVoice = voices.find(voice =>
|
||||||
|
voice.name === selectedOption.getAttribute('data-name') &&
|
||||||
|
voice.lang === selectedOption.getAttribute('data-lang')
|
||||||
|
);
|
||||||
|
if (selectedVoice) {
|
||||||
|
utterance.voice = selectedVoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utterance.pitch = parseFloat(pitchInput.value);
|
||||||
|
utterance.rate = parseFloat(rateInput.value);
|
||||||
|
utterance.volume = parseFloat(volumeInput.value);
|
||||||
|
|
||||||
|
// Set up MediaRecorder for audio capture
|
||||||
|
setupAudioCapture();
|
||||||
|
|
||||||
|
utterance.onstart = () => {
|
||||||
|
statusEl.textContent = 'Speaking...';
|
||||||
|
mediaRecorder.start();
|
||||||
|
};
|
||||||
|
|
||||||
|
utterance.onend = () => {
|
||||||
|
statusEl.textContent = 'Speech finished.';
|
||||||
|
mediaRecorder.stop();
|
||||||
|
};
|
||||||
|
|
||||||
|
utterance.onerror = (event) => {
|
||||||
|
statusEl.textContent = `Error occurred: ${event.error}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
synth.speak(utterance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up audio capture for download
|
||||||
|
function setupAudioCapture() {
|
||||||
|
audioChunks = [];
|
||||||
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const destination = audioContext.createMediaStreamDestination();
|
||||||
|
mediaRecorder = new MediaRecorder(destination.stream);
|
||||||
|
|
||||||
|
mediaRecorder.ondataavailable = (event) => {
|
||||||
|
if (event.data.size > 0) {
|
||||||
|
audioChunks.push(event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.onstop = () => {
|
||||||
|
audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button event listeners
|
||||||
|
speakBtn.addEventListener('click', speak);
|
||||||
|
|
||||||
|
pauseBtn.addEventListener('click', () => {
|
||||||
|
if (synth.speaking) {
|
||||||
|
synth.pause();
|
||||||
|
statusEl.textContent = 'Speech paused.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resumeBtn.addEventListener('click', () => {
|
||||||
|
if (synth.paused) {
|
||||||
|
synth.resume();
|
||||||
|
statusEl.textContent = 'Resuming speech...';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
if (synth.speaking || synth.paused) {
|
||||||
|
synth.cancel();
|
||||||
|
statusEl.textContent = 'Speech stopped.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadBtn.addEventListener('click', () => {
|
||||||
|
if (!audioBlob) {
|
||||||
|
statusEl.textContent = 'No audio to download. Please speak first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(audioBlob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'speech-output.wav';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
statusEl.textContent = 'Audio downloaded successfully.';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
70
src/pages/web-tools/transcribe.astro
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div>
|
||||||
|
<h1>Voice Recognition Demo</h1>
|
||||||
|
<button id="startBtn">Start Recording</button>
|
||||||
|
<button id="stopBtn" disabled>Stop Recording</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const resultDiv = document.getElementById('result');
|
||||||
|
|
||||||
|
let mediaRecorder;
|
||||||
|
let audioChunks = [];
|
||||||
|
const serverUrl = 'https://transcribe.teachertrainingkolkata.in/transcribe';
|
||||||
|
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
mediaRecorder = new MediaRecorder(stream, {
|
||||||
|
mimeType: 'audio/webm;codecs=opus',
|
||||||
|
audioBitsPerSecond: 16000
|
||||||
|
});
|
||||||
|
|
||||||
|
mediaRecorder.ondataavailable = (e) => {
|
||||||
|
audioChunks.push(e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.onstop = async () => {
|
||||||
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||||
|
audioChunks = [];
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', audioBlob, 'recording.webm');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(serverUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
resultDiv.textContent = data.text || "No speech detected";
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
resultDiv.textContent = 'Error sending audio to server';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.start(1000); // Collect data every 1 second
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
resultDiv.textContent = "Recording...";
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
resultDiv.textContent = 'Error accessing microphone';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
resultDiv.textContent = "Processing...";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
248
src/pages/web-tools/vosk.astro
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-3xl">
|
||||||
|
<h1 class="text-3xl font-bold mb-6 text-center">🎙️ Speech to Text</h1>
|
||||||
|
|
||||||
|
<div class="bg-gray-200 rounded-lg shadow-md p-6">
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="flex flex-wrap gap-3 mb-6">
|
||||||
|
<button id="startBtn" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed">
|
||||||
|
Start Recording
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" disabled class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed hidden">
|
||||||
|
Stop Recording
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||||
|
Clear Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div id="status" class="px-4 py-3 rounded-md mb-6 bg-gray-100 text-gray-700">
|
||||||
|
Ready to start
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transcription -->
|
||||||
|
<div id="transcription" class="p-4 border border-gray-200 rounded-md bg-gray-50 min-h-32 max-h-96 overflow-y-auto text-gray-700">
|
||||||
|
Transcribed text will appear here...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
class SpeechToTextApp {
|
||||||
|
constructor() {
|
||||||
|
this.ws = null;
|
||||||
|
this.audioContext = null;
|
||||||
|
this.processor = null;
|
||||||
|
this.stream = null;
|
||||||
|
this.isRecording = false;
|
||||||
|
|
||||||
|
this.startBtn = document.getElementById('startBtn');
|
||||||
|
this.stopBtn = document.getElementById('stopBtn');
|
||||||
|
this.clearBtn = document.getElementById('clearBtn');
|
||||||
|
this.status = document.getElementById('status');
|
||||||
|
this.transcription = document.getElementById('transcription');
|
||||||
|
|
||||||
|
this.initializeEventListeners();
|
||||||
|
this.connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeEventListeners() {
|
||||||
|
this.startBtn.addEventListener('click', () => this.startRecording());
|
||||||
|
this.stopBtn.addEventListener('click', () => this.stopRecording());
|
||||||
|
this.clearBtn.addEventListener('click', () => this.clearTranscription());
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWebSocket() {
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
// const wsUrl = `${wsProtocol}//${window.location.host}`;
|
||||||
|
const wsUrl = `${wsProtocol}//${`localhost:3000`}`;
|
||||||
|
console.log(wsUrl)
|
||||||
|
this.ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this.updateStatus('Connected to server', 'bg-green-100 text-green-700');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'transcription' && data.text) {
|
||||||
|
this.appendTranscription(data.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
this.updateStatus('Disconnected from server', 'bg-red-100 text-red-700');
|
||||||
|
setTimeout(() => this.connectWebSocket(), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
this.updateStatus('WebSocket error', 'bg-red-100 text-red-700');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async startRecording() {
|
||||||
|
try {
|
||||||
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {
|
||||||
|
sampleRate: 16000,
|
||||||
|
channelCount: 1,
|
||||||
|
echoCancellation: true,
|
||||||
|
noiseSuppression: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||||
|
sampleRate: 16000
|
||||||
|
});
|
||||||
|
|
||||||
|
const source = this.audioContext.createMediaStreamSource(this.stream);
|
||||||
|
|
||||||
|
await this.audioContext.audioWorklet.addModule('data:text/javascript,' + encodeURIComponent(`
|
||||||
|
class AudioProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.bufferSize = 4096;
|
||||||
|
this.buffer = new Float32Array(this.bufferSize);
|
||||||
|
this.bufferIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs) {
|
||||||
|
const input = inputs[0];
|
||||||
|
if (input.length > 0) {
|
||||||
|
const audioData = input[0];
|
||||||
|
|
||||||
|
for (let i = 0; i < audioData.length; i++) {
|
||||||
|
this.buffer[this.bufferIndex] = audioData[i];
|
||||||
|
this.bufferIndex++;
|
||||||
|
|
||||||
|
if (this.bufferIndex >= this.bufferSize) {
|
||||||
|
// Convert to WAV format
|
||||||
|
const int16Array = new Int16Array(this.bufferSize);
|
||||||
|
for (let j = 0; j < this.bufferSize; j++) {
|
||||||
|
int16Array[j] = Math.max(-32768, Math.min(32767, this.buffer[j] * 32768));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV header
|
||||||
|
const wavBuffer = this.createWAVBuffer(int16Array);
|
||||||
|
this.port.postMessage(wavBuffer);
|
||||||
|
|
||||||
|
this.bufferIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWAVBuffer(samples) {
|
||||||
|
const length = samples.length;
|
||||||
|
const buffer = new ArrayBuffer(44 + length * 2);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
// WAV header
|
||||||
|
const writeString = (offset, string) => {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeString(0, 'RIFF');
|
||||||
|
view.setUint32(4, 36 + length * 2, true);
|
||||||
|
writeString(8, 'WAVE');
|
||||||
|
writeString(12, 'fmt ');
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
view.setUint16(22, 1, true);
|
||||||
|
view.setUint32(24, 16000, true);
|
||||||
|
view.setUint32(28, 16000 * 2, true);
|
||||||
|
view.setUint16(32, 2, true);
|
||||||
|
view.setUint16(34, 16, true);
|
||||||
|
writeString(36, 'data');
|
||||||
|
view.setUint32(40, length * 2, true);
|
||||||
|
|
||||||
|
// Convert samples to bytes
|
||||||
|
let offset = 44;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
view.setInt16(offset, samples[i], true);
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProcessor('audio-processor', AudioProcessor);
|
||||||
|
`));
|
||||||
|
|
||||||
|
this.processor = new AudioWorkletNode(this.audioContext, 'audio-processor');
|
||||||
|
|
||||||
|
this.processor.port.onmessage = (event) => {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
source.connect(this.processor);
|
||||||
|
|
||||||
|
this.isRecording = true;
|
||||||
|
this.startBtn.disabled = true;
|
||||||
|
this.stopBtn.disabled = false;
|
||||||
|
this.stopBtn.classList.remove('hidden');
|
||||||
|
this.startBtn.textContent = 'Streaming...';
|
||||||
|
this.startBtn.classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||||
|
this.startBtn.classList.add('bg-red-600', 'hover:bg-red-700');
|
||||||
|
this.updateStatus('🔴 Streaming...', 'bg-green-100 text-green-700');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.updateStatus('Error accessing microphone: ' + error.message, 'bg-red-100 text-red-700');
|
||||||
|
console.error('Error starting recording:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRecording() {
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.getTracks().forEach(track => track.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.audioContext) {
|
||||||
|
this.audioContext.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRecording = false;
|
||||||
|
this.startBtn.disabled = false;
|
||||||
|
this.stopBtn.disabled = true;
|
||||||
|
this.stopBtn.classList.add('hidden');
|
||||||
|
this.startBtn.textContent = 'Start Recording';
|
||||||
|
this.startBtn.classList.remove('bg-red-600', 'hover:bg-red-700');
|
||||||
|
this.startBtn.classList.add('bg-green-600', 'hover:bg-green-700');
|
||||||
|
this.updateStatus('Recording stopped', 'bg-green-100 text-green-700');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTranscription() {
|
||||||
|
this.transcription.textContent = 'Transcribed text will appear here...';
|
||||||
|
this.transcription.classList.add('text-gray-500');
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTranscription(text) {
|
||||||
|
if (this.transcription.textContent === 'Transcribed text will appear here...') {
|
||||||
|
this.transcription.textContent = '';
|
||||||
|
this.transcription.classList.remove('text-gray-500');
|
||||||
|
}
|
||||||
|
this.transcription.textContent += text + ' ';
|
||||||
|
this.transcription.scrollTop = this.transcription.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(message, classes = 'bg-gray-100 text-gray-700') {
|
||||||
|
this.status.textContent = message;
|
||||||
|
this.status.className = `px-4 py-3 rounded-md mb-6 ${classes}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new SpeechToTextApp();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
100
src/pages/web-tools/web-speech.astro
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div>
|
||||||
|
<button id="startBtn">Start Speaking</button>
|
||||||
|
<button id="stopBtn">Stop</button>
|
||||||
|
<div id="outputText" style="min-height: 100px; border: 1px solid #ccc; padding: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
/**
|
||||||
|
* Speech-to-Text function that listens to microphone input and displays transcribed text
|
||||||
|
* @param {HTMLElement} outputElement - Element where the transcribed text will be displayed
|
||||||
|
* @param {function} onResult - Optional callback function that receives the transcribed text
|
||||||
|
* @returns {object} An object with start and stop methods to control the recognition
|
||||||
|
*/
|
||||||
|
function speechToText(outputElement, onResult) {
|
||||||
|
// Check if browser supports the Web Speech API
|
||||||
|
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
||||||
|
alert('Your browser does not support speech recognition. Try Chrome or Edge.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create speech recognition object
|
||||||
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
const recognition = new SpeechRecognition();
|
||||||
|
|
||||||
|
// Configure recognition settings
|
||||||
|
recognition.continuous = true;
|
||||||
|
recognition.interimResults = true;
|
||||||
|
recognition.lang = 'en-US'; // Set language - change as needed
|
||||||
|
|
||||||
|
let finalTranscript = '';
|
||||||
|
|
||||||
|
// Event handler for recognition results
|
||||||
|
recognition.onresult = (event) => {
|
||||||
|
let interimTranscript = '';
|
||||||
|
|
||||||
|
// Clear final transcript at the start of new session
|
||||||
|
if (event.resultIndex === 0) {
|
||||||
|
finalTranscript = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
|
const transcript = event.results[i][0].transcript;
|
||||||
|
|
||||||
|
if (event.results[i].isFinal) {
|
||||||
|
// Replace rather than append to avoid duplicates
|
||||||
|
finalTranscript = transcript;
|
||||||
|
if (onResult) onResult(finalTranscript);
|
||||||
|
} else {
|
||||||
|
interimTranscript = transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the output element
|
||||||
|
if (outputElement) {
|
||||||
|
outputElement.innerHTML = finalTranscript + (interimTranscript ? '<span style="color:#999">' + interimTranscript + '</span>' : '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = (event) => {
|
||||||
|
console.error('Speech recognition error', event.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = () => {
|
||||||
|
console.log('Speech recognition ended');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return control methods
|
||||||
|
return {
|
||||||
|
start: () => recognition.start(),
|
||||||
|
stop: () => recognition.stop(),
|
||||||
|
abort: () => recognition.abort(),
|
||||||
|
getFinalTranscript: () => finalTranscript
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DOM elements
|
||||||
|
const outputElement = document.getElementById('outputText');
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
|
||||||
|
// Initialize speech recognition
|
||||||
|
const recognizer = speechToText(outputElement, (finalText) => {
|
||||||
|
console.log('Final text:', finalText);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add button event listeners
|
||||||
|
startBtn.addEventListener('click', () => {
|
||||||
|
outputElement.textContent = 'Listening...';
|
||||||
|
recognizer.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
recognizer.stop();
|
||||||
|
console.log('Final transcript:', recognizer.getFinalTranscript());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
276
src/pages/web-tools/whisper-cpp.astro
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro"
|
||||||
|
---
|
||||||
|
<Layout title="">
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6">
|
||||||
|
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Whisper.cpp STT Streaming</h1>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex justify-center space-x-4 mb-4">
|
||||||
|
<button id="startBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
|
||||||
|
Start Recording
|
||||||
|
</button>
|
||||||
|
<button id="stopBtn" disabled class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||||
|
Stop Recording
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded">
|
||||||
|
Clear Text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="language">
|
||||||
|
Language
|
||||||
|
</label>
|
||||||
|
<select id="language" class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||||
|
<option value="auto">Auto-detect</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
<option value="it">Italian</option>
|
||||||
|
<option value="ja">Japanese</option>
|
||||||
|
<option value="zh">Chinese</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="model">
|
||||||
|
Model
|
||||||
|
</label>
|
||||||
|
<select id="model" class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||||
|
<option value="tiny">Tiny</option>
|
||||||
|
<option value="base">Base</option>
|
||||||
|
<option value="small">Small</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="large" selected>Large</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="status">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<div id="status" class="bg-gray-100 p-3 rounded text-sm text-gray-700">
|
||||||
|
Ready to start recording...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="transcript">
|
||||||
|
Transcript
|
||||||
|
</label>
|
||||||
|
<div id="transcript" class="bg-gray-50 p-4 rounded min-h-32 border border-gray-200">
|
||||||
|
<!-- Transcript will appear here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 mt-6">
|
||||||
|
<p>Note: This interface connects to a whisper.cpp server for processing. Audio is streamed in real-time.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<script is:inline>
|
||||||
|
// DOM Elements
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const statusDiv = document.getElementById('status');
|
||||||
|
const transcriptDiv = document.getElementById('transcript');
|
||||||
|
const languageSelect = document.getElementById('language');
|
||||||
|
const modelSelect = document.getElementById('model');
|
||||||
|
|
||||||
|
// Audio context and variables
|
||||||
|
let audioContext;
|
||||||
|
let mediaStream;
|
||||||
|
let processor;
|
||||||
|
let audioSocket;
|
||||||
|
let silenceTimeout;
|
||||||
|
const SILENCE_THRESHOLD = 0.02; // Adjust based on testing
|
||||||
|
const SILENCE_TIMEOUT_MS = 2000; // 2 seconds of silence before stopping
|
||||||
|
|
||||||
|
// WebSocket URL - adjust to your whisper.cpp server
|
||||||
|
const WS_URL = 'ws://localhost:8765';
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Check for WebAudio API support
|
||||||
|
if (!window.AudioContext && !window.webkitAudioContext) {
|
||||||
|
statusDiv.textContent = 'Web Audio API not supported in this browser';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for WebSocket support
|
||||||
|
if (!window.WebSocket) {
|
||||||
|
statusDiv.textContent = 'WebSocket not supported in this browser';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
startBtn.addEventListener('click', startRecording);
|
||||||
|
stopBtn.addEventListener('click', stopRecording);
|
||||||
|
clearBtn.addEventListener('click', clearTranscript);
|
||||||
|
|
||||||
|
async function startRecording() {
|
||||||
|
try {
|
||||||
|
statusDiv.textContent = 'Requesting microphone...';
|
||||||
|
|
||||||
|
// Get microphone access
|
||||||
|
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
// Initialize audio context
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||||
|
|
||||||
|
// Create script processor for audio processing
|
||||||
|
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||||
|
|
||||||
|
// Connect audio nodes
|
||||||
|
source.connect(processor);
|
||||||
|
processor.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Initialize WebSocket connection
|
||||||
|
audioSocket = new WebSocket(WS_URL);
|
||||||
|
|
||||||
|
audioSocket.onopen = () => {
|
||||||
|
statusDiv.textContent = 'Connected to server. Recording...';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
|
||||||
|
// Send configuration
|
||||||
|
audioSocket.send(JSON.stringify({
|
||||||
|
type: 'config',
|
||||||
|
language: languageSelect.value,
|
||||||
|
model: modelSelect.value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
audioSocket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'transcript') {
|
||||||
|
// Append to transcript
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'mb-2';
|
||||||
|
p.textContent = data.text;
|
||||||
|
transcriptDiv.appendChild(p);
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
transcriptDiv.scrollTop = transcriptDiv.scrollHeight;
|
||||||
|
} else if (data.type === 'status') {
|
||||||
|
statusDiv.textContent = data.message;
|
||||||
|
} else if (data.type === 'error') {
|
||||||
|
statusDiv.textContent = `Error: ${data.message}`;
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audioSocket.onclose = () => {
|
||||||
|
if (statusDiv.textContent !== 'Recording stopped.') {
|
||||||
|
statusDiv.textContent = 'Connection closed unexpectedly.';
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
audioSocket.onerror = (error) => {
|
||||||
|
statusDiv.textContent = `WebSocket error: ${error.message}`;
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process audio data
|
||||||
|
processor.onaudioprocess = (event) => {
|
||||||
|
if (!audioSocket || audioSocket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
const audioData = event.inputBuffer.getChannelData(0);
|
||||||
|
|
||||||
|
// Check for silence
|
||||||
|
const isSilent = isAudioSilent(audioData);
|
||||||
|
|
||||||
|
if (!isSilent) {
|
||||||
|
// Reset silence timeout
|
||||||
|
clearTimeout(silenceTimeout);
|
||||||
|
silenceTimeout = setTimeout(() => {
|
||||||
|
statusDiv.textContent = 'Silence detected, stopping recording...';
|
||||||
|
stopRecording();
|
||||||
|
}, SILENCE_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Convert Float32Array to Int16Array for WebSocket
|
||||||
|
const int16Data = convertFloat32ToInt16(audioData);
|
||||||
|
|
||||||
|
// Send audio data
|
||||||
|
audioSocket.send(int16Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.textContent = `Error: ${error.message}`;
|
||||||
|
console.error(error);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopRecording() {
|
||||||
|
statusDiv.textContent = 'Recording stopped.';
|
||||||
|
if (audioSocket && audioSocket.readyState === WebSocket.OPEN) {
|
||||||
|
audioSocket.send(JSON.stringify({ type: 'eof' }));
|
||||||
|
audioSocket.close();
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTranscript() {
|
||||||
|
transcriptDiv.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (processor) {
|
||||||
|
processor.disconnect();
|
||||||
|
processor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaStream) {
|
||||||
|
mediaStream.getTracks().forEach(track => track.stop());
|
||||||
|
mediaStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioContext) {
|
||||||
|
audioContext.close().catch(console.error);
|
||||||
|
audioContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(silenceTimeout);
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function isAudioSilent(audioData) {
|
||||||
|
// Calculate RMS (root mean square) of the audio buffer
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < audioData.length; i++) {
|
||||||
|
sum += audioData[i] * audioData[i];
|
||||||
|
}
|
||||||
|
const rms = Math.sqrt(sum / audioData.length);
|
||||||
|
return rms < SILENCE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertFloat32ToInt16(buffer) {
|
||||||
|
const length = buffer.length;
|
||||||
|
const int16Array = new Int16Array(length);
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const s = Math.max(-1, Math.min(1, buffer[i]));
|
||||||
|
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int16Array.buffer;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
433
yarn.lock
@@ -1340,6 +1340,11 @@
|
|||||||
"@types/babel__core" "^7.20.5"
|
"@types/babel__core" "^7.20.5"
|
||||||
react-refresh "^0.14.2"
|
react-refresh "^0.14.2"
|
||||||
|
|
||||||
|
"@zeit/schemas@2.36.0":
|
||||||
|
version "2.36.0"
|
||||||
|
resolved "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz"
|
||||||
|
integrity sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==
|
||||||
|
|
||||||
"@zxing/text-encoding@0.9.0":
|
"@zxing/text-encoding@0.9.0":
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz"
|
resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz"
|
||||||
@@ -1350,6 +1355,14 @@ abs-svg-path@^0.1.1:
|
|||||||
resolved "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz"
|
resolved "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz"
|
||||||
integrity sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==
|
integrity sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==
|
||||||
|
|
||||||
|
accepts@~1.3.5:
|
||||||
|
version "1.3.8"
|
||||||
|
resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz"
|
||||||
|
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
|
||||||
|
dependencies:
|
||||||
|
mime-types "~2.1.34"
|
||||||
|
negotiator "0.6.3"
|
||||||
|
|
||||||
acorn@^8.14.1:
|
acorn@^8.14.1:
|
||||||
version "8.14.1"
|
version "8.14.1"
|
||||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
|
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
|
||||||
@@ -1370,6 +1383,16 @@ agent-base@^7.0.2:
|
|||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz"
|
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz"
|
||||||
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
|
integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==
|
||||||
|
|
||||||
|
ajv@8.12.0:
|
||||||
|
version "8.12.0"
|
||||||
|
resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz"
|
||||||
|
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal "^3.1.1"
|
||||||
|
json-schema-traverse "^1.0.0"
|
||||||
|
require-from-string "^2.0.2"
|
||||||
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
ansi-align@^3.0.1:
|
ansi-align@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz"
|
||||||
@@ -1401,6 +1424,13 @@ ansi-styles@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
|
ansi-styles@^4.1.0:
|
||||||
|
version "4.3.0"
|
||||||
|
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
|
||||||
|
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||||
|
dependencies:
|
||||||
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
ansi-styles@^6.1.0, ansi-styles@^6.2.1:
|
ansi-styles@^6.1.0, ansi-styles@^6.2.1:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
|
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
|
||||||
@@ -1419,7 +1449,12 @@ anymatch@^3.1.3, anymatch@~3.1.2:
|
|||||||
normalize-path "^3.0.0"
|
normalize-path "^3.0.0"
|
||||||
picomatch "^2.0.4"
|
picomatch "^2.0.4"
|
||||||
|
|
||||||
arg@^5.0.2:
|
arch@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz"
|
||||||
|
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
|
||||||
|
|
||||||
|
arg@^5.0.2, arg@5.0.2:
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz"
|
resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz"
|
||||||
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
|
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
|
||||||
@@ -1616,6 +1651,20 @@ boolean@^3.0.1:
|
|||||||
resolved "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz"
|
resolved "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz"
|
||||||
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
|
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
|
||||||
|
|
||||||
|
boxen@7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz"
|
||||||
|
integrity sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==
|
||||||
|
dependencies:
|
||||||
|
ansi-align "^3.0.1"
|
||||||
|
camelcase "^7.0.0"
|
||||||
|
chalk "^5.0.1"
|
||||||
|
cli-boxes "^3.0.0"
|
||||||
|
string-width "^5.1.2"
|
||||||
|
type-fest "^2.13.0"
|
||||||
|
widest-line "^4.0.1"
|
||||||
|
wrap-ansi "^8.0.1"
|
||||||
|
|
||||||
boxen@8.0.1:
|
boxen@8.0.1:
|
||||||
version "8.0.1"
|
version "8.0.1"
|
||||||
resolved "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz"
|
resolved "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz"
|
||||||
@@ -1630,6 +1679,14 @@ boxen@8.0.1:
|
|||||||
widest-line "^5.0.0"
|
widest-line "^5.0.0"
|
||||||
wrap-ansi "^9.0.0"
|
wrap-ansi "^9.0.0"
|
||||||
|
|
||||||
|
brace-expansion@^1.1.7:
|
||||||
|
version "1.1.12"
|
||||||
|
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
|
||||||
|
integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
|
||||||
|
dependencies:
|
||||||
|
balanced-match "^1.0.0"
|
||||||
|
concat-map "0.0.1"
|
||||||
|
|
||||||
brace-expansion@^2.0.1:
|
brace-expansion@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz"
|
||||||
@@ -1691,6 +1748,11 @@ buffer@^6.0.3:
|
|||||||
base64-js "^1.3.1"
|
base64-js "^1.3.1"
|
||||||
ieee754 "^1.2.1"
|
ieee754 "^1.2.1"
|
||||||
|
|
||||||
|
bytes@3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz"
|
||||||
|
integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
|
||||||
|
|
||||||
cacheable-lookup@^5.0.3:
|
cacheable-lookup@^5.0.3:
|
||||||
version "5.0.4"
|
version "5.0.4"
|
||||||
resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz"
|
resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz"
|
||||||
@@ -1745,6 +1807,11 @@ camelcase-css@^2.0.1:
|
|||||||
resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
|
||||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||||
|
|
||||||
|
camelcase@^7.0.0:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz"
|
||||||
|
integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==
|
||||||
|
|
||||||
camelcase@^8.0.0:
|
camelcase@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz"
|
resolved "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz"
|
||||||
@@ -1768,11 +1835,31 @@ cfb@~1.2.1:
|
|||||||
adler-32 "~1.3.0"
|
adler-32 "~1.3.0"
|
||||||
crc-32 "~1.2.0"
|
crc-32 "~1.2.0"
|
||||||
|
|
||||||
|
chalk-template@0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz"
|
||||||
|
integrity sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==
|
||||||
|
dependencies:
|
||||||
|
chalk "^4.1.2"
|
||||||
|
|
||||||
|
chalk@^4.1.2:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
|
||||||
|
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.1.0"
|
||||||
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
chalk@^5.0.0, chalk@5.2.0:
|
chalk@^5.0.0, chalk@5.2.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz"
|
resolved "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz"
|
||||||
integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==
|
integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==
|
||||||
|
|
||||||
|
chalk@^5.0.1, chalk@5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz"
|
||||||
|
integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
|
||||||
|
|
||||||
chalk@^5.3.0:
|
chalk@^5.3.0:
|
||||||
version "5.4.1"
|
version "5.4.1"
|
||||||
resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz"
|
resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz"
|
||||||
@@ -1854,6 +1941,15 @@ cli-width@^4.1.0:
|
|||||||
resolved "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz"
|
resolved "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz"
|
||||||
integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==
|
integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==
|
||||||
|
|
||||||
|
clipboardy@3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz"
|
||||||
|
integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==
|
||||||
|
dependencies:
|
||||||
|
arch "^2.2.0"
|
||||||
|
execa "^5.1.1"
|
||||||
|
is-wsl "^2.2.0"
|
||||||
|
|
||||||
cliui@^8.0.1:
|
cliui@^8.0.1:
|
||||||
version "8.0.1"
|
version "8.0.1"
|
||||||
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
|
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
|
||||||
@@ -1955,6 +2051,36 @@ common-ancestor-path@^1.0.1:
|
|||||||
resolved "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz"
|
||||||
integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==
|
integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==
|
||||||
|
|
||||||
|
compressible@~2.0.16:
|
||||||
|
version "2.0.18"
|
||||||
|
resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz"
|
||||||
|
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
|
||||||
|
dependencies:
|
||||||
|
mime-db ">= 1.43.0 < 2"
|
||||||
|
|
||||||
|
compression@1.7.4:
|
||||||
|
version "1.7.4"
|
||||||
|
resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz"
|
||||||
|
integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
|
||||||
|
dependencies:
|
||||||
|
accepts "~1.3.5"
|
||||||
|
bytes "3.0.0"
|
||||||
|
compressible "~2.0.16"
|
||||||
|
debug "2.6.9"
|
||||||
|
on-headers "~1.0.2"
|
||||||
|
safe-buffer "5.1.2"
|
||||||
|
vary "~1.1.2"
|
||||||
|
|
||||||
|
concat-map@0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||||
|
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||||
|
|
||||||
|
content-disposition@0.5.2:
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz"
|
||||||
|
integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==
|
||||||
|
|
||||||
convert-source-map@^2.0.0:
|
convert-source-map@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
|
||||||
@@ -2048,6 +2174,13 @@ debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.7, debug@^4.4
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.3"
|
ms "^2.1.3"
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
version "2.6.9"
|
||||||
|
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
|
||||||
|
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||||
|
dependencies:
|
||||||
|
ms "2.0.0"
|
||||||
|
|
||||||
decode-named-character-reference@^1.0.0:
|
decode-named-character-reference@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz"
|
||||||
@@ -2067,6 +2200,11 @@ decompress-response@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-response "^3.1.0"
|
mimic-response "^3.1.0"
|
||||||
|
|
||||||
|
deep-extend@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz"
|
||||||
|
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||||
|
|
||||||
deepmerge@^4.3.1:
|
deepmerge@^4.3.1:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
|
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
|
||||||
@@ -2374,6 +2512,21 @@ events@^3.3.0:
|
|||||||
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
|
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
|
||||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||||
|
|
||||||
|
execa@^5.1.1:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz"
|
||||||
|
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
|
||||||
|
dependencies:
|
||||||
|
cross-spawn "^7.0.3"
|
||||||
|
get-stream "^6.0.0"
|
||||||
|
human-signals "^2.1.0"
|
||||||
|
is-stream "^2.0.0"
|
||||||
|
merge-stream "^2.0.0"
|
||||||
|
npm-run-path "^4.0.1"
|
||||||
|
onetime "^5.1.2"
|
||||||
|
signal-exit "^3.0.3"
|
||||||
|
strip-final-newline "^2.0.0"
|
||||||
|
|
||||||
execa@^7.0.0:
|
execa@^7.0.0:
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz"
|
resolved "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz"
|
||||||
@@ -2405,7 +2558,7 @@ extract-zip@^2.0.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@types/yauzl" "^2.9.1"
|
"@types/yauzl" "^2.9.1"
|
||||||
|
|
||||||
fast-deep-equal@^3.1.3:
|
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
||||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||||
@@ -2519,6 +2672,15 @@ fraction.js@^4.3.7:
|
|||||||
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
|
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
|
||||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
|
framer-motion@^12.18.1:
|
||||||
|
version "12.18.1"
|
||||||
|
resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-12.18.1.tgz"
|
||||||
|
integrity sha512-6o4EDuRPLk4LSZ1kRnnEOurbQ86MklVk+Y1rFBUKiF+d2pCdvMjWVu0ZkyMVCTwl5UyTH2n/zJEJx+jvTYuxow==
|
||||||
|
dependencies:
|
||||||
|
motion-dom "^12.18.1"
|
||||||
|
motion-utils "^12.18.1"
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
fs-extra@^11.1.0:
|
fs-extra@^11.1.0:
|
||||||
version "11.3.0"
|
version "11.3.0"
|
||||||
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz"
|
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz"
|
||||||
@@ -2598,7 +2760,7 @@ get-stream@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pump "^3.0.0"
|
pump "^3.0.0"
|
||||||
|
|
||||||
get-stream@^6.0.1:
|
get-stream@^6.0.0, get-stream@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz"
|
resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz"
|
||||||
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
|
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
|
||||||
@@ -2713,6 +2875,11 @@ h3@^1.15.0:
|
|||||||
ufo "^1.5.4"
|
ufo "^1.5.4"
|
||||||
uncrypto "^0.1.3"
|
uncrypto "^0.1.3"
|
||||||
|
|
||||||
|
has-flag@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
|
||||||
|
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||||
|
|
||||||
has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
|
has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz"
|
||||||
@@ -3121,6 +3288,11 @@ https-proxy-agent@^6.2.0:
|
|||||||
agent-base "^7.0.2"
|
agent-base "^7.0.2"
|
||||||
debug "4"
|
debug "4"
|
||||||
|
|
||||||
|
human-signals@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
|
||||||
|
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||||
|
|
||||||
human-signals@^4.3.0:
|
human-signals@^4.3.0:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz"
|
resolved "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz"
|
||||||
@@ -3136,6 +3308,11 @@ ieee754@^1.2.1:
|
|||||||
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
|
||||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||||
|
|
||||||
|
image-resize-compress@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.npmjs.org/image-resize-compress/-/image-resize-compress-2.1.1.tgz"
|
||||||
|
integrity sha512-aF4O1ZzAM0wDziIQZdqlxvDBe163J1Kiu+hSXbYNaGDcD4ggqAgRLLtRmpspnpJPHa4PkhVjwOshzBMHbOWESQ==
|
||||||
|
|
||||||
import-fresh@^3.3.0:
|
import-fresh@^3.3.0:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"
|
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"
|
||||||
@@ -3154,6 +3331,11 @@ inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
|
|||||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
|
ini@~1.3.0:
|
||||||
|
version "1.3.8"
|
||||||
|
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
|
||||||
|
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||||
|
|
||||||
inline-style-parser@0.1.1:
|
inline-style-parser@0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz"
|
resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz"
|
||||||
@@ -3234,6 +3416,11 @@ is-decimal@^2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz"
|
||||||
integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==
|
integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==
|
||||||
|
|
||||||
|
is-docker@^2.0.0:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz"
|
||||||
|
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
||||||
|
|
||||||
is-docker@^3.0.0:
|
is-docker@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz"
|
||||||
@@ -3303,6 +3490,11 @@ is-plain-obj@^4.0.0:
|
|||||||
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz"
|
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz"
|
||||||
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
|
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
|
||||||
|
|
||||||
|
is-port-reachable@4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz"
|
||||||
|
integrity sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==
|
||||||
|
|
||||||
is-regex@^1.2.1:
|
is-regex@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz"
|
||||||
@@ -3318,6 +3510,11 @@ is-regexp@^3.1.0:
|
|||||||
resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz"
|
||||||
integrity sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==
|
integrity sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==
|
||||||
|
|
||||||
|
is-stream@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz"
|
||||||
|
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
|
||||||
|
|
||||||
is-stream@^3.0.0:
|
is-stream@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz"
|
||||||
@@ -3340,6 +3537,13 @@ is-url@^1.2.4:
|
|||||||
resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz"
|
resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz"
|
||||||
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
|
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
|
||||||
|
|
||||||
|
is-wsl@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz"
|
||||||
|
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
|
||||||
|
dependencies:
|
||||||
|
is-docker "^2.0.0"
|
||||||
|
|
||||||
is-wsl@^3.1.0:
|
is-wsl@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz"
|
||||||
@@ -3373,6 +3577,11 @@ jiti@^1.21.6, jiti@>=1.21.0:
|
|||||||
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz"
|
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz"
|
||||||
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
|
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
|
||||||
|
|
||||||
|
js-cookie@^3.0.5:
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz"
|
||||||
|
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
|
||||||
|
|
||||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||||
@@ -3400,6 +3609,11 @@ json-parse-even-better-errors@^2.3.0:
|
|||||||
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
|
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==
|
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
||||||
|
|
||||||
|
json-schema-traverse@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz"
|
||||||
|
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||||
|
|
||||||
json-stringify-safe@^5.0.1:
|
json-stringify-safe@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
|
resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
|
||||||
@@ -4468,18 +4682,30 @@ micromatch@^4.0.8:
|
|||||||
braces "^3.0.3"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
mime-db@1.52.0:
|
"mime-db@>= 1.43.0 < 2", mime-db@1.52.0:
|
||||||
version "1.52.0"
|
version "1.52.0"
|
||||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
|
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
|
||||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
mime-types@^2.1.35:
|
mime-db@~1.33.0:
|
||||||
|
version "1.33.0"
|
||||||
|
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz"
|
||||||
|
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
|
||||||
|
|
||||||
|
mime-types@^2.1.35, mime-types@~2.1.34:
|
||||||
version "2.1.35"
|
version "2.1.35"
|
||||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
|
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
|
||||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db "1.52.0"
|
mime-db "1.52.0"
|
||||||
|
|
||||||
|
mime-types@2.1.18:
|
||||||
|
version "2.1.18"
|
||||||
|
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz"
|
||||||
|
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
|
||||||
|
dependencies:
|
||||||
|
mime-db "~1.33.0"
|
||||||
|
|
||||||
mimic-fn@^2.1.0:
|
mimic-fn@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz"
|
||||||
@@ -4514,7 +4740,14 @@ minimatch@^9.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^2.0.1"
|
brace-expansion "^2.0.1"
|
||||||
|
|
||||||
minimist@^1.2.6:
|
minimatch@3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||||
|
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||||
|
dependencies:
|
||||||
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
|
minimist@^1.2.0, minimist@^1.2.6:
|
||||||
version "1.2.8"
|
version "1.2.8"
|
||||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
|
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
|
||||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||||
@@ -4549,6 +4782,18 @@ mkdirp@^2.1.6:
|
|||||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz"
|
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz"
|
||||||
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
|
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
|
||||||
|
|
||||||
|
motion-dom@^12.18.1:
|
||||||
|
version "12.18.1"
|
||||||
|
resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-12.18.1.tgz"
|
||||||
|
integrity sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==
|
||||||
|
dependencies:
|
||||||
|
motion-utils "^12.18.1"
|
||||||
|
|
||||||
|
motion-utils@^12.18.1:
|
||||||
|
version "12.18.1"
|
||||||
|
resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-12.18.1.tgz"
|
||||||
|
integrity sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==
|
||||||
|
|
||||||
mri@^1.1.0:
|
mri@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz"
|
resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz"
|
||||||
@@ -4564,6 +4809,11 @@ ms@^2.1.3:
|
|||||||
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
|
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
|
||||||
|
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||||
|
|
||||||
msw@^2.7.1:
|
msw@^2.7.1:
|
||||||
version "2.7.6"
|
version "2.7.6"
|
||||||
resolved "https://registry.npmjs.org/msw/-/msw-2.7.6.tgz"
|
resolved "https://registry.npmjs.org/msw/-/msw-2.7.6.tgz"
|
||||||
@@ -4607,6 +4857,11 @@ nanoid@^3.3.8:
|
|||||||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz"
|
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz"
|
||||||
integrity sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==
|
integrity sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==
|
||||||
|
|
||||||
|
negotiator@0.6.3:
|
||||||
|
version "0.6.3"
|
||||||
|
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
|
||||||
|
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||||
|
|
||||||
neotraverse@^0.6.18:
|
neotraverse@^0.6.18:
|
||||||
version "0.6.18"
|
version "0.6.18"
|
||||||
resolved "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz"
|
resolved "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz"
|
||||||
@@ -4675,6 +4930,13 @@ not@^0.1.0:
|
|||||||
resolved "https://registry.npmjs.org/not/-/not-0.1.0.tgz"
|
resolved "https://registry.npmjs.org/not/-/not-0.1.0.tgz"
|
||||||
integrity sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==
|
integrity sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==
|
||||||
|
|
||||||
|
npm-run-path@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz"
|
||||||
|
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
|
||||||
|
dependencies:
|
||||||
|
path-key "^3.0.0"
|
||||||
|
|
||||||
npm-run-path@^5.1.0:
|
npm-run-path@^5.1.0:
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz"
|
resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz"
|
||||||
@@ -4713,6 +4975,11 @@ ofetch@^1.4.1:
|
|||||||
node-fetch-native "^1.6.4"
|
node-fetch-native "^1.6.4"
|
||||||
ufo "^1.5.4"
|
ufo "^1.5.4"
|
||||||
|
|
||||||
|
on-headers@~1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz"
|
||||||
|
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
|
||||||
|
|
||||||
once@^1.3.1, once@^1.4.0:
|
once@^1.3.1, once@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
||||||
@@ -4727,6 +4994,13 @@ onetime@^5.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn "^2.1.0"
|
mimic-fn "^2.1.0"
|
||||||
|
|
||||||
|
onetime@^5.1.2:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz"
|
||||||
|
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
|
||||||
|
dependencies:
|
||||||
|
mimic-fn "^2.1.0"
|
||||||
|
|
||||||
onetime@^6.0.0:
|
onetime@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz"
|
resolved "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz"
|
||||||
@@ -4877,7 +5151,12 @@ path-browserify@^1.0.1:
|
|||||||
resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz"
|
||||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||||
|
|
||||||
path-key@^3.1.0:
|
path-is-inside@1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz"
|
||||||
|
integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==
|
||||||
|
|
||||||
|
path-key@^3.0.0, path-key@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
|
||||||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||||
@@ -4905,6 +5184,11 @@ path-to-regexp@^6.3.0:
|
|||||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz"
|
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz"
|
||||||
integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
|
integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
|
||||||
|
|
||||||
|
path-to-regexp@3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz"
|
||||||
|
integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
|
||||||
|
|
||||||
path-type@^4.0.0:
|
path-type@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
|
||||||
@@ -5065,7 +5349,7 @@ pump@^3.0.0:
|
|||||||
end-of-stream "^1.1.0"
|
end-of-stream "^1.1.0"
|
||||||
once "^1.3.1"
|
once "^1.3.1"
|
||||||
|
|
||||||
punycode@^2.1.1, punycode@^2.3.1:
|
punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
|
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
@@ -5112,7 +5396,22 @@ radix3@^1.1.2:
|
|||||||
resolved "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz"
|
resolved "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz"
|
||||||
integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==
|
integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==
|
||||||
|
|
||||||
"react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^17.0.2 || ^18.0.0 || ^19.0.0", react-dom@>=16.8.0, react-dom@>=16.8.2, react-dom@>=18:
|
range-parser@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz"
|
||||||
|
integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==
|
||||||
|
|
||||||
|
rc@^1.0.1, rc@^1.1.6:
|
||||||
|
version "1.2.8"
|
||||||
|
resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz"
|
||||||
|
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||||
|
dependencies:
|
||||||
|
deep-extend "^0.6.0"
|
||||||
|
ini "~1.3.0"
|
||||||
|
minimist "^1.2.0"
|
||||||
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
"react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0", react-dom@>=16.8.0, react-dom@>=16.8.2, react-dom@>=18:
|
||||||
version "19.0.0"
|
version "19.0.0"
|
||||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz"
|
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz"
|
||||||
integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
|
integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
|
||||||
@@ -5240,7 +5539,7 @@ react-to-print@^3.0.5:
|
|||||||
resolved "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.5.tgz"
|
resolved "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.5.tgz"
|
||||||
integrity sha512-Z15MwMOzYCHWi26CZeFNwflAg7Nr8uWD6FTj+EkfIOjYyjr0MXGbI0c7rF4Fgrbj3XG9hFndb1ourxpPz2RAiA==
|
integrity sha512-Z15MwMOzYCHWi26CZeFNwflAg7Nr8uWD6FTj+EkfIOjYyjr0MXGbI0c7rF4Fgrbj3XG9hFndb1ourxpPz2RAiA==
|
||||||
|
|
||||||
react@*, "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ~19", "react@^17.0.2 || ^18.0.0 || ^19.0.0", react@^19.0.0, react@>=16, react@>=16.8.0, react@>=16.8.2, react@>=18:
|
react@*, "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ~19", "react@^17.0.2 || ^18.0.0 || ^19.0.0", "react@^18.0.0 || ^19.0.0", react@^19.0.0, react@>=16, react@>=16.8.0, react@>=16.8.2, react@>=18:
|
||||||
version "19.0.0"
|
version "19.0.0"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
|
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
|
||||||
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
|
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
|
||||||
@@ -5319,6 +5618,21 @@ regex@^5.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
regex-utilities "^2.3.0"
|
regex-utilities "^2.3.0"
|
||||||
|
|
||||||
|
registry-auth-token@3.3.2:
|
||||||
|
version "3.3.2"
|
||||||
|
resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz"
|
||||||
|
integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==
|
||||||
|
dependencies:
|
||||||
|
rc "^1.1.6"
|
||||||
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
registry-url@3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz"
|
||||||
|
integrity sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==
|
||||||
|
dependencies:
|
||||||
|
rc "^1.0.1"
|
||||||
|
|
||||||
rehype-attr@~2.1.0:
|
rehype-attr@~2.1.0:
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.npmjs.org/rehype-attr/-/rehype-attr-2.1.4.tgz"
|
resolved "https://registry.npmjs.org/rehype-attr/-/rehype-attr-2.1.4.tgz"
|
||||||
@@ -5698,11 +6012,16 @@ sade@^1.7.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mri "^1.1.0"
|
mri "^1.1.0"
|
||||||
|
|
||||||
safe-buffer@~5.2.0:
|
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
|
||||||
|
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||||
|
|
||||||
safe-regex-test@^1.1.0:
|
safe-regex-test@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz"
|
||||||
@@ -5759,6 +6078,36 @@ serialize-error@^7.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
type-fest "^0.13.1"
|
type-fest "^0.13.1"
|
||||||
|
|
||||||
|
serve-handler@6.1.6:
|
||||||
|
version "6.1.6"
|
||||||
|
resolved "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz"
|
||||||
|
integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==
|
||||||
|
dependencies:
|
||||||
|
bytes "3.0.0"
|
||||||
|
content-disposition "0.5.2"
|
||||||
|
mime-types "2.1.18"
|
||||||
|
minimatch "3.1.2"
|
||||||
|
path-is-inside "1.0.2"
|
||||||
|
path-to-regexp "3.3.0"
|
||||||
|
range-parser "1.2.0"
|
||||||
|
|
||||||
|
serve@^14.2.4:
|
||||||
|
version "14.2.4"
|
||||||
|
resolved "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz"
|
||||||
|
integrity sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==
|
||||||
|
dependencies:
|
||||||
|
"@zeit/schemas" "2.36.0"
|
||||||
|
ajv "8.12.0"
|
||||||
|
arg "5.0.2"
|
||||||
|
boxen "7.0.0"
|
||||||
|
chalk "5.0.1"
|
||||||
|
chalk-template "0.4.0"
|
||||||
|
clipboardy "3.0.0"
|
||||||
|
compression "1.7.4"
|
||||||
|
is-port-reachable "4.0.0"
|
||||||
|
serve-handler "6.1.6"
|
||||||
|
update-check "1.5.4"
|
||||||
|
|
||||||
set-cookie-parser@^2.6.0:
|
set-cookie-parser@^2.6.0:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz"
|
resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz"
|
||||||
@@ -5860,7 +6209,7 @@ shiki@^1.29.2:
|
|||||||
"@shikijs/vscode-textmate" "^10.0.1"
|
"@shikijs/vscode-textmate" "^10.0.1"
|
||||||
"@types/hast" "^3.0.4"
|
"@types/hast" "^3.0.4"
|
||||||
|
|
||||||
signal-exit@^3.0.2, signal-exit@^3.0.7:
|
signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
|
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
|
||||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||||
@@ -6062,11 +6411,21 @@ strip-bom@^3.0.0:
|
|||||||
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
|
||||||
integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
|
integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
|
||||||
|
|
||||||
|
strip-final-newline@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz"
|
||||||
|
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
|
||||||
|
|
||||||
strip-final-newline@^3.0.0:
|
strip-final-newline@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz"
|
||||||
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
|
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
|
||||||
|
|
||||||
|
strip-json-comments@~2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz"
|
||||||
|
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
||||||
|
|
||||||
strnum@^1.1.1:
|
strnum@^1.1.1:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz"
|
resolved "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz"
|
||||||
@@ -6113,6 +6472,13 @@ sumchecker@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
|
|
||||||
|
supports-color@^7.1.0:
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
|
||||||
|
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||||
|
dependencies:
|
||||||
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
supports-preserve-symlinks-flag@^1.0.0:
|
supports-preserve-symlinks-flag@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
||||||
@@ -6259,7 +6625,7 @@ tsconfig-paths@^4.2.0:
|
|||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
strip-bom "^3.0.0"
|
strip-bom "^3.0.0"
|
||||||
|
|
||||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.8.0:
|
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0:
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||||
@@ -6279,6 +6645,11 @@ type-fest@^0.21.3:
|
|||||||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
|
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
|
||||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||||
|
|
||||||
|
type-fest@^2.13.0:
|
||||||
|
version "2.19.0"
|
||||||
|
resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz"
|
||||||
|
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||||
|
|
||||||
type-fest@^4.21.0, type-fest@^4.26.1:
|
type-fest@^4.21.0, type-fest@^4.26.1:
|
||||||
version "4.37.0"
|
version "4.37.0"
|
||||||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz"
|
resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz"
|
||||||
@@ -6535,6 +6906,21 @@ update-browserslist-db@^1.1.1:
|
|||||||
escalade "^3.2.0"
|
escalade "^3.2.0"
|
||||||
picocolors "^1.1.1"
|
picocolors "^1.1.1"
|
||||||
|
|
||||||
|
update-check@1.5.4:
|
||||||
|
version "1.5.4"
|
||||||
|
resolved "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz"
|
||||||
|
integrity sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==
|
||||||
|
dependencies:
|
||||||
|
registry-auth-token "3.3.2"
|
||||||
|
registry-url "3.1.0"
|
||||||
|
|
||||||
|
uri-js@^4.2.2:
|
||||||
|
version "4.4.1"
|
||||||
|
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
|
||||||
|
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
|
||||||
|
dependencies:
|
||||||
|
punycode "^2.1.0"
|
||||||
|
|
||||||
url-parse@^1.5.3:
|
url-parse@^1.5.3:
|
||||||
version "1.5.10"
|
version "1.5.10"
|
||||||
resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz"
|
resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz"
|
||||||
@@ -6584,6 +6970,11 @@ uvu@^0.5.0:
|
|||||||
kleur "^4.0.3"
|
kleur "^4.0.3"
|
||||||
sade "^1.7.3"
|
sade "^1.7.3"
|
||||||
|
|
||||||
|
vary@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
|
||||||
|
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||||
|
|
||||||
vfile-location@^4.0.0:
|
vfile-location@^4.0.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz"
|
resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz"
|
||||||
@@ -6710,6 +7101,13 @@ which@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
isexe "^2.0.0"
|
||||||
|
|
||||||
|
widest-line@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz"
|
||||||
|
integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==
|
||||||
|
dependencies:
|
||||||
|
string-width "^5.0.1"
|
||||||
|
|
||||||
widest-line@^5.0.0:
|
widest-line@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz"
|
resolved "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz"
|
||||||
@@ -6754,6 +7152,15 @@ wrap-ansi@^7.0.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
wrap-ansi@^8.0.1:
|
||||||
|
version "8.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||||
|
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^6.1.0"
|
||||||
|
string-width "^5.0.1"
|
||||||
|
strip-ansi "^7.0.1"
|
||||||
|
|
||||||
wrap-ansi@^8.1.0:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
|
||||||
|
|||||||