tg buttons /viewContent, tags, validation address form and more /uploadContent

This commit is contained in:
Verticool 2025-02-01 19:17:40 +06:00
parent 372f84d1b1
commit 7705908484
16 changed files with 336 additions and 89 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
VITE_API_BASE_URL=https://my-public-node-1.projscale.dev/api/v1

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
node_modules node_modules
.DS_Store .DS_Store
dist dist
.env

65
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@tonconnect/ui-react": "^2.0.2", "@tonconnect/ui-react": "^2.0.2",
"@vkruglikov/react-telegram-web-app": "^2.1.9", "@vkruglikov/react-telegram-web-app": "^2.1.9",
"axios": "^1.6.7", "axios": "^1.6.7",
"buffer": "^6.0.3",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -2054,6 +2055,26 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/big-integer": { "node_modules/big-integer": {
"version": "1.6.52", "version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
@ -2139,6 +2160,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@ -3621,6 +3666,26 @@
"react-is": "^16.7.0" "react-is": "^16.7.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",

View File

@ -15,6 +15,7 @@
"@tonconnect/ui-react": "^2.0.2", "@tonconnect/ui-react": "^2.0.2",
"@vkruglikov/react-telegram-web-app": "^2.1.9", "@vkruglikov/react-telegram-web-app": "^2.1.9",
"axios": "^1.6.7", "axios": "^1.6.7",
"buffer": "^6.0.3",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -1,4 +1,5 @@
import "~/app/styles/globals.css"; import "~/app/styles/globals.css";
import '~/shared/libs/buffer';
import { useEffect } from "react"; import { useEffect } from "react";
import { useExpand, useWebApp } from "@vkruglikov/react-telegram-web-app"; import { useExpand, useWebApp } from "@vkruglikov/react-telegram-web-app";

View File

