Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
ANALYZE=false # true | false
# Set the Node.js environment (production: for live deployment, development: for local development)
NODE_ENV=production

NODE_ENV=production # production | development
# Enable or disable bundle analysis (true/false)
ANALYZE=false

# Open bundle analyzer automatically after build (true/false)
OPEN_ANALYZER=false

# Set the mode for bundle analyzer output (static: HTML file, json: JSON file)
ANALYZER_MODE=static
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# testing
/coverage
/analyze

# next.js
/.next/
Expand Down
7 changes: 4 additions & 3 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import bundleAnalyzer from '@next/bundle-analyzer'
import { env } from './src/env.mjs'

const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: false,
analyzerMode: 'static',
enabled: env.ANALYZE,
openAnalyzer: env.OPEN_ANALYZER,
analyzerMode: env.ANALYZER_MODE,
})

/**
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/compat": "^1.2.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.13.0",
"@next/bundle-analyzer": "^14.2.15",
"@next/eslint-plugin-next": "^14.2.15",
"@t3-oss/env-nextjs": "^0.11.1",
"@types/node": "^22.7.6",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
Expand Down
36 changes: 36 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ const RootLayout: React.FC<PropsWithChildren> = ({ children }) => {
className={cn(
GeistSans.variable,
GeistMono.variable,
'top-[-25px] antialiased',
'bg-black text-white antialiased',
)}
>
<body className='bg-black text-white'>{children}</body>
<body>{children}</body>
</html>
)
}
Expand Down
26 changes: 26 additions & 0 deletions src/app/tma__/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type React from 'react'
import type { PropsWithChildren } from 'react'
import type { Metadata, Viewport } from 'next'

import { Fragment } from 'react'
import { TelegramMiniAppRegister } from '~/src/features'

export const metadata: Metadata = {
//
}

export const viewport: Viewport = {
colorScheme: 'dark',
userScalable: false,
}

const TelegramWebAppsLayout: React.FC<PropsWithChildren> = ({ children }) => {
return (
<Fragment>
<TelegramMiniAppRegister>{children}</TelegramMiniAppRegister>
</Fragment>
)
}
TelegramWebAppsLayout.displayName = 'Root layout for telegram web app'

export default TelegramWebAppsLayout
25 changes: 25 additions & 0 deletions src/app/tma__/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type React from 'react'

import { cn } from '#utils'

const Homepage: React.FC = () => {
return (
<section className='grid min-h-dvh place-content-center'>
<span
className={cn(
'bg-clip-text text-2xl font-semibold text-transparent',
'bg-gradient-to-r from-white via-slate-500 to-white',
'animate-[shimmer-bg_2s_infinite] from-40% to-60%',
)}
style={{
backgroundSize: '300% 100%',
backgroundPositionX: '100%',
}}
>
Goodbye World!
</span>
</section>
)
}

export default Homepage
13 changes: 13 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* This file exports all components from the #components directory.
* It serves as a central point for importing components throughout the application.
* When adding new components, make sure to export them here for easy access.
*
* Example usage:
* Instead of:
* import { Button } from '#components/Button'
* import { Input } from '#components/Input'
*
* You can now use:
* import { Button, Input } from '#components'
*/
49 changes: 49 additions & 0 deletions src/env.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'

export const env = createEnv({
/*
* Server-side Environment variables, not available on the client.
* Will throw if you access these variables on the client.
*/
server: {
NODE_ENV: z.enum(['development', 'production']),

ANALYZE: z.string().transform(v => v === 'true'),
OPEN_ANALYZER: z.string().transform(v => v === 'true'),
ANALYZER_MODE: z.enum(['static', 'json']),
},

/*
* Environment variables available on the client (and server).
*/
client: {
NEXT_PUBLIC_NODE_ENV: z.enum(['development', 'production']),
},

/*
* Due to how Next.js bundles environment variables on Edge and Client,
* we need to manually destructure them to make sure all are included in bundle.
*/
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,

ANALYZE: process.env.ANALYZE,
OPEN_ANALYZER: process.env.OPEN_ANALYZER,
ANALYZER_MODE: process.env.ANALYZER_MODE,
},

/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
/**
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
* `SOME_VAR=''` will throw an error.
*/
emptyStringAsUndefined: true,
})

