├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── error.tsx │ ├── _assets │ │ ├── globals.css │ │ └── ton.svg │ ├── ton-connect │ │ ├── TONConnectPage.css │ │ └── page.tsx │ ├── layout.tsx │ ├── launch-params │ │ └── page.tsx │ ├── theme-params │ │ └── page.tsx │ ├── page.tsx │ └── init-data │ │ └── page.tsx ├── components │ ├── Link │ │ ├── Link.css │ │ └── Link.tsx │ ├── Root │ │ ├── styles.css │ │ └── Root.tsx │ ├── RGB │ │ ├── RGB.css │ │ └── RGB.tsx │ ├── DisplayData │ │ ├── DisplayData.css │ │ └── DisplayData.tsx │ ├── ErrorPage.tsx │ ├── Page.tsx │ ├── LocaleSwitcher │ │ └── LocaleSwitcher.tsx │ └── ErrorBoundary.tsx ├── core │ ├── i18n │ │ ├── types.ts │ │ ├── config.ts │ │ ├── provider.tsx │ │ ├── i18n.ts │ │ └── locale.ts │ └── init.ts ├── hooks │ └── useDidMount.ts ├── instrumentation-client.ts ├── css │ ├── bem.ts │ └── classnames.ts └── mockEnv.ts ├── assets └── ssl-warning.png ├── public ├── tonconnect-manifest.json └── locales │ ├── ru.json │ └── en.json ├── postcss.config.mjs ├── next.config.ts ├── .gitignore ├── tailwind.config.ts ├── tsconfig.json ├── package.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Telegram-Mini-Apps/nextjs-template/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/components/Link/Link.css: -------------------------------------------------------------------------------- 1 | .link { 2 | text-decoration: none; 3 | color: var(--tg-theme-link-color); 4 | } -------------------------------------------------------------------------------- /assets/ssl-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Telegram-Mini-Apps/nextjs-template/HEAD/assets/ssl-warning.png -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ErrorPage } from '@/components/ErrorPage'; 4 | 5 | export default ErrorPage; 6 | -------------------------------------------------------------------------------- /public/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ton.vote", 3 | "name": "TON Vote", 4 | "iconUrl": "https://ton.vote/logo.png" 5 | } -------------------------------------------------------------------------------- /src/app/_assets/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: var(--tg-theme-secondary-bg-color, white); 3 | padding: 0; 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/i18n/types.ts: -------------------------------------------------------------------------------- 1 | import type { locales } from './config'; 2 | 3 | type Locale = (typeof locales)[number]; 4 | 5 | export type { Locale }; 6 | -------------------------------------------------------------------------------- /public/locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n": { 3 | "header": "Поддержка i18n", 4 | "footer": "Вы можете выбрать другой язык в выпадающем меню." 5 | } 6 | } -------------------------------------------------------------------------------- /public/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n": { 3 | "header": "Application supports i18n", 4 | "footer": "You can select a different language from the dropdown menu." 5 | } 6 | } -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/components/Root/styles.css: -------------------------------------------------------------------------------- 1 | .root__loading { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } -------------------------------------------------------------------------------- /src/components/RGB/RGB.css: -------------------------------------------------------------------------------- 1 | .rgb { 2 | display: inline-flex; 3 | align-items: center; 4 | gap: 5px; 5 | } 6 | 7 | .rgb__icon { 8 | width: 18px; 9 | aspect-ratio: 1; 10 | border: 1px solid #555; 11 | border-radius: 50%; 12 | } 13 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | 4 | const withNextIntl = createNextIntlPlugin('./src/core/i18n/i18n.ts'); 5 | 6 | const nextConfig: NextConfig = {}; 7 | 8 | export default withNextIntl(nextConfig); 9 | -------------------------------------------------------------------------------- /src/core/i18n/config.ts: -------------------------------------------------------------------------------- 1 | export const defaultLocale = 'en'; 2 | 3 | export const timeZone = 'Europe/Amsterdam'; 4 | 5 | export const locales = [defaultLocale, 'ru'] as const; 6 | 7 | export const localesMap = [ 8 | { key: 'en', title: 'English' }, 9 | { key: 'ru', title: 'Русский' }, 10 | ]; 11 | -------------------------------------------------------------------------------- /src/components/DisplayData/DisplayData.css: -------------------------------------------------------------------------------- 1 | .display-data__header { 2 | font-weight: 400; 3 | } 4 | 5 | .display-data__line { 6 | padding: 16px 24px; 7 | } 8 | 9 | .display-data__line-title { 10 | color: var(--tg-theme-subtitle-text-color); 11 | } 12 | 13 | .display-data__line-value { 14 | word-break: break-word; 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useDidMount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | /** 4 | * @return True, if component was mounted. 5 | */ 6 | export function useDidMount(): boolean { 7 | const [didMount, setDidMount] = useState(false); 8 | 9 | useEffect(() => { 10 | setDidMount(true); 11 | }, []); 12 | 13 | return didMount; 14 | } -------------------------------------------------------------------------------- /src/app/ton-connect/TONConnectPage.css: -------------------------------------------------------------------------------- 1 | .ton-connect-page__placeholder { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | box-sizing: border-box; 8 | } 9 | 10 | .ton-connect-page__button { 11 | margin: 16px auto 0; 12 | } 13 | 14 | .ton-connect-page__button-connected { 15 | margin: 16px 24px 16px auto; 16 | } 17 | -------------------------------------------------------------------------------- /src/core/i18n/provider.tsx: -------------------------------------------------------------------------------- 1 | import { NextIntlClientProvider } from 'next-intl'; 2 | import { getMessages } from 'next-intl/server'; 3 | import React from 'react'; 4 | 5 | import { timeZone } from './config'; 6 | 7 | const I18nProvider: React.FC = async ({ 8 | children, 9 | }) => { 10 | const messages = await getMessages(); 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export { I18nProvider }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .idea 39 | 40 | certificates -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /src/components/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function ErrorPage({ 4 | error, 5 | reset, 6 | }: { 7 | error: Error & { digest?: string } 8 | reset?: () => void 9 | }) { 10 | useEffect(() => { 11 | // Log the error to an error reporting service 12 | console.error(error); 13 | }, [error]); 14 | 15 | return ( 16 |
17 |

