feat: minimal configuration

This commit is contained in:
2025-06-06 15:12:54 +03:00
parent 751ebe614a
commit 746c8ab0d7
18 changed files with 648 additions and 148 deletions

View File

@@ -0,0 +1,8 @@
@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap");
@import "tailwindcss";
body {
background: var(--tg-theme-secondary-bg-color, white);
font-family: "Montserrat", sans-serif;
font-optical-sizing: auto;
}

View File

@@ -1,26 +0,0 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,16 +1,7 @@
import Root from "@/components/Root";
import "@telegram-apps/telegram-ui/dist/styles.css";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
import "./_assets/globals.css";
export const metadata: Metadata = {
title: "Create Next App",
@@ -23,11 +14,9 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<html lang="ru" suppressHydrationWarning>
<body>
<Root>{children}</Root>
</body>
</html>
);

View File

@@ -1,103 +1,28 @@
import Image from "next/image";
import { Block } from "@/components/Block";
import { Page } from "@/components/Page";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
<Page back={false}>
<header className="flex h-12 w-full flex-col items-center justify-center bg-[var(--tg-theme-header-bg-color)]">
<span className="text-lg font-semibold text-[var(--tg-theme-text-color)]">Nwaifu Proxy</span>
</header>
<main className="flex h-full w-full flex-col items-center gap-3 px-2.5 py-3.5 text-[var(--tg-theme-text-color)]">
<Block name="Подписка">
<div className="flex flex-col items-center gap-0.5">
<span className="text-xs">Статус:</span>
<span className="text-xs">Активна</span>
</div>
<div className="flex flex-col items-center gap-0.5">
<span className="text-xs">Активна до:</span>
<span className="text-xs">01.01.2023</span>
</div>
</Block>
<Block name="Ссылка">
<div className="size-32 rounded-md bg-white"></div>
<button className="h-8 w-48 rounded-md bg-[var(--tg-theme-button-color)] text-xs">Копировать</button>
</Block>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
</Page>
);
}

14
src/components/Block.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { PropsWithChildren } from "react";
type BlockProps = PropsWithChildren<{ name?: string }>;
const Block: React.FC<BlockProps> = ({ children, name = "" }) => {
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-1.5 shadow-md">
{name && <span className="text-semibold text-sm">{name}</span>}
{children}
</div>
);
};
export { Block };

View File

@@ -0,0 +1,29 @@
import { Component, ComponentType, GetDerivedStateFromError, PropsWithChildren } from "react";
export interface ErrorBoundaryProps extends PropsWithChildren {
fallback: ComponentType<{ error: Error }>;
}
interface ErrorBoundaryState {
error?: Error;
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = {};
static getDerivedStateFromError: GetDerivedStateFromError<ErrorBoundaryProps, ErrorBoundaryState> = (error) => ({
error,
});
componentDidCatch(error: Error): void {
this.setState({ error });
}
render() {
const {
state: { error },
props: { fallback: Fallback, children },
} = this;
return error ? <Fallback error={error} /> : children;
}
}

View File

@@ -0,0 +1,25 @@
import { useEffect } from "react";
interface ErrorPageProps {
error: Error & { digest?: string };
reset?: () => void;
}
const ErrorPage: React.FC<ErrorPageProps> = ({ error, reset }) => {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>An unhandled error occurred!</h2>
<blockquote>
<code>{error.message}</code>
</blockquote>
{reset && <button onClick={reset}>Try again</button>}
</div>
);
};
ErrorPage.displayName = "ErrorPage";
export { ErrorPage };

25
src/components/Page.tsx Normal file
View File

@@ -0,0 +1,25 @@
"use client";
import { backButton } from "@telegram-apps/sdk-react";
import { useRouter } from "next/navigation";
import { PropsWithChildren, useEffect } from "react";
type PageProps = PropsWithChildren<{ back?: boolean }>;
const Page: React.FC<PageProps> = ({ children, back = true }) => {
const router = useRouter();
useEffect(() => {
if (back) backButton.show();
else backButton.hide();
}, [back]);
useEffect(() => {
return backButton.onClick(() => {
router.back();
});
}, [router]);
return <>{children}</>;
};
Page.displayName = "Page";
export { Page };

37
src/components/Root.tsx Normal file
View File

@@ -0,0 +1,37 @@
"use client";
import { useDidMount } from "@/hooks/useDidMount";
import { miniApp, useLaunchParams, useSignal } from "@telegram-apps/sdk-react";
import { AppRoot } from "@telegram-apps/telegram-ui";
import { PropsWithChildren } from "react";
import { ErrorBoundary } from "./ErrorBoundary";
import { ErrorPage } from "./ErrorPage";
const RootInner: React.FC<PropsWithChildren> = ({ children }) => {
const lp = useLaunchParams();
const isDark = useSignal(miniApp.isDark);
return (
<AppRoot
appearance={isDark ? "dark" : "light"}
platform={["macos", "ios"].includes(lp.tgWebAppPlatform) ? "ios" : "base"}
>
{children}
</AppRoot>
);
};
const Root: React.FC<PropsWithChildren> = (props: PropsWithChildren) => {
const didMount = useDidMount();
return didMount ? (
<ErrorBoundary fallback={ErrorPage}>
<RootInner {...props} />
</ErrorBoundary>
) : (
<div>Loading</div>
);
};
Root.displayName = "Root";
RootInner.displayName = "RootInner";
export default Root;