export default env
Empty file.
Empty file.
Empty file.
66 changes: 66 additions & 0 deletions src/features/TelegramWebApp/register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client'

import type React from 'react'
import type { PropsWithChildren } from 'react'

import env from '#env'
import { Fragment } from 'react'
import { init } from '@/init'
import {
initDataUser,
useLaunchParams,
useSignal,
} from '@telegram-apps/sdk-react'
import { useClientOnce, useDidMount, useTelegramMock } from '#hooks'
import { cn } from '#utils'

const TelegramMiniApp: React.FC<PropsWithChildren> = ({ children }) => {
const isDev = env.NEXT_PUBLIC_NODE_ENV === 'development'

if (isDev) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useTelegramMock()
}

const lp = useLaunchParams()
const debug = isDev || lp.startParam === 'debug'

useClientOnce(() => {
init(debug)
})

const client = useSignal(initDataUser)

return (
<Fragment>
<section className='fixed left-0 top-0 bg-black/75 backdrop-blur-sm'>
<pre className='font-mono text-white'>
{JSON.stringify(client, null, 2)}
</pre>
</section>

{children}
</Fragment>
)
}

const TelegramMiniAppLoader: React.FC = () => {
return (
<div
className={cn(
'size-20 animate-spin rounded-full',
'border-4 border-white border-t-transparent',
)}
/>
)
}

export const TelegramMiniAppRegister: React.FC<PropsWithChildren> = props => {
const isMounted = useDidMount()

if (!isMounted) {
return <TelegramMiniAppLoader />
}

return <TelegramMiniApp {...props} />
}
15 changes: 15 additions & 0 deletions src/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* This file exports all features from the #features directory.
* It serves as a central point for importing features throughout the application.
* When adding new features, make sure to export them here for easy access.
*
* Example usage:
* Instead of:
* import { TelegramMiniAppRegister } from '#features/Button'
* import { SomeFeatureRegister } from '#features/Input'
*
* You can now use:
* import { TelegramMiniAppRegister, SomeFeatureRegister } from '#features'
*/

export { TelegramMiniAppRegister } from '#features/TelegramWebApp/register'
17 changes: 17 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* This file exports all functions from the #hooks directory.
* It serves as a central point for importing hook functions throughout the application.
* When adding new hooks, make sure to export them here for easy access.
*
* Example usage:
* Instead of:
* import { useDidMount } from '#hooks/useDidMount'
* import { useSomeFn } from '#hooks/useSomeFn'
*
* You can now use:
* import { useDidMount, useSomeFn } from '#hooks'
*/

export { useTelegramMock } from '#hooks/useTelegramMock'
export { useClientOnce } from '#hooks/useClientOnce'
export { useDidMount } from '#hooks/useDidMount'
34 changes: 34 additions & 0 deletions src/hooks/useClientOnce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useRef } from 'react'

type UseClientOnceFn = () => void

/**
* A custom React hook that ensures a function is called only once on the client side.
*
* @param {UseClientOnceFn} fn - The function to be executed once on the client side.
*
* @example ```ts
* useClientOnce(() => {
* console.log('This will be logged only once on the client side');
* });
* ```
* @see https://github.com/Telegram-Mini-Apps/telegram-apps/blob/master/playgrounds/next/src/hooks/useClientOnce.ts
*/
export function useClientOnce(fn: UseClientOnceFn): void {
const hasRun = useRef(false)

/**
* Confirms that the code is running in a browser environment (`typeof window !== 'undefined`),
* which prevents execution on the server side.
*
* By checking outside React lifecycle hooks (like `useEffect`), the hook minimizes unnecessary renders
* and allows `fn` to execute immediately on component initialization, but only once in the client environment.
* The `hasRun` ref ensures `fn` is only called once per component lifecycle.
*/
if (typeof window !== 'undefined' && !hasRun.current) {
hasRun.current = true
fn()
}

return
}
Loading