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

View File

@ -12,6 +12,8 @@ import { useUploadFile } from "~/shared/services/file";
import { Progress } from "~/shared/ui/progress"; import { Progress } from "~/shared/ui/progress";
import { useCreateNewContent } from "~/shared/services/content"; import { useCreateNewContent } from "~/shared/services/content";
import { BackButton } from "~/shared/ui/back-button"; import { BackButton } from "~/shared/ui/back-button";
import { useTonConnectUI } from "@tonconnect/ui-react";
type PresubmitStepProps = { type PresubmitStepProps = {
prevStep(): void; prevStep(): void;
@ -20,6 +22,8 @@ type PresubmitStepProps = {
export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => { export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
const WebApp = useWebApp(); const WebApp = useWebApp();
const [tonConnectUI] = useTonConnectUI();
const rootStore = useRootStore(); const rootStore = useRootStore();
const [impactOccurred] = useHapticFeedback(); const [impactOccurred] = useHapticFeedback();
@ -35,22 +39,22 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
let coverUploadResult = { content_id_v1: "" }; let coverUploadResult = { content_id_v1: "" };
const fileUploadResult = await uploadFile.mutateAsync( const fileUploadResult = await uploadFile.mutateAsync(
rootStore.file as File, rootStore.file as File,
); );
if (rootStore.allowCover && rootStore.cover) { if (rootStore.allowCover && rootStore.cover) {
coverUploadResult = await uploadCover.mutateAsync( coverUploadResult = await uploadCover.mutateAsync(
rootStore.cover as File, rootStore.cover as File,
); );
} }
await createContent.mutateAsync({ const createContentResponse = await createContent.mutateAsync({
title: rootStore.name, title: rootStore.name,
// Это для одного автора // Это для одного автора
...(rootStore.author ...(rootStore.author
? { authors: [rootStore.author] } ? { authors: [rootStore.author] }
: { authors: [] }), : { authors: [] }),
// Откомментировать при условии того что вы принимаете много авторов // Откомментировать при условии того что вы принимаете много авторов
// следует отметить что вы должны еще откомментровать AuthorsStep в RootPage // следует отметить что вы должны еще откомментровать AuthorsStep в RootPage
@ -69,243 +73,264 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
// Если откомментировать поле resaleLicensePrice в price-step то // Если откомментировать поле resaleLicensePrice в price-step то
// это отработает как надо // это отработает как надо
...(rootStore.allowResale ...(rootStore.allowResale
? { ? {
allowResale: true, allowResale: true,
resaleLicensePrice: String( 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(); WebApp.close();
// @ts-expect-error Type issues // @ts-expect-error Type issues
} catch (error: never) { } catch (error: never) {
await tonConnectUI.disconnect();
console.error("An error occurred during the submission process:", error); console.error("An error occurred during the submission process:", error);
if (error?.status === 400) { if (error?.status === 400) {
alert( alert(
"Введенные данные неверные, проверьте правильность введенных данных.", "Введенные данные неверные, проверьте правильность введенных данных.",
); );
} }
} }
}; };
return ( return (
<section className={"mt-4 px-4 pb-8"}> <section className={"mt-4 px-4 pb-8"}>
<BackButton onClick={prevStep} /> <BackButton onClick={prevStep} />
<div className={"mb-[30px] flex flex-col text-sm"}> <div className={"mb-[30px] flex flex-col text-sm"}>
<span className={"ml-4"}>/Подтвердите правильность данных</span> <span className={"ml-4"}>/Подтвердите правильность данных</span>
<div> <div>
5/<span className={"text-[#7B7B7B]"}>5</span> 5/<span className={"text-[#7B7B7B]"}>5</span>
</div>
</div> </div>
</div>
<section className={"flex flex-col gap-2"}> <section className={"flex flex-col gap-2"}>
<FormLabel label={"Название"}> <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 <div
className={ className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold" "w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
} }
> >
{rootStore.author} {rootStore.name}
</div> </div>
</FormLabel> </FormLabel>
)}
<FormLabel label={"Цена"}> {rootStore.author && (
<div <FormLabel label={"Имя автора/исполнителя"}>
className={ <div
"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.price} TON >
</div> {rootStore.author}
</FormLabel> </div>
</FormLabel>
)}
{rootStore.allowResale && ( <FormLabel label={"Цена"}>
<FormLabel label={"Цена копии"}>
<div <div
className={ className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold" "w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
} }
> >
{rootStore.licenseResalePrice} TON {rootStore.price} TON
</div> </div>
</FormLabel> </FormLabel>
)}
<FormLabel label={"Файл"}> {rootStore.allowResale && (
{rootStore.fileSrc && !uploadFile.isUploading && ( <FormLabel label={"Цена копии"}>
<div <div
className={ className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm" "w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
} }
> >
<ReactPlayer {rootStore.licenseResalePrice} TON
playsinline={true} </div>
controls={true} </FormLabel>
width="100%"
config={{ file: { attributes: { playsInline: true } } }}
url={rootStore.fileSrc}
/>
</div>
)} )}
{uploadFile.isUploading && uploadFile.isLoading && ( <FormLabel label={"Файл"}>
<Progress value={uploadFile.uploadProgress} /> {rootStore.fileSrc && !uploadFile.isUploading && (
)} <div
</FormLabel> className={
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm"
{rootStore.allowCover && ( }
<FormLabel label={"Обложка"}> >
<div <ReactPlayer
className={ playsinline={true}
"flex w-full items-center border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold" controls={true}
} width="100%"
> config={{ file: { attributes: { playsInline: true } } }}
<div url={rootStore.fileSrc}
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"}
/> />
)} </div>
)}
{uploadCover.isUploading && uploadCover.isLoading && ( {uploadFile.isUploading && uploadFile.isLoading && (
<Progress value={uploadCover.uploadProgress} /> <Progress value={uploadFile.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> </FormLabel>
)}
{rootStore.royalty.map((royalty, index) => ( {rootStore.allowCover && (
<div key={index} className={"flex flex-col gap-[20px]"}> <FormLabel label={"Обложка"}>
<div className={"flex w-full items-center gap-1"}> <div
<div className={"w-[83%]"}> className={
<FormLabel "flex w-full items-center border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"
labelClassName={"flex"} }
label={`Роялти_${index + 1}`}
> >
<div <div
className={ style={{
"break-all border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold" height: isCoverExpanded ? "261px" : "68px",
} }}
className={"bg-bg w-full"}
> >
{royalty.address} {rootStore.cover && !uploadCover.isUploading && (
</div> <img
</FormLabel> src={URL.createObjectURL(rootStore.cover)}
</div> alt={"cover"}
className={"h-full w-full object-cover object-center"}
/>
)}
<div className={"h-auto w-[18%]"}> {uploadCover.isUploading && uploadCover.isLoading && (
<FormLabel labelClassName={"text-center"} label={"%"}> <Progress value={uploadCover.uploadProgress} />
<div )}
className={ </div>
"flex items-center justify-center border border-white bg-[#2B2B2B] py-[8px] text-sm font-bold"
} <button
onClick={() => {
impactOccurred("light");
setCoverExpanded((p) => !p);
}}
style={{
height: isCoverExpanded ? "261px" : "68px",
}}
className={"flex w-[45px] items-center justify-center"}
> >
{royalty.value.toString()} {!isCoverExpanded && (
</div> <svg
</FormLabel> width="45"
</div> height="14"
</div> viewBox="0 0 45 14"
</div> 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) => (*/} {isCoverExpanded && (
{/* <FormLabel key={index} label={`Автор_${index + 1}`}>*/} <svg
{/* <div*/} width="45"
{/* className={*/} height="15"
{/* "break-all border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm font-bold"*/} viewBox="0 0 45 15"
{/* }*/} fill="none"
{/* >*/} xmlns="http://www.w3.org/2000/svg"
{/* {author}*/} >
{/* </div>*/} <path
{/* </FormLabel>*/} 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> </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>
<div className={"flex flex-col gap-[20px]"}> <div className={"flex flex-col gap-[20px]"}>
<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"}> <p className={"text-xs"}>

View File

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

View File

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

View File

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