38
src/core/init.ts Normal file
View File

@@ -0,0 +1,38 @@
import {
bindThemeParamsCssVars,
bindViewportCssVars,
init,
mountBackButton,
mountMiniAppSync,
mountViewport,
restoreInitData,
setDebug,
} from "@telegram-apps/sdk-react";
const initServer = async (options: { debug: boolean; eruda: boolean }) => {
setDebug(options.debug);
init();
if (options.eruda) {
void import("eruda").then(({ default: eruda }) => {
eruda.init();
eruda.position({ x: window.innerWidth - 50, y: 0 });
});
}
mountBackButton.ifAvailable();
restoreInitData();
if (mountMiniAppSync.isAvailable()) {
mountMiniAppSync();
bindThemeParamsCssVars();
}
if (mountViewport.isAvailable()) {
mountViewport().then(() => {
bindViewportCssVars();
});
}
};
export { initServer };

11
src/hooks/useDidMount.ts Normal file
View File

@@ -0,0 +1,11 @@
import { useEffect, useState } from "react";
const useDidMount: () => boolean = () => {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
return didMount;
};
export { useDidMount };

View File

@@ -0,0 +1,16 @@
import { retrieveLaunchParams } from "@telegram-apps/sdk-react";
import { initServer } from "./core/init";
import { mockEnv } from "./mockEnv";
console.log("init");
mockEnv().then(() => {
try {
const launchParams = retrieveLaunchParams();
const { tgWebAppPlatform: platform } = launchParams;
const debug = (launchParams.tgWebAppStartParam || "").includes("debug") || process.env.NODE_ENV === "development";
initServer({ debug, eruda: debug && ["ios", "android"].includes(platform) });
} catch (e) {
console.log(e);
}
});

5
src/lib/cn.ts Normal file
View File

@@ -0,0 +1,5 @@
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export default function cn(...inputs: ClassValue[]) {
return twMerge(clsx(...inputs));
}

67
src/mockEnv.ts Normal file
View File

@@ -0,0 +1,67 @@
import { emitEvent, isTMA, mockTelegramEnv } from "@telegram-apps/sdk-react";
const mockEnv = async () => {
return process.env.NODE_ENV !== "development"
? undefined
: isTMA("complete").then((isTma) => {
if (!isTma) {
const themeParams = {
accent_text_color: "#6ab2f2",
bg_color: "#17212b",
button_color: "#5288c1",
button_text_color: "#ffffff",
destructive_text_color: "#ec3942",
header_bg_color: "#17212b",
hint_color: "#708499",
link_color: "#6ab3f3",
secondary_bg_color: "#232e3c",
section_bg_color: "#17212b",
section_header_text_color: "#6ab3f3",
subtitle_text_color: "#708499",
text_color: "#f5f5f5",
} as const;
const noInsets = { left: 0, top: 0, bottom: 0, right: 0 } as const;
mockTelegramEnv({
onEvent(e) {
if (e[0] === "web_app_request_theme") {
return emitEvent("theme_changed", { theme_params: themeParams });
}
if (e[0] === "web_app_request_viewport") {
return emitEvent("viewport_changed", {
height: window.innerHeight,
width: window.innerWidth,
is_expanded: true,
is_state_stable: true,
});
}
if (e[0] === "web_app_request_content_safe_area") {
return emitEvent("content_safe_area_changed", noInsets);
}
if (e[0] === "web_app_request_safe_area") {
return emitEvent("safe_area_changed", noInsets);
}
},
launchParams: new URLSearchParams([
["tgWebAppThemeParams", JSON.stringify(themeParams)],
[
"tgWebAppData",
new URLSearchParams([
["auth_date", ((new Date().getTime() / 1000) | 0).toString()],
["hash", "some-hash"],
["signature", "some-signature"],
["user", JSON.stringify({ id: 1, first_name: "Vladislav" })],
]).toString(),
],
["tgWebAppVersion", "8.4"],
["tgWebAppPlatform", "tdesktop"],
]),
});
console.info(
"⚠️ As long as the current environment was not considered as the Telegram-based one, it was mocked. Take a note, that you should not do it in production and current behavior is only specific to the development process. Environment mocking is also applied only in development mode. So, after building the application, you will not see this behavior and related warning, leading to crashing the application outside Telegram."
);
}
});
};
export { mockEnv };