nuxt-telegram-mini-app 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/.vscode/settings.json +55 -0
- package/.vscode/tailwind.json +55 -0
- package/CONTRIBUTING.md +406 -0
- package/LICENSE +21 -0
- package/README.md +640 -0
- package/app/app.vue +87 -0
- package/app/assets/css/main.css +53 -0
- package/app/assets/css/tailwind.css +3 -0
- package/app/components/ErrorBoundary.vue +81 -0
- package/app/components/Hero.vue +61 -0
- package/app/components/tg/Button.vue +128 -0
- package/app/components/tg/Cell.vue +91 -0
- package/app/components/tg/Content.vue +42 -0
- package/app/components/tg/Nav.vue +107 -0
- package/app/components/tg/Section.vue +50 -0
- package/app/composables/telegram.ts +342 -0
- package/app/error.vue +161 -0
- package/app/pages/components.vue +279 -0
- package/app/pages/functions.vue +107 -0
- package/app/pages/index.vue +211 -0
- package/app/pages/utilities.vue +402 -0
- package/app/types/telegram-webapp.ts +160 -0
- package/app/utils/color.ts +37 -0
- package/eslint.config.mjs +6 -0
- package/nuxt.config.ts +55 -0
- package/package.json +46 -0
- package/public/_redirects +2 -0
- package/public/favicon.ico +0 -0
- package/public/img/hero-user.svg +8 -0
- package/public/img/nuxt-logo.svg +11 -0
- package/public/robots.txt +2 -0
- package/server/api/verify-telegram-data.post.ts +150 -0
- package/tailwind.config.ts +39 -0
- package/tests/components.spec.ts +311 -0
- package/tests/pages.spec.ts +426 -0
- package/tests/telegram.spec.ts +105 -0
- package/tests/utils.spec.ts +47 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TgContent>
|
|
3
|
+
<Hero
|
|
4
|
+
title="Utilities Demo"
|
|
5
|
+
/>
|
|
6
|
+
|
|
7
|
+
<TgSection title="Link Utilities" inset>
|
|
8
|
+
<div class="p-4 space-y-3">
|
|
9
|
+
<div class="space-y-2">
|
|
10
|
+
<h3 class="text-sm font-medium text-subtitle">External Links</h3>
|
|
11
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
12
|
+
<TgButton
|
|
13
|
+
title="Open GitHub"
|
|
14
|
+
status="outline"
|
|
15
|
+
haptic
|
|
16
|
+
@click="() => openLink('https://github.com/patricktobias86/nuxt-telegram-mini-app')"
|
|
17
|
+
/>
|
|
18
|
+
<TgButton
|
|
19
|
+
title="Open Nuxt Docs"
|
|
20
|
+
status="outline"
|
|
21
|
+
haptic
|
|
22
|
+
@click="() => openLink('https://nuxt.com')"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="space-y-2">
|
|
28
|
+
<h3 class="text-sm font-medium text-subtitle">Telegram Links</h3>
|
|
29
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
30
|
+
<TgButton
|
|
31
|
+
title="Telegram Channel"
|
|
32
|
+
status="outline"
|
|
33
|
+
haptic
|
|
34
|
+
@click="() => openTelegramLink('https://t.me/telegram')"
|
|
35
|
+
/>
|
|
36
|
+
<TgButton
|
|
37
|
+
title="Bot Platform"
|
|
38
|
+
status="outline"
|
|
39
|
+
haptic
|
|
40
|
+
@click="() => openTelegramLink('https://t.me/botfather')"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="space-y-2">
|
|
46
|
+
<h3 class="text-sm font-medium text-subtitle">Sharing</h3>
|
|
47
|
+
<div class="grid grid-cols-1 gap-2">
|
|
48
|
+
<TgButton
|
|
49
|
+
title="Share This App"
|
|
50
|
+
status="primary"
|
|
51
|
+
haptic="notification-success"
|
|
52
|
+
@click="() => shareURL(shareUrl)"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</TgSection>
|
|
58
|
+
|
|
59
|
+
<TgSection title="Data Validation" inset>
|
|
60
|
+
<div class="p-4 space-y-4">
|
|
61
|
+
<div class="space-y-2">
|
|
62
|
+
<h3 class="text-sm font-medium text-subtitle">Init Data Status</h3>
|
|
63
|
+
<div class="space-y-2">
|
|
64
|
+
<div class="flex items-center justify-between p-3 bg-secondary-bg rounded-lg">
|
|
65
|
+
<span class="text-sm">Data Available</span>
|
|
66
|
+
<div class="flex items-center gap-2">
|
|
67
|
+
<div :class="[
|
|
68
|
+
'w-2 h-2 rounded-full',
|
|
69
|
+
initDataAvailable ? 'bg-green-500' : 'bg-red-500'
|
|
70
|
+
]" />
|
|
71
|
+
<span class="text-xs text-hint">{{ initDataAvailable ? 'Yes' : 'No' }}</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="flex items-center justify-between p-3 bg-secondary-bg rounded-lg">
|
|
76
|
+
<span class="text-sm">User Identified</span>
|
|
77
|
+
<div class="flex items-center gap-2">
|
|
78
|
+
<div :class="[
|
|
79
|
+
'w-2 h-2 rounded-full',
|
|
80
|
+
userIdentified ? 'bg-green-500' : 'bg-red-500'
|
|
81
|
+
]" />
|
|
82
|
+
<span class="text-xs text-hint">{{ userIdentified ? 'Yes' : 'No' }}</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="flex items-center justify-between p-3 bg-secondary-bg rounded-lg">
|
|
87
|
+
<span class="text-sm">Raw Data Length</span>
|
|
88
|
+
<span class="text-xs text-hint">{{ rawDataLength }} chars</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="space-y-2">
|
|
94
|
+
<h3 class="text-sm font-medium text-subtitle">User Information</h3>
|
|
95
|
+
<div class="space-y-2 text-sm">
|
|
96
|
+
<div v-if="userData.username" class="flex justify-between p-2 bg-secondary-bg rounded">
|
|
97
|
+
<span class="text-hint">Username:</span>
|
|
98
|
+
<span>@{{ userData.username }}</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div v-if="userData.firstName" class="flex justify-between p-2 bg-secondary-bg rounded">
|
|
101
|
+
<span class="text-hint">First Name:</span>
|
|
102
|
+
<span>{{ userData.firstName }}</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div v-if="userData.lastName" class="flex justify-between p-2 bg-secondary-bg rounded">
|
|
105
|
+
<span class="text-hint">Last Name:</span>
|
|
106
|
+
<span>{{ userData.lastName }}</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div v-if="userData.languageCode" class="flex justify-between p-2 bg-secondary-bg rounded">
|
|
109
|
+
<span class="text-hint">Language:</span>
|
|
110
|
+
<span>{{ userData.languageCode }}</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div v-if="!userData.username && !userData.firstName" class="p-2 bg-secondary-bg rounded text-center text-hint">
|
|
113
|
+
No user data available (not in Telegram context)
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</TgSection>
|
|
119
|
+
|
|
120
|
+
<TgSection title="Color Utilities" inset>
|
|
121
|
+
<div class="p-4 space-y-4">
|
|
122
|
+
<div class="space-y-2">
|
|
123
|
+
<h3 class="text-sm font-medium text-subtitle">Color Converter</h3>
|
|
124
|
+
<div class="space-y-3">
|
|
125
|
+
<input
|
|
126
|
+
v-model="testColor"
|
|
127
|
+
type="text"
|
|
128
|
+
placeholder="Enter a color (hex, rgb, hsl, or name)"
|
|
129
|
+
class="w-full px-3 py-2 bg-secondary-bg border border-section-separator rounded-lg text-text placeholder-hint"
|
|
130
|
+
/>
|
|
131
|
+
<div class="flex items-center justify-between p-3 bg-secondary-bg rounded-lg">
|
|
132
|
+
<div class="flex items-center gap-2">
|
|
133
|
+
<div
|
|
134
|
+
class="w-6 h-6 rounded border border-section-separator"
|
|
135
|
+
:style="{ backgroundColor: convertedColor }"
|
|
136
|
+
/>
|
|
137
|
+
<span class="text-sm">Converted:</span>
|
|
138
|
+
</div>
|
|
139
|
+
<span class="text-xs font-mono">{{ convertedColor }}</span>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="space-y-2">
|
|
145
|
+
<h3 class="text-sm font-medium text-subtitle">Theme Color Analysis</h3>
|
|
146
|
+
<div class="grid grid-cols-1 gap-2 text-xs">
|
|
147
|
+
<div v-for="color in colorAnalysis" :key="color.name"
|
|
148
|
+
class="flex items-center justify-between p-2 bg-secondary-bg rounded">
|
|
149
|
+
<div class="flex items-center gap-2">
|
|
150
|
+
<div
|
|
151
|
+
class="w-4 h-4 rounded border border-section-separator"
|
|
152
|
+
:style="{ backgroundColor: color.hex }"
|
|
153
|
+
/>
|
|
154
|
+
<span>{{ color.name }}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<span class="font-mono">{{ color.hex }}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</TgSection>
|
|
162
|
+
|
|
163
|
+
<TgSection title="Performance Monitoring" inset>
|
|
164
|
+
<div class="p-4 space-y-4">
|
|
165
|
+
<div class="space-y-2">
|
|
166
|
+
<h3 class="text-sm font-medium text-subtitle">App Performance</h3>
|
|
167
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
168
|
+
<div class="p-3 bg-secondary-bg rounded-lg">
|
|
169
|
+
<div class="text-xs text-hint mb-1">Viewport Size</div>
|
|
170
|
+
<div class="text-sm font-medium">{{ viewportInfo }}</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="p-3 bg-secondary-bg rounded-lg">
|
|
173
|
+
<div class="text-xs text-hint mb-1">Device Pixel Ratio</div>
|
|
174
|
+
<div class="text-sm font-medium">{{ devicePixelRatio }}</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="p-3 bg-secondary-bg rounded-lg">
|
|
177
|
+
<div class="text-xs text-hint mb-1">Connection</div>
|
|
178
|
+
<div class="text-sm font-medium">{{ connectionType }}</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="p-3 bg-secondary-bg rounded-lg">
|
|
181
|
+
<div class="text-xs text-hint mb-1">Memory Usage</div>
|
|
182
|
+
<div class="text-sm font-medium">{{ memoryInfo }}</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="space-y-2">
|
|
188
|
+
<h3 class="text-sm font-medium text-subtitle">Render Performance</h3>
|
|
189
|
+
<div class="space-y-2">
|
|
190
|
+
<div class="flex justify-between items-center">
|
|
191
|
+
<span class="text-sm">Render Count</span>
|
|
192
|
+
<span class="text-xs text-hint">{{ renderCount }}</span>
|
|
193
|
+
</div>
|
|
194
|
+
<TgButton
|
|
195
|
+
title="Force Re-render"
|
|
196
|
+
status="outline"
|
|
197
|
+
haptic="impact-light"
|
|
198
|
+
@click="forceRerender"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</TgSection>
|
|
204
|
+
|
|
205
|
+
<TgSection title="Local Storage Demo" inset>
|
|
206
|
+
<div class="p-4 space-y-4">
|
|
207
|
+
<div class="space-y-2">
|
|
208
|
+
<h3 class="text-sm font-medium text-subtitle">Storage Operations</h3>
|
|
209
|
+
<div class="space-y-3">
|
|
210
|
+
<div class="flex gap-2">
|
|
211
|
+
<input
|
|
212
|
+
v-model="storageKey"
|
|
213
|
+
type="text"
|
|
214
|
+
placeholder="Storage key"
|
|
215
|
+
class="flex-1 px-3 py-2 bg-secondary-bg border border-section-separator rounded-lg text-text placeholder-hint"
|
|
216
|
+
/>
|
|
217
|
+
<input
|
|
218
|
+
v-model="storageValue"
|
|
219
|
+
type="text"
|
|
220
|
+
placeholder="Storage value"
|
|
221
|
+
class="flex-1 px-3 py-2 bg-secondary-bg border border-section-separator rounded-lg text-text placeholder-hint"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="grid grid-cols-2 gap-2">
|
|
225
|
+
<TgButton
|
|
226
|
+
title="Save"
|
|
227
|
+
status="primary"
|
|
228
|
+
haptic
|
|
229
|
+
:disabled="!storageKey || !storageValue"
|
|
230
|
+
@click="saveToStorage"
|
|
231
|
+
/>
|
|
232
|
+
<TgButton
|
|
233
|
+
title="Load"
|
|
234
|
+
status="outline"
|
|
235
|
+
haptic
|
|
236
|
+
:disabled="!storageKey"
|
|
237
|
+
@click="loadFromStorage"
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div class="space-y-2">
|
|
244
|
+
<h3 class="text-sm font-medium text-subtitle">Stored Data</h3>
|
|
245
|
+
<div class="p-3 bg-secondary-bg rounded-lg">
|
|
246
|
+
<pre class="text-xs text-hint whitespace-pre-wrap">{{ storedDataDisplay }}</pre>
|
|
247
|
+
</div>
|
|
248
|
+
<TgButton
|
|
249
|
+
title="Clear All Storage"
|
|
250
|
+
status="destructive"
|
|
251
|
+
haptic="notification-warning"
|
|
252
|
+
@click="clearStorage"
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</TgSection>
|
|
257
|
+
|
|
258
|
+
<div class="h-2"></div>
|
|
259
|
+
</TgContent>
|
|
260
|
+
|
|
261
|
+
</template>
|
|
262
|
+
|
|
263
|
+
<script setup lang="ts">
|
|
264
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
265
|
+
import { useRequestURL, navigateTo } from '#app'
|
|
266
|
+
import {
|
|
267
|
+
useInitData,
|
|
268
|
+
useThemeParams,
|
|
269
|
+
useViewport,
|
|
270
|
+
useMainButton,
|
|
271
|
+
openLink,
|
|
272
|
+
openTelegramLink,
|
|
273
|
+
shareURL
|
|
274
|
+
} from '~/composables/telegram'
|
|
275
|
+
import { toHex } from '~/utils/color'
|
|
276
|
+
|
|
277
|
+
const init = useInitData()
|
|
278
|
+
const theme = useThemeParams()
|
|
279
|
+
const viewport = useViewport()
|
|
280
|
+
const main = useMainButton()
|
|
281
|
+
|
|
282
|
+
// Sharing
|
|
283
|
+
const reqURL = useRequestURL()
|
|
284
|
+
const shareUrl = computed(() => import.meta.client ? window.location.href : reqURL.href)
|
|
285
|
+
|
|
286
|
+
// Data validation
|
|
287
|
+
const initDataAvailable = computed(() => !!init.state.value)
|
|
288
|
+
const userIdentified = computed(() => !!init.user.value?.id)
|
|
289
|
+
const rawDataLength = computed(() => init.raw.value?.length || 0)
|
|
290
|
+
|
|
291
|
+
const userData = computed(() => ({
|
|
292
|
+
username: init.user.value?.username || '',
|
|
293
|
+
firstName: init.user.value?.first_name || '',
|
|
294
|
+
lastName: init.user.value?.last_name || '',
|
|
295
|
+
languageCode: init.user.value?.language_code || ''
|
|
296
|
+
}))
|
|
297
|
+
|
|
298
|
+
// Color utilities
|
|
299
|
+
const testColor = ref('#0088cc')
|
|
300
|
+
const convertedColor = computed(() => toHex(testColor.value) || testColor.value)
|
|
301
|
+
|
|
302
|
+
const colorAnalysis = computed(() => [
|
|
303
|
+
{ name: 'Background', hex: toHex(theme.backgroundColor.value) || '—' },
|
|
304
|
+
{ name: 'Text', hex: toHex(theme.textColor.value) || '—' },
|
|
305
|
+
{ name: 'Button', hex: toHex(theme.buttonColor.value) || '—' },
|
|
306
|
+
{ name: 'Link', hex: toHex(theme.linkColor.value) || '—' },
|
|
307
|
+
{ name: 'Hint', hex: toHex(theme.hintColor.value) || '—' },
|
|
308
|
+
{ name: 'Secondary BG', hex: toHex(theme.secondaryBackgroundColor.value) || '—' }
|
|
309
|
+
])
|
|
310
|
+
|
|
311
|
+
// Performance monitoring
|
|
312
|
+
const renderCount = ref(0)
|
|
313
|
+
const viewportInfo = computed(() => `${viewport.width.value}×${viewport.height.value}`)
|
|
314
|
+
const devicePixelRatio = computed(() => import.meta.client ? window.devicePixelRatio : 1)
|
|
315
|
+
const connectionType = computed(() => {
|
|
316
|
+
if (!import.meta.client) return 'Unknown'
|
|
317
|
+
// @ts-ignore
|
|
318
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection
|
|
319
|
+
return connection?.effectiveType || 'Unknown'
|
|
320
|
+
})
|
|
321
|
+
const memoryInfo = computed(() => {
|
|
322
|
+
if (!import.meta.client) return 'Unknown'
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
const memory = performance.memory
|
|
325
|
+
if (memory) {
|
|
326
|
+
return `${Math.round(memory.usedJSHeapSize / 1024 / 1024)}MB`
|
|
327
|
+
}
|
|
328
|
+
return 'Unknown'
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
function forceRerender() {
|
|
332
|
+
renderCount.value++
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Storage demo
|
|
336
|
+
const storageKey = ref('demo-key')
|
|
337
|
+
const storageValue = ref('Hello World')
|
|
338
|
+
const storedData = ref<Record<string, string>>({})
|
|
339
|
+
|
|
340
|
+
const storedDataDisplay = computed(() => {
|
|
341
|
+
const data = Object.keys(storedData.value).length ? storedData.value : { 'No data': 'stored yet' }
|
|
342
|
+
return JSON.stringify(data, null, 2)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
function saveToStorage() {
|
|
346
|
+
if (!import.meta.client || !storageKey.value || !storageValue.value) return
|
|
347
|
+
try {
|
|
348
|
+
localStorage.setItem(storageKey.value, storageValue.value)
|
|
349
|
+
loadAllStoredData()
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('Storage error:', error)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function loadFromStorage() {
|
|
356
|
+
if (!import.meta.client || !storageKey.value) return
|
|
357
|
+
try {
|
|
358
|
+
const value = localStorage.getItem(storageKey.value)
|
|
359
|
+
if (value !== null) {
|
|
360
|
+
storageValue.value = value
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error('Storage error:', error)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function loadAllStoredData() {
|
|
368
|
+
if (!import.meta.client) return
|
|
369
|
+
const data: Record<string, string> = {}
|
|
370
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
371
|
+
const key = localStorage.key(i)
|
|
372
|
+
if (key) {
|
|
373
|
+
data[key] = localStorage.getItem(key) || ''
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
storedData.value = data
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function clearStorage() {
|
|
380
|
+
if (!import.meta.client) return
|
|
381
|
+
localStorage.clear()
|
|
382
|
+
storedData.value = {}
|
|
383
|
+
storageKey.value = 'demo-key'
|
|
384
|
+
storageValue.value = 'Hello World'
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Hide Main Button since we're using TgNav for navigation
|
|
388
|
+
onMounted(() => {
|
|
389
|
+
renderCount.value++
|
|
390
|
+
loadAllStoredData()
|
|
391
|
+
|
|
392
|
+
main.mount()
|
|
393
|
+
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
try {
|
|
396
|
+
main.setParams({ is_visible: false })
|
|
397
|
+
} catch (e) {
|
|
398
|
+
console.warn('Failed to hide main button:', e)
|
|
399
|
+
}
|
|
400
|
+
}, 500)
|
|
401
|
+
})
|
|
402
|
+
</script>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// TypeScript definitions for Telegram Web App
|
|
2
|
+
// Based on https://core.telegram.org/bots/webapps
|
|
3
|
+
|
|
4
|
+
export interface TelegramWebAppUser {
|
|
5
|
+
id: number
|
|
6
|
+
is_bot?: boolean
|
|
7
|
+
first_name: string
|
|
8
|
+
last_name?: string
|
|
9
|
+
username?: string
|
|
10
|
+
language_code?: string
|
|
11
|
+
is_premium?: boolean
|
|
12
|
+
added_to_attachment_menu?: boolean
|
|
13
|
+
allows_write_to_pm?: boolean
|
|
14
|
+
photo_url?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TelegramWebAppChat {
|
|
18
|
+
id: number
|
|
19
|
+
type: 'group' | 'supergroup' | 'channel'
|
|
20
|
+
title: string
|
|
21
|
+
username?: string
|
|
22
|
+
photo_url?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TelegramWebAppInitData {
|
|
26
|
+
query_id?: string
|
|
27
|
+
user?: TelegramWebAppUser
|
|
28
|
+
receiver?: TelegramWebAppUser
|
|
29
|
+
chat?: TelegramWebAppChat
|
|
30
|
+
chat_type?: 'sender' | 'private' | 'group' | 'supergroup' | 'channel'
|
|
31
|
+
chat_instance?: string
|
|
32
|
+
start_param?: string
|
|
33
|
+
can_send_after?: number
|
|
34
|
+
auth_date: number
|
|
35
|
+
hash: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TelegramWebAppThemeParams {
|
|
39
|
+
bg_color?: string
|
|
40
|
+
text_color?: string
|
|
41
|
+
hint_color?: string
|
|
42
|
+
link_color?: string
|
|
43
|
+
button_color?: string
|
|
44
|
+
button_text_color?: string
|
|
45
|
+
secondary_bg_color?: string
|
|
46
|
+
header_bg_color?: string
|
|
47
|
+
accent_text_color?: string
|
|
48
|
+
section_bg_color?: string
|
|
49
|
+
section_header_text_color?: string
|
|
50
|
+
subtitle_text_color?: string
|
|
51
|
+
destructive_text_color?: string
|
|
52
|
+
section_separator_color?: string
|
|
53
|
+
bottom_bar_bg_color?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TelegramWebAppBackButton {
|
|
57
|
+
isVisible: boolean
|
|
58
|
+
onClick(callback: () => void): void
|
|
59
|
+
offClick(callback: () => void): void
|
|
60
|
+
show(): void
|
|
61
|
+
hide(): void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TelegramWebAppMainButton {
|
|
65
|
+
text: string
|
|
66
|
+
color: string
|
|
67
|
+
textColor: string
|
|
68
|
+
isVisible: boolean
|
|
69
|
+
isActive: boolean
|
|
70
|
+
readonly isProgressVisible: boolean
|
|
71
|
+
setText(text: string): void
|
|
72
|
+
onClick(callback: () => void): void
|
|
73
|
+
offClick(callback: () => void): void
|
|
74
|
+
show(): void
|
|
75
|
+
hide(): void
|
|
76
|
+
enable(): void
|
|
77
|
+
disable(): void
|
|
78
|
+
showProgress(leaveActive?: boolean): void
|
|
79
|
+
hideProgress(): void
|
|
80
|
+
setParams(params: {
|
|
81
|
+
text?: string
|
|
82
|
+
color?: string
|
|
83
|
+
text_color?: string
|
|
84
|
+
is_active?: boolean
|
|
85
|
+
is_visible?: boolean
|
|
86
|
+
}): void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TelegramWebAppHapticFeedback {
|
|
90
|
+
impactOccurred(style: 'light' | 'medium' | 'heavy'): void
|
|
91
|
+
notificationOccurred(type: 'error' | 'success' | 'warning'): void
|
|
92
|
+
selectionChanged(): void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TelegramWebApp {
|
|
96
|
+
initData: string
|
|
97
|
+
initDataUnsafe: TelegramWebAppInitData
|
|
98
|
+
version: string
|
|
99
|
+
platform: string
|
|
100
|
+
colorScheme: 'light' | 'dark'
|
|
101
|
+
themeParams: TelegramWebAppThemeParams
|
|
102
|
+
isExpanded: boolean
|
|
103
|
+
viewportHeight: number
|
|
104
|
+
viewportStableHeight: number
|
|
105
|
+
headerColor: string
|
|
106
|
+
backgroundColor: string
|
|
107
|
+
bottomBarColor: string
|
|
108
|
+
isClosingConfirmationEnabled: boolean
|
|
109
|
+
isVerticalSwipesEnabled: boolean
|
|
110
|
+
BackButton: TelegramWebAppBackButton
|
|
111
|
+
MainButton: TelegramWebAppMainButton
|
|
112
|
+
HapticFeedback: TelegramWebAppHapticFeedback
|
|
113
|
+
|
|
114
|
+
isVersionAtLeast(version: string): boolean
|
|
115
|
+
setHeaderColor(color: string): void
|
|
116
|
+
setBackgroundColor(color: string): void
|
|
117
|
+
setBottomBarColor(color: string): void
|
|
118
|
+
enableClosingConfirmation(): void
|
|
119
|
+
disableClosingConfirmation(): void
|
|
120
|
+
enableVerticalSwipes(): void
|
|
121
|
+
disableVerticalSwipes(): void
|
|
122
|
+
onEvent(eventType: string, eventHandler: () => void): void
|
|
123
|
+
offEvent(eventType: string, eventHandler: () => void): void
|
|
124
|
+
sendData(data: string): void
|
|
125
|
+
switchInlineQuery(query: string, choose_chat_types?: string[]): void
|
|
126
|
+
openLink(url: string, options?: { try_instant_view?: boolean }): void
|
|
127
|
+
openTelegramLink(url: string): void
|
|
128
|
+
openInvoice(url: string, callback?: (status: string) => void): void
|
|
129
|
+
showPopup(params: {
|
|
130
|
+
title?: string
|
|
131
|
+
message: string
|
|
132
|
+
buttons?: Array<{
|
|
133
|
+
id?: string
|
|
134
|
+
type?: 'default' | 'ok' | 'close' | 'cancel' | 'destructive'
|
|
135
|
+
text: string
|
|
136
|
+
}>
|
|
137
|
+
}, callback?: (buttonId: string) => void): void
|
|
138
|
+
showAlert(message: string, callback?: () => void): void
|
|
139
|
+
showConfirm(message: string, callback?: (confirmed: boolean) => void): void
|
|
140
|
+
showScanQrPopup(params: {
|
|
141
|
+
text?: string
|
|
142
|
+
}, callback?: (text: string) => boolean): void
|
|
143
|
+
closeScanQrPopup(): void
|
|
144
|
+
readTextFromClipboard(callback?: (text: string) => void): void
|
|
145
|
+
requestWriteAccess(callback?: (granted: boolean) => void): void
|
|
146
|
+
requestContact(callback?: (granted: boolean) => void): void
|
|
147
|
+
ready(): void
|
|
148
|
+
expand(): void
|
|
149
|
+
close(): void
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
declare global {
|
|
153
|
+
interface Window {
|
|
154
|
+
Telegram?: {
|
|
155
|
+
WebApp: TelegramWebApp
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type RGB = { r?: number; g?: number; b?: number }
|
|
2
|
+
|
|
3
|
+
export function toHex(value?: RGB | `#${string}` | string | null): string {
|
|
4
|
+
if (!value) return '—'
|
|
5
|
+
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
const v = value.trim()
|
|
8
|
+
return v.startsWith('#') ? v : '—'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { r, g, b } = value
|
|
12
|
+
if (typeof r !== 'number' || typeof g !== 'number' || typeof b !== 'number') return '—'
|
|
13
|
+
const to = (n: number) => n.toString(16).padStart(2, '0')
|
|
14
|
+
return `#${to(r)}${to(g)}${to(b)}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toRgb(value?: RGB | `#${string}` | string | null): { r: number; g: number; b: number } | null {
|
|
18
|
+
if (!value) return null
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
const v = value.trim()
|
|
21
|
+
if (!v.startsWith('#')) return null
|
|
22
|
+
const hex = v.replace('#', '')
|
|
23
|
+
const clean = hex.length === 3
|
|
24
|
+
? hex.split('').map(c => c + c).join('')
|
|
25
|
+
: hex
|
|
26
|
+
if (clean.length !== 6) return null
|
|
27
|
+
const r = parseInt(clean.slice(0, 2), 16)
|
|
28
|
+
const g = parseInt(clean.slice(2, 4), 16)
|
|
29
|
+
const b = parseInt(clean.slice(4, 6), 16)
|
|
30
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null
|
|
31
|
+
return { r, g, b }
|
|
32
|
+
}
|
|
33
|
+
const { r, g, b } = value
|
|
34
|
+
if (typeof r !== 'number' || typeof g !== 'number' || typeof b !== 'number') return null
|
|
35
|
+
return { r, g, b }
|
|
36
|
+
}
|
|
37
|
+
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
|
+
export default defineNuxtConfig({
|
|
3
|
+
extends: [],
|
|
4
|
+
ssr: false,
|
|
5
|
+
compatibilityDate: '2025-09-06',
|
|
6
|
+
srcDir: 'app',
|
|
7
|
+
devtools: {
|
|
8
|
+
enabled: true,
|
|
9
|
+
|
|
10
|
+
timeline: {
|
|
11
|
+
enabled: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
runtimeConfig: {
|
|
15
|
+
public: {
|
|
16
|
+
// ENV is read at build-time; exposed to client as appEnv
|
|
17
|
+
appEnv: process.env.ENV || ''
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
app: {
|
|
21
|
+
head: {
|
|
22
|
+
meta: [
|
|
23
|
+
{
|
|
24
|
+
name: 'viewport',
|
|
25
|
+
content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
script: [
|
|
29
|
+
{
|
|
30
|
+
src: 'https://telegram.org/js/telegram-web-app.js?58'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
pages: true,
|
|
36
|
+
router: {
|
|
37
|
+
options: {
|
|
38
|
+
// Use history mode so Telegram's #tgWebAppData is preserved as a URL hash
|
|
39
|
+
hashMode: false
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
nitro: {
|
|
43
|
+
prerender: {
|
|
44
|
+
crawlLinks: true,
|
|
45
|
+
routes: ['/'],
|
|
46
|
+
failOnError: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
// Additional SPA configuration
|
|
50
|
+
experimental: {
|
|
51
|
+
payloadExtraction: false
|
|
52
|
+
},
|
|
53
|
+
css: ['~/assets/css/tailwind.css', '~/assets/css/main.css'],
|
|
54
|
+
modules: ['@nuxt/icon', '@nuxtjs/tailwindcss', '@nuxt/eslint', '@nuxt/fonts'],
|
|
55
|
+
})
|