feat: multi-subs working

This commit is contained in:
2025-06-11 11:54:28 +03:00
parent 2738a8e8bd
commit 3953e550b1
3 changed files with 74 additions and 42 deletions

View File

@@ -7,8 +7,9 @@ import { cn } from "@/utils/cn";
import { useRawInitData } from "@telegram-apps/sdk-react"; import { useRawInitData } from "@telegram-apps/sdk-react";
import { ChevronLeft, ChevronRight, ClipboardList } from "lucide-react"; import { ChevronLeft, ChevronRight, ClipboardList } from "lucide-react";
import { QRCodeSVG } from "qrcode.react"; import { QRCodeSVG } from "qrcode.react";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { getUrl } from "./actions"; import { getUrl } from "./actions";
import { EmblaCarouselType } from "embla-carousel";
interface ProxySubData { interface ProxySubData {
url: string; url: string;
@@ -22,16 +23,23 @@ export default function Home() {
const [chosen, setChosen] = useState(0); const [chosen, setChosen] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [notFound, setNotFound] = useState(false); const [notFound, setNotFound] = useState(false);
const carouselApi = useRef<EmblaCarouselType | null>(null);
const initData = useRawInitData(); const initData = useRawInitData();
const [isCopied, setIsCopied] = useState<boolean[]>([]);
useEffect(() => {
if (!proxyData.length) return;
setIsCopied(Array(proxyData.length).fill(false));
}, [proxyData]);
const onCopyClick = async (url: string) => { const onCopyClick = async (url: string) => {
if (!proxyData.length) return; if (!proxyData.length) return;
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
setIsCopied(true); setIsCopied((prev) => prev.map((_, i) => i === chosen));
setTimeout(() => { setTimeout(() => {
setIsCopied(false); setIsCopied((prev) => prev.map(() => false));
}, 2000); }, 2000);
}; };
const [isCopied, setIsCopied] = useState(false);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
@@ -66,32 +74,43 @@ export default function Home() {
<main className="absolute bottom-0 left-0 flex w-full flex-col items-center gap-3 px-2.5 py-3.5 text-[var(--tg-theme-text-color)]"> <main className="absolute bottom-0 left-0 flex w-full flex-col items-center gap-3 px-2.5 py-3.5 text-[var(--tg-theme-text-color)]">
{proxyData.length && ( {proxyData.length && (
<> <>
<Block name="Ссылка"> <Block name={proxyData.length > 1 ? `Ссылка ${chosen + 1}/${proxyData.length}` : "Ссылка"}>
<div className="flex w-full flex-row items-center justify-between"> <div
<ChevronLeft onClick={() => setChosen(chosen - 1 < 0 ? proxyData.length - 1 : chosen - 1)} /> className={cn(
<Carousel loop> "flex w-full flex-row items-center justify-between px-8",
{proxyData.map((item, index) => ( proxyData.length === 1 && "justify-center"
<div )}
className="relative flex size-52 flex-[0_0_100%] flex-col items-center justify-center rounded-md bg-white" >
onClick={() => onCopyClick(item.url)} {proxyData.length > 1 && <ChevronLeft onClick={() => carouselApi.current?.scrollPrev()} />}
key={index} <div className="w-52">
> <Carousel
loop
setApi={(api) => (carouselApi.current = api)}
onSelectIndex={(index) => setChosen(index)}
>
{proxyData.map((item, index) => (
<div <div
className={cn( className="relative flex size-52 max-w-52 flex-[0_0_100%] flex-col items-center justify-center rounded-md bg-white"
"absolute top-0 left-0 flex h-full w-full flex-col items-center justify-center rounded-md bg-[var(--tg-theme-button-color)] opacity-0 transition-opacity", onClick={() => onCopyClick(item.url)}
isCopied && "opacity-95" key={index}
)}
> >
<span className="flex flex-row text-lg font-semibold text-[var(--tg-theme-button-text-color)]"> <div
Скопировано className={cn(
<ClipboardList className="text-[var(--tg-theme-button-text-color)]" /> "absolute top-0 left-0 flex h-full w-full flex-col items-center justify-center rounded-md bg-[var(--tg-theme-button-color)] opacity-0 transition-opacity",
</span> isCopied[index] && "opacity-95"
)}
>
<span className="flex flex-row text-lg font-semibold text-[var(--tg-theme-button-text-color)]">
Скопировано
<ClipboardList className="text-[var(--tg-theme-button-text-color)]" />
</span>
</div>
<QRCodeSVG value={item.url} className="size-48" />
</div> </div>
<QRCodeSVG value={item.url} className="size-48" /> ))}
</div> </Carousel>
))} </div>
</Carousel> {proxyData.length > 1 && <ChevronRight onClick={() => carouselApi.current?.scrollNext()} />}
<ChevronRight onClick={() => setChosen(chosen + 1 > proxyData.length - 1 ? 0 : chosen + 1)} />
</div> </div>
<span className="text-center text-sm font-semibold">Нажмите на QR, чтобы скопировать!</span> <span className="text-center text-sm font-semibold">Нажмите на QR, чтобы скопировать!</span>
</Block> </Block>

View File

@@ -1,10 +1,16 @@
import { cn } from "@/utils/cn";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
type BlockProps = PropsWithChildren<{ name?: string }>; type BlockProps = PropsWithChildren<{ name?: string; className?: string }>;
const Block: React.FC<BlockProps> = ({ children, name = "" }) => { const Block: React.FC<BlockProps> = ({ children, name = "", className = "" }) => {
return ( return (
<div className="flex h-fit w-full flex-col items-center gap-2.5 rounded-lg bg-[var(--tg-theme-section-bg-color)] py-2.5 shadow-md"> <div
className={cn(
"flex h-fit w-full flex-col items-center gap-2.5 rounded-lg bg-[var(--tg-theme-section-bg-color)] py-2.5 shadow-md",
className
)}
>
{name && <span className="text-semibold text-lg">{name}</span>} {name && <span className="text-semibold text-lg">{name}</span>}
{children} {children}
</div> </div>

View File

@@ -1,24 +1,31 @@
"use client"; "use client";
import { EmblaOptionsType } from "embla-carousel"; import { EmblaCarouselType, EmblaOptionsType } from "embla-carousel";
import useEmblaCarousel from "embla-carousel-react"; import useEmblaCarousel from "embla-carousel-react";
import { PropsWithChildren, useEffect, useState } from "react"; import { PropsWithChildren, useEffect, useState } from "react";
type Props = PropsWithChildren & EmblaOptionsType; type Props = PropsWithChildren &
EmblaOptionsType & {
setApi?: (api: EmblaCarouselType) => void;
onSelectIndex?: (index: number) => void;
};
const Carousel: React.FC<Props> = ({ children, ...options }: Props) => { const Carousel: React.FC<Props> = ({ children, setApi, onSelectIndex, ...options }: Props) => {
const [emblaRef, emblaApi] = useEmblaCarousel(options); const [emblaRef, emblaApi] = useEmblaCarousel(options);
const [, setSelectedIndex] = useState(0); const [, setSelectedIndex] = useState(0);
useEffect(() => { useEffect(() => {
const selectHandler = () => { if (!emblaApi) return;
const index = emblaApi?.selectedScrollSnap(); if (emblaApi && setApi) setApi(emblaApi);
setSelectedIndex(index || 0);
};
emblaApi?.on("select", selectHandler);
return () => {
emblaApi?.off("select", selectHandler);
};
}, [emblaApi]);
const selectHandler = () => {
const index = emblaApi.selectedScrollSnap();
setSelectedIndex(index);
onSelectIndex?.(index);
};
emblaApi.on("select", selectHandler);
return () => {
emblaApi.off("select", selectHandler);
};
}, [emblaApi, setApi, onSelectIndex]);
return ( return (
<> <>
<div className="overflow-hidden" ref={emblaRef}> <div className="overflow-hidden" ref={emblaRef}>