An unhandled error occurred!

18 |
19 | 20 | {error.message} 21 | 22 |
23 | {reset && } 24 |
25 | ); 26 | } -------------------------------------------------------------------------------- /src/core/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server'; 2 | 3 | import { defaultLocale, locales } from './config'; 4 | import { getLocale } from './locale'; 5 | import type { Locale } from './types'; 6 | 7 | const i18nRequestConfig = getRequestConfig(async () => { 8 | const locale = (await getLocale()) as Locale; 9 | 10 | return { 11 | locale, 12 | messages: 13 | locale === defaultLocale || !locales.includes(locale) 14 | ? (await import(`@public/locales/${defaultLocale}.json`)).default 15 | : (await import(`@public/locales/${locale}.json`)).default, 16 | }; 17 | }); 18 | 19 | export default i18nRequestConfig; 20 | -------------------------------------------------------------------------------- /src/components/RGB/RGB.tsx: -------------------------------------------------------------------------------- 1 | import type { RGB as RGBType } from '@tma.js/sdk-react'; 2 | import type { FC } from 'react'; 3 | import type { HTMLAttributes } from 'react'; 4 | 5 | import { bem } from '@/css/bem'; 6 | import { classNames } from '@/css/classnames'; 7 | 8 | import './RGB.css'; 9 | 10 | const [b, e] = bem('rgb'); 11 | 12 | export type RGBProps = HTMLAttributes & { 13 | color: RGBType; 14 | }; 15 | 16 | export const RGB: FC = ({ color, className, ...rest }) => ( 17 | 18 | 19 | {color} 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /src/core/i18n/locale.ts: -------------------------------------------------------------------------------- 1 | //use server is required 2 | 'use server'; 3 | 4 | import { cookies } from 'next/headers'; 5 | 6 | import { defaultLocale } from './config'; 7 | import type { Locale } from './types'; 8 | 9 | // In this example the locale is read from a cookie. You could alternatively 10 | // also read it from a database, backend service, or any other source. 11 | const COOKIE_NAME = 'NEXT_LOCALE'; 12 | 13 | const getLocale = async () => { 14 | return (await cookies()).get(COOKIE_NAME)?.value || defaultLocale; 15 | }; 16 | 17 | const setLocale = async (locale?: string) => { 18 | (await cookies()).set(COOKIE_NAME, (locale as Locale) || defaultLocale); 19 | }; 20 | 21 | export { getLocale, setLocale }; 22 | -------------------------------------------------------------------------------- /src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { backButton } from '@tma.js/sdk-react'; 4 | import { PropsWithChildren, useEffect } from 'react'; 5 | import { useRouter } from 'next/navigation'; 6 | 7 | export function Page({ children, back = true }: PropsWithChildren<{ 8 | /** 9 | * True if it is allowed to go back from this page. 10 | * @default true 11 | */ 12 | back?: boolean 13 | }>) { 14 | const router = useRouter(); 15 | 16 | useEffect(() => { 17 | if (back) { 18 | backButton.show(); 19 | } else { 20 | backButton.hide(); 21 | } 22 | }, [back]); 23 | 24 | useEffect(() => { 25 | return backButton.onClick(() => { 26 | router.back(); 27 | }); 28 | }, [router]); 29 | 30 | return <>{children}; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/LocaleSwitcher/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Select } from '@telegram-apps/telegram-ui'; 4 | import { useLocale } from 'next-intl'; 5 | import { FC } from 'react'; 6 | 7 | import { localesMap } from '@/core/i18n/config'; 8 | import { setLocale } from '@/core/i18n/locale'; 9 | import { Locale } from '@/core/i18n/types'; 10 | 11 | export const LocaleSwitcher: FC = () => { 12 | const locale = useLocale(); 13 | 14 | const onChange = (value: string) => { 15 | const locale = value as Locale; 16 | setLocale(locale); 17 | }; 18 | 19 | return ( 20 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/instrumentation-client.ts: -------------------------------------------------------------------------------- 1 | // This file is normally used for setting up analytics and other 2 | // services that require one-time initialization on the client. 3 | 4 | import { retrieveLaunchParams } from '@tma.js/sdk-react'; 5 | import { init } from './core/init'; 6 | import { mockEnv } from './mockEnv'; 7 | 8 | mockEnv().then(() => { 9 | try { 10 | const launchParams = retrieveLaunchParams(); 11 | const { tgWebAppPlatform: platform } = launchParams; 12 | const debug = 13 | (launchParams.tgWebAppStartParam || '').includes('debug') || 14 | process.env.NODE_ENV === 'development'; 15 | 16 | // Configure all application dependencies. 17 | init({ 18 | debug, 19 | eruda: debug && ['ios', 'android'].includes(platform), 20 | mockForMacOS: platform === 'macos', 21 | }); 22 | } catch (e) { 23 | console.log(e); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | import type { Metadata } from 'next'; 3 | import { getLocale } from 'next-intl/server'; 4 | 5 | import { Root } from '@/components/Root/Root'; 6 | import { I18nProvider } from '@/core/i18n/provider'; 7 | 8 | import '@telegram-apps/telegram-ui/dist/styles.css'; 9 | import 'normalize.css/normalize.css'; 10 | import './_assets/globals.css'; 11 | 12 | export const metadata: Metadata = { 13 | title: 'Your Application Title Goes Here', 14 | description: 'Your application description goes here', 15 | }; 16 | 17 | export default async function RootLayout({ children }: PropsWithChildren) { 18 | const locale = await getLocale(); 19 | 20 | return ( 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "react-jsx", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ], 28 | "@public/*": [ 29 | "./public/*" 30 | ] 31 | }, 32 | "target": "ES2017" 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts", 39 | ".next/dev/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "dev:https": "next dev --experimental-https", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@telegram-apps/telegram-ui": "^2.1.13", 14 | "@tma.js/sdk-react": "^3.0.11", 15 | "@tonconnect/ui-react": "^2.3.1", 16 | "eruda": "^3.4.3", 17 | "next": "16.0.7", 18 | "next-intl": "^4.5.5", 19 | "normalize.css": "^8.0.1", 20 | "react": "^19", 21 | "react-dom": "^19" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/postcss": "^4.1.17", 25 | "@types/node": "^24", 26 | "@types/react": "^19", 27 | "@types/react-dom": "^19", 28 | "eslint": "^9", 29 | "eslint-config-next": "16.0.4", 30 | "postcss": "^8", 31 | "tailwindcss": "^4.1.17", 32 | "typescript": "^5.9.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/_assets/ton.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | type ComponentType, 4 | type GetDerivedStateFromError, 5 | type PropsWithChildren, 6 | } from 'react'; 7 | 8 | export interface ErrorBoundaryProps extends PropsWithChildren { 9 | fallback: ComponentType<{ error: Error }>; 10 | } 11 | 12 | interface ErrorBoundaryState { 13 | error?: Error; 14 | } 15 | 16 | export class ErrorBoundary extends Component { 17 | state: ErrorBoundaryState = {}; 18 | 19 | // eslint-disable-next-line max-len 20 | static getDerivedStateFromError: GetDerivedStateFromError = (error) => ({ error }); 21 | 22 | componentDidCatch(error: Error) { 23 | this.setState({ error }); 24 | } 25 | 26 | render() { 27 | const { 28 | state: { 29 | error, 30 | }, 31 | props: { 32 | fallback: Fallback, 33 | children, 34 | }, 35 | } = this; 36 | 37 | return error ? : children; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/launch-params/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useLaunchParams } from '@tma.js/sdk-react'; 4 | import { List } from '@telegram-apps/telegram-ui'; 5 | 6 | import { DisplayData } from '@/components/DisplayData/DisplayData'; 7 | import { Page } from '@/components/Page'; 8 | 9 | export default function LaunchParamsPage() { 10 | const lp = useLaunchParams(); 11 | 12 | return ( 13 | 14 | 15 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/css/bem.ts: -------------------------------------------------------------------------------- 1 | import { classNames, isRecord } from './classnames'; 2 | 3 | export interface BlockFn { 4 | (...mods: any): string; 5 | } 6 | 7 | export interface ElemFn { 8 | (elem: string, ...mods: any): string; 9 | } 10 | 11 | /** 12 | * Applies mods to the specified element. 13 | * @param element - element name. 14 | * @param mod - mod to apply. 15 | */ 16 | function applyMods(element: string, mod: any): string { 17 | if (Array.isArray(mod)) { 18 | return classNames(mod.map((m) => applyMods(element, m))); 19 | } 20 | if (isRecord(mod)) { 21 | return classNames( 22 | Object.entries(mod).map(([mod, v]) => v && applyMods(element, mod)), 23 | ); 24 | } 25 | const v = classNames(mod); 26 | return v && `${element}--${v}`; 27 | } 28 | 29 | /** 30 | * Computes final classname for the specified element. 31 | * @param element - element name. 32 | * @param mods - mod to apply. 33 | */ 34 | function computeClassnames(element: string, ...mods: any): string { 35 | return classNames(element, applyMods(element, mods)); 36 | } 37 | 38 | /** 39 | * @returns A tuple, containing two functions. The first one generates classnames list for the 40 | * block, the second one generates classnames for its elements. 41 | * @param block - BEM block name. 42 | */ 43 | export function bem(block: string): [BlockFn, ElemFn] { 44 | return [ 45 | (...mods) => computeClassnames(block, mods), 46 | (elem, ...mods) => computeClassnames(`${block}__${elem}`, mods), 47 | ]; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/theme-params/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { themeParams, useSignal, useLaunchParams } from '@tma.js/sdk-react'; 4 | import { List } from '@telegram-apps/telegram-ui'; 5 | import { useMemo } from 'react'; 6 | 7 | import { DisplayData } from '@/components/DisplayData/DisplayData'; 8 | import { Page } from '@/components/Page'; 9 | 10 | export default function ThemeParamsPage() { 11 | const tp = useSignal(themeParams.state); 12 | const lp = useLaunchParams(); 13 | 14 | // Fallback to launch params if themeParams.state is empty 15 | const themeParamsData = useMemo(() => { 16 | const state = tp || {}; 17 | const hasState = Object.keys(state).length > 0; 18 | 19 | if (hasState) { 20 | return state; 21 | } 22 | 23 | // Fallback to launch params theme params 24 | return lp.tgWebAppThemeParams || {}; 25 | }, [tp, lp.tgWebAppThemeParams]); 26 | 27 | const rows = useMemo(() => { 28 | return Object.entries(themeParamsData).map(([title, value]) => ({ 29 | title: title 30 | .replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`) 31 | .replace(/background/, 'bg'), 32 | value, 33 | })); 34 | }, [themeParamsData]); 35 | 36 | if (rows.length === 0) { 37 | return ( 38 | 39 | 40 | 43 | 44 | 45 | ); 46 | } 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import { openLink } from '@tma.js/sdk-react'; 2 | import { type FC, type MouseEventHandler, type JSX, useCallback } from 'react'; 3 | import { 4 | type LinkProps as NextLinkProps, 5 | default as NextLink, 6 | } from 'next/link'; 7 | 8 | import { classNames } from '@/css/classnames'; 9 | 10 | import './Link.css'; 11 | 12 | export interface LinkProps 13 | extends NextLinkProps, 14 | Omit {} 15 | 16 | export const Link: FC = ({ 17 | className, 18 | onClick: propsOnClick, 19 | href, 20 | ...rest 21 | }) => { 22 | const onClick = useCallback>( 23 | (e) => { 24 | propsOnClick?.(e); 25 | 26 | // Compute if target path is external. In this case we would like to open link using 27 | // TMA method. 28 | let path: string; 29 | if (typeof href === 'string') { 30 | path = href; 31 | } else { 32 | const { search = '', pathname = '', hash = '' } = href; 33 | path = `${pathname}?${search}#${hash}`; 34 | } 35 | 36 | const targetUrl = new URL(path, window.location.toString()); 37 | const currentUrl = new URL(window.location.toString()); 38 | const isExternal = 39 | targetUrl.protocol !== currentUrl.protocol || 40 | targetUrl.host !== currentUrl.host; 41 | 42 | if (isExternal) { 43 | e.preventDefault(); 44 | openLink(targetUrl.toString()); 45 | } 46 | }, 47 | [href, propsOnClick], 48 | ); 49 | 50 | return ( 51 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { type PropsWithChildren, useEffect } from 'react'; 4 | import { 5 | initData, 6 | miniApp, 7 | useLaunchParams, 8 | useSignal, 9 | } from '@tma.js/sdk-react'; 10 | import { TonConnectUIProvider } from '@tonconnect/ui-react'; 11 | import { AppRoot } from '@telegram-apps/telegram-ui'; 12 | 13 | import { ErrorBoundary } from '@/components/ErrorBoundary'; 14 | import { ErrorPage } from '@/components/ErrorPage'; 15 | import { useDidMount } from '@/hooks/useDidMount'; 16 | import { setLocale } from '@/core/i18n/locale'; 17 | 18 | import './styles.css'; 19 | 20 | function RootInner({ children }: PropsWithChildren) { 21 | const lp = useLaunchParams(); 22 | 23 | const isDark = useSignal(miniApp.isDark); 24 | const initDataUser = useSignal(initData.user); 25 | 26 | // Set the user locale. 27 | useEffect(() => { 28 | initDataUser && setLocale(initDataUser.language_code); 29 | }, [initDataUser]); 30 | 31 | return ( 32 | 33 | 39 | {children} 40 | 41 | 42 | ); 43 | } 44 | 45 | export function Root(props: PropsWithChildren) { 46 | // Unfortunately, Telegram Mini Apps does not allow us to use all features of 47 | // the Server Side Rendering. That's why we are showing loader on the server 48 | // side. 49 | const didMount = useDidMount(); 50 | 51 | return didMount ? ( 52 | 53 | 54 | 55 | ) : ( 56 |
Loading
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/DisplayData/DisplayData.tsx: -------------------------------------------------------------------------------- 1 | import { isRGB } from '@tma.js/sdk-react'; 2 | import { Cell, Checkbox, Section } from '@telegram-apps/telegram-ui'; 3 | import type { FC, ReactNode } from 'react'; 4 | 5 | import { RGB } from '@/components/RGB/RGB'; 6 | import { Link } from '@/components/Link/Link'; 7 | import { bem } from '@/css/bem'; 8 | 9 | import './DisplayData.css'; 10 | 11 | const [, e] = bem('display-data'); 12 | 13 | export type DisplayDataRow = { title: string } & ( 14 | | { type: 'link'; value?: string } 15 | | { value: ReactNode } 16 | ); 17 | 18 | export interface DisplayDataProps { 19 | header?: ReactNode; 20 | footer?: ReactNode; 21 | rows: DisplayDataRow[]; 22 | } 23 | 24 | export const DisplayData: FC = ({ header, rows }) => ( 25 |
26 | {rows.map((item, idx) => { 27 | let valueNode: ReactNode; 28 | 29 | if (item.value === undefined) { 30 | valueNode = empty; 31 | } else { 32 | if ('type' in item) { 33 | valueNode = Open; 34 | } else if (typeof item.value === 'string') { 35 | valueNode = isRGB(item.value) ? ( 36 | 37 | ) : ( 38 | item.value 39 | ); 40 | } else if (typeof item.value === 'boolean') { 41 | valueNode = ; 42 | } else { 43 | valueNode = item.value; 44 | } 45 | } 46 | 47 | return ( 48 | 55 | {valueNode} 56 | 57 | ); 58 | })} 59 |
60 | ); 61 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Section, Cell, Image, List } from '@telegram-apps/telegram-ui'; 4 | import { useTranslations } from 'next-intl'; 5 | 6 | import { Link } from '@/components/Link/Link'; 7 | import { LocaleSwitcher } from '@/components/LocaleSwitcher/LocaleSwitcher'; 8 | import { Page } from '@/components/Page'; 9 | 10 | import tonSvg from './_assets/ton.svg'; 11 | 12 | export default function Home() { 13 | const t = useTranslations('i18n'); 14 | 15 | return ( 16 | 17 | 18 |
22 | 23 | 30 | } 31 | subtitle="Connect your TON wallet" 32 | > 33 | TON Connect 34 | 35 | 36 |
37 |
41 | 42 | 43 | Init Data 44 | 45 | 46 | 47 | 48 | Launch Parameters 49 | 50 | 51 | 52 | 53 | Theme Parameters 54 | 55 | 56 |
57 |
58 | 59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/core/init.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setDebug, 3 | backButton, 4 | initData, 5 | init as initSDK, 6 | miniApp, 7 | viewport, 8 | mockTelegramEnv, 9 | type ThemeParams, 10 | themeParams, 11 | retrieveLaunchParams, 12 | emitEvent, 13 | } from '@tma.js/sdk-react'; 14 | 15 | /** 16 | * Initializes the application and configures its dependencies. 17 | */ 18 | export async function init(options: { 19 | debug: boolean; 20 | eruda: boolean; 21 | mockForMacOS: boolean; 22 | }): Promise { 23 | // Set @tma.js/sdk-react debug mode and initialize it. 24 | setDebug(options.debug); 25 | initSDK(); 26 | 27 | // Add Eruda if needed. 28 | options.eruda && 29 | void import('eruda').then(({ default: eruda }) => { 30 | eruda.init(); 31 | eruda.position({ x: window.innerWidth - 50, y: 0 }); 32 | }); 33 | 34 | // Telegram for macOS has a ton of bugs, including cases, when the client doesn't 35 | // even response to the "web_app_request_theme" method. It also generates an incorrect 36 | // event for the "web_app_request_safe_area" method. 37 | if (options.mockForMacOS) { 38 | let firstThemeSent = false; 39 | mockTelegramEnv({ 40 | onEvent(event, next) { 41 | if (event.name === 'web_app_request_theme') { 42 | let tp: Partial = {}; 43 | if (firstThemeSent) { 44 | const state = themeParams.state; 45 | tp = state as Partial; 46 | } else { 47 | firstThemeSent = true; 48 | const lp = retrieveLaunchParams(); 49 | tp = (lp.tgWebAppThemeParams || {}) as Partial; 50 | } 51 | return emitEvent('theme_changed', { theme_params: tp as any }); 52 | } 53 | 54 | if (event.name === 'web_app_request_safe_area') { 55 | return emitEvent('safe_area_changed', { 56 | left: 0, 57 | top: 0, 58 | right: 0, 59 | bottom: 0, 60 | }); 61 | } 62 | 63 | next(); 64 | }, 65 | }); 66 | } 67 | 68 | // Mount all components used in the project. 69 | backButton.mount(); 70 | initData.restore(); 71 | 72 | try { 73 | miniApp.mount(); 74 | themeParams.bindCssVars(); 75 | } catch (e) { 76 | // miniApp not available 77 | } 78 | 79 | try { 80 | viewport.mount().then(() => { 81 | viewport.bindCssVars(); 82 | }); 83 | } catch (e) { 84 | // viewport not available 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/css/classnames.ts: -------------------------------------------------------------------------------- 1 | export function isRecord(v: unknown): v is Record { 2 | return !!v && typeof v === 'object' && !Array.isArray(v); 3 | } 4 | 5 | /** 6 | * Function which joins passed values with space following these rules: 7 | * 1. If value is non-empty string, it will be added to output. 8 | * 2. If value is object, only those keys will be added, which values are truthy. 9 | * 3. If value is array, classNames will be called with this value spread. 10 | * 4. All other values are ignored. 11 | * 12 | * You can find this function to similar one from the package {@link https://www.npmjs.com/package/classnames|classnames}. 13 | * @param values - values array. 14 | * @returns Final class name. 15 | */ 16 | export function classNames(...values: any[]): string { 17 | return values 18 | .map((value) => { 19 | if (typeof value === 'string') { 20 | return value; 21 | } 22 | 23 | if (isRecord(value)) { 24 | return classNames( 25 | Object.entries(value).map((entry) => entry[1] && entry[0]), 26 | ); 27 | } 28 | 29 | if (Array.isArray(value)) { 30 | return classNames(...value); 31 | } 32 | }) 33 | .filter(Boolean) 34 | .join(' '); 35 | } 36 | 37 | type UnionStringKeys = U extends U 38 | ? { [K in keyof U]-?: U[K] extends string | undefined ? K : never }[keyof U] 39 | : never; 40 | 41 | type UnionRequiredKeys = U extends U 42 | ? { 43 | [K in UnionStringKeys]: object extends Pick ? never : K; 44 | }[UnionStringKeys] 45 | : never; 46 | 47 | type UnionOptionalKeys = Exclude, UnionRequiredKeys>; 48 | 49 | export type MergeClassNames = 50 | // Removes all types from union that will be ignored by the mergeClassNames function. 51 | Exclude< 52 | Tuple[number], 53 | number | string | null | undefined | any[] | boolean 54 | > extends infer Union 55 | ? { [K in UnionRequiredKeys]: string } & { 56 | [K in UnionOptionalKeys]?: string; 57 | } 58 | : never; 59 | 60 | /** 61 | * Merges two sets of classnames. 62 | * 63 | * The function expects to pass an array of objects with values that could be passed to 64 | * the `classNames` function. 65 | * @returns An object with keys from all objects with merged values. 66 | * @see classNames 67 | */ 68 | export function mergeClassNames( 69 | ...partials: T 70 | ): MergeClassNames { 71 | return partials.reduce>((acc, partial) => { 72 | if (isRecord(partial)) { 73 | Object.entries(partial).forEach(([key, value]) => { 74 | const className = classNames((acc as any)[key], value); 75 | if (className) { 76 | (acc as any)[key] = className; 77 | } 78 | }); 79 | } 80 | return acc; 81 | }, {} as MergeClassNames); 82 | } 83 | -------------------------------------------------------------------------------- /src/app/init-data/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 'use client'; 3 | 4 | import { useMemo } from 'react'; 5 | import { 6 | initData, 7 | type User, 8 | useSignal, 9 | useRawInitData, 10 | } from '@tma.js/sdk-react'; 11 | import { List, Placeholder } from '@telegram-apps/telegram-ui'; 12 | 13 | import { 14 | DisplayData, 15 | type DisplayDataRow, 16 | } from '@/components/DisplayData/DisplayData'; 17 | import { Page } from '@/components/Page'; 18 | 19 | function getUserRows(user: User): DisplayDataRow[] { 20 | return Object.entries(user).map(([title, value]) => ({ title, value })); 21 | } 22 | 23 | export default function InitDataPage() { 24 | const initDataRaw = useRawInitData(); 25 | const initDataState = useSignal(initData.state); 26 | 27 | const initDataRows = useMemo(() => { 28 | if (!initDataState || !initDataRaw) { 29 | return; 30 | } 31 | return [ 32 | { title: 'raw', value: initDataRaw }, 33 | ...Object.entries(initDataState).reduce( 34 | (acc, [title, value]) => { 35 | if (value instanceof Date) { 36 | acc.push({ title, value: value.toISOString() }); 37 | } else if (!value || typeof value !== 'object') { 38 | acc.push({ title, value }); 39 | } 40 | return acc; 41 | }, 42 | [], 43 | ), 44 | ]; 45 | }, [initDataState, initDataRaw]); 46 | 47 | const userRows = useMemo(() => { 48 | return initDataState && initDataState.user 49 | ? getUserRows(initDataState.user) 50 | : undefined; 51 | }, [initDataState]); 52 | 53 | const receiverRows = useMemo(() => { 54 | return initDataState && initDataState.receiver 55 | ? getUserRows(initDataState.receiver) 56 | : undefined; 57 | }, [initDataState]); 58 | 59 | const chatRows = useMemo(() => { 60 | return !initDataState?.chat 61 | ? undefined 62 | : Object.entries(initDataState.chat).map(([title, value]) => ({ 63 | title, 64 | value, 65 | })); 66 | }, [initDataState]); 67 | 68 | if (!initDataRows) { 69 | return ( 70 | 71 | 75 | Telegram sticker 80 | 81 | 82 | ); 83 | } 84 | return ( 85 | 86 | 87 | 88 | {userRows && } 89 | {receiverRows && ( 90 | 91 | )} 92 | {chatRows && } 93 | 94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/app/ton-connect/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { openLink } from '@tma.js/sdk-react'; 4 | import { TonConnectButton, useTonWallet } from '@tonconnect/ui-react'; 5 | import { 6 | Avatar, 7 | Cell, 8 | List, 9 | Navigation, 10 | Placeholder, 11 | Section, 12 | Text, 13 | Title, 14 | } from '@telegram-apps/telegram-ui'; 15 | 16 | import { DisplayData } from '@/components/DisplayData/DisplayData'; 17 | import { Page } from '@/components/Page'; 18 | import { bem } from '@/css/bem'; 19 | 20 | import './TONConnectPage.css'; 21 | 22 | const [, e] = bem('ton-connect-page'); 23 | 24 | export default function TONConnectPage() { 25 | const wallet = useTonWallet(); 26 | 27 | if (!wallet) { 28 | return ( 29 | 30 | 35 | 36 | To display the data related to the TON Connect, it is required 37 | to connect your wallet 38 | 39 | 40 | 41 | } 42 | /> 43 | 44 | ); 45 | } 46 | 47 | const { 48 | account: { chain, publicKey, address }, 49 | device: { appName, appVersion, maxProtocolVersion, platform, features }, 50 | } = wallet; 51 | 52 | return ( 53 | 54 | 55 | {'imageUrl' in wallet && ( 56 | <> 57 |
58 | 66 | } 67 | after={About wallet} 68 | subtitle={wallet.appName} 69 | onClick={(e) => { 70 | e.preventDefault(); 71 | openLink(wallet.aboutUrl); 72 | }} 73 | > 74 | {wallet.name} 75 | 76 |
77 | 78 | 79 | )} 80 | 88 | (typeof f === 'object' ? f.name : undefined)) 99 | .filter((v) => v) 100 | .join(', '), 101 | }, 102 | ]} 103 | /> 104 |
105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/mockEnv.ts: -------------------------------------------------------------------------------- 1 | import { mockTelegramEnv, isTMA, emitEvent } from '@tma.js/sdk-react'; 2 | 3 | // It is important, to mock the environment only for development purposes. When building the 4 | // application, the code inside will be tree-shaken, so you will not see it in your final bundle. 5 | export async function mockEnv(): Promise { 6 | return process.env.NODE_ENV !== 'development' 7 | ? undefined 8 | : isTMA('complete').then((isTma) => { 9 | if (!isTma){ 10 | const themeParams = { 11 | accent_text_color: '#6ab2f2', 12 | bg_color: '#17212b', 13 | button_color: '#5288c1', 14 | button_text_color: '#ffffff', 15 | destructive_text_color: '#ec3942', 16 | header_bg_color: '#17212b', 17 | hint_color: '#708499', 18 | link_color: '#6ab3f3', 19 | secondary_bg_color: '#232e3c', 20 | section_bg_color: '#17212b', 21 | section_header_text_color: '#6ab3f3', 22 | subtitle_text_color: '#708499', 23 | text_color: '#f5f5f5', 24 | } as const; 25 | const noInsets = { left: 0, top: 0, bottom: 0, right: 0 } as const; 26 | 27 | mockTelegramEnv({ 28 | onEvent(e, next) { 29 | // Here you can write your own handlers for all known Telegram Mini Apps methods. 30 | if (e.name === 'web_app_request_theme') { 31 | return emitEvent('theme_changed', { theme_params: themeParams as any }); 32 | } 33 | if (e.name === 'web_app_request_viewport') { 34 | return emitEvent('viewport_changed', { 35 | height: window.innerHeight, 36 | width: window.innerWidth, 37 | is_expanded: true, 38 | is_state_stable: true, 39 | }); 40 | } 41 | if (e.name === 'web_app_request_content_safe_area') { 42 | return emitEvent('content_safe_area_changed', noInsets); 43 | } 44 | if (e.name === 'web_app_request_safe_area') { 45 | return emitEvent('safe_area_changed', noInsets); 46 | } 47 | next(); 48 | }, 49 | launchParams: new URLSearchParams([ 50 | // Discover more launch parameters: 51 | // https://docs.telegram-mini-apps.com/platform/launch-parameters#parameters-list 52 | ['tgWebAppThemeParams', JSON.stringify(themeParams)], 53 | // Your init data goes here. Learn more about it here: 54 | // https://docs.telegram-mini-apps.com/platform/init-data#parameters-list 55 | // 56 | // Note that to make sure, you are using a valid init data, you must pass it exactly as it 57 | // is sent from the Telegram application. The reason is in case you will sort its keys 58 | // (auth_date, hash, user, etc.) or values your own way, init data validation will more 59 | // likely to fail on your server side. So, to make sure you are working with a valid init 60 | // data, it is better to take a real one from your application and paste it here. It should 61 | // look something like this (a correctly encoded URL search params): 62 | // ``` 63 | // user=%7B%22id%22%3A279058397%2C%22first_name%22%3A%22Vladislav%22%2C%22last_name%22... 64 | // ``` 65 | // But in case you don't really need a valid init data, use this one: 66 | ['tgWebAppData', new URLSearchParams([ 67 | ['auth_date', (new Date().getTime() / 1000 | 0).toString()], 68 | ['hash', 'some-hash'], 69 | ['signature', 'some-signature'], 70 | ['user', JSON.stringify({ id: 1, first_name: 'Vladislav' })], 71 | ]).toString()], 72 | ['tgWebAppVersion', '8.4'], 73 | ['tgWebAppPlatform', 'tdesktop'], 74 | ]), 75 | }); 76 | 77 | console.info( 78 | '⚠️ 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.', 79 | ); 80 | } 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Mini Apps Next.js Template 2 | 3 | This template demonstrates how developers can implement a web application on the 4 | Telegram Mini Apps platform using the following technologies and libraries: 5 | 6 | - [Next.js](https://nextjs.org/) 7 | - [TypeScript](https://www.typescriptlang.org/) 8 | - [TON Connect](https://docs.ton.org/develop/dapps/ton-connect/overview) 9 | - [@telegram-apps SDK](https://docs.telegram-mini-apps.com/packages/telegram-apps-sdk/2-x) 10 | - [Telegram UI](https://github.com/Telegram-Mini-Apps/TelegramUI) 11 | 12 | > The template was created using [pnpm](https://pnpm.io/). Therefore, it is 13 | > required to use it for this project as well. Using other package managers, you 14 | > will receive a corresponding error. 15 | 16 | ## Install Dependencies 17 | 18 | If you have just cloned this template, you should install the project 19 | dependencies using the command: 20 | 21 | ```Bash 22 | pnpm install 23 | ``` 24 | 25 | ## Scripts 26 | 27 | This project contains the following scripts: 28 | 29 | - `dev`. Runs the application in development mode. 30 | - `dev:https`. Runs the application in development mode using self-signed SSL 31 | certificate. 32 | - `build`. Builds the application for production. 33 | - `start`. Starts the Next.js server in production mode. 34 | - `lint`. Runs [eslint](https://eslint.org/) to ensure the code quality meets 35 | the required 36 | standards. 37 | 38 | To run a script, use the `pnpm run` command: 39 | 40 | ```Bash 41 | pnpm run {script} 42 | # Example: pnpm run build 43 | ``` 44 | 45 | ## Create Bot and Mini App 46 | 47 | Before you start, make sure you have already created a Telegram Bot. Here is 48 | a [comprehensive guide](https://docs.telegram-mini-apps.com/platform/creating-new-app) 49 | on how to do it. 50 | 51 | ## Run 52 | 53 | Although Mini Apps are designed to be opened 54 | within [Telegram applications](https://docs.telegram-mini-apps.com/platform/about#supported-applications), 55 | you can still develop and test them outside of Telegram during the development 56 | process. 57 | 58 | To run the application in the development mode, use the `dev` script: 59 | 60 | ```bash 61 | pnpm run dev 62 | ``` 63 | 64 | After this, you will see a similar message in your terminal: 65 | 66 | ```bash 67 | ▲ Next.js 14.2.3 68 | - Local: http://localhost:3000 69 | 70 | ✓ Starting... 71 | ✓ Ready in 2.9s 72 | ``` 73 | 74 | To view the application, you need to open the `Local` 75 | link (`http://localhost:3000` in this example) in your browser. 76 | 77 | It is important to note that some libraries in this template, such as 78 | `@telegram-apps/sdk`, are not intended for use outside of Telegram. 79 | 80 | Nevertheless, they appear to function properly. This is because the 81 | `src/hooks/useTelegramMock.ts` file, which is imported in the application's 82 | `Root` component, employs the `mockTelegramEnv` function to simulate the 83 | Telegram environment. This trick convinces the application that it is 84 | running in a Telegram-based environment. Therefore, be cautious not to use this 85 | function in production mode unless you fully understand its implications. 86 | 87 | ### Run Inside Telegram 88 | 89 | Although it is possible to run the application outside of Telegram, it is 90 | recommended to develop it within Telegram for the most accurate representation 91 | of its real-world functionality. 92 | 93 | To run the application inside Telegram, [@BotFather](https://t.me/botfather) 94 | requires an HTTPS link. 95 | 96 | This template already provides a solution. 97 | 98 | To retrieve a link with the HTTPS protocol, consider using the `dev:https` 99 | script: 100 | 101 | ```bash 102 | $ pnpm run dev:https 103 | 104 | ▲ Next.js 14.2.3 105 | - Local: https://localhost:3000 106 | 107 | ✓ Starting... 108 | ✓ Ready in 2.4s 109 | ``` 110 | 111 | Visiting the `Local` link (`https://localhost:3000` in this example) in your 112 | browser, you will see the following warning: 113 | 114 | ![SSL Warning](assets/ssl-warning.png) 115 | 116 | This browser warning is normal and can be safely ignored as long as the site is 117 | secure. Click the `Proceed to localhost (unsafe)` button to continue and view 118 | the application. 119 | 120 | Once the application is displayed correctly, submit the 121 | link `https://127.0.0.1:3000` (`https://localhost:3000` is considered as invalid 122 | by BotFather) as the Mini App link to [@BotFather](https://t.me/botfather). 123 | Then, navigate to [https://web.telegram.org/k/](https://web.telegram.org/k/), 124 | find your bot, and launch the Telegram Mini App. This approach provides the full 125 | development experience. 126 | 127 | ## Deploy 128 | 129 | The easiest way to deploy your Next.js app is to use 130 | the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) 131 | from the creators of Next.js. 132 | 133 | Check out 134 | the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for 135 | more details. 136 | 137 | ## Useful Links 138 | 139 | - [Platform documentation](https://docs.telegram-mini-apps.com/) 140 | - [@telegram-apps/sdk-react documentation](https://docs.telegram-mini-apps.com/packages/telegram-apps-sdk-react) 141 | - [Telegram developers community chat](https://t.me/devs) --------------------------------------------------------------------------------