bump
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
73
README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
16
index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>my-react-app</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
1000
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,1000 @@
|
|||
{
|
||||
"name": "my-react-app",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "my-react-app",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
|
||||
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.122.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
|
||||
"integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
|
||||
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
||||
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-rc.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rolldown/plugin-babel": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-react-compiler": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.122.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.12"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
|
||||
"integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.12",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "my-react-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
1
public/favicon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
24
public/icons.svg
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/images/windows_10.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
public/images/windows_7.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
public/images/windows_xp.jpg
Normal file
|
After Width: | Height: | Size: 347 KiB |
116
public/recorderWorker.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/* Recorder.js worker — https://github.com/mattdiamond/Recorderjs */
|
||||
var recLength = 0,
|
||||
recBuffers = [],
|
||||
sampleRate,
|
||||
numChannels;
|
||||
|
||||
this.onmessage = function(e) {
|
||||
switch (e.data.command) {
|
||||
case 'init': init(e.data.config); break;
|
||||
case 'record': record(e.data.buffer); break;
|
||||
case 'exportWAV': exportWAV(e.data.type); break;
|
||||
case 'getBuffer': getBuffer(); break;
|
||||
case 'clear': clear(); break;
|
||||
}
|
||||
};
|
||||
|
||||
function init(config) {
|
||||
sampleRate = config.sampleRate;
|
||||
numChannels = config.numChannels;
|
||||
initBuffers();
|
||||
}
|
||||
|
||||
function record(inputBuffer) {
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
recBuffers[channel].push(inputBuffer[channel]);
|
||||
}
|
||||
recLength += inputBuffer[0].length;
|
||||
}
|
||||
|
||||
function exportWAV(type) {
|
||||
var buffers = [];
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
buffers.push(mergeBuffers(recBuffers[channel], recLength));
|
||||
}
|
||||
var interleaved = numChannels === 2
|
||||
? interleave(buffers[0], buffers[1])
|
||||
: buffers[0];
|
||||
var dataview = encodeWAV(interleaved);
|
||||
var audioBlob = new Blob([dataview], { type: type });
|
||||
this.postMessage({ command: 'exportWAV', data: audioBlob });
|
||||
}
|
||||
|
||||
function getBuffer() {
|
||||
var buffers = [];
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
buffers.push(mergeBuffers(recBuffers[channel], recLength));
|
||||
}
|
||||
this.postMessage({ command: 'getBuffer', data: buffers });
|
||||
}
|
||||
|
||||
function clear() {
|
||||
recLength = 0;
|
||||
recBuffers = [];
|
||||
initBuffers();
|
||||
}
|
||||
|
||||
function initBuffers() {
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
recBuffers[channel] = [];
|
||||
}
|
||||
}
|
||||
|
||||
function mergeBuffers(recBuffers, recLength) {
|
||||
var result = new Float32Array(recLength);
|
||||
var offset = 0;
|
||||
for (var i = 0; i < recBuffers.length; i++) {
|
||||
result.set(recBuffers[i], offset);
|
||||
offset += recBuffers[i].length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function interleave(inputL, inputR) {
|
||||
var length = inputL.length + inputR.length;
|
||||
var result = new Float32Array(length);
|
||||
var index = 0, inputIndex = 0;
|
||||
while (inputIndex < inputL.length) {
|
||||
result[index++] = inputL[inputIndex];
|
||||
result[index++] = inputR[inputIndex];
|
||||
inputIndex++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(output, offset, input) {
|
||||
for (var i = 0; i < input.length; i++, offset += 2) {
|
||||
var s = Math.max(-1, Math.min(1, input[i]));
|
||||
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
}
|
||||
|
||||
function writeString(view, offset, string) {
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWAV(samples) {
|
||||
var buffer = new ArrayBuffer(44 + samples.length * 2);
|
||||
var view = new DataView(buffer);
|
||||
writeString(view, 0, 'RIFF');
|
||||
view.setUint32(4, 36 + samples.length * 2, true);
|
||||
writeString(view, 8, 'WAVE');
|
||||
writeString(view, 12, 'fmt ');
|
||||
view.setUint32(16, 16, true);
|
||||
view.setUint16(20, 1, true);
|
||||
view.setUint16(22, numChannels, true);
|
||||
view.setUint32(24, sampleRate, true);
|
||||
view.setUint32(28, sampleRate * numChannels * 2, true);
|
||||
view.setUint16(32, numChannels * 2, true);
|
||||
view.setUint16(34, 16, true);
|
||||
writeString(view, 36, 'data');
|
||||
view.setUint32(40, samples.length * 2, true);
|
||||
floatTo16BitPCM(view, 44, samples);
|
||||
return view;
|
||||
}
|
||||
28
public/recorderWorklet.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* AudioWorkletProcessor для записи PCM-данных.
|
||||
* Отправляет Float32Array-чанки на главный поток через port.
|
||||
*/
|
||||
class RecorderProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this._active = true;
|
||||
this.port.onmessage = (e) => {
|
||||
if (e.data === 'stop') this._active = false;
|
||||
};
|
||||
}
|
||||
|
||||
process(inputs) {
|
||||
if (!this._active) return false; // выгружаем процессор
|
||||
|
||||
const input = inputs[0];
|
||||
if (input && input.length > 0) {
|
||||
// Копируем каналы — данные валидны только внутри process()
|
||||
const channels = input.map(ch => new Float32Array(ch));
|
||||
// Transferable — избегаем лишних копий
|
||||
this.port.postMessage(channels, channels.map(c => c.buffer));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('recorder-processor', RecorderProcessor);
|
||||
BIN
public/yandex_search.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
267
src/App.css
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
|
||||
.day2-desktop {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.quest-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 15px;
|
||||
llotext-align: center;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.quest-panel.day2 {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
||||
}
|
||||
|
||||
.quest-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quest-day {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.quest-task {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.update-panel-day1 {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
min-width: 280px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.system-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.version-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.version-details strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.update-status {
|
||||
font-size: 12px;
|
||||
color: #4caf50;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.panel-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.update-btn, .end-day-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.update-btn:hover {
|
||||
background: #45a049;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.end-day-btn {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.end-day-btn:hover {
|
||||
background: #f57c00;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.update-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: white;
|
||||
padding: 20px 40px;
|
||||
border-radius: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
z-index: 2000;
|
||||
animation: fadeInOut 2s ease-in-out;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
|
||||
15% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
85% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
|
||||
}
|
||||
|
||||
.day1-decoration {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.watermark {
|
||||
color: rgba(255,255,255,0.3);
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
display: inline-block;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.taskbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.start-button {
|
||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
||||
padding: 8px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin-right: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.start-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.taskbar-icons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.taskbar-item {
|
||||
padding: 8px 15px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.taskbar-item:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.taskbar-item.minimized {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.system-tray {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
background: rgba(0,0,0,0.5);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
background: rgba(0,0,0,0.5);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
.icons-container {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.quest-panel.day2 {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
||||
}
|
||||
|
||||
.icons-container {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.desktop {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
18
src/App.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React, { useState } from 'react';
|
||||
// import Day1Desktop from './Day1Desktop';
|
||||
import MainApp from './MainApp';
|
||||
import ChatterboxTTS from './components/deepfake/ChatterboxTTS';
|
||||
|
||||
export type WallpaperType = 'xp' | 'win7' | 'win10';
|
||||
|
||||
const App: React.FC = () => {
|
||||
// const [day1Complete, setDay1Complete] = useState(false);
|
||||
|
||||
// if (!day1Complete) {
|
||||
// return <Day1Desktop onComplete={() => setDay1Complete(true)} />;
|
||||
// }
|
||||
return <ChatterboxTTS/>
|
||||
return <MainApp />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
126
src/Day1Desktop.css
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
.day1-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Raleway", sans-serif;
|
||||
}
|
||||
|
||||
.day1-wallpaper {
|
||||
width: 100%;
|
||||
height:100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.day1-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-family: "Raleway", sans-serif;
|
||||
background: rgba(47, 37, 191, 0.4);
|
||||
padding: 15px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day1-header-content {
|
||||
display: flex;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.day1-panel {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(47, 37, 191, 0.4);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.day1-version-info {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.day1-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.day1-update-btn {
|
||||
padding: 10px 20px;
|
||||
background: #2a0c84;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.day1-update-btn:hover:not(:disabled) {
|
||||
background: #4313a3;
|
||||
}
|
||||
|
||||
.day1-update-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.day1-end-btn {
|
||||
padding: 10px 20px;
|
||||
background: #e0af0f;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.day1-end-btn:hover {
|
||||
background: #f38518;
|
||||
}
|
||||
|
||||
.day1-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.day1-modal {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.day1-modal-title {
|
||||
color: #f44336;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.day1-modal-text {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.day1-modal-btn {
|
||||
padding: 10px 20px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.day1-modal-btn:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
100
src/Day1Desktop.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import React, { useState } from 'react';
|
||||
import type { WallpaperType } from './App';
|
||||
import './Day1Desktop.css';
|
||||
|
||||
interface Day1DesktopProps {
|
||||
onComplete: (type: WallpaperType) => void;
|
||||
}
|
||||
|
||||
const Day1Desktop: React.FC<Day1DesktopProps> = ({ onComplete }) => {
|
||||
const [version, setVersion] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const getImage = () => {
|
||||
if (version === 1) return '/images/windows_xp.jpg';
|
||||
if (version === 2) return '/images/windows_7.jpg';
|
||||
return '/images/windows_10.jpg';
|
||||
};
|
||||
|
||||
const getVersionName = () => {
|
||||
if (version === 1) return 'Windows XP';
|
||||
if (version === 2) return 'Windows 7';
|
||||
return 'Windows 10';
|
||||
};
|
||||
|
||||
const getWallpaperType = (): WallpaperType => {
|
||||
if (version === 1) return 'xp';
|
||||
if (version === 2) return 'win7';
|
||||
return 'win10';
|
||||
};
|
||||
|
||||
const updateSystem = () => {
|
||||
if (version === 1) {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setVersion(2);
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
} else if (version === 2) {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setVersion(3);
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const endDay = () => {
|
||||
if (version === 3) {
|
||||
onComplete(getWallpaperType());
|
||||
} else {
|
||||
setError(`Ошибка! Нельзя завершить день на старой версии ${getVersionName()}. Нужно обновиться до Windows 10`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="day1-container">
|
||||
<img src={getImage()} alt="wallpaper" className="day1-wallpaper" />
|
||||
|
||||
<div className="day1-header">
|
||||
<div className="day1-header-content">
|
||||
<span>Кейс 1: Обновление системы</span>
|
||||
<span>Выберите наиболее подходящую версию Windows</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="day1-panel">
|
||||
<div className="day1-version-info">
|
||||
<strong>Текущая версия:</strong> {getVersionName()}
|
||||
</div>
|
||||
<div className="day1-buttons">
|
||||
{version !== 3 && (
|
||||
<button
|
||||
className="day1-update-btn"
|
||||
onClick={updateSystem}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Обновление...' : 'Обновить до Windows 10'}
|
||||
</button>
|
||||
)}
|
||||
<button className="day1-end-btn" onClick={endDay}>
|
||||
Закончить день 1
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="day1-modal-overlay">
|
||||
<div className="day1-modal">
|
||||
<h3 className="day1-modal-title">Ошибка</h3>
|
||||
<p className="day1-modal-text">{error}</p>
|
||||
<button className="day1-modal-btn" onClick={() => setError('')}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Day1Desktop;
|
||||
98
src/MainApp.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React, { useState } from 'react';
|
||||
import DesktopIcon from './components/DesktopIcon';
|
||||
import Window from './components/Window';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import './App.css';
|
||||
import VScodeApp from './apps/vscode/vscode';
|
||||
import YandexApp from './apps/yandex/Yandex';
|
||||
import TerminalApp from './apps/terminal/Terminal';
|
||||
|
||||
export interface WindowType {
|
||||
id: string;
|
||||
title: string;
|
||||
content: any;
|
||||
icon: string;
|
||||
isMinimized: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const MainApp: React.FC = () => {
|
||||
const [windows, setWindows] = useState<WindowType[]>([]);
|
||||
const [nextId, setNextId] = useState(1);
|
||||
|
||||
const openWindow = (title: string, content: any, icon: string, url: string) => {
|
||||
const newWindow: WindowType = {
|
||||
id: `window-${nextId}`,
|
||||
title,
|
||||
content,
|
||||
icon,
|
||||
isMinimized: false,
|
||||
url,
|
||||
};
|
||||
setWindows([...windows, newWindow]);
|
||||
setNextId(nextId + 1);
|
||||
};
|
||||
|
||||
const closeWindow = (id: string) => {
|
||||
setWindows(windows.filter(w => w.id !== id));
|
||||
};
|
||||
|
||||
const minimizeWindow = (id: string) => {
|
||||
setWindows(windows.map(w =>
|
||||
w.id === id ? { ...w, isMinimized: !w.isMinimized } : w
|
||||
));
|
||||
};
|
||||
|
||||
const restoreWindow = (id: string) => {
|
||||
setWindows(windows.map(w =>
|
||||
w.id === id ? { ...w, isMinimized: false } : w
|
||||
));
|
||||
};
|
||||
|
||||
const openVS = () => {
|
||||
openWindow('VS Code', <VScodeApp />, '💻', 'https://code.visualstudio.com');
|
||||
};
|
||||
|
||||
const openYandex = () => {
|
||||
openWindow('Yandex', <YandexApp />, '🌐', 'https://yandex.ru');
|
||||
};
|
||||
|
||||
const openTerminal = () => {
|
||||
openWindow('Terminal', <TerminalApp />, 'T', 'https://oop.com');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="desktop" style={{
|
||||
backgroundImage: 'url(/images/windows_10.jpg)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div className="icons-container">
|
||||
<DesktopIcon icon="💻" name="VS Code" onClick={openVS} />
|
||||
<DesktopIcon icon="🌐" name="Yandex" onClick={openYandex} />
|
||||
<DesktopIcon icon="T" name="Terminal" onClick={openTerminal} />
|
||||
</div>
|
||||
|
||||
{windows.map(window => !window.isMinimized && (
|
||||
<Window
|
||||
key={window.id}
|
||||
window={window}
|
||||
onClose={closeWindow}
|
||||
onMinimize={minimizeWindow}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Sidebar
|
||||
windows={windows}
|
||||
onRestore={restoreWindow}
|
||||
onClose={closeWindow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainApp;
|
||||
101
src/apps/terminal/Terminal.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
|
||||
const TerminalApp = () => {
|
||||
const [currentTaskState, setTaskState] = useState(false);
|
||||
const [is_updated_libraries_state, set_updated_libraries_state] = useState(false);
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [history, setHistory] = useState([
|
||||
{ type: 'system', text: 'Добро пожаловать в мини-терминал. Введите "help" для списка команд.' }
|
||||
]);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Автопрокрутка вниз при новых сообщениях
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [history]);
|
||||
|
||||
const commands = {
|
||||
help: () => 'Доступные команды: help, clear, apt',
|
||||
|
||||
"apt": () => "apt is package manager. Use 'apt help' for more info",
|
||||
"apt help": () => "apt avalible commands: apt update; apt upgrade;",
|
||||
"apt update": () => {
|
||||
set_updated_libraries_state(true);
|
||||
return "Yup, package list update, you can update system"
|
||||
},
|
||||
"apt upgrade": () => {
|
||||
if(is_updated_libraries_state) {
|
||||
setTaskState(true) // mark as done
|
||||
alert("Done!")
|
||||
}
|
||||
return "Yup, system and libraries updated for latest version"
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
setHistory([]);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCommand = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const cleanInput = input.trim().toLowerCase();
|
||||
const newHistory = [...history, { type: 'user', text: `> ${input}` }];
|
||||
|
||||
if (cleanInput) {
|
||||
if (cleanInput in commands) {
|
||||
const result = commands[cleanInput as keyof typeof commands]();
|
||||
if (result){
|
||||
newHistory.push({ type: 'bot', text: result });
|
||||
setHistory(newHistory);
|
||||
}
|
||||
} else {
|
||||
newHistory.push({ type: 'error', text: `Команда "${cleanInput}" не найдена. Введите "help".` });
|
||||
setHistory(newHistory);
|
||||
}
|
||||
}
|
||||
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.terminal}>
|
||||
{history.map((line, i) => (
|
||||
<div key={i} style={{ ...styles.line, ...styles[line.type as keyof typeof styles] }}>
|
||||
{line.text}
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
<div style={styles.inputLine}>
|
||||
<span style={styles.prompt}>$</span>
|
||||
<input
|
||||
style={styles.input}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleCommand}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{currentTaskState}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: { backgroundColor: '#1e1e1e', color: '#00ff00', padding: '20px', fontFamily: 'monospace', height: '400px', display: 'flex', flexDirection: 'column' as const, borderRadius: '8px' },
|
||||
terminal: { flex: 1, overflowY: 'auto' as const, marginBottom: '10px' },
|
||||
line: { marginBottom: '4px', whiteSpace: 'pre-wrap' as const, textAlign: 'justify' as const },
|
||||
user: { color: '#fff' },
|
||||
error: { color: '#ff5555' },
|
||||
system: { color: '#aaa' },
|
||||
inputLine: { display: 'flex', alignItems: 'center' },
|
||||
prompt: { marginRight: '8px', fontWeight: 'bold' },
|
||||
input: { background: 'transparent', border: 'none', color: '#00ff00', outline: 'none', flex: 1, fontFamily: 'monospace', fontSize: '16px' }
|
||||
};
|
||||
|
||||
export default TerminalApp;
|
||||
3
src/apps/vscode/vscode.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.vscode_main_window {
|
||||
background: blue;
|
||||
}
|
||||
14
src/apps/vscode/vscode.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import './vscode.css';
|
||||
|
||||
const VScodeApp: React.FC = () => {
|
||||
return (
|
||||
<div className='vscode_main_window'>
|
||||
<h1>Vscode</h1>
|
||||
<button>1</button>
|
||||
<button>22</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VScodeApp;
|
||||
5
src/apps/yandex/Yandex.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.yandex {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("/yandex_search.jpg");
|
||||
}
|
||||
14
src/apps/yandex/Yandex.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import './Yandex.css';
|
||||
|
||||
const YandexApp: React.FC = () => {
|
||||
return (
|
||||
<div className='yandex'>
|
||||
{/* <h1>Vscode</h1>
|
||||
<button>1</button>
|
||||
<button>22</button> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default YandexApp;
|
||||
BIN
src/assets/hero.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
1
src/assets/react.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
1
src/assets/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
29
src/components/DekstopIcon.css
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
.desktop-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.desktop-icon:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
54
src/components/DesktopIcon.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
|
||||
interface DesktopIconProps {
|
||||
icon: string;
|
||||
name: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const DesktopIcon: React.FC<DesktopIconProps> = ({ icon, name, onClick }) => {
|
||||
const styles = {
|
||||
desktopIcon: {
|
||||
display: 'flex' as const,
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '80px',
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.2s',
|
||||
color: 'white',
|
||||
textShadow: '0 1px 2px rgba(0,0,0,0.5)',
|
||||
},
|
||||
iconImage: {
|
||||
fontSize: '48px',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
iconName: {
|
||||
fontSize: '12px',
|
||||
textAlign: 'center' as const,
|
||||
fontWeight: 500,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={styles.desktopIcon}
|
||||
onClick={onClick}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
|
||||
e.currentTarget.style.transform = 'scale(1.05)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
<div style={styles.iconImage}>{icon}</div>
|
||||
<span style={styles.iconName}>{name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopIcon;
|
||||
66
src/components/Sidebar.css
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
.sidebar-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(32, 32, 32, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
z-index: 1000;
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
min-width: 120px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sidebar-item.minimized {
|
||||
background: rgba(255,255,255,0.05);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-close:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: #ff4444;
|
||||
}
|
||||
125
src/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import React from 'react';
|
||||
|
||||
interface WindowData {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
icon: string;
|
||||
isMinimized: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
windows: WindowData[];
|
||||
onRestore: (id: string) => void;
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ windows, onRestore, onClose }) => {
|
||||
const styles = {
|
||||
sidebar: {
|
||||
position: 'fixed' as const,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: 'rgba(32, 32, 32, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
padding: '8px 16px',
|
||||
zIndex: 1000,
|
||||
borderTop: '1px solid rgba(255,255,255,0.2)',
|
||||
overflowX: 'auto' as const,
|
||||
},
|
||||
item: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '6px 12px',
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
color: 'white',
|
||||
fontSize: '13px',
|
||||
minWidth: '120px',
|
||||
justifyContent: 'space-between' as const,
|
||||
},
|
||||
itemMinimized: {
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
opacity: 0.7,
|
||||
},
|
||||
icon: {
|
||||
fontSize: '16px',
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
whiteSpace: 'nowrap' as const,
|
||||
overflow: 'hidden' as const,
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
closeBtn: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
transition: 'all 0.2s',
|
||||
},
|
||||
};
|
||||
|
||||
if (windows.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.sidebar}>
|
||||
{windows.map((window) => (
|
||||
<div
|
||||
key={window.id}
|
||||
style={{
|
||||
...styles.item,
|
||||
...(window.isMinimized ? styles.itemMinimized : {})
|
||||
}}
|
||||
onClick={() => {
|
||||
if (window.isMinimized) {
|
||||
onRestore(window.id);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = window.isMinimized
|
||||
? 'rgba(255,255,255,0.05)'
|
||||
: 'rgba(255,255,255,0.1)';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<span style={styles.icon}>{window.icon}</span>
|
||||
<span style={styles.title}>{window.title}</span>
|
||||
<button
|
||||
style={styles.closeBtn}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose(window.id);
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
|
||||
e.currentTarget.style.color = '#ff4444';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = 'white';
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
117
src/components/WallpaperSelector.css
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
.wallpaper-selector {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.selector-container {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 20px;
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
box-shadow: 0 30px 80px rgba(0,0,0,0.3);
|
||||
max-width: 1200px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.quest-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.windows-logo {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.selector-container h1 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.wallpaper-options {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
justify-content: center;
|
||||
margin-bottom: 50px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wallpaper-option {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.wallpaper-option:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.wallpaper-preview {
|
||||
width: 280px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.xp-preview {
|
||||
background: linear-gradient(135deg, #3a8c3a 0%, #1e5c1e 100%);
|
||||
}
|
||||
|
||||
.win7-preview {
|
||||
background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
|
||||
}
|
||||
|
||||
.win10-preview {
|
||||
background: linear-gradient(135deg, #0078d7 0%, #005a9e 100%);
|
||||
}
|
||||
|
||||
.option-info {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-info h3 {
|
||||
margin: 0 0 5px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.option-info p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.selector-footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
52
src/components/WallpaperSelector.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import type { WallpaperType } from '../App';
|
||||
import './WallpaperSelector.css';
|
||||
|
||||
interface WallpaperSelectorProps {
|
||||
onSelect: (type: WallpaperType) => void;
|
||||
}
|
||||
|
||||
const WallpaperSelector: React.FC<WallpaperSelectorProps> = ({ onSelect }) => {
|
||||
return (
|
||||
<div className="wallpaper-selector">
|
||||
<div className="selector-container">
|
||||
<div className="quest-badge">День 1</div>
|
||||
<div className="windows-logo">🪟</div>
|
||||
<h1>Квест: Выбор темы</h1>
|
||||
<p className="subtitle">Попробуй разные версии Windows и почувствуй разницу!</p>
|
||||
|
||||
<div className="wallpaper-options">
|
||||
<div className="wallpaper-option" onClick={() => onSelect('xp')}>
|
||||
<div className="wallpaper-preview xp-preview">🏔️</div>
|
||||
<div className="option-info">
|
||||
<h3>Windows XP</h3>
|
||||
<p>Классический стиль 2001 года</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wallpaper-option" onClick={() => onSelect('win7')}>
|
||||
<div className="wallpaper-preview win7-preview">🐟</div>
|
||||
<div className="option-info">
|
||||
<h3>Windows 7</h3>
|
||||
<p>Современная классика 2009 года</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wallpaper-option" onClick={() => onSelect('win10')}>
|
||||
<div className="wallpaper-preview win10-preview">🪟</div>
|
||||
<div className="option-info">
|
||||
<h3>Windows 10</h3>
|
||||
<p>Современный дизайн 2015 года</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="selector-footer">
|
||||
<p>💡 Подсказка: Выберите любую тему, чтобы продолжить квест</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WallpaperSelector;
|
||||
91
src/components/Window.css
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
.window {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
min-height: 400px;
|
||||
background: rgb(255, 255, 255);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
animation: fadeIn 0.2s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.window-header {
|
||||
background: #1a0000;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.window-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.window-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.window-btn.minimize:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.window-btn.close:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.window-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
49
src/components/Window.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import './Window.css';
|
||||
import type { WindowType } from '../MainApp';
|
||||
|
||||
interface WindowProps {
|
||||
window: WindowType;
|
||||
onClose: (id: string) => void;
|
||||
onMinimize: (id: string) => void;
|
||||
}
|
||||
|
||||
const Window: React.FC<WindowProps> = ({ window, onClose, onMinimize }) => {
|
||||
return (
|
||||
<div className="window">
|
||||
<div className="window-header">
|
||||
<div className="window-title">
|
||||
<span className="window-icon">{window.icon}</span>
|
||||
<span>{window.title}</span>
|
||||
</div>
|
||||
<div className="window-controls">
|
||||
<button
|
||||
className="window-btn minimize"
|
||||
onClick={() => onMinimize(window.id)}
|
||||
title="Minimize"
|
||||
>
|
||||
─
|
||||
</button>
|
||||
<button
|
||||
className="window-btn close"
|
||||
onClick={() => onClose(window.id)}
|
||||
title="Close"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="window-content">
|
||||
{window.content}
|
||||
{/* <iframe
|
||||
src={window.url}
|
||||
title={window.title}
|
||||
className="window-iframe"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals"
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Window;
|
||||
348
src/components/deepfake/ChatterboxTTS.css
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
.chatterbox-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.chatterbox-container h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="number"],
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.char-counter {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Voice section */
|
||||
.voice-section {
|
||||
background: #f8f9ff;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
margin-bottom: 25px;
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.voice-section.recording {
|
||||
border-color: #e74c3c;
|
||||
background: #fdf2f2;
|
||||
}
|
||||
|
||||
.voice-section.has-recording {
|
||||
border-color: #4caf50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.voice-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.check-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.record-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.record-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.record-btn.recording {
|
||||
background: #e74c3c;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.record-status {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.record-timer {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-top: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.voice-text {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border: 2px dashed #667eea;
|
||||
}
|
||||
|
||||
.voice-preview {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.voice-preview audio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.voice-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.voice-actions button {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-retry {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-use {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* File upload */
|
||||
.or-divider {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.or-divider::before,
|
||||
.or-divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 40%;
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.or-divider::before { left: 0; }
|
||||
.or-divider::after { right: 0; }
|
||||
|
||||
.file-input-wrapper {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.file-input-wrapper:hover {
|
||||
border-color: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.file-input-wrapper.has-file {
|
||||
border-color: #4caf50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
margin-top: 10px;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* Sliders */
|
||||
.slider-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.slider-group input[type="range"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Submit */
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading-block {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.error-block {
|
||||
color: #e74c3c;
|
||||
background: #fdf2f2;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Result */
|
||||
.result-block {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.result-block h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-block audio {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
margin-top: 15px;
|
||||
padding: 10px 20px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
356
src/components/deepfake/ChatterboxTTS.tsx
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Recorder } from '../../lib/recorder';
|
||||
import './ChatterboxTTS.css';
|
||||
|
||||
const API_URL = 'https://back.hack.kinsle.ru/process-audio';
|
||||
|
||||
const ChatterboxTTS = () => {
|
||||
// Recording state
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [hasRecording, setHasRecording] = useState(false);
|
||||
const [isUsingRecordedVoice, setIsUsingRecordedVoice] = useState(false);
|
||||
const [recordStatus, setRecordStatus] = useState('Нажмите для записи');
|
||||
const [recordTime, setRecordTime] = useState('');
|
||||
const [showVoiceText, setShowVoiceText] = useState(false);
|
||||
const [showVoicePreview, setShowVoicePreview] = useState(false);
|
||||
const [recordedAudioUrl, setRecordedAudioUrl] = useState('');
|
||||
|
||||
// File state
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
// Form state
|
||||
const [text, setText] = useState('В прошлом месяце мы достигли нового рубежа.');
|
||||
const [language, setLanguage] = useState('ru');
|
||||
const [exaggeration, setExaggeration] = useState(0.5);
|
||||
const [temperature, setTemperature] = useState(0.8);
|
||||
const [cfgWeight, setCfgWeight] = useState(0.5);
|
||||
const [seed, setSeed] = useState(0);
|
||||
|
||||
// UI state
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [resultUrl, setResultUrl] = useState('');
|
||||
|
||||
// Refs (не тригерят ре-рендер)
|
||||
const recorderRef = useRef<Recorder | null>(null);
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const recordedBlobRef = useRef<Blob | null>(null);
|
||||
const recordingStartRef = useRef<number>(0);
|
||||
const timerIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const fileRef = useRef<File | null>(null);
|
||||
|
||||
// Запрос микрофона при монтировании
|
||||
useEffect(() => {
|
||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
.then(stream => stream.getTracks().forEach(t => t.stop()))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const updateTimer = () => {
|
||||
const elapsed = Math.floor((Date.now() - recordingStartRef.current) / 1000);
|
||||
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
|
||||
const secs = (elapsed % 60).toString().padStart(2, '0');
|
||||
setRecordTime(`${mins}:${secs}`);
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
streamRef.current = stream;
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
audioContextRef.current = audioContext;
|
||||
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
const recorder = await Recorder.create(source, { numChannels: 1 });
|
||||
recorderRef.current = recorder;
|
||||
|
||||
setIsRecording(true);
|
||||
setHasRecording(false);
|
||||
setShowVoiceText(true);
|
||||
setShowVoicePreview(false);
|
||||
setIsUsingRecordedVoice(false);
|
||||
setRecordStatus('Идёт запись...');
|
||||
setRecordTime('00:00');
|
||||
|
||||
recordingStartRef.current = Date.now();
|
||||
updateTimer();
|
||||
timerIntervalRef.current = setInterval(updateTimer, 1000);
|
||||
} catch (err) {
|
||||
alert('Не удалось получить доступ к микрофону: ' + (err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
const recorder = recorderRef.current;
|
||||
if (!recorder) return;
|
||||
|
||||
recorder.stop();
|
||||
|
||||
// Сразу обновляем UI — не ждём окончания экспорта
|
||||
setIsRecording(false);
|
||||
setRecordStatus('Обработка...');
|
||||
if (timerIntervalRef.current) {
|
||||
clearInterval(timerIntervalRef.current);
|
||||
timerIntervalRef.current = null;
|
||||
}
|
||||
setRecordTime('');
|
||||
|
||||
recorder.exportWAV((blob) => {
|
||||
recorder.clear();
|
||||
recorder.destroy();
|
||||
recorderRef.current = null;
|
||||
|
||||
streamRef.current?.getTracks().forEach(t => t.stop());
|
||||
streamRef.current = null;
|
||||
audioContextRef.current?.close();
|
||||
audioContextRef.current = null;
|
||||
|
||||
recordedBlobRef.current = blob;
|
||||
setRecordedAudioUrl(URL.createObjectURL(blob));
|
||||
setShowVoicePreview(true);
|
||||
setShowVoiceText(false);
|
||||
setRecordStatus('Запись завершена');
|
||||
setHasRecording(true);
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecordClick = () => {
|
||||
if (isRecording) stopRecording();
|
||||
else startRecording();
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setShowVoicePreview(false);
|
||||
setHasRecording(false);
|
||||
setIsUsingRecordedVoice(false);
|
||||
setRecordStatus('Нажмите для записи');
|
||||
recordedBlobRef.current = null;
|
||||
};
|
||||
|
||||
const handleUseRecording = () => {
|
||||
setIsUsingRecordedVoice(true);
|
||||
setRecordStatus('✅ Голосовое сообщение сохранено');
|
||||
// Сбрасываем файл
|
||||
fileRef.current = null;
|
||||
setFileName('');
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const updateFileName = (file: File) => {
|
||||
fileRef.current = file;
|
||||
setFileName(file.name);
|
||||
setIsUsingRecordedVoice(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) updateFileName(file);
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) updateFileName(file);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
setResultUrl('');
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
if (isUsingRecordedVoice) {
|
||||
if (!recordedBlobRef.current) {
|
||||
throw new Error('Запись голоса потеряна — запишите снова');
|
||||
}
|
||||
formData.append('audio_file', recordedBlobRef.current, 'voice_recording.wav');
|
||||
} else if (fileRef.current) {
|
||||
formData.append('audio_file', fileRef.current);
|
||||
} else {
|
||||
throw new Error('Выберите аудио файл или запишите голос');
|
||||
}
|
||||
|
||||
formData.append('text', text);
|
||||
formData.append('language_id', language);
|
||||
formData.append('exaggeration', String(exaggeration));
|
||||
formData.append('temperature', String(temperature));
|
||||
formData.append('seed_num', String(seed));
|
||||
formData.append('cfg_weight', String(cfgWeight));
|
||||
|
||||
console.log('[ChatterboxTTS] отправка:', {
|
||||
audio: isUsingRecordedVoice ? 'recorded blob' : fileRef.current?.name,
|
||||
text, language, exaggeration, temperature, cfgWeight, seed,
|
||||
});
|
||||
|
||||
const response = await fetch(API_URL, { method: 'POST', body: formData });
|
||||
|
||||
const contentType = response.headers.get('Content-Type') ?? '';
|
||||
console.log('[ChatterboxTTS] ответ:', response.status, contentType);
|
||||
|
||||
if (!response.ok || contentType.includes('application/json')) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
const msg = (errData as { error?: string }).error;
|
||||
if (msg) throw new Error(msg);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
throw new Error('Сервер вернул JSON вместо аудио — возможно audio_file не принят');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
setResultUrl(URL.createObjectURL(blob));
|
||||
} catch (err) {
|
||||
setError('❌ Ошибка: ' + (err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const voiceSectionClass = [
|
||||
'voice-section',
|
||||
isRecording ? 'recording' : '',
|
||||
hasRecording && !isRecording ? 'has-recording' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div className="chatterbox-container">
|
||||
<h1>🎙️ Chatterbox TTS</h1>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Запись голоса */}
|
||||
<div className={voiceSectionClass}>
|
||||
<div className="voice-label">
|
||||
🎤 Запишите голосовое сообщение для проверки на бота
|
||||
{isUsingRecordedVoice && <span className="check-badge">✓ Готово</span>}
|
||||
</div>
|
||||
|
||||
<button type="button" className={`record-btn${isRecording ? ' recording' : ''}`} onClick={handleRecordClick}>
|
||||
{isRecording ? '⏹' : '⏺'}
|
||||
</button>
|
||||
|
||||
<div className="record-status">{recordStatus}</div>
|
||||
{recordTime && <div className="record-timer">{recordTime}</div>}
|
||||
|
||||
{showVoiceText && (
|
||||
<div className="voice-text">
|
||||
📢 Произнесите: <span style={{ color: '#667eea' }}>"Хакатон 2026 французский стиль"</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showVoicePreview && (
|
||||
<div className="voice-preview">
|
||||
<audio src={recordedAudioUrl} controls />
|
||||
<div className="voice-actions">
|
||||
<button type="button" className="btn-retry" onClick={handleRetry}>🔄 Перезаписать</button>
|
||||
<button type="button" className="btn-use" onClick={handleUseRecording}>✅ Использовать</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Загрузка файла */}
|
||||
<div className="or-divider">или загрузите файл</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div
|
||||
className={`file-input-wrapper${fileName ? ' has-file' : ''}`}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
|
||||
onDragLeave={() => setIsDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
style={isDragOver ? { borderColor: '#667eea' } : undefined}
|
||||
>
|
||||
<input ref={fileInputRef} type="file" accept="audio/*" onChange={handleFileChange} style={{ display: 'none' }} />
|
||||
<div>{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}</div>
|
||||
{fileName && <div className="file-name">{fileName}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Текст */}
|
||||
<div className="form-group">
|
||||
<label>Текст для синтеза (макс 300 символов)</label>
|
||||
<textarea maxLength={300} value={text} onChange={e => setText(e.target.value)} placeholder="Введите текст..." />
|
||||
<div className="char-counter"><span>{text.length}</span>/300</div>
|
||||
</div>
|
||||
|
||||
{/* Язык */}
|
||||
<div className="form-group">
|
||||
<label>Язык</label>
|
||||
<select value={language} onChange={e => setLanguage(e.target.value)}>
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="de">German</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="hi">Hindi</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Слайдеры */}
|
||||
<div className="form-group">
|
||||
<label>Exaggeration (экспрессивность)</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.25" max="2" step="0.05" value={exaggeration} onChange={e => setExaggeration(Number(e.target.value))} />
|
||||
<span className="slider-value">{exaggeration}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Temperature (разнообразие)</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.05" max="5" step="0.05" value={temperature} onChange={e => setTemperature(Number(e.target.value))} />
|
||||
<span className="slider-value">{temperature}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>CFG Weight</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.2" max="1" step="0.05" value={cfgWeight} onChange={e => setCfgWeight(Number(e.target.value))} />
|
||||
<span className="slider-value">{cfgWeight}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="two-col">
|
||||
<div className="form-group">
|
||||
<label>Random Seed (0 = random)</label>
|
||||
<input type="number" value={seed} min={0} onChange={e => setSeed(Number(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit-btn" disabled={loading}>
|
||||
🚀 Сгенерировать аудио
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{loading && (
|
||||
<div className="loading-block">
|
||||
<div className="spinner" />
|
||||
<p>Генерируем аудио... Это может занять 10-30 секунд</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="error-block">{error}</div>}
|
||||
|
||||
{resultUrl && (
|
||||
<div className="result-block">
|
||||
<h3>✅ Готово!</h3>
|
||||
<audio src={resultUrl} controls />
|
||||
<br />
|
||||
<a href={resultUrl} className="download-btn" download="output.wav">💾 Скачать WAV</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatterboxTTS;
|
||||
0
src/index.css
Normal file
78
src/lib/recorder.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Recorder — AudioWorklet-based replacement for Recorderjs
|
||||
* https://github.com/mattdiamond/Recorderjs
|
||||
*
|
||||
* AudioWorkletNode заменяет устаревший ScriptProcessorNode.
|
||||
* WAV-кодирование остаётся в отдельном Worker (recorderWorker.js).
|
||||
*/
|
||||
|
||||
export class Recorder {
|
||||
private node: AudioWorkletNode;
|
||||
private worker: Worker;
|
||||
private currCallback: ((blob: Blob) => void) | null = null;
|
||||
|
||||
private constructor(node: AudioWorkletNode, worker: Worker) {
|
||||
this.node = node;
|
||||
this.worker = worker;
|
||||
|
||||
// Пробрасываем PCM-чанки из воркл'ета в WAV-воркер
|
||||
node.port.onmessage = (e: MessageEvent<Float32Array[]>) => {
|
||||
this.worker.postMessage({ command: 'record', buffer: e.data });
|
||||
};
|
||||
|
||||
this.worker.onmessage = (e: MessageEvent<{ command: string; data: Blob }>) => {
|
||||
if (e.data.command === 'exportWAV' && this.currCallback) {
|
||||
this.currCallback(e.data.data);
|
||||
this.currCallback = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Фабричный метод — async из-за audioWorklet.addModule */
|
||||
static async create(
|
||||
source: MediaStreamAudioSourceNode,
|
||||
cfg?: { numChannels?: number; workletPath?: string; workerPath?: string },
|
||||
): Promise<Recorder> {
|
||||
const context = source.context as AudioContext;
|
||||
const numChannels = cfg?.numChannels ?? 1;
|
||||
|
||||
await context.audioWorklet.addModule(cfg?.workletPath ?? '/recorderWorklet.js');
|
||||
|
||||
const node = new AudioWorkletNode(context, 'recorder-processor', {
|
||||
numberOfInputs: 1,
|
||||
numberOfOutputs: 0,
|
||||
channelCount: numChannels,
|
||||
});
|
||||
|
||||
const worker = new Worker(cfg?.workerPath ?? '/recorderWorker.js');
|
||||
worker.postMessage({
|
||||
command: 'init',
|
||||
config: { sampleRate: context.sampleRate, numChannels },
|
||||
});
|
||||
|
||||
source.connect(node);
|
||||
|
||||
return new Recorder(node, worker);
|
||||
}
|
||||
|
||||
record() { /* данные уже идут через node.port.onmessage */ }
|
||||
|
||||
stop() {
|
||||
// Сообщаем воркл'ету завершить process() и отключиться
|
||||
this.node.port.postMessage('stop');
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.worker.postMessage({ command: 'clear' });
|
||||
}
|
||||
|
||||
exportWAV(cb: (blob: Blob) => void, type = 'audio/wav') {
|
||||
this.currCallback = cb;
|
||||
this.worker.postMessage({ command: 'exportWAV', type });
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.node.disconnect();
|
||||
this.worker.terminate();
|
||||
}
|
||||
}
|
||||
13
src/main.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
197
src/styles/shared.css
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.wallpaper-selector {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.selector-container {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 20px;
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
box-shadow: 0 30px 80px rgba(0,0,0,0.3);
|
||||
max-width: 1200px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.quest-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.windows-logo {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.selector-container h1 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.quest-description {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.wallpaper-options {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
justify-content: center;
|
||||
margin-bottom: 50px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wallpaper-option {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.wallpaper-option:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.wallpaper-preview {
|
||||
width: 280px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.wallpaper-option:hover .preview-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.preview-overlay span {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.xp-preview {
|
||||
background: linear-gradient(135deg, #3a8c3a 0%, #1e5c1e 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xp-preview::before {
|
||||
content: "🏔️";
|
||||
position: absolute;
|
||||
font-size: 60px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.win7-preview {
|
||||
background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.win7-preview::before {
|
||||
content: "🐟";
|
||||
position: absolute;
|
||||
font-size: 60px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.win10-preview {
|
||||
background: linear-gradient(135deg, #0078d7 0%, #005a9e 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.win10-preview::before {
|
||||
content: "🪟";
|
||||
position: absolute;
|
||||
font-size: 60px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.option-info {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-info h3 {
|
||||
margin: 0 0 5px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.option-info p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.selector-footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
26
src/types.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export interface WindowData {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
icon: string;
|
||||
isMinimized: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface DesktopIconProps {
|
||||
icon: string;
|
||||
name: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface WindowProps {
|
||||
window: WindowData;
|
||||
onClose: (id: string) => void;
|
||||
onMinimize: (id: string) => void;
|
||||
}
|
||||
|
||||
export interface SidebarProps {
|
||||
windows: WindowData[];
|
||||
onRestore: (id: string) => void;
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
758
test.html
Normal file
|
|
@ -0,0 +1,758 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Chatterbox TTS Test</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Voice Recording Section */
|
||||
.voice-section {
|
||||
background: #f8f9ff;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
margin-bottom: 25px;
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.voice-section.recording {
|
||||
border-color: #e74c3c;
|
||||
background: #fdf2f2;
|
||||
}
|
||||
|
||||
.voice-section.has-recording {
|
||||
border-color: #4caf50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.voice-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.record-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.record-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.record-btn.recording {
|
||||
background: #e74c3c;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.record-status {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.record-timer {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-top: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.voice-text {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border: 2px dashed #667eea;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voice-text.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.voice-preview {
|
||||
margin-top: 15px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voice-preview.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.voice-preview audio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.voice-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.voice-actions button {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-retry {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-use {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* File upload alternative */
|
||||
.or-divider {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.or-divider::before,
|
||||
.or-divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 40%;
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.or-divider::before { left: 0; }
|
||||
.or-divider::after { right: 0; }
|
||||
|
||||
.file-input-wrapper {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.file-input-wrapper:hover {
|
||||
border-color: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.file-input-wrapper.has-file {
|
||||
border-color: #4caf50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button[type="submit"]:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result audio {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
margin-top: 15px;
|
||||
padding: 10px 20px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
background: #fdf2f2;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.check-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎙️ Chatterbox TTS</h1>
|
||||
|
||||
<form id="ttsForm">
|
||||
<!-- Голосовая запись -->
|
||||
<div class="voice-section" id="voiceSection">
|
||||
<div class="voice-label">
|
||||
🎤 Запишите голосовое сообщение для проверки на бота
|
||||
<span class="check-badge" id="checkBadge" style="display: none;">✓ Готово</span>
|
||||
</div>
|
||||
|
||||
<button type="button" class="record-btn" id="recordBtn">⏺</button>
|
||||
|
||||
<div class="record-status" id="recordStatus">Нажмите для записи</div>
|
||||
<div class="record-timer" id="recordTimer" style="display: none;">00:00</div>
|
||||
|
||||
<div class="voice-text" id="voiceText">
|
||||
📢 Произнесите: <span style="color: #667eea;">"Хакатон 2026 французский стиль"</span>
|
||||
</div>
|
||||
|
||||
<div class="voice-preview" id="voicePreview">
|
||||
<audio id="recordedAudio" controls></audio>
|
||||
<div class="voice-actions">
|
||||
<button type="button" class="btn-retry" id="retryBtn">🔄 Перезаписать</button>
|
||||
<button type="button" class="btn-use" id="useBtn">✅ Использовать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Или загрузить файл -->
|
||||
<div class="or-divider">или загрузите файл</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="file-input-wrapper" id="dropZone">
|
||||
<input type="file" id="audioFile" accept="audio/*">
|
||||
<div id="fileLabel">📁 Кликни или перетащи аудио файл</div>
|
||||
<div id="fileName" style="margin-top: 10px; font-weight: 600; color: #667eea;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Текст для синтеза -->
|
||||
<div class="form-group">
|
||||
<label for="text">Текст для синтеза (макс 300 символов)</label>
|
||||
<textarea id="text" maxlength="300" placeholder="Введите текст...">В прошлом месяце мы достигли нового рубежа.</textarea>
|
||||
<div style="text-align: right; font-size: 12px; color: #999; margin-top: 5px;">
|
||||
<span id="charCount">0</span>/300
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Язык -->
|
||||
<div class="form-group">
|
||||
<label for="language">Язык</label>
|
||||
<select id="language">
|
||||
<option value="en">English</option>
|
||||
<option value="ru" selected>Russian</option>
|
||||
<option value="de">German</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="hi">Hindi</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Слайдеры -->
|
||||
<div class="form-group">
|
||||
<label>Exaggeration (экспрессивность)</label>
|
||||
<div class="slider-group">
|
||||
<input type="range" id="exaggeration" min="0.25" max="2" step="0.05" value="0.5">
|
||||
<span class="slider-value" id="exaggerationVal">0.5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Temperature (разнообразие)</label>
|
||||
<div class="slider-group">
|
||||
<input type="range" id="temperature" min="0.05" max="5" step="0.05" value="0.8">
|
||||
<span class="slider-value" id="temperatureVal">0.8</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>CFG Weight</label>
|
||||
<div class="slider-group">
|
||||
<input type="range" id="cfgWeight" min="0.2" max="1" step="0.05" value="0.5">
|
||||
<span class="slider-value" id="cfgWeightVal">0.5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="two-col">
|
||||
<div class="form-group">
|
||||
<label for="seed">Random Seed (0 = random)</label>
|
||||
<input type="number" id="seed" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn">🚀 Сгенерировать аудио</button>
|
||||
</form>
|
||||
|
||||
<!-- Лоадер -->
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Генерируем аудио... Это может занять 10-30 секунд</p>
|
||||
</div>
|
||||
|
||||
<!-- Ошибка -->
|
||||
<div class="error" id="error"></div>
|
||||
|
||||
<!-- Результат -->
|
||||
<div class="result" id="result">
|
||||
<h3 style="margin-bottom: 15px; color: #333;">✅ Готово!</h3>
|
||||
<audio id="audioPlayer" controls></audio>
|
||||
<br>
|
||||
<a href="#" class="download-btn" id="downloadBtn" download="output.wav">💾 Скачать WAV</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = 'https://back.hack.kinsle.ru/process-audio';
|
||||
|
||||
// Элементы
|
||||
const form = document.getElementById('ttsForm');
|
||||
const voiceSection = document.getElementById('voiceSection');
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
const recordStatus = document.getElementById('recordStatus');
|
||||
const recordTimer = document.getElementById('recordTimer');
|
||||
const voiceText = document.getElementById('voiceText');
|
||||
const voicePreview = document.getElementById('voicePreview');
|
||||
const recordedAudio = document.getElementById('recordedAudio');
|
||||
const retryBtn = document.getElementById('retryBtn');
|
||||
const useBtn = document.getElementById('useBtn');
|
||||
const checkBadge = document.getElementById('checkBadge');
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('audioFile');
|
||||
const fileLabel = document.getElementById('fileLabel');
|
||||
const fileName = document.getElementById('fileName');
|
||||
const text = document.getElementById('text');
|
||||
const charCount = document.getElementById('charCount');
|
||||
const loading = document.getElementById('loading');
|
||||
const result = document.getElementById('result');
|
||||
const error = document.getElementById('error');
|
||||
const audioPlayer = document.getElementById('audioPlayer');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
|
||||
// Переменные для записи
|
||||
let mediaRecorder = null;
|
||||
let recordedChunks = [];
|
||||
let recordingStartTime = null;
|
||||
let recordingTimer = null;
|
||||
let recordedBlob = null;
|
||||
let isUsingRecordedVoice = false;
|
||||
|
||||
// Запрос разрешения на микрофон при загрузке
|
||||
async function initMicrophone() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
} catch (err) {
|
||||
console.log('Микрофон не доступен:', err);
|
||||
}
|
||||
}
|
||||
initMicrophone();
|
||||
|
||||
// Начать/остановить запись
|
||||
recordBtn.addEventListener('click', async () => {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
stopRecording();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
});
|
||||
|
||||
async function startRecording() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
const mimeType =
|
||||
MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' :
|
||||
MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' :
|
||||
MediaRecorder.isTypeSupported('audio/ogg;codecs=opus') ? 'audio/ogg;codecs=opus' : '';
|
||||
|
||||
mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : {});
|
||||
recordedChunks = [];
|
||||
|
||||
mediaRecorder.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
recordedChunks.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
const actualType = mediaRecorder.mimeType || 'audio/webm';
|
||||
recordedBlob = new Blob(recordedChunks, { type: actualType });
|
||||
const url = URL.createObjectURL(recordedBlob);
|
||||
recordedAudio.src = url;
|
||||
|
||||
voicePreview.classList.add('show');
|
||||
recordStatus.textContent = 'Запись завершена';
|
||||
recordBtn.textContent = '⏺';
|
||||
recordBtn.classList.remove('recording');
|
||||
voiceSection.classList.remove('recording');
|
||||
voiceSection.classList.add('has-recording');
|
||||
|
||||
// Останавливаем все треки
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
|
||||
// UI обновления
|
||||
recordBtn.textContent = '⏹';
|
||||
recordBtn.classList.add('recording');
|
||||
voiceSection.classList.add('recording');
|
||||
recordStatus.textContent = 'Идёт запись...';
|
||||
voiceText.classList.add('show');
|
||||
recordTimer.style.display = 'block';
|
||||
voicePreview.classList.remove('show');
|
||||
isUsingRecordedVoice = false;
|
||||
checkBadge.style.display = 'none';
|
||||
|
||||
// Таймер
|
||||
recordingStartTime = Date.now();
|
||||
updateTimer();
|
||||
recordingTimer = setInterval(updateTimer, 1000);
|
||||
|
||||
} catch (err) {
|
||||
alert('Не удалось получить доступ к микрофону: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
clearInterval(recordingTimer);
|
||||
recordTimer.style.display = 'none';
|
||||
voiceText.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
|
||||
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
|
||||
const secs = (elapsed % 60).toString().padStart(2, '0');
|
||||
recordTimer.textContent = `${mins}:${secs}`;
|
||||
}
|
||||
|
||||
// Перезаписать
|
||||
retryBtn.addEventListener('click', () => {
|
||||
voicePreview.classList.remove('show');
|
||||
voiceSection.classList.remove('has-recording');
|
||||
recordStatus.textContent = 'Нажмите для записи';
|
||||
recordedBlob = null;
|
||||
isUsingRecordedVoice = false;
|
||||
checkBadge.style.display = 'none';
|
||||
});
|
||||
|
||||
// Использовать запись
|
||||
useBtn.addEventListener('click', () => {
|
||||
isUsingRecordedVoice = true;
|
||||
checkBadge.style.display = 'inline-flex';
|
||||
recordStatus.textContent = '✅ Голосовое сообщение сохранено';
|
||||
|
||||
// Сбрасываем файл если был
|
||||
fileInput.value = '';
|
||||
fileLabel.textContent = '📁 Кликни или перетащи аудио файл';
|
||||
fileName.textContent = '';
|
||||
dropZone.classList.remove('has-file');
|
||||
});
|
||||
|
||||
// File upload (drag & drop)
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.style.borderColor = '#667eea';
|
||||
});
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.style.borderColor = '#ccc';
|
||||
});
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.style.borderColor = '#ccc';
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length) {
|
||||
fileInput.files = files;
|
||||
updateFileName(files[0].name);
|
||||
}
|
||||
});
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (fileInput.files.length) {
|
||||
updateFileName(fileInput.files[0].name);
|
||||
// Сбрасываем голосовую запись
|
||||
isUsingRecordedVoice = false;
|
||||
checkBadge.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function updateFileName(name) {
|
||||
fileLabel.textContent = '✅ Файл выбран';
|
||||
fileName.textContent = name;
|
||||
dropZone.classList.add('has-file');
|
||||
}
|
||||
|
||||
// Character counter
|
||||
text.addEventListener('input', () => {
|
||||
charCount.textContent = text.value.length;
|
||||
});
|
||||
charCount.textContent = text.value.length;
|
||||
|
||||
// Slider updates
|
||||
document.getElementById('exaggeration').addEventListener('input', (e) => {
|
||||
document.getElementById('exaggerationVal').textContent = e.target.value;
|
||||
});
|
||||
document.getElementById('temperature').addEventListener('input', (e) => {
|
||||
document.getElementById('temperatureVal').textContent = e.target.value;
|
||||
});
|
||||
document.getElementById('cfgWeight').addEventListener('input', (e) => {
|
||||
document.getElementById('cfgWeightVal').textContent = e.target.value;
|
||||
});
|
||||
|
||||
// Form submit
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Проверяем что есть источник аудио
|
||||
if (!isUsingRecordedVoice && !fileInput.files.length) {
|
||||
alert('Пожалуйста, запишите голосовое сообщение или загрузите аудио файл');
|
||||
return;
|
||||
}
|
||||
|
||||
result.classList.remove('show');
|
||||
error.classList.remove('show');
|
||||
loading.classList.add('show');
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
// Добавляем аудио (голосовое или файл)
|
||||
if (isUsingRecordedVoice && recordedBlob) {
|
||||
const ext = recordedBlob.type.includes('ogg') ? 'ogg' : 'webm';
|
||||
formData.append('audio_file', recordedBlob, `voice_recording.${ext}`);
|
||||
} else if (fileInput.files.length > 0) {
|
||||
formData.append('audio_file', fileInput.files[0]);
|
||||
}
|
||||
|
||||
formData.append('text', text.value);
|
||||
formData.append('language_id', document.getElementById('language').value);
|
||||
formData.append('exaggeration', document.getElementById('exaggeration').value);
|
||||
formData.append('temperature', document.getElementById('temperature').value);
|
||||
formData.append('seed_num', document.getElementById('seed').value);
|
||||
formData.append('cfg_weight', document.getElementById('cfgWeight').value);
|
||||
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
throw new Error(errData.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
audioPlayer.src = url;
|
||||
downloadBtn.href = url;
|
||||
result.classList.add('show');
|
||||
|
||||
} catch (err) {
|
||||
error.textContent = '❌ Ошибка: ' + err.message;
|
||||
error.classList.add('show');
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading.classList.remove('show');
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
28
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// "strict": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "erasableSyntaxOnly": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
// "strict": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "erasableSyntaxOnly": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||