init
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode;
|
||||
icon?: JSX.Element;
|
||||
color?: "primary" | "secondary";
|
||||
width?: "fit" | "full";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
type?: "submit" | "reset" | "button";
|
||||
rounded?: string;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
color = "primary",
|
||||
icon,
|
||||
width = "fit",
|
||||
disabled = false,
|
||||
className,
|
||||
onClick,
|
||||
type,
|
||||
rounded,
|
||||
}: ButtonProps) {
|
||||
const widthClass = width === "full" ? "w-full" : "w-fit";
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
if (type !== "submit") e.preventDefault();
|
||||
onClick?.();
|
||||
}}
|
||||
className={`group cursor-pointer relative px-6 py-2${
|
||||
rounded ? " rounded-" + rounded : ""
|
||||
} min-w-fit ${
|
||||
(color === "primary" ? "bg-gradient" : "") ||
|
||||
(color === "secondary" ? " outline-1 outline-[#3D425C]" : "")
|
||||
} ${icon ? "pr-4" : ""} flex gap-1 items-center overflow-hidden ${widthClass} ${className ?? ""} justify-between`}
|
||||
>
|
||||
<span className="group-hover:opacity-10 absolute top-0 left-0 w-full h-full transition-opacity bg-black opacity-0"></span>
|
||||
<span className={"relative font-medium" + (icon ? "" : " m-auto")}>
|
||||
{children}
|
||||
</span>
|
||||
<span className="relative">{icon}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
FieldValues,
|
||||
Path,
|
||||
useController,
|
||||
useFormContext,
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
|
||||
export function CheckboxesGroup<IFieldValues extends FieldValues>({
|
||||
options,
|
||||
name,
|
||||
}: {
|
||||
options: string[];
|
||||
name: Path<IFieldValues>;
|
||||
}) {
|
||||
const { control } = useFormContext<IFieldValues>();
|
||||
|
||||
const {
|
||||
field: { ref, onChange, ...inputProps },
|
||||
} = useController({ control, name });
|
||||
|
||||
const values: string[] = useWatch({ control, name });
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap lg:gap-[0.556vw] gap-2">
|
||||
{options.map((option) => (
|
||||
<label
|
||||
htmlFor={name + "_" + option}
|
||||
key={option}
|
||||
className={`cursor-pointer transition-colors lg:rounded-[1.111vw] rounded-2xl font-medium text-nowrap select-none lg:px-[1.667vw] px-6 lg:py-[1.181vw] py-[17px] btnm ${
|
||||
values.includes(option)
|
||||
? "bg-white text-black"
|
||||
: "bg-[#37393B99] hover:bg-[#37393B]"
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
<input
|
||||
id={name + "_" + option}
|
||||
className="hidden"
|
||||
type="checkbox"
|
||||
{...inputProps}
|
||||
checked={values.includes(option)}
|
||||
ref={ref}
|
||||
onChange={() => {
|
||||
onChange(
|
||||
values.includes(option)
|
||||
? values.filter((x) => x !== option)
|
||||
: [...values, option]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import CheckIcon from "@/components/icons/CheckIcon";
|
||||
|
||||
function CustomCheckbox({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string, checked: boolean) => void;
|
||||
}) {
|
||||
const [checked, setChecked] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(value, checked);
|
||||
}, [checked, onChange, value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={() => setChecked(!checked)}
|
||||
className="flex gap-x-[10px] items-center hover:cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
checked ? "bg-gradient" : "bg-[#37393B]"
|
||||
} w-[20px] h-[20px] radius-[5px] rounded relative`}
|
||||
>
|
||||
{checked && (
|
||||
<div className="text-white lg:size-[1.389vw] size-4 absolute top-0">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="md:text-sm">{value}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomCheckbox;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { type Ref } from "react";
|
||||
import {
|
||||
formatRuPhoneDisplay,
|
||||
normalizeRuPhoneFromInput,
|
||||
} from "@/lib/phoneRu";
|
||||
|
||||
const inputClassName =
|
||||
"placeholder:btnl placeholder:font-medium placeholder:select-none peer btnl w-full h-full bg-transparent rounded-none transition-all outline-none";
|
||||
|
||||
interface PhoneInputRuProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onBlur?: () => void;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
id?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function PhoneInputRu({
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
inputRef,
|
||||
id = "tel",
|
||||
placeholder = "+7 (XXX) XXX - XX - XX",
|
||||
}: PhoneInputRuProps) {
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="tel"
|
||||
autoComplete="tel"
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
className={inputClassName}
|
||||
value={formatRuPhoneDisplay(value)}
|
||||
onBlur={onBlur}
|
||||
onChange={(e) => {
|
||||
if (!e.nativeEvent.type.startsWith("input")) return;
|
||||
const cleanValue = e.target.value.replace(/\s/g, "");
|
||||
const inputType = (e.nativeEvent as InputEvent).inputType;
|
||||
onChange(normalizeRuPhoneFromInput(cleanValue, inputType));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import MuteIcon from "@/components/icons/MuteIcon";
|
||||
import UnmutedIcon from "@/components/icons/UnmutedIcon";
|
||||
|
||||
export function VideoMutingBtn({
|
||||
handleClick,
|
||||
muted,
|
||||
}: {
|
||||
muted: boolean;
|
||||
handleClick: () => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [point, setPoint] = useState([0, 0]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
const move = (e: MouseEvent) => setPoint([e.clientX, e.clientY]);
|
||||
el?.addEventListener("mousemove", move);
|
||||
|
||||
return () => {
|
||||
el?.removeEventListener("mousemove", move);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="absolute left-0 top-0 h-[calc(5/6*100%)] w-full z-[7]">
|
||||
<div ref={ref} className="group relative w-full h-full">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-[#37393B99] p-[1.736vw] [backdrop-filter:blur(30.72px)] rounded-full group-hover:opacity-100 transition-opacity group-hover:cursor-none opacity-0 sticky outline-none"
|
||||
style={{ left: point[0] - 32, top: point[1] - 32 }}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{muted ? (
|
||||
<div className="text-white lg:size-[1.944vw] size-7">
|
||||
<UnmutedIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-white lg:size-[1.944vw] size-7">
|
||||
<MuteIcon />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
ComponentProps,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { VideoMutingBtn } from "./VideoMutingBtn";
|
||||
import { VideoProgressBar } from "./VideoProgressBar";
|
||||
|
||||
export const VideoPlayer = forwardRef<
|
||||
HTMLVideoElement,
|
||||
{
|
||||
src: string;
|
||||
showMutingBtn: boolean;
|
||||
children?: React.ReactNode;
|
||||
} & ComponentProps<"video">
|
||||
>(
|
||||
(
|
||||
{ src, showMutingBtn, children, loop = true, autoPlay = true, className },
|
||||
ref
|
||||
) => {
|
||||
const progressbarRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => videoRef.current!);
|
||||
|
||||
const [muted, setMuted] = useState(autoPlay);
|
||||
const [playing, setPlaying] = useState(autoPlay);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
function handleProgressbarClick(e: React.MouseEvent) {
|
||||
const video = videoRef.current;
|
||||
const bar = progressbarRef.current;
|
||||
if (!video || !bar) return;
|
||||
video.currentTime =
|
||||
(video.duration * (e.clientX - bar.getBoundingClientRect().x)) /
|
||||
bar.clientWidth;
|
||||
setProgress(
|
||||
((video.currentTime ?? 0) / (video.duration ?? 1)) * 100
|
||||
);
|
||||
}
|
||||
|
||||
function handlePlaybackClick() {
|
||||
if (!videoRef.current) return;
|
||||
setPlaying(videoRef.current.paused);
|
||||
videoRef.current[videoRef.current.paused ? "play" : "pause"]();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const timeUpdateHandler = () =>
|
||||
setProgress(((video.currentTime ?? 0) / (video.duration ?? 1)) * 100);
|
||||
|
||||
video.addEventListener("timeupdate", timeUpdateHandler);
|
||||
return () => video.removeEventListener("timeupdate", timeUpdateHandler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={src}
|
||||
autoPlay={autoPlay}
|
||||
muted={muted}
|
||||
loop={loop}
|
||||
playsInline
|
||||
className={`lg:rounded-[1.111vw] rounded-2xl w-full h-full object-cover${
|
||||
className ? " " + className : ""
|
||||
}`}
|
||||
/>
|
||||
{showMutingBtn && (
|
||||
<VideoMutingBtn
|
||||
handleClick={() => setMuted(!videoRef.current!.muted)}
|
||||
muted={muted}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 rounded-2xl [background:linear-gradient(to_top,rgba(20,22,31,0.6),rgba(20,22,31,0))]" />
|
||||
<AnimatePresence>
|
||||
{muted && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<VideoProgressBar
|
||||
muted={muted}
|
||||
progress={progress}
|
||||
progressbarRef={progressbarRef}
|
||||
playing={playing}
|
||||
handlePlaybackClick={handlePlaybackClick}
|
||||
handleProgressbarClick={handleProgressbarClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
VideoPlayer.displayName = "VideoPlayer";
|
||||
@@ -0,0 +1,69 @@
|
||||
import { MouseEventHandler, RefObject, useState } from "react";
|
||||
import PauseIcon from "@/components/icons/PauseIcon";
|
||||
import PlayIcon from "@/components/icons/PlayIcon";
|
||||
|
||||
export function VideoProgressBar({
|
||||
muted,
|
||||
progressbarRef,
|
||||
playing,
|
||||
handlePlaybackClick,
|
||||
handleProgressbarClick,
|
||||
progress,
|
||||
}: {
|
||||
muted: boolean;
|
||||
progress: number;
|
||||
progressbarRef: RefObject<HTMLDivElement | null>;
|
||||
playing: boolean;
|
||||
handlePlaybackClick: MouseEventHandler<HTMLButtonElement>;
|
||||
handleProgressbarClick: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bottom-2 left-2 right-2 absolute z-10 select-none flex items-stretch gap-1 ${
|
||||
muted ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="p-[18px] bg-[#37393B99] rounded-2xl cursor-pointer"
|
||||
onClick={handlePlaybackClick}
|
||||
>
|
||||
{playing ? (
|
||||
<div className="text-white lg:size-[1.389vw] size-5">
|
||||
<PauseIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-white lg:size-[1.389vw] size-5">
|
||||
<PlayIcon />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
className="flex-1 rounded-2xl bg-[#37393B99] px-6 cursor-pointer flex items-center select-none"
|
||||
onMouseDown={(e) => {
|
||||
setIsMouseDown(true);
|
||||
handleProgressbarClick(e);
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
if (isMouseDown) handleProgressbarClick(e);
|
||||
}}
|
||||
onMouseUp={() => setIsMouseDown(false)}
|
||||
onMouseLeave={() => setIsMouseDown(false)}
|
||||
>
|
||||
<div
|
||||
ref={progressbarRef}
|
||||
className="h-1 bg-[#7A7A7A] relative rounded-3xl cursor-pointer w-full"
|
||||
>
|
||||
<div
|
||||
className="left-0 h-1 bg-white rounded-3xl transition-all"
|
||||
style={{
|
||||
width: progress + "%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user