| update audio player & other fixes
This commit is contained in:
parent
b03533dea6
commit
c5c36cb078
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue