feat: minimal configuration
This commit is contained in:
8
src/app/_assets/globals.css
Normal file
8
src/app/_assets/globals.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
119
src/app/page.tsx
119
src/app/page.tsx
@@ -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
14
src/components/Block.tsx
Normal 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 };
|
||||
29
src/components/ErrorBoundary.tsx
Normal file
29
src/components/ErrorBoundary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
25
src/components/ErrorPage.tsx
Normal file
25
src/components/ErrorPage.tsx
Normal 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
25
src/components/Page.tsx
Normal 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
37
src/components/Root.tsx
Normal 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
38
src/core/init.ts
Normal 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
11
src/hooks/useDidMount.ts
Normal 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 };
|
||||
16
src/instrumentation-client.ts
Normal file
16
src/instrumentation-client.ts
Normal 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
5
src/lib/cn.ts
Normal 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
67
src/mockEnv.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user