diff --git a/bun.lock b/bun.lock index 50e172d..96eaac1 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,8 @@ "@telegram-apps/init-data-node": "^2.0.7", "@telegram-apps/sdk-react": "^3.3.0", "@telegram-apps/telegram-ui": "^2.1.8", + "embla-carousel": "^8.6.0", + "embla-carousel-react": "^8.6.0", "ioredis": "^5.6.1", "lucide-react": "^0.513.0", "next": "15.3.3", @@ -492,6 +494,12 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], + + "embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="], + + "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], diff --git a/next.config.ts b/next.config.ts index e9ffa30..3915163 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + reactStrictMode: true, }; export default nextConfig; diff --git a/package.json b/package.json index a360f5a..2686e1b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@telegram-apps/init-data-node": "^2.0.7", "@telegram-apps/sdk-react": "^3.3.0", "@telegram-apps/telegram-ui": "^2.1.8", + "embla-carousel": "^8.6.0", + "embla-carousel-react": "^8.6.0", "ioredis": "^5.6.1", "lucide-react": "^0.513.0", "next": "15.3.3", diff --git a/prisma/migrations/20250610075025_make_tg_id_not_unique/migration.sql b/prisma/migrations/20250610075025_make_tg_id_not_unique/migration.sql new file mode 100644 index 0000000..0b04179 --- /dev/null +++ b/prisma/migrations/20250610075025_make_tg_id_not_unique/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "User_tgId_key"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dfc0aaf..58ad751 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,6 +9,6 @@ datasource db { model User { id String @id @default(uuid()) - tgId String? @unique + tgId String? email String? @unique } diff --git a/src/app/_dto/db.ts b/src/app/_dto/db.ts new file mode 100644 index 0000000..29019e1 --- /dev/null +++ b/src/app/_dto/db.ts @@ -0,0 +1,5 @@ +export interface User { + id: string; + tgId: string | null; + email: string | null; +} diff --git a/src/app/actions.ts b/src/app/actions.ts index af66f4b..9f89019 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -4,6 +4,7 @@ import { authFetch } from "@/lib/login"; import { getValidUrl } from "@/lib/url"; import { prisma } from "@/utils/prisma"; import { redis } from "@/utils/redis"; +import { User } from "@prisma/client"; import { parse, validate } from "@telegram-apps/init-data-node"; import { ClientSettings, InboundResponse } from "./_dto/inbounds"; @@ -39,18 +40,19 @@ export async function getUrl(initData: string = "") { throw new Error("User not found"); } const cachedUser = await redis.get(`user:${initDataParsed.user.id}`); - const user = cachedUser + const users: User[] = cachedUser ? JSON.parse(cachedUser) - : await prisma.user.findFirst({ + : await prisma.user.findMany({ where: { tgId: initDataParsed.user.id.toString(), }, }); - if (!user) { + if (!users.length) { throw new Error("User not found"); } - await redis.set(`user:${initDataParsed.user.id}`, JSON.stringify(user), "EX", 60); - return await getUrlApi(user.email || ""); + await redis.set(`user:${initDataParsed.user.id}`, JSON.stringify(users), "EX", 60); + const usersUrl = await Promise.all(users.map(async (user) => await getUrlApi(user.email || ""))); + return usersUrl; } catch (e) { throw e; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 02d04ea..09cc6a6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,22 +1,30 @@ "use client"; import { Block } from "@/components/Block"; +import { Carousel } from "@/components/Carousel/Carousel"; import { Loader } from "@/components/Loader"; import { Page } from "@/components/Page"; import { cn } from "@/utils/cn"; import { useRawInitData } from "@telegram-apps/sdk-react"; -import { ClipboardList } from "lucide-react"; +import { ChevronLeft, ChevronRight, ClipboardList } from "lucide-react"; import { QRCodeSVG } from "qrcode.react"; import { useEffect, useState } from "react"; import { getUrl } from "./actions"; +interface ProxySubData { + url: string; + expiryTime: number; +} + +type ProxySubArray = ProxySubData[]; + export default function Home() { - const [url, setUrl] = useState(""); - const [expiryTime, setExpiryTime] = useState(0); + const [proxyData, setProxyData] = useState([]); + const [chosen, setChosen] = useState(0); const [isLoading, setIsLoading] = useState(true); const [notFound, setNotFound] = useState(false); const initData = useRawInitData(); - const onCopyClick = async () => { - if (!url.length) return; + const onCopyClick = async (url: string) => { + if (!proxyData.length) return; await navigator.clipboard.writeText(url); setIsCopied(true); setTimeout(() => { @@ -28,8 +36,7 @@ export default function Home() { const fetchData = async () => { try { const data = await getUrl(initData); - setUrl(data.url); - setExpiryTime(data.expiryTime); + setProxyData(data); setIsLoading(false); } catch (e) { console.error(e); @@ -57,40 +64,53 @@ export default function Home() { Nwaifu Proxy
- {url && ( + {proxyData.length && ( <> -
-
- - Скопировано - - -
- +
+ setChosen(chosen - 1 < 0 ? proxyData.length - 1 : chosen - 1)} /> + + {proxyData.map((item, index) => ( +
onCopyClick(item.url)} + key={index} + > +
+ + Скопировано + + +
+ +
+ ))} +
+ setChosen(chosen + 1 > proxyData.length - 1 ? 0 : chosen + 1)} />
Нажмите на QR, чтобы скопировать!
Статус: - {expiryTime > Date.now() || expiryTime === 0 ? "Активна" : "Не Активна"} + + {proxyData[chosen].expiryTime > Date.now() || proxyData[chosen].expiryTime === 0 + ? "Активна" + : "Не Активна"} +
Активна до: - {expiryTime === 0 + {proxyData[chosen].expiryTime === 0 ? "Всегда" - : expiryTime > Date.now() - ? new Date(expiryTime).toLocaleString("ru-RU") + : proxyData[chosen].expiryTime > Date.now() + ? new Date(proxyData[chosen].expiryTime).toLocaleString("ru-RU") : "Не Активна"}
diff --git a/src/components/Carousel/Carousel.tsx b/src/components/Carousel/Carousel.tsx new file mode 100644 index 0000000..50df042 --- /dev/null +++ b/src/components/Carousel/Carousel.tsx @@ -0,0 +1,31 @@ +"use client"; +import { EmblaOptionsType } from "embla-carousel"; +import useEmblaCarousel from "embla-carousel-react"; +import { PropsWithChildren, useEffect, useState } from "react"; + +type Props = PropsWithChildren & EmblaOptionsType; + +const Carousel: React.FC = ({ children, ...options }: Props) => { + const [emblaRef, emblaApi] = useEmblaCarousel(options); + const [, setSelectedIndex] = useState(0); + useEffect(() => { + const selectHandler = () => { + const index = emblaApi?.selectedScrollSnap(); + setSelectedIndex(index || 0); + }; + emblaApi?.on("select", selectHandler); + return () => { + emblaApi?.off("select", selectHandler); + }; + }, [emblaApi]); + + return ( + <> +
+
{children}
+
+ + ); +}; + +export { Carousel }; diff --git a/src/components/Carousel/Dots.tsx b/src/components/Carousel/Dots.tsx new file mode 100644 index 0000000..0eaec61 --- /dev/null +++ b/src/components/Carousel/Dots.tsx @@ -0,0 +1,30 @@ +"use client"; +import { cn } from "@/utils/cn"; + +type Props = { + itemsLength: number; + selectedIndex: number; +}; + +const Dots: React.FC = ({ itemsLength, selectedIndex }) => { + const arr = new Array(itemsLength).fill(0); + return ( +
+ {arr.map((_, i) => { + const selected = i === selectedIndex; + return ( + + ); + })} +
+ ); +}; + +Dots.displayName = "Dots"; +export { Dots };