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

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

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
import "~/app/styles/globals.css";
import '~/shared/libs/buffer';
import { useEffect } from "react";
import { useExpand, useWebApp } from "@vkruglikov/react-telegram-web-app";
+66 -39
View File
@@ -26,18 +26,26 @@
/*Input Range*/
/* Custom styles for the range input */
input[type="range"] {
-webkit-appearance: none; /* Remove default appearance */
width: 100%; /* Full width */
height: 2px; /* Track height */
background: linear-gradient(
to right,
white 0%, /* Start color of passed track */
white var(--value-percentage, 0%), /* End color of passed track */
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 */
-webkit-appearance: none;
/* Remove default appearance */
width: 100%;
/* Full width */
height: 2px;
/* Track height */
background: linear-gradient(to right,
white 0%,
/* Start color of passed track */
white var(--value-percentage, 0%),
/* End color of passed track */
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;
}
@@ -45,45 +53,64 @@
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 9px; /* Thumb width */
height: 9px; /* Thumb height */
background: #fff; /* Thumb color (blue-500) */
border-radius: 9999px; /* Rounded-full thumb */
cursor: pointer; /* Pointer cursor on hover */
width: 9px;
/* Thumb width */
height: 9px;
/* Thumb height */
background: #fff;
/* Thumb color (blue-500) */
border-radius: 9999px;
/* Rounded-full thumb */
cursor: pointer;
/* Pointer cursor on hover */
transition: background 0.3s;
}
/* For Firefox */
input[type="range"]::-moz-range-thumb {
appearance: none;
width: 9px; /* Thumb width */
height: 9px; /* Thumb height */
background: #fff; /* Thumb color (blue-500) */
border-radius: 9999px; /* Rounded-full thumb */
cursor: pointer; /* Pointer cursor on hover */
width: 9px;
/* Thumb width */
height: 9px;
/* Thumb height */
background: #fff;
/* Thumb color (blue-500) */
border-radius: 9999px;
/* Rounded-full thumb */
cursor: pointer;
/* Pointer cursor on hover */
transition: background 0.3s;
}
input[type="range"]::-moz-range-track {
-webkit-appearance: none; /* Remove default appearance */
width: 100%; /* Full width */
height: 2px; /* Track height */
background: linear-gradient(
to right,
white 0%, /* Start color of passed track */
white var(--value-percentage, 0%), /* End color of passed track */
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 */
-webkit-appearance: none;
/* Remove default appearance */
width: 100%;
/* Full width */
height: 2px;
/* Track height */
background: linear-gradient(to right,
white 0%,
/* Start color of passed track */
white var(--value-percentage, 0%),
/* End color of passed track */
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;
}
/* English comment: override react-tag-input classes */
.ReactTags__tagInputField {
@apply bg-transparent text-gray placeholder:text-gray outline-none w-full h-8;
@apply border border-white py-[20px] !important;
@apply bg-[#2B2B2B] outline-none w-full h-8 text-sm !important;
@apply border border-white px-[10px] py-[18px] !important;
@apply whitespace-pre !important;
/* English comment:
'bg-transparent' to blend with your dark background
'text-white' to have white text
@@ -94,7 +121,7 @@
/* English comment: style for the tag itself when it's rendered */
.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:
'bg-[#363636]' to have a dark gray background
'text-white' keeps the text white
@@ -105,11 +132,11 @@
}
.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:
'ml-1' a small margin to separate the 'x' or close symbol
'text-gray-400' by default, and change to white on hover
'cursor-pointer' so it looks clickable
*/
}
}
}
+19 -1
View File
@@ -64,7 +64,7 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
content: fileUploadResult.content_id_v1,
image: coverUploadResult.content_id_v1,
price: String(rootStore.price * 10 ** 9),
hashtags: rootStore.hashtags,
royaltyParams: rootStore.royalty.map((member) => ({
...member,
value: member.value * 100,
@@ -151,6 +151,24 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
</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={"Цена"}>
<div
className={
+23 -14
View File
@@ -9,10 +9,10 @@ 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 MIN_PRICE = 0.15;
const MIN_RESALE_PRICE = 0.15;
const RECOMMENDED_PRICE = 0.15;
// const RECOMMENDED_PRICE = 0.15;
// const RECOMMENDED_RESALE_PRICE = 0.15;
type PriceStepProps = {
@@ -25,9 +25,9 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
const formSchema = useMemo(() => {
const parsePrice = (value: unknown) => {
if (typeof value === "string") {
// Replace commas with dots and parse the value
const parsedValue = parseFloat(value.replace(",", "."));
if (typeof value === "string" || typeof value === "number") {
const stringValue = value.toString().replace(",", ".");
const parsedValue = parseFloat(stringValue);
return isNaN(parsedValue) ? undefined : parsedValue;
}
return undefined;
@@ -37,10 +37,10 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
return z.object({
price: z.preprocess(
parsePrice,
z.number().min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`)
z.number({required_error: 'Цена не соответствует требованиям'}).min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`)
),
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(),
});
}
@@ -48,7 +48,7 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
return z.object({
price: z.preprocess(
parsePrice,
z.number().min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`)
z.number({required_error: 'Цена не соответствует требованиям'}).min(MIN_PRICE, `Цена должна быть минимум ${MIN_PRICE} TON.`)
),
});
}, [rootStore.allowResale]);
@@ -59,9 +59,9 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
resolver: zodResolver(formSchema),
mode: "onChange",
defaultValues: {
price: rootStore.price,
price: rootStore.price || MIN_PRICE,
//@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"}>
<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>
{/* <p className={"text-xs"}>Рекомендуемая стоимость {RECOMMENDED_PRICE} TON.</p> */}
</div>
<Input
error={form.formState.errors?.price}
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>
</div>
<Button
+43 -9
View File
@@ -12,17 +12,30 @@ import { ConfirmModal } from "~/pages/root/steps/royalty-step/components/confirm
import { useRootStore } from "~/shared/stores/root";
import { BackButton } from "~/shared/ui/back-button";
import { useTonConnectUI } from "@tonconnect/ui-react";
import { Address } from "@ton/core";
import { FieldError } from "react-hook-form";
type RoyaltyStepProps = {
prevStep(): 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) => {
const [impactOccurred] = useHapticFeedback();
const [isDeleteAllOpen, setDeleteAllOpen] = useState(false);
const [isSpreadOpen, setSpreadOpen] = useState(false);
const [addressErrors, setAddressErrors] = useState<Record<number, FieldError | undefined>>({});
const { royalty, setRoyalty, isPercentHintOpen, setPercentHintOpen } =
useRootStore();
@@ -42,11 +55,23 @@ export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
};
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) =>
i === index ? { ...member, address } : member,
i === index ? { ...member, address } : member
);
setRoyalty(newRoyalty);
};
const handlePercentChange = (index: number, value: string) => {
const percentNumber = parseInt(value, 10) || 0;
@@ -67,20 +92,28 @@ export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
const isValid = useMemo(() => {
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]);
const [tonConnectUI] = useTonConnectUI();
// Устанавливаем адрес из tonConnectUI.account при загрузке страницы
useEffect(() => {
console.log('tonconnectUI', tonConnectUI)
if (tonConnectUI.account) {
setRoyalty([{ address: tonConnectUI.account.address, value: 100 }]);
if (!tonConnectUI.account) return;
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 (
<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"}>
{royalty.map((member, index) => (
<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%]"}>
<FormLabel
labelClassName={"flex"}
@@ -145,7 +178,8 @@ export const RoyaltyStep = ({ nextStep, prevStep }: RoyaltyStepProps) => {
value={member.address}
onChange={(e) => handleWalletChange(index, e.target.value)}
placeholder={"[ Введите адрес криптокошелька TON ]"}
/>
error={addressErrors[index]}
/>
</FormLabel>
</div>
+39 -4
View File
@@ -2,7 +2,6 @@ import ReactPlayer from "react-player/lazy";
import { useTonConnectUI } from "@tonconnect/ui-react";
import { useWebApp } from "@vkruglikov/react-telegram-web-app";
import { Button } from "~/shared/ui/button";
import { usePurchaseContent, useViewContent } from "~/shared/services/content";
import { fromNanoTON } from "~/shared/utils";
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 (
<main className={"flex w-full flex-col gap-[50px] px-4"}>
{content?.data?.content_type.startsWith("audio") && content?.data?.display_options?.metadata?.image && (
@@ -111,7 +146,7 @@ export const ViewContentPage = () => {
</p>
</section>
{!haveLicense && <Button
{/* {!haveLicense && <Button
onClick={handleBuyContent}
className={"mb-4 mt-[30px] h-[48px]"}
label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.resale?.price)} ТОН`}
@@ -125,7 +160,7 @@ export const ViewContentPage = () => {
}}
className={"mb-4 mt-[-20px] h-[48px] bg-darkred"}
label={`Загрузить свой контент`}
/>
/> */}
</main>
);
};
+2
View File
@@ -0,0 +1,2 @@
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;
+1
View File
@@ -8,6 +8,7 @@ type UseCreateNewContentPayload = {
content: string;
image: string;
description: string;
hashtags: string[];
price: string;
resaleLicensePrice: string; // nanoTON bignum (default = 0)
allowResale: boolean;
+1 -1
View File
@@ -80,7 +80,7 @@ export const useRootStore = create<RootStore>((set) => ({
royalty: [{ address: "", value: 100 }],
setRoyalty: (royalty) => set({ royalty }),
price: 0,
price: 0.15,
setPrice: (price: number) => set({ price }),
allowResale: false,
+33 -9
View File
@@ -1,7 +1,6 @@
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";
// English comment: no extra Tag interface, just cast to any
export const HashtagInput = () => {
const { hashtags, setHashtags } = useRootStore();
@@ -9,9 +8,7 @@ export const HashtagInput = () => {
// English comment: local state as string[] for simplicity
const [tags, setTags] = useState<string[]>(hashtags);
const KeyCodes = { comma: 188, enter: 13 };
const delimiters = [KeyCodes.comma, KeyCodes.enter];
const separators = [SEPARATORS.ENTER, SEPARATORS.COMMA]
const handleDelete = (i: number) => {
const newTags = tags.filter((_, index) => index !== i);
setTags(newTags);
@@ -20,20 +17,47 @@ export const HashtagInput = () => {
// English comment: pass "any" to the function
const handleAddition = (newTag: any) => {
// English comment: newTag might be { id, text, ... } from react-tag-input
const updatedTags = [...tags, newTag?.text || newTag?.id || ""];
// Clean up text from commas and trim whitespace
const text = newTag?.text || newTag?.id || "";
const cleanText = text.replace(/,/g, '').trim();
// Skip empty tags
if (!cleanText) return;
const updatedTags = [...tags, cleanText];
setTags(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 (
<ReactTags
tags={tags.map((t) => ({ id: t, text: t })) as any}
delimiters={delimiters as any}
separators={separators as any}
handleDelete={handleDelete as any}
handleAddition={handleAddition as any}
allowDragDrop={false}
placeholder="[ enter a hashtag ]"
placeholder="[ введите тэги через запятую ]"
inputProps={{
onKeyUp: handleKeyUp as any
}}
/>
);
};