Locazia: v2.0 (dev beta)
This commit is contained in:
parent
a08f742237
commit
88f8efa389
|
|
@ -1,2 +1,3 @@
|
|||
.idea
|
||||
node_modules
|
||||
.DS_Store
|
||||
|
|
|
|||
84
README.md
84
README.md
|
|
@ -1,70 +1,30 @@
|
|||
# Getting Started with Create React App
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
## Available Scripts
|
||||
Currently, two official plugins are available:
|
||||
|
||||
In the project directory, you can run:
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
### `npm start`
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
### `npm test`
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link href="/vite.svg" rel="icon" type="image/svg+xml" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
|
||||
<meta content="true" name="HandheldFriendly" />
|
||||
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
|
||||
<title>MyMusic</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
76
package.json
76
package.json
|
|
@ -1,43 +1,49 @@
|
|||
{
|
||||
"name": "web2-client",
|
||||
"version": "0.1.0",
|
||||
"name": "my-music",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@vkruglikov/react-telegram-web-app": "^2.1.9",
|
||||
"axios": "^1.6.7",
|
||||
"clsx": "^2.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
"react-hook-form": "^7.51.0",
|
||||
"react-player": "^2.15.1",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^18.2.56",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.4",
|
||||
"vite-tsconfig-paths": "^4.3.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
plugins: [require.resolve("prettier-plugin-tailwindcss")],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
|
|
@ -1,44 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>MY</title>
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"short_name": "Web2MYClient",
|
||||
"name": "MY – Web2 Client",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './oldapp';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
77
src/App.tsx
77
src/App.tsx
|
|
@ -1,77 +0,0 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
import Blank from './pages/Blank';
|
||||
import { apiEndpoint } from "./constantsGlob";
|
||||
import UploadContentPage from './pages/UploadContentPage';
|
||||
import './index.css';
|
||||
|
||||
function App() {
|
||||
const [apiToken, setApiToken] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (window.Telegram && window.Telegram.WebApp) {
|
||||
console.log(`Requesting API token from ${apiEndpoint}/auth.twa`);
|
||||
|
||||
fetch(`${apiEndpoint}/auth.twa`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"twa_data": window.Telegram.WebApp.initData
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data["auth_v1_token"] === undefined) {
|
||||
window.location.href = "/unauthorized";
|
||||
return;
|
||||
}
|
||||
setApiToken(data["auth_v1_token"]);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with your fetch operation:', error);
|
||||
});
|
||||
|
||||
fetch(`${apiEndpoint}/account`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': apiToken,
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with your fetch operation:', error);
|
||||
});
|
||||
} else {
|
||||
console.log("Telegram WebApp not found");
|
||||
window.location.href = "/unauthorized";
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div className="App">
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Blank />} />
|
||||
<Route path="/uploadContent" element={<UploadContentPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import "~/app/styles/globals.css";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useExpand, useWebApp } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { Providers } from "~/app/providers";
|
||||
import { AppRouter } from "~/app/router";
|
||||
|
||||
export const App = () => {
|
||||
const WebApp = useWebApp();
|
||||
const [, expand] = useExpand();
|
||||
|
||||
useEffect(() => {
|
||||
WebApp.enableClosingConfirmation();
|
||||
expand();
|
||||
|
||||
WebApp.setHeaderColor("#1d1d1b");
|
||||
WebApp.setBackgroundColor("#1d1d1b");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<AppRouter />
|
||||
</Providers>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { ReactNode } from "react";
|
||||
import { QueryClientProvider } from "react-query";
|
||||
import { WebAppProvider } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { queryClient } from "~/shared/libs";
|
||||
|
||||
type ProvidersProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const Providers = ({ children }: ProvidersProps) => {
|
||||
return (
|
||||
<WebAppProvider options={{ smoothButtonsTransition: true }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<main className="antialiased">{children}</main>
|
||||
</QueryClientProvider>
|
||||
</WebAppProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export const Routes = {
|
||||
Root: "/uploadContent",
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
import { Routes } from "~/app/router/constants";
|
||||
import { RootPage } from "~/pages/root";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: Routes.Root, element: <RootPage /> },
|
||||
]);
|
||||
|
||||
export const AppRouter = () => {
|
||||
return <RouterProvider router={router} />;
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
@import url("https://fonts.cdnfonts.com/css/menlo");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer {
|
||||
* {
|
||||
@apply caret-primary select-none;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
@apply bg-gray text-white;
|
||||
font-family: "Menlo", sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply transition-all active:opacity-60;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply transition-all active:opacity-60;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
console.log('process.env.REACT_APP_API_ENDPOINT', process.env.REACT_APP_API_ENDPOINT);
|
||||
|
||||
const apiEndpoint: string = 'https://music-gateway.letsw.app/api/v1';
|
||||
|
||||
export {
|
||||
apiEndpoint
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import { App } from "~/app";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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>
|
||||
);
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
|
@ -1,11 +0,0 @@
|
|||
|
||||
const Blank = () => {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<p className="text-2xl text-gray-800 mono-font">Web2 Client: Page not found</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Blank;
|
||||
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
|
||||
interface RoyaltyRecipient {
|
||||
address: string;
|
||||
percentage: string;
|
||||
}
|
||||
|
||||
const UploadContentPage = () => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [content, setContent] = useState<File | null>(null);
|
||||
const [authors, setAuthors] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [price, setPrice] = useState('');
|
||||
const [allowResale, setAllowResale] = useState(false);
|
||||
const [royaltyRecipients, setRoyaltyRecipients] = useState<RoyaltyRecipient[]>([{ address: '', percentage: '0' }]);
|
||||
const [totalPercentage, setTotalPercentage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let total = royaltyRecipients.reduce((acc, curr) => acc + parseFloat(curr.percentage), 0);
|
||||
if (isNaN(total)) { total = 0; }
|
||||
setTotalPercentage(total);
|
||||
}, [royaltyRecipients]);
|
||||
|
||||
const addRecipient = () => {
|
||||
setRoyaltyRecipients([...royaltyRecipients, { address: '', percentage: '0' }]);
|
||||
};
|
||||
|
||||
const updateRecipient = (index: number, field: 'address' | 'percentage', value: string) => {
|
||||
const updatedRecipients = [...royaltyRecipients];
|
||||
if (field === 'percentage') {
|
||||
updatedRecipients[index].percentage = value;
|
||||
} else {
|
||||
updatedRecipients[index][field] = value;
|
||||
}
|
||||
setRoyaltyRecipients(updatedRecipients);
|
||||
};
|
||||
|
||||
const removeRecipient = (index: number) => {
|
||||
if (royaltyRecipients.length > 1) {
|
||||
const updatedRecipients = [...royaltyRecipients];
|
||||
updatedRecipients.splice(index, 1);
|
||||
setRoyaltyRecipients(updatedRecipients);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
console.log(title, image, content, authors, description, price, allowResale, royaltyRecipients);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-5 bg-white shadow-md rounded-lg">
|
||||
<h1 className="text-xl font-semibold text-gray-800 mb-6">Загрузка контента</h1>
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Название"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="input border-gray-300 rounded-md p-2 w-full"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Авторы"
|
||||
value={title}
|
||||
onChange={(e) => setAuthors(e.target.value)}
|
||||
className="input border-gray-300 rounded-md p-2 w-full"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Описание"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="textarea border-gray-300 rounded-md p-2 w-full h-32"
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Обложка (1x1)</label>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => setImage(e.target.files ? e.target.files[0] : null)}
|
||||
className="file-input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Контент (mp3, wav, ogg)</label>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => setContent(e.target.files ? e.target.files[0] : null)}
|
||||
className="file-input"
|
||||
/>
|
||||
</div>
|
||||
<hr/>
|
||||
<table className="table-auto flex-wrap border-solid border-2 border-indigo-600 divide-y-2 divide-indigo-600">
|
||||
{royaltyRecipients.map((recipient, index) => (
|
||||
<tr>
|
||||
<td className="w-4/6 p-0">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Адрес получателя"
|
||||
value={recipient.address}
|
||||
onChange={(e) => updateRecipient(index, 'address', e.target.value)}
|
||||
className="border-indigo-600 border-e-2 w-full flex-grow"
|
||||
/>
|
||||
</td>
|
||||
<td className="p-0">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Процент роялти"
|
||||
value={recipient.percentage}
|
||||
onChange={(e) => updateRecipient(index, 'percentage', e.target.value)}
|
||||
className="border-indigo-600 border-s-0 w-full flex-grow"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
/>
|
||||
</td>
|
||||
<td className="p-0">
|
||||
{index >= 1 && (
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRecipient(index)}
|
||||
className="btn w-full bg-red-600 hover:text-red-700 px-4 py-2 text-white hover:bg-red-800 transition duration-150"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
<button type="button" onClick={addRecipient} className="btn text-right">
|
||||
Добавить получателя роялти
|
||||
</button>
|
||||
{totalPercentage !== 100 && (
|
||||
<p className="text-red-500">Сумма процентов должна равняться 100%</p>
|
||||
)}
|
||||
<div>Сумма процентов: {totalPercentage}%</div>
|
||||
<hr/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Цена в TON"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
className="input border-gray-300 rounded-md p-2 w-full"
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="allowResale"
|
||||
type="checkbox"
|
||||
checked={allowResale}
|
||||
onChange={(e) => setAllowResale(e.target.checked)}
|
||||
className="checkbox w-4 h-4 text-blue-600 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="allowResale" className="ml-2 text-sm text-gray-700">Разрешить перепродажу с отчислением
|
||||
реферального роялти?</label>
|
||||
</div>
|
||||
<button type="submit"
|
||||
className="btn bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded h-14 bg-gradient-to-r from-purple-500 to-pink-500">
|
||||
Загрузить контент
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadContentPage;
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { WelcomeStep } from "./steps/welcome-step";
|
||||
import { DataStep } from "./steps/data-step";
|
||||
import { RoyaltyStep } from "./steps/royalty-step";
|
||||
import { PresubmitStep } from "./steps/presubmit-step";
|
||||
|
||||
import { useSteps } from "~/shared/hooks/use-steps";
|
||||
import { PriceStep } from "~/pages/root/steps/price-step";
|
||||
|
||||
export const RootPage = () => {
|
||||
const { ActiveSection } = useSteps(({ nextStep, prevStep }) => {
|
||||
return [
|
||||
<WelcomeStep nextStep={nextStep} />,
|
||||
<DataStep prevStep={prevStep} nextStep={nextStep} />,
|
||||
// <AuthorsStep prevStep={prevStep} nextStep={nextStep} />,
|
||||
<RoyaltyStep prevStep={prevStep} nextStep={nextStep} />,
|
||||
<PriceStep prevStep={prevStep} nextStep={nextStep} />,
|
||||
<PresubmitStep prevStep={prevStep} />,
|
||||
];
|
||||
});
|
||||
|
||||
return ActiveSection;
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { Input } from "~/shared/ui/input";
|
||||
import { FormLabel } from "~/shared/ui/form-label";
|
||||
import { XMark } from "~/shared/ui/icons/x-mark.tsx";
|
||||
import { Button } from "~/shared/ui/button";
|
||||
import { useRootStore } from "~/shared/stores/root";
|
||||
import { BackButton } from "~/shared/ui/back-button";
|
||||
|
||||
type AuthorsStepProps = {
|
||||
prevStep(): void;
|
||||
nextStep(): void;
|
||||
};
|
||||
|
||||
export const AuthorsStep = ({ nextStep, prevStep }: AuthorsStepProps) => {
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const { authors, setAuthors } = useRootStore();
|
||||
|
||||
const handleAdd = () => {
|
||||
impactOccurred("light");
|
||||
setAuthors([...authors, ""]);
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
impactOccurred("light");
|
||||
|
||||
const newAuthors = authors.filter((_, i) => i !== index);
|
||||
setAuthors(newAuthors);
|
||||
};
|
||||
|
||||
const handleChange = (index: number, value: string) => {
|
||||
const newAuthors = authors.map((member, i) =>
|
||||
i === index ? value : member,
|
||||
);
|
||||
|
||||
setAuthors(newAuthors);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={"mt-4 px-4 pb-8"}>
|
||||
<BackButton onClick={prevStep} />
|
||||
|
||||
<div className={"mb-[30px] flex flex-col text-sm"}>
|
||||
<span className={"ml-4"}>/Заполните информацию об авторах</span>
|
||||
<div>
|
||||
2/<span className={"text-[#7B7B7B]"}>5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={"flex flex-col gap-1.5"}>
|
||||
{authors.map((member, index) => (
|
||||
<FormLabel
|
||||
key={index}
|
||||
labelClassName={"flex"}
|
||||
formLabelAddon={
|
||||
<button onClick={() => handleRemove(index)}>
|
||||
<XMark />
|
||||
</button>
|
||||
}
|
||||
label={`Автор_${index + 1}`}
|
||||
>
|
||||
<Input
|
||||
value={member}
|
||||
onChange={(e) => handleChange(index, e.target.value)}
|
||||
placeholder={"[ Введите_имя ]"}
|
||||
/>
|
||||
</FormLabel>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
className={
|
||||
"mt-[30px] flex w-full items-center justify-center border border-white py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
Добавить_автора
|
||||
</button>
|
||||
|
||||
<Button
|
||||
label={"Готово"}
|
||||
className={"mt-[30px]"}
|
||||
includeArrows={true}
|
||||
onClick={nextStep}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { XMark } from "~/shared/ui/icons/x-mark.tsx";
|
||||
|
||||
type CoverButtonProps = {
|
||||
src: string;
|
||||
onClick(): void;
|
||||
};
|
||||
|
||||
export const CoverButton = ({ src, onClick }: CoverButtonProps) => {
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const handleClick = () => {
|
||||
impactOccurred("light");
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex w-full items-center gap-2 border border-white px-[10px] py-[8px]"
|
||||
}
|
||||
>
|
||||
<div className={"bg-primary h-[50px] w-[50px] shrink-0 overflow-hidden"}>
|
||||
<img
|
||||
src={src}
|
||||
className={"h-full w-full object-cover object-center"}
|
||||
alt={"cover-image"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={
|
||||
"flex w-full items-center justify-between gap-1 border border-white px-[10px] py-[8px]"
|
||||
}
|
||||
>
|
||||
<div />
|
||||
<div className={"flex gap-2 text-sm"}>Удалить</div>
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import ReactPlayer from "react-player/lazy";
|
||||
|
||||
import { FormLabel } from "~/shared/ui/form-label";
|
||||
import { Input } from "~/shared/ui/input";
|
||||
import { FileButton } from "~/shared/ui/file-button";
|
||||
import { Button } from "~/shared/ui/button";
|
||||
import { CoverButton } from "~/pages/root/steps/data-step/components/cover-button";
|
||||
import { HiddenFileInput } from "~/shared/ui/hidden-file-input";
|
||||
import { useRootStore } from "~/shared/stores/root";
|
||||
import { Checkbox } from "~/shared/ui/checkbox";
|
||||
import { BackButton } from "~/shared/ui/back-button";
|
||||
|
||||
type DataStepProps = {
|
||||
prevStep(): void;
|
||||
nextStep(): void;
|
||||
};
|
||||
|
||||
export const DataStep = ({ nextStep, prevStep }: DataStepProps) => {
|
||||
const rootStore = useRootStore();
|
||||
|
||||
const formSchema = useMemo(() => {
|
||||
return z.object({
|
||||
name: z.string().min(1),
|
||||
author: z.string().optional(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onChange",
|
||||
|
||||
defaultValues: {
|
||||
name: rootStore.name,
|
||||
author: rootStore?.author,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.handleSubmit(async (values: FormValues) => {
|
||||
try {
|
||||
rootStore.setName(values.name);
|
||||
|
||||
if (values.author) {
|
||||
rootStore.setAuthor(values.author);
|
||||
}
|
||||
|
||||
nextStep();
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={"mt-4 px-4 pb-8"}>
|
||||
<BackButton onClick={prevStep} />
|
||||
|
||||
<div className={"mb-[30px] flex flex-col text-sm"}>
|
||||
<span className={"ml-4"}>/Заполните информацию о контенте</span>
|
||||
<div>
|
||||
1/<span className={"text-[#7B7B7B]"}>5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-[20px]"}>
|
||||
<FormLabel label={"Название"}>
|
||||
<Input
|
||||
placeholder={"[ Введите название ]"}
|
||||
{...form.register("name")}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormLabel label={"Имя автора/исполнителя"}>
|
||||
<Input
|
||||
placeholder={"[ введите имя автора/исполнителя ]"}
|
||||
{...form.register("author")}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormLabel label={"Файл"}>
|
||||
<HiddenFileInput
|
||||
id={"file"}
|
||||
shouldProcess={false}
|
||||
accept={"video/mp4,video/x-m4v,video/*,audio/mp3,audio/*"}
|
||||
onChange={(file) => {
|
||||
rootStore.setFile(file);
|
||||
rootStore.setFileSrc(URL.createObjectURL(file));
|
||||
}}
|
||||
/>
|
||||
|
||||
{!rootStore.fileSrc && <FileButton htmlFor={"file"} />}
|
||||
|
||||
{rootStore.fileSrc && (
|
||||
<div
|
||||
className={
|
||||
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
<ReactPlayer
|
||||
playsinline={true}
|
||||
controls={true}
|
||||
width={"100%"}
|
||||
url={rootStore.fileSrc}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FormLabel>
|
||||
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<FormLabel
|
||||
label={"Разрешить обложку"}
|
||||
labelClassName={"flex"}
|
||||
formLabelAddon={
|
||||
<Checkbox
|
||||
onClick={() => rootStore.setAllowCover(!rootStore.allowCover)}
|
||||
checked={rootStore.allowCover}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{rootStore.allowCover && (
|
||||
<FormLabel label={"Обложка"}>
|
||||
<HiddenFileInput
|
||||
id={"cover"}
|
||||
accept={"image/*"}
|
||||
onChange={(cover) => {
|
||||
rootStore.setCover(cover);
|
||||
}}
|
||||
/>
|
||||
|
||||
{rootStore.cover ? (
|
||||
<CoverButton
|
||||
src={URL.createObjectURL(rootStore.cover)}
|
||||
onClick={() => {
|
||||
rootStore.setCover(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FileButton htmlFor={"cover"} />
|
||||
)}
|
||||
</FormLabel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={"mt-[30px]"}
|
||||
onClick={handleSubmit}
|
||||
includeArrows={true}
|
||||
label={"Готово"}
|
||||
disabled={
|
||||
!form.formState.isValid ||
|
||||
!rootStore.file ||
|
||||
(rootStore.allowCover && !rootStore.cover)
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
import {
|
||||
useHapticFeedback,
|
||||
useWebApp,
|
||||
} from "@vkruglikov/react-telegram-web-app";
|
||||
import { useState } from "react";
|
||||
import ReactPlayer from "react-player/lazy";
|
||||
|
||||
import { Button } from "~/shared/ui/button";
|
||||
import { useRootStore } from "~/shared/stores/root";
|
||||
import { FormLabel } from "~/shared/ui/form-label";
|
||||
import { useUploadFile } from "~/shared/services/file";
|
||||
import { Progress } from "~/shared/ui/progress";
|
||||
import { useCreateNewContent } from "~/shared/services/content";
|
||||
import { BackButton } from "~/shared/ui/back-button";
|
||||
|
||||
type PresubmitStepProps = {
|
||||
prevStep(): void;
|
||||
};
|
||||
|
||||
export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
||||
const WebApp = useWebApp();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const [isCoverExpanded, setCoverExpanded] = useState(false);
|
||||
|
||||
const uploadCover = useUploadFile();
|
||||
const uploadFile = useUploadFile();
|
||||
|
||||
const createContent = useCreateNewContent();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
let coverUploadResult = { content_id_v1: "" };
|
||||
|
||||
const fileUploadResult = await uploadFile.mutateAsync(
|
||||
rootStore.file as File,
|
||||
);
|
||||
|
||||
if (rootStore.allowCover && rootStore.cover) {
|
||||
coverUploadResult = await uploadCover.mutateAsync(
|
||||
rootStore.cover as File,
|
||||
);
|
||||
}
|
||||
|
||||
await createContent.mutateAsync({
|
||||
title: rootStore.name,
|
||||
|
||||
// Это для одного автора
|
||||
...(rootStore.author
|
||||
? { authors: [rootStore.author] }
|
||||
: { authors: [] }),
|
||||
|
||||
// Откомментировать при условии того что вы принимаете много авторов
|
||||
// следует отметить что вы должны еще откомментровать AuthorsStep в RootPage
|
||||
// authors: rootStore.authors,
|
||||
|
||||
content: fileUploadResult.content_id_v1,
|
||||
image: coverUploadResult.content_id_v1,
|
||||
price: String(rootStore.price * 10 ** 9),
|
||||
|
||||
royaltyParams: rootStore.royalty.map((member) => ({
|
||||
...member,
|
||||
value: member.value * 100,
|
||||
})),
|
||||
description: "",
|
||||
|
||||
// Если откомментировать поле resaleLicensePrice в price-step то
|
||||
// это отработает как надо
|
||||
...(rootStore.allowResale
|
||||
? {
|
||||
allowResale: true,
|
||||
resaleLicensePrice: String(
|
||||
rootStore.licenseResalePrice * 10 ** 9,
|
||||
),
|
||||
}
|
||||
: { allowResale: false, resaleLicensePrice: "0" }),
|
||||
});
|
||||
|
||||
WebApp.close();
|
||||
} catch (error) {
|
||||
console.error("An error occurred during the submission process:", error);
|
||||
alert(`Возникла ошибка, ${JSON.stringify(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={"mt-4 px-4 pb-8"}>
|
||||
<BackButton onClick={prevStep} />
|
||||
|
||||
<div className={"mb-[30px] flex flex-col text-sm"}>
|
||||
<span className={"ml-4"}>/Подтвердите правильность данных</span>
|
||||
<div>
|
||||
5/<span className={"text-[#7B7B7B]"}>5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={"flex flex-col gap-2"}>
|
||||
<FormLabel label={"Название"}>
|
||||
<div
|
||||
className={
|
||||
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
|
||||
}
|
||||
>
|
||||
{rootStore.name}
|
||||
</div>
|
||||
</FormLabel>
|
||||
|
||||
{rootStore.author && (
|
||||
<FormLabel label={"Имя автора/исполнителя"}>
|
||||
<div
|
||||
className={
|
||||
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
|
||||
}
|
||||
>
|
||||
{rootStore.author}
|
||||
</div>
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
<FormLabel label={"Цена"}>
|
||||
<div
|
||||
className={
|
||||
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
|
||||
}
|
||||
>
|
||||
{rootStore.price} TON
|
||||
</div>
|
||||
</FormLabel>
|
||||
|
||||
{rootStore.allowResale && (
|
||||
<FormLabel label={"Цена копии"}>
|
||||
<div
|
||||
className={
|
||||
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
|
||||
}
|
||||
>
|
||||
{rootStore.licenseResalePrice} TON
|
||||
</div>
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
<FormLabel label={"Файл"}>
|
||||
{rootStore.fileSrc && !uploadFile.isUploading && (
|
||||
<div
|
||||
className={
|
||||
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
<ReactPlayer
|
||||
playsinline={true}
|
||||
controls={true}
|
||||
width={"100%"}
|
||||
url={rootStore.fileSrc}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploadFile.isUploading && uploadFile.isLoading && (
|
||||
<Progress value={uploadFile.uploadProgress} />
|
||||
)}
|
||||
</FormLabel>
|
||||
|
||||
{rootStore.allowCover && (
|
||||
<FormLabel label={"Обложка"}>
|
||||
<div
|
||||
className={
|
||||
"flex w-full items-center border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: isCoverExpanded ? "261px" : "68px",
|
||||
}}
|
||||
className={"bg-bg w-full"}
|
||||
>
|
||||
{rootStore.cover && !uploadCover.isUploading && (
|
||||
<img
|
||||
src={URL.createObjectURL(rootStore.cover)}
|
||||
alt={"cover"}
|
||||
className={"h-full w-full object-cover object-center"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{uploadCover.isUploading && uploadCover.isLoading && (
|
||||
<Progress value={uploadCover.uploadProgress} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
impactOccurred("light");
|
||||
setCoverExpanded((p) => !p);
|
||||
}}
|
||||
style={{
|
||||
height: isCoverExpanded ? "261px" : "68px",
|
||||
}}
|
||||
className={"flex w-[45px] items-center justify-center"}
|
||||
>
|
||||
{!isCoverExpanded && (
|
||||
<svg
|
||||
width="45"
|
||||
height="14"
|
||||
viewBox="0 0 45 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M31.2963 5.18519H29.2222V3.11111H28.1852V5.18519H26.1111V6.22222H28.1852V8.2963H29.2222V6.22222H31.2963V5.18519Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M33.0841 9.33333C33.9396 8.31744 34.4083 7.0318 34.4074 5.7037C34.4074 4.57562 34.0729 3.47287 33.4462 2.5349C32.8194 1.59693 31.9286 0.865871 30.8864 0.434171C29.8442 0.00247134 28.6974 -0.110481 27.591 0.109598C26.4846 0.329676 25.4683 0.872901 24.6706 1.67058C23.8729 2.46826 23.3297 3.48456 23.1096 4.59097C22.8895 5.69738 23.0025 6.8442 23.4342 7.88642C23.8659 8.92863 24.5969 9.81943 25.5349 10.4462C26.4729 11.0729 27.5756 11.4074 28.7037 11.4074C30.0318 11.4083 31.3174 10.9396 32.3333 10.0841L36.2668 14L37 13.2668L33.0841 9.33333ZM28.7037 10.3704C27.7807 10.3704 26.8785 10.0967 26.111 9.5839C25.3436 9.07112 24.7455 8.34228 24.3923 7.48956C24.0391 6.63684 23.9466 5.69853 24.1267 4.79328C24.3068 3.88804 24.7512 3.05652 25.4039 2.40387C26.0565 1.75123 26.888 1.30677 27.7933 1.12671C28.6985 0.946644 29.6368 1.03906 30.4896 1.39227C31.3423 1.74548 32.0711 2.34362 32.5839 3.11104C33.0967 3.87847 33.3704 4.78073 33.3704 5.7037C33.369 6.94096 32.8769 8.12715 32.002 9.00202C31.1271 9.87689 29.941 10.369 28.7037 10.3704Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{isCoverExpanded && (
|
||||
<svg
|
||||
width="45"
|
||||
height="15"
|
||||
viewBox="0 0 45 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M31.2963 5.68519H26.1111V6.72222L31.2963 6.72222L31.2963 5.68519Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M33.0841 9.83333C33.9396 8.81744 34.4083 7.5318 34.4074 6.20371C34.4074 5.07562 34.0729 3.97287 33.4462 3.0349C32.8194 2.09693 31.9286 1.36587 30.8864 0.934171C29.8442 0.502471 28.6974 0.389519 27.591 0.609598C26.4846 0.829676 25.4683 1.3729 24.6706 2.17058C23.8729 2.96826 23.3297 3.98456 23.1096 5.09097C22.8895 6.19738 23.0025 7.3442 23.4342 8.38642C23.8659 9.42863 24.5969 10.3194 25.5349 10.9462C26.4729 11.5729 27.5756 11.9074 28.7037 11.9074C30.0318 11.9083 31.3174 11.4396 32.3333 10.5841L36.2668 14.5L37 13.7668L33.0841 9.83333ZM28.7037 10.8704C27.7807 10.8704 26.8785 10.5967 26.111 10.0839C25.3436 9.57112 24.7455 8.84228 24.3923 7.98956C24.0391 7.13684 23.9466 6.19853 24.1267 5.29328C24.3068 4.38804 24.7512 3.55652 25.4039 2.90387C26.0565 2.25123 26.888 1.80677 27.7933 1.62671C28.6985 1.44664 29.6368 1.53906 30.4896 1.89227C31.3423 2.24548 32.0711 2.84362 32.5839 3.61104C33.0967 4.37847 33.3704 5.28073 33.3704 6.20371C33.369 7.44096 32.8769 8.62715 32.002 9.50202C31.1271 10.3769 29.941 10.869 28.7037 10.8704Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
{rootStore.royalty.map((royalty, index) => (
|
||||
<div key={index} className={"flex flex-col gap-[20px]"}>
|
||||
<div className={"flex w-full items-center gap-1"}>
|
||||
<div className={"w-[83%]"}>
|
||||
<FormLabel
|
||||
labelClassName={"flex"}
|
||||
label={`Роялти_${index + 1}`}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"break-all border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
|
||||
}
|
||||
>
|
||||
{royalty.address}
|
||||
</div>
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<div className={"h-auto w-[18%]"}>
|
||||
<FormLabel labelClassName={"text-center"} label={"%"}>
|
||||
<div
|
||||
className={
|
||||
"flex items-center justify-center border border-white bg-[#2B2B2B] py-[8px] text-sm font-bold"
|
||||
}
|
||||
>
|
||||
{royalty.value.toString()}
|
||||
</div>
|
||||
</FormLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/*{rootStore.authors.map((author, index) => (*/}
|
||||
{/* <FormLabel key={index} label={`Автор_${index + 1}`}>*/}
|
||||
{/* <div*/}
|
||||
{/* className={*/}
|
||||
{/* "break-all border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"*/}
|
||||
{/* }*/}
|
||||
{/* >*/}
|
||||
{/* {author}*/}
|
||||
{/* </div>*/}
|
||||
{/* </FormLabel>*/}
|
||||
{/*))}*/}
|
||||
</section>
|
||||
|
||||
<Button
|
||||
isLoading={
|
||||
uploadFile.isLoading ||
|
||||
uploadCover.isLoading ||
|
||||
createContent.isLoading
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
label={"Все верно!"}
|
||||
className={"mt-[30px] py-[16px]"}
|
||||
includeArrows={true}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { FormLabel } from "~/shared/ui/form-label";
|
||||
import { Input } from "~/shared/ui/input";
|
||||
import { Button } from "~/shared/ui/button";
|
||||
import { useRootStore } from "~/shared/stores/root";
|
||||
import { BackButton } from "~/shared/ui/back-button";
|
||||
|
||||
const MIN_PRICE = 0.07;
|
||||
const MIN_RESALE_PRICE = 0.07;
|
||||
|
||||
const RECOMMENDED_PRICE = 0.15;
|
||||
// const RECOMMENDED_RESALE_PRICE = 0.15;
|
||||
|
||||
type PriceStepProps = {
|
||||
prevStep(): void;
|
||||
nextStep(): void;
|
||||
};
|
||||
|
||||
export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
|
||||
const rootStore = useRootStore();
|
||||
|
||||
const formSchema = useMemo(() => {
|
||||
if (rootStore.allowResale) {
|
||||
return z.object({
|
||||
price: z.preprocess(
|
||||
(value) => {
|
||||
const parsed = parseFloat(value as string);
|
||||
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
z
|
||||
.number()
|
||||
.min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`),
|
||||
),
|
||||
|
||||
resaleLicensePrice: z
|
||||
.preprocess(
|
||||
(value) => {
|
||||
if (value === undefined || value === "" || value === 0)
|
||||
return undefined;
|
||||
|
||||
const parsed = parseFloat(value as string);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
|
||||
z
|
||||
.number()
|
||||
.min(
|
||||
MIN_RESALE_PRICE,
|
||||
`Цена копии должна быть минимум ${MIN_RESALE_PRICE} TON.`,
|
||||
),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
}
|
||||
|
||||
return z.object({
|
||||
price: z.preprocess(
|
||||
(value) => {
|
||||
const parsed = parseFloat(value as string);
|
||||
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
},
|
||||
z.number().min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`),
|
||||
),
|
||||
});
|
||||
}, [rootStore.allowResale]);
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onChange",
|
||||
|
||||
defaultValues: {
|
||||
price: rootStore.price,
|
||||
|
||||
//@ts-expect-error Fix typings
|
||||
resaleLicensePrice: rootStore?.licenseResalePrice,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void form.trigger();
|
||||
}, [rootStore.allowResale, form]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.handleSubmit(async (values: FormValues) => {
|
||||
try {
|
||||
rootStore.setPrice(values.price);
|
||||
|
||||
//@ts-expect-error Fix typings
|
||||
if (values?.resaleLicensePrice) {
|
||||
//@ts-expect-error Fix typings
|
||||
rootStore.setLicenseResalePrice(values?.resaleLicensePrice);
|
||||
}
|
||||
|
||||
nextStep();
|
||||
} catch (error) {
|
||||
console.error("Error: ", error);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={"mt-4 px-4 pb-8"}>
|
||||
<BackButton onClick={prevStep} />
|
||||
|
||||
<div className={"mb-[30px] flex flex-col text-sm"}>
|
||||
<span className={"ml-4"}>/Укажите цену</span>
|
||||
<div>
|
||||
4/<span className={"text-[#7B7B7B]"}>5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-[20px]"}>
|
||||
<FormLabel label={"Цена TON"}>
|
||||
<div className={"my-2 flex flex-col gap-1.5"}>
|
||||
<p className={"text-xs"}>Минимальная стоимость {MIN_PRICE} TON.</p>
|
||||
<p className={"text-xs"}>
|
||||
Рекомендуемая стоимость {RECOMMENDED_PRICE} TON.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
error={form.formState.errors?.price}
|
||||
placeholder={"[ Введите цену ]"}
|
||||
{...form.register("price")}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
{/*<div className={"flex flex-col gap-2"}>*/}
|
||||
{/* <FormLabel*/}
|
||||
{/* labelClassName={"flex"}*/}
|
||||
{/* label={"Разрешить копии"}*/}
|
||||
{/* formLabelAddon={*/}
|
||||
{/* <Checkbox*/}
|
||||
{/* checked={rootStore.allowResale}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* rootStore.setAllowResale(!rootStore.allowResale);*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
{/* }*/}
|
||||
{/* />*/}
|
||||
|
||||
{/* {rootStore.allowResale && (*/}
|
||||
{/* <FormLabel label={"Цена копии TON"}>*/}
|
||||
{/* <div className={"my-2 flex flex-col gap-1.5"}>*/}
|
||||
{/* <p className={"text-xs"}>*/}
|
||||
{/* Это цена, по которой пользователи будут покупать и*/}
|
||||
{/* перепродавать ваш контент.*/}
|
||||
{/* </p>*/}
|
||||
|
||||
{/* <p className={"text-xs"}>*/}
|
||||
{/* Минимальная стоимость {MIN_RESALE_PRICE} TON.*/}
|
||||
{/* </p>*/}
|
||||
{/* <p className={"text-xs"}>*/}
|
||||
{/* Рекомендуемая стоимость {RECOMMENDED_RESALE_PRICE} TON.*/}
|
||||
{/* </p>*/}
|
||||
{/* </div>*/}
|
||||
|
||||
{/* <Input*/}
|
||||
{/* //@ts-expect-error Fix typings*/}
|
||||
{/* error={form.formState.errors?.resaleLicensePrice}*/}
|
||||
{/* placeholder={"[ Введите цену копии ]"}*/}
|
||||
{/* //@ts-expect-error Fix typings*/}
|
||||
{/* {...form.register("resaleLicensePrice")}*/}
|
||||
{/* />*/}
|
||||
{/* </FormLabel>*/}
|
||||
{/* )}*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={"mt-[30px]"}
|
||||
onClick={handleSubmit}
|
||||
includeArrows={true}
|
||||
label={"Далее"}
|
||||
disabled={!form.formState.isValid}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
type ConfirmModalProps = {
|
||||
text: string;
|
||||
confirmLabel: string;
|
||||
onConfirm(): void;
|
||||
onCancel(): void;
|
||||
};
|
||||
|
||||
export const ConfirmModal = ({
|
||||
text,
|
||||
confirmLabel,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmModalProps) => {
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const handleClick = (fn: () => void) => {
|
||||
impactOccurred("light");
|
||||
fn();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"fixed left-0 top-0 z-30 flex h-full w-full items-center justify-center bg-black/80 px-[15px]"
|
||||
}
|
||||
>
|
||||
<div className={"flex flex-col gap-[30px]"}>
|
||||
<div
|
||||
className={
|
||||
"border border-white bg-[#1D1D1B] px-[10px] py-[16px] text-center"
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
|
||||
<div className={"flex w-full gap-2"}>
|
||||
<button
|
||||
className={
|
||||
"flex flex-1 items-center justify-center gap-3 border border-white bg-[#1D1D1B] py-[16px]"
|
||||
}
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="1.44674"
|
||||
y1="11.1464"
|
||||
x2="11.3462"
|
||||
y2="1.24695"
|
||||
stroke="#E30613"
|
||||
/>
|
||||
<line
|
||||
x1="1.15385"
|
||||
y1="1.14645"
|
||||
x2="11.0533"
|
||||
y2="11.0459"
|
||||
stroke="#E30613"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className={"underline"} onClick={() => handleClick(onCancel)}>
|
||||
Отменить
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={
|
||||
"flex flex-1 items-center justify-center gap-3 border border-white bg-[#1D1D1B] py-[16px]"
|
||||
}
|
||||
>
|
||||
<svg
|
||||
width="17"
|
||||
height="12"
|
||||
viewBox="0 0 17 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1.25 5.22222L6.43182 10.5L16.25 0.5" stroke="#00FFFF" />
|
||||
</svg>
|
||||
|
||||
<span
|
||||
className={"underline"}
|
||||
onClick={() => handleClick(onConfirm)}
|
||||
>
|
||||
{confirmLabel}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Button } from "~/shared/ui/button";
|
||||
|
||||
export const PercentHint = ({ close }: { close(): void }) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"fixed left-0 top-0 z-30 flex h-full w-full items-center justify-center bg-black/80 px-[15px]"
|
||||
}
|
||||
>
|
||||
<div className={"flex flex-col gap-3"}>
|
||||
<div
|
||||
className={
|
||||
"border-primary border bg-[#1D1D1B] px-[10px] py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
Сумма роялти должна составлять 100%
|
||||
</div>
|
||||
|
||||
<Button onClick={close} label={"Понятно"} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { Input } from "~/shared/ui/input";
|
||||
import { FormLabel } from "~/shared/ui/form-label";
|
||||
import { XMark } from "~/shared/ui/icons/x-mark.tsx";
|
||||
import { Button } from "~/shared/ui/button";
|
||||
import { PercentHint } from "~/pages/root/steps/royalty-step/components/percent-hint";
|
||||
import { cn } from "~/shared/utils";
|
||||
import { Spread } from "~/shared/ui/icons/spread.tsx";
|
||||
import { ConfirmModal } from "~/pages/root/steps/royalty-step/components/confirm-modal";
|
||||
import { useRootStore } from "~/shared/stores/root";
|
||||
import { BackButton } from "~/shared/ui/back-button";
|
||||
|
||||
type RoyaltyStepProps = {
|
||||
prevStep(): void;
|
||||
nextStep(): void;
|
||||
};
|
||||
|
||||
export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const [isDeleteAllOpen, setDeleteAllOpen] = useState(false);
|
||||
const [isSpreadOpen, setSpreadOpen] = useState(false);
|
||||
|
||||
const { royalty, setRoyalty, isPercentHintOpen, setPercentHintOpen } =
|
||||
useRootStore();
|
||||
|
||||
const handleAdd = () => {
|
||||
impactOccurred("light");
|
||||
setRoyalty([...royalty, { address: "", value: 0 }]);
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
if (royalty.length === 1) return;
|
||||
|
||||
impactOccurred("light");
|
||||
|
||||
const newRoyalty = royalty.filter((_, i) => i !== index);
|
||||
setRoyalty(newRoyalty);
|
||||
};
|
||||
|
||||
const handleWalletChange = (index: number, address: string) => {
|
||||
const newRoyalty = royalty.map((member, i) =>
|
||||
i === index ? { ...member, address } : member,
|
||||
);
|
||||
setRoyalty(newRoyalty);
|
||||
};
|
||||
|
||||
const handlePercentChange = (index: number, value: string) => {
|
||||
const percentNumber = parseInt(value, 10) || 0;
|
||||
const newRoyalty = royalty.map((royalty, i) =>
|
||||
i === index ? { ...royalty, value: percentNumber } : royalty,
|
||||
);
|
||||
setRoyalty(newRoyalty);
|
||||
};
|
||||
|
||||
const spreadPercentageEqually = () => {
|
||||
const equalPercent = 100 / royalty.length;
|
||||
const updatedRoyalty = royalty.map((member) => ({
|
||||
...member,
|
||||
value: equalPercent,
|
||||
}));
|
||||
setRoyalty(updatedRoyalty);
|
||||
};
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return (
|
||||
royalty.every((member) => member.address && member.value >= 0) &&
|
||||
royalty.reduce((acc, curr) => acc + curr.value, 0) === 100
|
||||
);
|
||||
}, [royalty]);
|
||||
|
||||
return (
|
||||
<section className={"mt-4 px-4 pb-8"}>
|
||||
{isPercentHintOpen && (
|
||||
<PercentHint close={() => setPercentHintOpen(false)} />
|
||||
)}
|
||||
|
||||
{isDeleteAllOpen && (
|
||||
<ConfirmModal
|
||||
text={"Удалить всю информацию об авторах?"}
|
||||
confirmLabel={"Удалить"}
|
||||
onConfirm={() => {
|
||||
setRoyalty([{ address: "", value: 100 }]);
|
||||
setDeleteAllOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setDeleteAllOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSpreadOpen && (
|
||||
<ConfirmModal
|
||||
text={"Распределить роялти на всех авторов равномерно?"}
|
||||
confirmLabel={"Распределить"}
|
||||
onConfirm={() => {
|
||||
spreadPercentageEqually();
|
||||
setSpreadOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setSpreadOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BackButton onClick={prevStep} />
|
||||
|
||||
<div className={"mb-[30px] flex flex-col text-sm"}>
|
||||
<span className={"ml-4"}>/Заполните информацию об роялти</span>
|
||||
<div>
|
||||
3/<span className={"text-[#7B7B7B]"}>5</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className={"flex flex-col gap-1.5"}>
|
||||
{royalty.map((member, index) => (
|
||||
<div key={index} className={"flex flex-col gap-[20px]"}>
|
||||
<div className={"flex w-full items-center gap-1"}>
|
||||
<div className={"w-[83%]"}>
|
||||
<FormLabel
|
||||
labelClassName={"flex"}
|
||||
formLabelAddon={
|
||||
index > 0 && (
|
||||
<button onClick={() => handleRemove(index)}>
|
||||
<XMark />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
label={`Роялти_${index + 1}`}
|
||||
>
|
||||
<Input
|
||||
value={member.address}
|
||||
onChange={(e) => handleWalletChange(index, e.target.value)}
|
||||
placeholder={"[ Введите адрес криптокошелька TON ]"}
|
||||
/>
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<div className={"w-[18%]"}>
|
||||
<FormLabel labelClassName={"text-center"} label={"%"}>
|
||||
<Input
|
||||
value={member.value.toString()}
|
||||
onChange={(e) => handlePercentChange(index, e.target.value)}
|
||||
placeholder={"[ % ]"}
|
||||
className={cn({
|
||||
"pointer-events-none z-50 border-primary":
|
||||
isPercentHintOpen,
|
||||
})}
|
||||
/>
|
||||
</FormLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
className={
|
||||
"mt-[30px] flex w-full items-center justify-center border border-white py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
Добавить_роялти
|
||||
</button>
|
||||
|
||||
{royalty.length > 1 && (
|
||||
<div className={"mt-[30px] flex items-center gap-2"}>
|
||||
<button
|
||||
onClick={() => {
|
||||
impactOccurred("light");
|
||||
setDeleteAllOpen(true);
|
||||
}}
|
||||
className={
|
||||
"flex w-full items-center justify-center gap-3 border border-white py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
Удалить все
|
||||
<XMark />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
impactOccurred("light");
|
||||
setSpreadOpen(true);
|
||||
}}
|
||||
className={
|
||||
"flex w-full items-center justify-center gap-3 border border-white py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
Распределить <Spread />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
label={"Готово"}
|
||||
className={"mt-[30px]"}
|
||||
includeArrows={true}
|
||||
onClick={nextStep}
|
||||
disabled={!isValid}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { Button } from "~/shared/ui/button";
|
||||
import { useAuth } from "~/shared/services/auth";
|
||||
|
||||
type WelcomeStepProps = {
|
||||
nextStep(): void;
|
||||
};
|
||||
|
||||
export const WelcomeStep = ({ nextStep }: WelcomeStepProps) => {
|
||||
const auth = useAuth();
|
||||
|
||||
const handleNextClick = async () => {
|
||||
const res = await auth.mutateAsync();
|
||||
sessionStorage.setItem("auth_v1_token", res.data.auth_v1_token);
|
||||
nextStep();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={"mt-4 px-4"}>
|
||||
<div className={"flex gap-2 text-sm"}>
|
||||
<span>/ Добро пожаловать в MY</span>
|
||||
|
||||
<div className={"flex items-center gap-0"}>
|
||||
<span>[</span>
|
||||
<div className={"mb-0.5 h-3 w-3 rounded-full bg-primary"} />
|
||||
<span>]:</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"mt-2"}>
|
||||
<p className={"text-sm"}>
|
||||
децентрализованную систему монетизации контента. для продолжения
|
||||
необходимо подключить криптокошелек TON
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={"Подключить криптокошелёк TON"}
|
||||
className={"mt-[30px]"}
|
||||
includeArrows={true}
|
||||
isLoading={auth.isLoading}
|
||||
onClick={handleNextClick}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { ReactNode, useMemo, useState } from "react";
|
||||
|
||||
export const useSteps = (
|
||||
sections: ({
|
||||
nextStep,
|
||||
prevStep,
|
||||
}: {
|
||||
nextStep(): void;
|
||||
prevStep(): void;
|
||||
}) => ReactNode[],
|
||||
) => {
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
const nextStep = () => {
|
||||
return setStep((s) => s + 1);
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
return setStep((s) => s - 1);
|
||||
};
|
||||
|
||||
const ActiveSection = useMemo(() => {
|
||||
return sections({ nextStep, prevStep })[step];
|
||||
}, [step, sections]);
|
||||
|
||||
return {
|
||||
ActiveSection,
|
||||
setStep,
|
||||
step,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./request";
|
||||
export * from "./query-client";
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { QueryClient } from "react-query";
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import axios from "axios";
|
||||
|
||||
export const APP_API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string;
|
||||
|
||||
export const request = axios.create({
|
||||
baseURL: APP_API_BASE_URL,
|
||||
});
|
||||
|
||||
request.interceptors.request.use((config) => {
|
||||
const auth_v1_token = sessionStorage.getItem("auth_v1_token");
|
||||
|
||||
if (auth_v1_token) {
|
||||
config.headers.Authorization = auth_v1_token;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { useMutation } from "react-query";
|
||||
import { useWebApp } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { request } from "~/shared/libs";
|
||||
|
||||
export const useAuth = () => {
|
||||
const WebApp = useWebApp();
|
||||
|
||||
return useMutation(["auth"], () => {
|
||||
return request.post<{
|
||||
connected_wallet: null | {
|
||||
version: string;
|
||||
address: string;
|
||||
ton_balance: string;
|
||||
};
|
||||
|
||||
auth_v1_token: string;
|
||||
}>("/auth.twa", {
|
||||
twa_data: WebApp.initData,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { useMutation } from "react-query";
|
||||
|
||||
import { request } from "~/shared/libs";
|
||||
import { Royalty } from "~/shared/stores/root";
|
||||
|
||||
type UseCreateNewContentPayload = {
|
||||
title: string;
|
||||
content: string;
|
||||
image: string;
|
||||
description: string;
|
||||
price: string;
|
||||
resaleLicensePrice: string; // nanoTON bignum (default = 0)
|
||||
allowResale: boolean;
|
||||
authors: string[];
|
||||
royaltyParams: Royalty[];
|
||||
};
|
||||
|
||||
export const useCreateNewContent = () => {
|
||||
return useMutation(
|
||||
["create-new-content"],
|
||||
(payload: UseCreateNewContentPayload) => {
|
||||
return request.post<{
|
||||
message: string;
|
||||
}>("/blockchain.sendNewContentMessage", payload);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const usePurchaseContent = () => {
|
||||
return useMutation(
|
||||
["purchase-content"],
|
||||
(payload: { content_address: string; price: string }) => {
|
||||
return request.post<{
|
||||
message: string;
|
||||
}>("/blockchain.sendPurchaseContentMessage", payload);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { useMutation } from "react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import { request } from "~/shared/libs";
|
||||
|
||||
export const useUploadFile = () => {
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const mutation = useMutation(["upload-file"], (file: File) => {
|
||||
setIsUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
return request
|
||||
.post<{
|
||||
content_sha256: string;
|
||||
content_id_v1: string;
|
||||
content_url: string;
|
||||
}>("/storage", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / (progressEvent?.total as number) ??
|
||||
0,
|
||||
);
|
||||
setUploadProgress(percentCompleted);
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
setIsUploading(false);
|
||||
return response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsUploading(false);
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
return { ...mutation, uploadProgress, isUploading };
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
export type Royalty = {
|
||||
address: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
type RootStore = {
|
||||
name: string;
|
||||
setName: (name: string) => void;
|
||||
|
||||
author: string;
|
||||
setAuthor: (author: string) => void;
|
||||
|
||||
file: File | null;
|
||||
setFile: (file: File) => void;
|
||||
|
||||
fileSrc: string;
|
||||
setFileSrc: (fileSrc: string) => void;
|
||||
|
||||
allowCover: boolean;
|
||||
setAllowCover: (allowCover: boolean) => void;
|
||||
|
||||
cover: File | null;
|
||||
setCover: (cover: File | null) => void;
|
||||
|
||||
isPercentHintOpen: boolean;
|
||||
setPercentHintOpen: (isPercentHintOpen: boolean) => void;
|
||||
|
||||
authors: string[];
|
||||
setAuthors: (authors: string[]) => void;
|
||||
|
||||
royalty: Royalty[];
|
||||
setRoyalty: (authors: Royalty[]) => void;
|
||||
|
||||
price: number;
|
||||
setPrice: (price: number) => void;
|
||||
|
||||
allowResale: boolean;
|
||||
setAllowResale: (allowResale: boolean) => void;
|
||||
|
||||
licenseResalePrice: number;
|
||||
setLicenseResalePrice: (licenseResalePrice: number) => void;
|
||||
};
|
||||
|
||||
export const useRootStore = create<RootStore>((set) => ({
|
||||
name: "",
|
||||
setName: (name) => set({ name }),
|
||||
|
||||
author: "",
|
||||
setAuthor: (author) => set({ author }),
|
||||
|
||||
file: null,
|
||||
setFile: (file) => set({ file }),
|
||||
|
||||
fileSrc: "",
|
||||
setFileSrc: (fileSrc) => set({ fileSrc }),
|
||||
|
||||
allowCover: false,
|
||||
setAllowCover: (allowCover) => set({ allowCover }),
|
||||
|
||||
cover: null,
|
||||
setCover: (cover) => set({ cover }),
|
||||
|
||||
isPercentHintOpen: true,
|
||||
setPercentHintOpen: (isPercentHintOpen) => set({ isPercentHintOpen }),
|
||||
|
||||
authors: [],
|
||||
setAuthors: (authors) => set({ authors }),
|
||||
|
||||
royalty: [{ address: "", value: 100 }],
|
||||
setRoyalty: (royalty) => set({ royalty }),
|
||||
|
||||
price: 0,
|
||||
setPrice: (price: number) => set({ price }),
|
||||
|
||||
allowResale: false,
|
||||
setAllowResale: (allowResale) => set({ allowResale }),
|
||||
|
||||
licenseResalePrice: 0,
|
||||
setLicenseResalePrice: (licenseResalePrice) => set({ licenseResalePrice }),
|
||||
}));
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
BackButton as NativeBackButton,
|
||||
useHapticFeedback,
|
||||
} from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
type BackButtonProps = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const BackButton = ({ onClick }: BackButtonProps) => {
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const handleClick = () => {
|
||||
impactOccurred("light");
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return <NativeBackButton onClick={handleClick} />;
|
||||
};
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { cn } from "~/shared/utils";
|
||||
|
||||
type ButtonProps = {
|
||||
label: string;
|
||||
onClick?(): void;
|
||||
includeArrows?: boolean;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const Button = ({
|
||||
includeArrows,
|
||||
label,
|
||||
className,
|
||||
disabled,
|
||||
onClick,
|
||||
isLoading,
|
||||
}: ButtonProps) => {
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const handleClick = () => {
|
||||
impactOccurred("light");
|
||||
onClick?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-center border border-white bg-primary py-[8px] text-sm",
|
||||
{
|
||||
"pointer-events-none bg-[#7B7B7B80]": disabled,
|
||||
"pointer-events-none animate-pulse": isLoading,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{includeArrows && <span className={"mr-3"}>{">>>"}</span>}
|
||||
{label}
|
||||
{includeArrows && <span className={"ml-3"}>{"<<<"}</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
type CheckboxProps = {
|
||||
onClick(): void;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
export const Checkbox = ({ onClick, checked }: CheckboxProps) => {
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
const handleClick = () => {
|
||||
impactOccurred("light");
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={"flex h-8 w-8 items-center justify-center bg-[#2B2B2B] p-2"}
|
||||
>
|
||||
{checked && <div className={"h-full w-full bg-primary"} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
type FileButtonProps = {
|
||||
htmlFor: string;
|
||||
};
|
||||
|
||||
export const FileButton = ({ htmlFor }: FileButtonProps) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
className={
|
||||
"flex w-full items-center justify-center border border-white py-[8px] text-sm"
|
||||
}
|
||||
>
|
||||
<span className={"underline"}>Загрузить_файл</span>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
import { cn } from "~/shared/utils";
|
||||
|
||||
type FormLabelProps = {
|
||||
label: string;
|
||||
children?: ReactNode;
|
||||
labelClassName?: string;
|
||||
formLabelAddon?: ReactNode;
|
||||
};
|
||||
|
||||
export const FormLabel = ({
|
||||
label,
|
||||
children,
|
||||
labelClassName,
|
||||
formLabelAddon,
|
||||
}: FormLabelProps) => {
|
||||
return (
|
||||
<div className={"flex flex-col gap-1"}>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full items-center justify-between text-sm",
|
||||
labelClassName,
|
||||
)}
|
||||
>
|
||||
{label} {formLabelAddon}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { ChangeEvent } from "react";
|
||||
|
||||
import { processFile } from "~/shared/utils";
|
||||
|
||||
type HiddenFileInputProps = {
|
||||
id: string;
|
||||
onChange(file: File): void;
|
||||
accept?: string;
|
||||
shouldProcess?: boolean;
|
||||
};
|
||||
|
||||
export const HiddenFileInput = ({
|
||||
id,
|
||||
onChange,
|
||||
accept,
|
||||
shouldProcess = true,
|
||||
}: HiddenFileInputProps) => {
|
||||
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target?.files?.[0];
|
||||
|
||||
if (file) {
|
||||
if (shouldProcess) {
|
||||
onChange(await processFile(file));
|
||||
} else {
|
||||
onChange(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
onChange={handleInputChange}
|
||||
accept={accept}
|
||||
type={"file"}
|
||||
id={id}
|
||||
hidden
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
export const Spread = () => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="11"
|
||||
viewBox="0 0 20 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.66328 0.251953H5.77656L1.17109 10.1133H0.0519533L4.66328 0.251953ZM11.5938 0.251953H12.707L8.10156 10.1133H6.98242L11.5938 0.251953ZM18.5242 0.251953H19.6375L15.032 10.1133H13.9129L18.5242 0.251953Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
export const XMark = () => {
|
||||
return (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="0.747033"
|
||||
y1="10.6464"
|
||||
x2="10.6465"
|
||||
y2="0.746952"
|
||||
stroke="white"
|
||||
/>
|
||||
<line
|
||||
x1="0.454139"
|
||||
y1="0.646447"
|
||||
x2="10.3536"
|
||||
y2="10.5459"
|
||||
stroke="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { forwardRef, InputHTMLAttributes } from "react";
|
||||
import { FieldError } from "react-hook-form";
|
||||
|
||||
import { cn } from "~/shared/utils";
|
||||
|
||||
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
error?: FieldError;
|
||||
};
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
return (
|
||||
<div className={"flex w-full flex-col gap-1.5"}>
|
||||
<input
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(
|
||||
"border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm outline-none",
|
||||
props?.className,
|
||||
)}
|
||||
/>
|
||||
|
||||
{props.error && (
|
||||
<span className={"text-xs text-red-500"}>{props.error.message}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { cn, getIndexArray } from "~/shared/utils";
|
||||
|
||||
type ProgressProps = {
|
||||
value: number; // from 0 to 100
|
||||
};
|
||||
|
||||
export const Progress = ({ value }: ProgressProps) => {
|
||||
const activeBars = Math.round((value / 100) * 25);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-[5px] border border-white px-[10px] py-[8px]">
|
||||
{getIndexArray(25).map((idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={cn("h-[14px] w-full", {
|
||||
"bg-primary": idx < activeBars,
|
||||
"bg-[#2B2B2B]": idx >= activeBars,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="ml-2 text-sm">{value}%</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { twMerge } from "tailwind-merge";
|
||||
import clsx, { ClassValue } from "clsx";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => {
|
||||
return twMerge(clsx(inputs));
|
||||
};
|
||||
|
||||
export const bufferToFile = (buffer: ArrayBuffer, filename: string) => {
|
||||
const blob = new Blob([buffer], { type: "image/*" });
|
||||
return new File([blob], filename);
|
||||
};
|
||||
|
||||
export const processFile = async (file: File) => {
|
||||
const buffer = await file.arrayBuffer();
|
||||
return bufferToFile(buffer, file.name);
|
||||
};
|
||||
|
||||
export const getIndexArray = (len: number) =>
|
||||
new Array(len).fill("").map((_, i) => i);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
gray: "#1d1d1b",
|
||||
primary: "#e40615",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
|
@ -14,17 +14,16 @@
|
|||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src", "./tailwind.config.ts"],
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import TSPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), TSPaths()],
|
||||
});
|
||||
Loading…
Reference in New Issue