| update audio player & other fixes

This commit is contained in:
rakhimovkamran 2024-08-19 05:01:17 +05:00
parent b03533dea6
commit c5c36cb078
8 changed files with 602 additions and 371 deletions

View File

@ -22,4 +22,61 @@
a {
@apply transition-all active:opacity-60;
}
/*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 */
transition: background 0.3s;
}
/* Style the thumb (toggle) */
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 */
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 */
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 */
transition: background 0.3s;
}
}

View File

@ -12,6 +12,7 @@ import { CoverButton } from "~/pages/root/steps/data-step/components/cover-butto
import { HiddenFileInput } from "~/shared/ui/hidden-file-input";
import { useRootStore } from "~/shared/stores/root";
import { Checkbox } from "~/shared/ui/checkbox";
import { AudioPlayer } from "~/shared/ui/audio-player";
type DataStepProps = {
nextStep(): void;
@ -47,6 +48,7 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
if (values.author) {
rootStore.setAuthor(values.author);
}
nextStep();
} catch (error) {
console.error("Error:", error);
@ -55,112 +57,114 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
};
return (
<section className={"mt-4 px-4 pb-8"}>
<div className={"mb-[30px] flex flex-col text-sm"}>
<span className={"ml-4"}>/Заполните информацию о контенте</span>
<div>
1/<span className={"text-[#7B7B7B]"}>5</span>
<section className={"mt-4 px-4 pb-8"}>
<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>
<div className={"flex flex-col gap-[20px]"}>
<FormLabel label={"Название"}>
<Input
placeholder={"[ Введите название ]"}
error={form.formState.errors?.name}
{...form.register("name")}
/>
</FormLabel>
<div className={"flex flex-col gap-[20px]"}>
<FormLabel label={"Название"}>
<Input
placeholder={"[ Введите название ]"}
error={form.formState.errors?.name}
{...form.register("name")}
/>
</FormLabel>
<FormLabel label={"Имя автора/исполнителя"}>
<Input
placeholder={"[ введите имя автора/исполнителя ]"}
error={form.formState.errors?.author}
{...form.register("author")}
/>
</FormLabel>
<FormLabel label={"Имя автора/исполнителя"}>
<Input
placeholder={"[ введите имя автора/исполнителя ]"}
error={form.formState.errors?.author}
{...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%"
config={{ file: { attributes: { playsInline: true } } }}
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);
<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.setFileType(file.type); // Save the file type for conditional rendering
}}
/>
/>
{rootStore.cover ? (
<CoverButton
src={URL.createObjectURL(rootStore.cover)}
onClick={() => {
rootStore.setCover(null);
}}
/>
) : (
<FileButton htmlFor={"cover"} />
)}
</FormLabel>
)}
{!rootStore.fileSrc && <FileButton htmlFor={"file"} />}
{rootStore.fileSrc && (
<div
className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm"
}
>
{rootStore.fileType?.startsWith("audio") ? (
<AudioPlayer src={rootStore.fileSrc} />
) : (
<ReactPlayer
playsinline={true}
controls={true}
width="100%"
config={{ file: { attributes: { playsInline: true } } }}
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>
</div>
<Button
className={"mt-[30px]"}
onClick={handleSubmit}
includeArrows={true}
label={"Готово"}
disabled={
!form.formState.isValid ||
!rootStore.file ||
(rootStore.allowCover && !rootStore.cover)
}
/>
</section>
<Button
className={"mt-[30px]"}
onClick={handleSubmit}
includeArrows={true}
label={"Готово"}
disabled={
!form.formState.isValid ||
!rootStore.file ||
(rootStore.allowCover && !rootStore.cover)
}
/>
</section>
);
};

View File

@ -12,6 +12,8 @@ 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";
import { useTonConnectUI } from "@tonconnect/ui-react";
type PresubmitStepProps = {
prevStep(): void;
@ -20,6 +22,8 @@ type PresubmitStepProps = {
export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
const WebApp = useWebApp();
const [tonConnectUI] = useTonConnectUI();
const rootStore = useRootStore();
const [impactOccurred] = useHapticFeedback();
@ -35,22 +39,22 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
let coverUploadResult = { content_id_v1: "" };
const fileUploadResult = await uploadFile.mutateAsync(
rootStore.file as File,
rootStore.file as File,
);
if (rootStore.allowCover && rootStore.cover) {
coverUploadResult = await uploadCover.mutateAsync(
rootStore.cover as File,
rootStore.cover as File,
);
}
await createContent.mutateAsync({
const createContentResponse = await createContent.mutateAsync({
title: rootStore.name,
// Это для одного автора
...(rootStore.author
? { authors: [rootStore.author] }
: { authors: [] }),
? { authors: [rootStore.author] }
: { authors: [] }),
// Откомментировать при условии того что вы принимаете много авторов
// следует отметить что вы должны еще откомментровать AuthorsStep в RootPage
@ -69,243 +73,264 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
// Если откомментировать поле resaleLicensePrice в price-step то
// это отработает как надо
...(rootStore.allowResale
? {
? {
allowResale: true,
resaleLicensePrice: String(
rootStore.licenseResalePrice * 10 ** 9,
rootStore.licenseResalePrice * 10 ** 9,
),
}
: { allowResale: false, resaleLicensePrice: "0" }),
: { allowResale: false, resaleLicensePrice: "0" }),
});
if (createContentResponse.data) {
const transactionResponse = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 120,
messages: [
{
amount: createContentResponse.data.amount,
address: createContentResponse.data.address,
payload: createContentResponse.data.payload,
},
],
});
if (transactionResponse.boc) {
WebApp.close();
await tonConnectUI.disconnect();
} else {
console.error("Transaction failed:", transactionResponse);
}
}
WebApp.close();
// @ts-expect-error Type issues
} catch (error: never) {
await tonConnectUI.disconnect();
console.error("An error occurred during the submission process:", error);
if (error?.status === 400) {
alert(
"Введенные данные неверные, проверьте правильность введенных данных.",
"Введенные данные неверные, проверьте правильность введенных данных.",
);
}
}
};
return (
<section className={"mt-4 px-4 pb-8"}>
<BackButton onClick={prevStep} />
<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 className={"mb-[30px] flex flex-col text-sm"}>
<span className={"ml-4"}>/Подтвердите правильность данных</span>
<div>
5/<span className={"text-[#7B7B7B]"}>5</span>
</div>
</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={"Имя автора/исполнителя"}>
<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"
}
className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
}
>
{rootStore.author}
{rootStore.name}
</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.author && (
<FormLabel label={"Имя автора/исполнителя"}>
<div
className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
}
>
{rootStore.author}
</div>
</FormLabel>
)}
{rootStore.allowResale && (
<FormLabel label={"Цена копии"}>
<FormLabel label={"Цена"}>
<div
className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
}
className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
}
>
{rootStore.licenseResalePrice} TON
{rootStore.price} 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%"
config={{ file: { attributes: { playsInline: true } } }}
url={rootStore.fileSrc}
/>
</div>
{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>
)}
{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"}
<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%"
config={{ file: { attributes: { playsInline: true } } }}
url={rootStore.fileSrc}
/>
)}
</div>
)}
{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>
{uploadFile.isUploading && uploadFile.isLoading && (
<Progress value={uploadFile.uploadProgress} />
)}
</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}`}
{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
className={
"break-all border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
}
style={{
height: isCoverExpanded ? "261px" : "68px",
}}
className={"bg-bg w-full"}
>
{royalty.address}
</div>
</FormLabel>
</div>
{rootStore.cover && !uploadCover.isUploading && (
<img
src={URL.createObjectURL(rootStore.cover)}
alt={"cover"}
className={"h-full w-full object-cover object-center"}
/>
)}
<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"
}
{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"}
>
{royalty.value.toString()}
</div>
</FormLabel>
</div>
</div>
</div>
))}
{!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>
)}
{/*{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>*/}
{/*))}*/}
{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>
<Button
isLoading={
uploadFile.isLoading ||
uploadCover.isLoading ||
createContent.isLoading
}
onClick={handleSubmit}
label={"Все верно!"}
className={"mt-[30px] py-[16px]"}
includeArrows={true}
/>
</section>
);
};

View File

@ -118,7 +118,7 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
</div>
<div className={"flex flex-col gap-[20px]"}>
<FormLabel label={"Цена TON"}>
<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"}>

View File

@ -1,11 +1,12 @@
import ReactPlayer from "react-player/lazy";
import { useTonConnectUI } from "@tonconnect/ui-react";
import { useMemo } from "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 } from "react";
import { AudioPlayer } from "~/shared/ui/audio-player";
export const ViewContentPage = () => {
const WebApp = useWebApp();
@ -16,81 +17,83 @@ export const ViewContentPage = () => {
const [tonConnectUI] = useTonConnectUI();
const transaction = useMemo(() => {
const address = content?.data?.encrypted?.cid;
return {
validUntil: Math.floor(Date.now() / 1000) + 120,
messages: [
{ amount: content?.data?.encrypted?.license?.listen?.price, address },
],
};
}, [content?.data]);
const handleBuyContent = async () => {
const handleBuyContent = useCallback(async () => {
try {
if (!tonConnectUI.connected) {
await tonConnectUI.openModal();
return;
}
const res = await tonConnectUI.sendTransaction(transaction);
const contentResponse = await purchaseContent({
content_address: content?.data?.encrypted?.cid,
license_type: "listen",
});
if (res.boc) {
await purchaseContent({
content_address: content?.data?.encrypted?.cid,
license_type: "listen",
});
const transactionResponse = await tonConnectUI.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 120,
messages: [
{
amount: contentResponse.data.amount,
address: contentResponse.data.address,
payload: contentResponse.data.payload,
},
],
});
if (transactionResponse.boc) {
WebApp.close();
await tonConnectUI.disconnect();
} else {
console.error("Transaction failed:", res);
console.error("Transaction failed:", transactionResponse);
}
} catch (error) {
await tonConnectUI.disconnect();
console.error("Error handling Ton Connect subscription:", error);
}
};
}, [content]);
return (
<main className={"flex w-full flex-col gap-[50px] px-4"}>
{content?.data?.display_options?.metadata?.image && (
<div className={"mt-[30px] h-[314px] w-full"}>
<img
alt={"content_image"}
className={"h-full w-full object-cover object-center"}
src={content?.data?.display_options?.metadata?.image}
/>
</div>
)}
<main className={"flex w-full flex-col gap-[50px] px-4"}>
{content?.data?.display_options?.metadata?.image && (
<div className={"mt-[30px] h-[314px] w-full"}>
<img
alt={"content_image"}
className={"h-full w-full object-cover object-center"}
src={content?.data?.display_options?.metadata?.image}
/>
</div>
)}
<ReactPlayer
playsinline={true}
controls={true}
width="100%"
config={{ file: { attributes: { playsInline: true } } }}
url={content?.data?.display_options?.content_url}
/>
{content?.data?.content_type.startsWith("audio") ? (
<AudioPlayer src={content?.data?.display_options?.content_url} />
) : (
<ReactPlayer
playsinline={true}
controls={true}
width="100%"
config={{ file: { attributes: { playsInline: true } } }}
url={content?.data?.display_options?.content_url}
/>
)}
<section className={"flex flex-col"}>
<h1 className={"text-[20px] font-bold"}>
{content?.data?.display_options?.metadata?.name}
</h1>
{/*<h2>Russian</h2>*/}
{/*<h2>2022</h2>*/}
<p className={"mt-2 text-[12px]"}>
{content?.data?.display_options?.metadata?.description}
</p>
</section>
<section className={"flex flex-col"}>
<h1 className={"text-[20px] font-bold"}>
{content?.data?.display_options?.metadata?.name}
</h1>
{/*<h2>Russian</h2>*/}
{/*<h2>2022</h2>*/}
<p className={"mt-2 text-[12px]"}>
{content?.data?.display_options?.metadata?.description}
</p>
</section>
<Button
onClick={handleBuyContent}
className={"mb-4 mt-[30px] h-[48px]"}
label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.listen?.price)} ТОН`}
includeArrows={true}
/>
</main>
<Button
onClick={handleBuyContent}
className={"mb-4 mt-[30px] h-[48px]"}
label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.listen?.price)} ТОН`}
includeArrows={true}
/>
</main>
);
};

View File

@ -17,12 +17,14 @@ type UseCreateNewContentPayload = {
export const useCreateNewContent = () => {
return useMutation(
["create-new-content"],
(payload: UseCreateNewContentPayload) => {
return request.post<{
message: string;
}>("/blockchain.sendNewContentMessage", payload);
},
["create-new-content"],
(payload: UseCreateNewContentPayload) => {
return request.post<{
address: string;
amount: string;
payload: string;
}>("/blockchain.sendNewContentMessage", payload);
},
);
};
@ -45,18 +47,22 @@ export const useViewContent = (contentId: string) => {
export const usePurchaseContent = () => {
return useMutation(
["purchase", "content"],
({
content_address,
license_type,
}: {
content_address: string;
license_type: "listen" | "resale";
}) => {
return request.post("/blockchain.sendPurchaseContentMessage", {
content_address,
license_type,
});
},
["purchase", "content"],
({
content_address,
license_type,
}: {
content_address: string;
license_type: "listen" | "resale";
}) => {
return request.post<{
address: string;
amount: string;
payload: string;
}>("/blockchain.sendPurchaseContentMessage", {
content_address,
license_type,
});
},
);
};

View File

@ -15,6 +15,9 @@ type RootStore = {
file: File | null;
setFile: (file: File) => void;
fileType: string;
setFileType: (type: string) => void;
fileSrc: string;
setFileSrc: (fileSrc: string) => void;
@ -53,6 +56,9 @@ export const useRootStore = create<RootStore>((set) => ({
file: null,
setFile: (file) => set({ file }),
fileType: "",
setFileType: (fileType) => set({ fileType }),
fileSrc: "",
setFileSrc: (fileSrc) => set({ fileSrc }),

View File

@ -0,0 +1,130 @@
import { FC, useEffect, useRef, useState } from "react";
interface AudioPlayerProps {
src: string;
}
export const AudioPlayer: FC<AudioPlayerProps> = ({ src }) => {
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [currentTime, setCurrentTime] = useState<number>(0);
const [duration, setDuration] = useState<number>(0);
const audioRef = useRef<HTMLAudioElement | null>(null);
const rangeRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const handleLoadedMetadata = () => {
setDuration(audio.duration);
};
const handleTimeUpdate = () => {
setCurrentTime(audio.currentTime);
updateRangeBackground(audio.currentTime);
};
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
audio.addEventListener("timeupdate", handleTimeUpdate);
return () => {
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
audio.removeEventListener("timeupdate", handleTimeUpdate);
};
}, []);
const togglePlayPause = () => {
const audio = audioRef.current;
if (!audio) return;
if (isPlaying) {
audio.pause();
} else {
audio.play();
}
setIsPlaying(!isPlaying);
};
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const audio = audioRef.current;
if (!audio) return;
const newTime = parseFloat(e.target.value);
audio.currentTime = newTime;
setCurrentTime(newTime);
updateRangeBackground(newTime);
};
const formatTime = (time: number): string => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
};
const updateRangeBackground = (currentTime: number) => {
if (rangeRef.current) {
const percentage = (currentTime / duration) * 100;
rangeRef.current.style.setProperty(
"--value-percentage",
`${percentage}%`,
);
}
};
return (
<div className="flex items-center space-x-4">
<audio ref={audioRef} src={src} />
<button
onClick={togglePlayPause}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-gray focus:outline-none"
>
{isPlaying ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-6"
>
<path
fillRule="evenodd"
d="M6.75 5.25a.75.75 0 0 1 .75-.75H9a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H7.5a.75.75 0 0 1-.75-.75V5.25Zm7.5 0A.75.75 0 0 1 15 4.5h1.5a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H15a.75.75 0 0 1-.75-.75V5.25Z"
clipRule="evenodd"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="size-6"
>
<path
fillRule="evenodd"
d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z"
clipRule="evenodd"
/>
</svg>
)}
</button>
<div className={"flex w-full flex-col gap-2"}>
<input
type="range"
ref={rangeRef}
min="0"
max={duration}
value={currentTime}
onChange={handleSliderChange}
className="flex-grow"
/>
<div className="flex items-center justify-between text-xs">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
</div>
);
};