@ -26,18 +26,26 @@
/*Input Range*/ /*Input Range*/
/* Custom styles for the range input */ /* Custom styles for the range input */
input[type="range"] { input[type="range"] {
-webkit-appearance: none; /* Remove default appearance */ -webkit-appearance: none;
width: 100%; /* Full width */ /* Remove default appearance */
height: 2px; /* Track height */ width: 100%;
background: linear-gradient( /* Full width */
to right, height: 2px;
white 0%, /* Start color of passed track */ /* Track height */
white var(--value-percentage, 0%), /* End color of passed track */ background: linear-gradient(to right,
gray var(--value-percentage, 0%), /* Start color of remaining track */ white 0%,
gray 100% /* End color of remaining track */ /* Start color of passed track */
); white var(--value-percentage, 0%),
border-radius: 9999px; /* Rounded-full track */ /* End color of passed track */
outline: none; /* Remove outline */ gray var(--value-percentage, 0%),
/* Start color of remaining track */
gray 100%
/* End color of remaining track */
);
border-radius: 9999px;
/* Rounded-full track */
outline: none;
/* Remove outline */
transition: background 0.3s; transition: background 0.3s;
} }
@ -45,45 +53,64 @@
input[type="range"]::-webkit-slider-thumb { input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 9px; /* Thumb width */ width: 9px;
height: 9px; /* Thumb height */ /* Thumb width */
background: #fff; /* Thumb color (blue-500) */ height: 9px;
border-radius: 9999px; /* Rounded-full thumb */ /* Thumb height */
cursor: pointer; /* Pointer cursor on hover */ background: #fff;
/* Thumb color (blue-500) */
border-radius: 9999px;
/* Rounded-full thumb */
cursor: pointer;
/* Pointer cursor on hover */
transition: background 0.3s; transition: background 0.3s;
} }
/* For Firefox */ /* For Firefox */
input[type="range"]::-moz-range-thumb { input[type="range"]::-moz-range-thumb {
appearance: none; appearance: none;
width: 9px; /* Thumb width */ width: 9px;
height: 9px; /* Thumb height */ /* Thumb width */
background: #fff; /* Thumb color (blue-500) */ height: 9px;
border-radius: 9999px; /* Rounded-full thumb */ /* Thumb height */
cursor: pointer; /* Pointer cursor on hover */ background: #fff;
/* Thumb color (blue-500) */
border-radius: 9999px;
/* Rounded-full thumb */
cursor: pointer;
/* Pointer cursor on hover */
transition: background 0.3s; transition: background 0.3s;
} }
input[type="range"]::-moz-range-track { input[type="range"]::-moz-range-track {
-webkit-appearance: none; /* Remove default appearance */ -webkit-appearance: none;
width: 100%; /* Full width */ /* Remove default appearance */
height: 2px; /* Track height */ width: 100%;
background: linear-gradient( /* Full width */
to right, height: 2px;
white 0%, /* Start color of passed track */ /* Track height */
white var(--value-percentage, 0%), /* End color of passed track */ background: linear-gradient(to right,
gray var(--value-percentage, 0%), /* Start color of remaining track */ white 0%,
gray 100% /* End color of remaining track */ /* Start color of passed track */
); white var(--value-percentage, 0%),
border-radius: 9999px; /* Rounded-full track */ /* End color of passed track */
outline: none; /* Remove outline */ gray var(--value-percentage, 0%),
/* Start color of remaining track */
gray 100%
/* End color of remaining track */
);
border-radius: 9999px;
/* Rounded-full track */
outline: none;
/* Remove outline */
transition: background 0.3s; transition: background 0.3s;
} }
/* English comment: override react-tag-input classes */ /* English comment: override react-tag-input classes */
.ReactTags__tagInputField { .ReactTags__tagInputField {
@apply bg-transparent text-gray placeholder:text-gray outline-none w-full h-8; @apply bg-[#2B2B2B] outline-none w-full h-8 text-sm !important;
@apply border border-white py-[20px] !important; @apply border border-white px-[10px] py-[18px] !important;
@apply whitespace-pre !important;
/* English comment: /* English comment:
'bg-transparent' to blend with your dark background 'bg-transparent' to blend with your dark background
'text-white' to have white text 'text-white' to have white text
@ -94,7 +121,7 @@
/* English comment: style for the tag itself when it's rendered */ /* English comment: style for the tag itself when it's rendered */
.ReactTags__selected .ReactTags__tag { .ReactTags__selected .ReactTags__tag {
@apply bg-[#363636] text-white text-sm inline-flex items-center px-2 py-1 rounded mr-1; @apply bg-[#363636] text-white text-sm inline-flex items-center px-2 py-1 mb-2 rounded mr-1 !important;
/* English comment: /* English comment:
'bg-[#363636]' to have a dark gray background 'bg-[#363636]' to have a dark gray background
'text-white' keeps the text white 'text-white' keeps the text white
@ -105,11 +132,11 @@
} }
.ReactTags__selected .ReactTags__remove { .ReactTags__selected .ReactTags__remove {
@apply ml-1 text-gray hover:text-white cursor-pointer; @apply ml-1 text-gray hover:text-white cursor-pointer !important;
/* English comment: /* English comment:
'ml-1' a small margin to separate the 'x' or close symbol 'ml-1' a small margin to separate the 'x' or close symbol
'text-gray-400' by default, and change to white on hover 'text-gray-400' by default, and change to white on hover
'cursor-pointer' so it looks clickable 'cursor-pointer' so it looks clickable
*/ */
} }
} }

View File

@ -64,7 +64,7 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
content: fileUploadResult.content_id_v1, content: fileUploadResult.content_id_v1,
image: coverUploadResult.content_id_v1, image: coverUploadResult.content_id_v1,
price: String(rootStore.price * 10 ** 9), price: String(rootStore.price * 10 ** 9),
hashtags: rootStore.hashtags,
royaltyParams: rootStore.royalty.map((member) => ({ royaltyParams: rootStore.royalty.map((member) => ({
...member, ...member,
value: member.value * 100, value: member.value * 100,
@ -151,6 +151,24 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
</FormLabel> </FormLabel>
)} )}
{rootStore.author && (
<FormLabel label={"Теги"}>
<div
className="flex flex-wrap gap-1">
{rootStore.hashtags.map((tag, index) => (
<div
key={index}
className={
"bg-[#363636] text-white text-sm inline-flex items-center px-2 py-1 rounded mr-1"
}
>
{tag}
</div>
))}
</div>
</FormLabel>
)}
<FormLabel label={"Цена"}> <FormLabel label={"Цена"}>
<div <div
className={ className={

View File

@ -9,10 +9,10 @@ import { Button } from "~/shared/ui/button";
import { useRootStore } from "~/shared/stores/root"; import { useRootStore } from "~/shared/stores/root";
import { BackButton } from "~/shared/ui/back-button"; import { BackButton } from "~/shared/ui/back-button";
const MIN_PRICE = 0.07; const MIN_PRICE = 0.15;
const MIN_RESALE_PRICE = 0.07; const MIN_RESALE_PRICE = 0.15;
const RECOMMENDED_PRICE = 0.15; // const RECOMMENDED_PRICE = 0.15;
// const RECOMMENDED_RESALE_PRICE = 0.15; // const RECOMMENDED_RESALE_PRICE = 0.15;
type PriceStepProps = { type PriceStepProps = {
@ -25,9 +25,9 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
const formSchema = useMemo(() => { const formSchema = useMemo(() => {
const parsePrice = (value: unknown) => { const parsePrice = (value: unknown) => {
if (typeof value === "string") { if (typeof value === "string" || typeof value === "number") {
// Replace commas with dots and parse the value const stringValue = value.toString().replace(",", ".");
const parsedValue = parseFloat(value.replace(",", ".")); const parsedValue = parseFloat(stringValue);
return isNaN(parsedValue) ? undefined : parsedValue; return isNaN(parsedValue) ? undefined : parsedValue;
} }
return undefined; return undefined;
@ -37,10 +37,10 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
return z.object({ return z.object({
price: z.preprocess( price: z.preprocess(
parsePrice, parsePrice,
z.number().min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`) z.number({required_error: 'Цена не соответствует требованиям'}).min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`)
), ),
resaleLicensePrice: z resaleLicensePrice: z
.preprocess(parsePrice, z.number().min(MIN_RESALE_PRICE, `Цена копии должна быть минимум ${MIN_RESALE_PRICE} TON.`)) .preprocess(parsePrice, z.number({required_error: 'Цена не соответствует требованиям'}).min(MIN_RESALE_PRICE, `Цена копии должна быть минимум ${MIN_RESALE_PRICE} TON.`))
.optional(), .optional(),
}); });
} }
@ -48,7 +48,7 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
return z.object({ return z.object({
price: z.preprocess( price: z.preprocess(
parsePrice, parsePrice,
z.number().min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`) z.number({required_error: 'Цена не соответствует требованиям'}).min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`)
), ),
}); });
}, [rootStore.allowResale]); }, [rootStore.allowResale]);
@ -59,9 +59,9 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
mode: "onChange", mode: "onChange",
defaultValues: { defaultValues: {
price: rootStore.price, price: rootStore.price || MIN_PRICE,
//@ts-expect-error Fix typings //@ts-expect-error Fix typings
resaleLicensePrice: rootStore?.licenseResalePrice, resaleLicensePrice: rootStore?.licenseResalePrice || MIN_RESALE_PRICE,
}, },
}); });
@ -98,13 +98,22 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
<FormLabel label={"Цена продажи TON"}> <FormLabel label={"Цена продажи TON"}>
<div className={"my-2 flex flex-col gap-1.5"}> <div className={"my-2 flex flex-col gap-1.5"}>
<p className={"text-xs"}>Минимальная стоимость {MIN_PRICE} TON.</p> <p className={"text-xs"}>Минимальная стоимость {MIN_PRICE} TON.</p>
<p className={"text-xs"}>Рекомендуемая стоимость {RECOMMENDED_PRICE} TON.</p> {/* <p className={"text-xs"}>Рекомендуемая стоимость {RECOMMENDED_PRICE} TON.</p> */}
</div> </div>
<Input <Input
error={form.formState.errors?.price} error={form.formState.errors?.price}
placeholder={"[ Введите цену ]"} placeholder={"[ Введите цену ]"}
{...form.register("price")} inputMode="decimal"
/> pattern="[0-9]*[.,]?[0-9]*"
{...form.register("price", {
onChange: (e) => {
const value = e.target.value;
if (!/^\d*[.,]?\d*$/.test(value)) {
e.target.value = value.replace(/[^\d.,]/g, '');
}
}
})}
/>
</FormLabel> </FormLabel>
</div> </div>
<Button <Button

View File

@ -12,17 +12,30 @@ import { ConfirmModal } from "~/pages/root/steps/royalty-step/components/confirm
import { useRootStore } from "~/shared/stores/root"; import { useRootStore } from "~/shared/stores/root";
import { BackButton } from "~/shared/ui/back-button"; import { BackButton } from "~/shared/ui/back-button";
import { useTonConnectUI } from "@tonconnect/ui-react"; import { useTonConnectUI } from "@tonconnect/ui-react";
import { Address } from "@ton/core";
import { FieldError } from "react-hook-form";
type RoyaltyStepProps = { type RoyaltyStepProps = {
prevStep(): void; prevStep(): void;
nextStep(): void; nextStep(): void;
}; };
const isValidTonAddress = (address: string): boolean => {
try {
if (!address) return false;
Address.parse(address);
return true;
} catch {
return false;
}
};
export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => { export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
const [impactOccurred] = useHapticFeedback(); const [impactOccurred] = useHapticFeedback();
const [isDeleteAllOpen, setDeleteAllOpen] = useState(false); const [isDeleteAllOpen, setDeleteAllOpen] = useState(false);
const [isSpreadOpen, setSpreadOpen] = useState(false); const [isSpreadOpen, setSpreadOpen] = useState(false);
const [addressErrors, setAddressErrors] = useState<Record<number, FieldError | undefined>>({});
const { royalty, setRoyalty, isPercentHintOpen, setPercentHintOpen } = const { royalty, setRoyalty, isPercentHintOpen, setPercentHintOpen } =
useRootStore(); useRootStore();
@ -42,11 +55,23 @@ export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
}; };
const handleWalletChange = (index: number, address: string) => { const handleWalletChange = (index: number, address: string) => {
const isValid = isValidTonAddress(address);
setAddressErrors({
...addressErrors,
[index]: !isValid
? {
type: 'validation',
message: 'Неверный адрес TON'
}
: undefined
});
const newRoyalty = royalty.map((member, i) => const newRoyalty = royalty.map((member, i) =>
i === index ? { ...member, address } : member, i === index ? { ...member, address } : member
); );
setRoyalty(newRoyalty); setRoyalty(newRoyalty);
}; };
const handlePercentChange = (index: number, value: string) => { const handlePercentChange = (index: number, value: string) => {
const percentNumber = parseInt(value, 10) || 0; const percentNumber = parseInt(value, 10) || 0;
@ -67,20 +92,28 @@ export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
const isValid = useMemo(() => { const isValid = useMemo(() => {
return ( return (
royalty.every((member) => member.address && member.value >= 0) && royalty.every((member) => isValidTonAddress(member.address) && member.value >= 0) &&
royalty.reduce((acc, curr) => acc + curr.value, 0) === 100 royalty.reduce((acc, curr) => acc + curr.value, 0) === 100
); );
}, [royalty]); }, [royalty]);
const [tonConnectUI] = useTonConnectUI(); const [tonConnectUI] = useTonConnectUI();
// Устанавливаем адрес из tonConnectUI.account при загрузке страницы // Устанавливаем адрес из tonConnectUI.account при загрузке страницы
useEffect(() => { useEffect(() => {
console.log('tonconnectUI', tonConnectUI) if (!tonConnectUI.account) return;
if (tonConnectUI.account) {
setRoyalty([{ address: tonConnectUI.account.address, value: 100 }]); if (royalty.length === 0) {
// First initialization with 100%
setRoyalty([{
address: Address.parse(tonConnectUI.account.address).toString({
bounceable: true,
urlSafe: true,
testOnly: false,
}),
value: 100
}]);
} }
}, [tonConnectUI.account, setRoyalty]); }, [tonConnectUI.account, setRoyalty, royalty]);
return ( return (
<section className={"mt-4 px-4 pb-8"}> <section className={"mt-4 px-4 pb-8"}>
@ -128,7 +161,7 @@ export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
<section className={"flex flex-col gap-1.5"}> <section className={"flex flex-col gap-1.5"}>
{royalty.map((member, index) => ( {royalty.map((member, index) => (
<div key={index} className={"flex flex-col gap-[20px]"}> <div key={index} className={"flex flex-col gap-[20px]"}>
<div className={"flex w-full items-center gap-1"}> <div className={"flex w-full items-start gap-1"}>
<div className={"w-[83%]"}> <div className={"w-[83%]"}>
<FormLabel <FormLabel
labelClassName={"flex"} labelClassName={"flex"}
@ -145,7 +178,8 @@ export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
value={member.address} value={member.address}
onChange={(e) => handleWalletChange(index, e.target.value)} onChange={(e) => handleWalletChange(index, e.target.value)}
placeholder={"[ Введите адрес криптокошелька TON ]"} placeholder={"[ Введите адрес криптокошелька TON ]"}
/> error={addressErrors[index]}
/>
</FormLabel> </FormLabel>
</div> </div>

View File

@ -2,7 +2,6 @@ import ReactPlayer from "react-player/lazy";
import { useTonConnectUI } from "@tonconnect/ui-react"; import { useTonConnectUI } from "@tonconnect/ui-react";
import { useWebApp } from "@vkruglikov/react-telegram-web-app"; import { useWebApp } from "@vkruglikov/react-telegram-web-app";
import { Button } from "~/shared/ui/button";
import { usePurchaseContent, useViewContent } from "~/shared/services/content"; import { usePurchaseContent, useViewContent } from "~/shared/services/content";
import { fromNanoTON } from "~/shared/utils"; import { fromNanoTON } from "~/shared/utils";
import {useCallback, useEffect, useMemo} from "react"; import {useCallback, useEffect, useMemo} from "react";
@ -72,7 +71,43 @@ export const ViewContentPage = () => {
}, []); }, []);
useEffect(() => {
const mainButton = WebApp.MainButton;
const secondaryButton = WebApp.SecondaryButton;
try {
// Set main button color
mainButton.color = '#e40615';
mainButton.textColor = '#FFFFFF';
// Set secondary button color
secondaryButton.color = '#363636';
secondaryButton.textColor = '#FFFFFF';
if (!haveLicense) {
mainButton.text = `Купить за ${fromNanoTON(content?.data?.encrypted?.license?.resale?.price)} ТОН`;
mainButton.show();
mainButton.onClick(handleBuyContent);
} else {
mainButton.hide();
}
secondaryButton.text = 'Загрузить свой контент';
secondaryButton.show();
secondaryButton.onClick(() => {
WebApp.openTelegramLink('https://t.me/MY_UploaderRobot');
});
return () => {
mainButton.hide();
mainButton.offClick(handleBuyContent);
secondaryButton.hide();
secondaryButton.offClick();
};
} catch (error) {
console.error('Error setting up Telegram WebApp buttons:', error);
}
}, [content, haveLicense, WebApp, handleBuyContent]);
return ( return (
<main className={"flex w-full flex-col gap-[50px] px-4"}> <main className={"flex w-full flex-col gap-[50px] px-4"}>
{content?.data?.content_type.startsWith("audio") && content?.data?.display_options?.metadata?.image && ( {content?.data?.content_type.startsWith("audio") && content?.data?.display_options?.metadata?.image && (
@ -111,7 +146,7 @@ export const ViewContentPage = () => {
</p> </p>
</section> </section>
{!haveLicense && <Button {/* {!haveLicense && <Button
onClick={handleBuyContent} onClick={handleBuyContent}
className={"mb-4 mt-[30px] h-[48px]"} className={"mb-4 mt-[30px] h-[48px]"}
label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.resale?.price)} ТОН`} label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.resale?.price)} ТОН`}
@ -125,7 +160,7 @@ export const ViewContentPage = () => {
}} }}
className={"mb-4 mt-[-20px] h-[48px] bg-darkred"} className={"mb-4 mt-[-20px] h-[48px] bg-darkred"}
label={`Загрузить свой контент`} label={`Загрузить свой контент`}
/> /> */}
</main> </main>
); );
}; };

View File

@ -0,0 +1,2 @@
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;

View File

@ -8,6 +8,7 @@ type UseCreateNewContentPayload = {
content: string; content: string;
image: string; image: string;
description: string; description: string;
hashtags: string[];
price: string; price: string;
resaleLicensePrice: string; // nanoTON bignum (default = 0) resaleLicensePrice: string; // nanoTON bignum (default = 0)
allowResale: boolean; allowResale: boolean;

View File

@ -80,7 +80,7 @@ export const useRootStore = create<RootStore>((set) => ({
royalty: [{ address: "", value: 100 }], royalty: [{ address: "", value: 100 }],
setRoyalty: (royalty) => set({ royalty }), setRoyalty: (royalty) => set({ royalty }),
price: 0, price: 0.15,
setPrice: (price: number) => set({ price }), setPrice: (price: number) => set({ price }),
allowResale: false, allowResale: false,

View File

@ -1,7 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { WithContext as ReactTags } from "react-tag-input"; import { WithContext as ReactTags, SEPARATORS } from "react-tag-input";
import { useRootStore } from "~/shared/stores/root"; import { useRootStore } from "~/shared/stores/root";
// English comment: no extra Tag interface, just cast to any // English comment: no extra Tag interface, just cast to any
export const HashtagInput = () => { export const HashtagInput = () => {
const { hashtags, setHashtags } = useRootStore(); const { hashtags, setHashtags } = useRootStore();
@ -9,9 +8,7 @@ export const HashtagInput = () => {
// English comment: local state as string[] for simplicity // English comment: local state as string[] for simplicity
const [tags, setTags] = useState<string[]>(hashtags); const [tags, setTags] = useState<string[]>(hashtags);
const KeyCodes = { comma: 188, enter: 13 }; const separators = [SEPARATORS.ENTER, SEPARATORS.COMMA]
const delimiters = [KeyCodes.comma, KeyCodes.enter];
const handleDelete = (i: number) => { const handleDelete = (i: number) => {
const newTags = tags.filter((_, index) => index !== i); const newTags = tags.filter((_, index) => index !== i);
setTags(newTags); setTags(newTags);
@ -20,20 +17,47 @@ export const HashtagInput = () => {
// English comment: pass "any" to the function // English comment: pass "any" to the function
const handleAddition = (newTag: any) => { const handleAddition = (newTag: any) => {
// English comment: newTag might be { id, text, ... } from react-tag-input // Clean up text from commas and trim whitespace
const updatedTags = [...tags, newTag?.text || newTag?.id || ""]; const text = newTag?.text || newTag?.id || "";
const cleanText = text.replace(/,/g, '').trim();
// Skip empty tags
if (!cleanText) return;
const updatedTags = [...tags, cleanText];
setTags(updatedTags); setTags(updatedTags);
setHashtags(updatedTags); setHashtags(updatedTags);
}; };
// Simulate Enter keypress for Android
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true,
which: 13,
keyCode: 13,
});
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Unidentified') {
const lastChar = e.currentTarget.value.slice(-1);
if (lastChar === ',') {
e.currentTarget.dispatchEvent(enterEvent);
}
}
};
return ( return (
<ReactTags <ReactTags
tags={tags.map((t) => ({ id: t, text: t })) as any} tags={tags.map((t) => ({ id: t, text: t })) as any}
delimiters={delimiters as any} separators={separators as any}
handleDelete={handleDelete as any} handleDelete={handleDelete as any}
handleAddition={handleAddition as any} handleAddition={handleAddition as any}
allowDragDrop={false} allowDragDrop={false}
placeholder="[ enter a hashtag ]" placeholder="[ введите тэги через запятую ]"
inputProps={{
onKeyUp: handleKeyUp as any
}}
/> />
); );
}; };

View File

@ -4,4 +4,19 @@ import TSPaths from "vite-tsconfig-paths";
export default defineConfig({ export default defineConfig({
plugins: [react(), TSPaths()], plugins: [react(), TSPaths()],
}); define: {
global: 'globalThis',
},
resolve: {
alias: {
buffer: 'buffer/',
}
},
optimizeDeps: {
esbuildOptions: {
define: {
global: 'globalThis'
}
}
}
});

View File

@ -225,10 +225,10 @@
"@babel/helper-validator-identifier" "^7.22.20" "@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@esbuild/darwin-arm64@0.19.12": "@esbuild/win32-x64@0.19.12":
version "0.19.12" version "0.19.12"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz" resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz"
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0" version "4.4.0"
@ -381,10 +381,10 @@
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz" resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz"
integrity sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q== integrity sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==
"@rollup/rollup-darwin-arm64@4.12.0": "@rollup/rollup-win32-x64-msvc@4.12.0":
version "4.12.0" version "4.12.0"
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz" resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz"
integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ== integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==
"@ton/core@^0.59.1": "@ton/core@^0.59.1":
version "0.59.1" version "0.59.1"
@ -860,6 +860,11 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
big-integer@^1.6.16: big-integer@^1.6.16:
version "1.6.52" version "1.6.52"
resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz" resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz"
@ -916,6 +921,14 @@ browserslist@^4.22.2, browserslist@^4.23.0, "browserslist@>= 4.21.0":
node-releases "^2.0.14" node-releases "^2.0.14"
update-browserslist-db "^1.0.13" update-browserslist-db "^1.0.13"
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz"
@ -1569,11 +1582,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@ -1764,6 +1772,11 @@ hoist-non-react-statics@^3.3.2:
dependencies: dependencies:
react-is "^16.7.0" react-is "^16.7.0"
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^5.2.0, ignore@^5.2.4: ignore@^5.2.0, ignore@^5.2.4:
version "5.3.1" version "5.3.1"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz"