create-teact 0.1.0-alpha.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/README.md +84 -0
- package/index.ts +559 -0
- package/package.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# create-teact
|
|
2
|
+
|
|
3
|
+
Interactive project scaffolder for Teact. The fastest way to start a new Telegram bot.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun create teact my-bot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with other package managers:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx create-teact my-bot
|
|
15
|
+
pnpm create teact my-bot
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What It Does
|
|
19
|
+
|
|
20
|
+
1. Prompts for a project name (if not provided as an argument)
|
|
21
|
+
2. Lets you pick a template: `router`, `counter`, `full`, or `empty`
|
|
22
|
+
3. Lets you select features: storage, conversations, streaming, auth, i18n, payments
|
|
23
|
+
4. Asks which package manager to use (`bun`, `npm`, or `pnpm`)
|
|
24
|
+
5. Asks whether to install dependencies
|
|
25
|
+
6. Generates the project with `package.json`, `tsconfig.json`, `teact.config.ts`, `.env`, `.gitignore`, and `src/` files
|
|
26
|
+
7. Feature pages are wired into the router and main menu automatically
|
|
27
|
+
|
|
28
|
+
## Templates
|
|
29
|
+
|
|
30
|
+
| Template | Description |
|
|
31
|
+
|----------|-------------|
|
|
32
|
+
| `router` | Multi-page bot with routing and pages directory (recommended) |
|
|
33
|
+
| `counter` | Minimal counter bot with inline keyboard |
|
|
34
|
+
| `full` | Router + all features pre-wired (storage, conversations, streaming, auth, i18n, payments) |
|
|
35
|
+
| `empty` | Bare project with no pages |
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
When you pick the `router` template, you can opt into features:
|
|
40
|
+
|
|
41
|
+
| Feature | What it adds |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| Storage | `teact.config.ts` with `storagePlugin`, Settings page with `useStorage` |
|
|
44
|
+
| Conversations | `conversationsPlugin` in config |
|
|
45
|
+
| Streaming | `streamPlugin` in config, StreamDemo page with `useStream` |
|
|
46
|
+
| Auth | `authPlugin` in config |
|
|
47
|
+
| i18n | Locale JSON files, LanguagePage with `useLocale`, `createI18n` in index |
|
|
48
|
+
| Payments | StorePage with `useInvoice`, `PAYMENT_PROVIDER_TOKEN` in `.env` |
|
|
49
|
+
|
|
50
|
+
## Generated Structure
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
my-bot/
|
|
54
|
+
├── teact.config.ts # Plugin configuration
|
|
55
|
+
├── package.json
|
|
56
|
+
├── tsconfig.json
|
|
57
|
+
├── .env # Bot token + provider token
|
|
58
|
+
├── .gitignore
|
|
59
|
+
└── src/
|
|
60
|
+
├── index.tsx # Bot entry — router, commands, providers
|
|
61
|
+
├── pages/
|
|
62
|
+
│ ├── MainMenu.tsx # Home page with feature buttons
|
|
63
|
+
│ ├── About.tsx
|
|
64
|
+
│ ├── Settings.tsx # (storage)
|
|
65
|
+
│ ├── StreamDemo.tsx # (streaming)
|
|
66
|
+
│ ├── LanguagePage.tsx # (i18n)
|
|
67
|
+
│ └── StorePage.tsx # (payments)
|
|
68
|
+
└── locales/ # (i18n)
|
|
69
|
+
├── en.json
|
|
70
|
+
└── am.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## After Scaffolding
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd my-bot
|
|
77
|
+
# Add your bot token to .env
|
|
78
|
+
bun dev
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## See Also
|
|
82
|
+
|
|
83
|
+
- [`@teactjs/cli`](../cli) for the full CLI tool
|
|
84
|
+
- [Root README](../../README.md) for getting started
|
package/index.ts
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
4
|
+
import { resolve, join } from 'path';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
p.intro('create-teact');
|
|
11
|
+
|
|
12
|
+
const nameArg = args[0];
|
|
13
|
+
const name = nameArg ?? (await p.text({
|
|
14
|
+
message: 'Project name',
|
|
15
|
+
placeholder: 'my-bot',
|
|
16
|
+
validate(v) {
|
|
17
|
+
if (!v) return 'Name is required';
|
|
18
|
+
if (/[^a-z0-9-_]/.test(v)) return 'Use lowercase letters, numbers, hyphens, or underscores';
|
|
19
|
+
},
|
|
20
|
+
}) as string);
|
|
21
|
+
|
|
22
|
+
if (p.isCancel(name)) { p.cancel('Cancelled'); process.exit(0); }
|
|
23
|
+
|
|
24
|
+
const dir = resolve(process.cwd(), name);
|
|
25
|
+
if (existsSync(dir)) {
|
|
26
|
+
p.log.error(`Directory "${name}" already exists`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const template = await p.select({
|
|
31
|
+
message: 'Pick a template',
|
|
32
|
+
options: [
|
|
33
|
+
{ value: 'router', label: 'Router', hint: 'multi-page bot with navigation (recommended)' },
|
|
34
|
+
{ value: 'counter', label: 'Counter', hint: 'simple stateful counter' },
|
|
35
|
+
{ value: 'full', label: 'Full-Stack', hint: 'router + all features pre-wired' },
|
|
36
|
+
{ value: 'empty', label: 'Empty', hint: 'bare minimum' },
|
|
37
|
+
],
|
|
38
|
+
}) as string;
|
|
39
|
+
|
|
40
|
+
if (p.isCancel(template)) { p.cancel('Cancelled'); process.exit(0); }
|
|
41
|
+
|
|
42
|
+
const features = await p.multiselect({
|
|
43
|
+
message: 'Select features',
|
|
44
|
+
options: [
|
|
45
|
+
{ value: 'storage', label: 'Storage', hint: 'persistent state with useStorage' },
|
|
46
|
+
{ value: 'conversations', label: 'Conversations', hint: 'multi-step flows' },
|
|
47
|
+
{ value: 'streaming', label: 'Streaming', hint: 'live text updates with useStream' },
|
|
48
|
+
{ value: 'auth', label: 'Auth', hint: 'role-based access + useAuthSession' },
|
|
49
|
+
{ value: 'i18n', label: 'Internationalization', hint: 'multi-language with i18next' },
|
|
50
|
+
{ value: 'payments', label: 'Payments', hint: 'Telegram payments with useInvoice' },
|
|
51
|
+
],
|
|
52
|
+
initialValues: template === 'full' ? ['storage', 'conversations', 'streaming', 'auth', 'i18n', 'payments'] : [],
|
|
53
|
+
required: false,
|
|
54
|
+
}) as string[];
|
|
55
|
+
|
|
56
|
+
if (p.isCancel(features)) { p.cancel('Cancelled'); process.exit(0); }
|
|
57
|
+
|
|
58
|
+
const pm = await p.select({
|
|
59
|
+
message: 'Package manager',
|
|
60
|
+
options: [
|
|
61
|
+
{ value: 'bun', label: 'bun', hint: 'recommended' },
|
|
62
|
+
{ value: 'npm', label: 'npm' },
|
|
63
|
+
{ value: 'pnpm', label: 'pnpm' },
|
|
64
|
+
],
|
|
65
|
+
}) as string;
|
|
66
|
+
|
|
67
|
+
if (p.isCancel(pm)) { p.cancel('Cancelled'); process.exit(0); }
|
|
68
|
+
|
|
69
|
+
const shouldInstall = await p.confirm({ message: 'Install dependencies?' });
|
|
70
|
+
if (p.isCancel(shouldInstall)) { p.cancel('Cancelled'); process.exit(0); }
|
|
71
|
+
|
|
72
|
+
const spin = p.spinner();
|
|
73
|
+
spin.start('Generating project');
|
|
74
|
+
|
|
75
|
+
mkdirSync(join(dir, 'src', 'pages'), { recursive: true });
|
|
76
|
+
|
|
77
|
+
const has = (f: string) => template === 'full' || features.includes(f);
|
|
78
|
+
|
|
79
|
+
const deps: Record<string, string> = {
|
|
80
|
+
'@teactjs/core': '^0.1.0-alpha.1',
|
|
81
|
+
'@teactjs/ui': '^0.1.0-alpha.1',
|
|
82
|
+
'@teactjs/telegram': '^0.1.0-alpha.1',
|
|
83
|
+
'@teactjs/cli': '^0.1.0-alpha.1',
|
|
84
|
+
react: '^19.0.0',
|
|
85
|
+
};
|
|
86
|
+
if (has('storage')) deps['@teactjs/storage'] = '^0.1.0-alpha.1';
|
|
87
|
+
if (has('i18n')) {
|
|
88
|
+
deps['i18next'] = '^23.0.0';
|
|
89
|
+
deps['react-i18next'] = '^15.0.0';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({
|
|
93
|
+
name,
|
|
94
|
+
version: '0.1.0',
|
|
95
|
+
private: true,
|
|
96
|
+
type: 'module',
|
|
97
|
+
scripts: {
|
|
98
|
+
dev: 'teact dev',
|
|
99
|
+
build: 'teact build',
|
|
100
|
+
start: 'bun run dist/index.js',
|
|
101
|
+
},
|
|
102
|
+
dependencies: deps,
|
|
103
|
+
devDependencies: {
|
|
104
|
+
typescript: '^5.7.0',
|
|
105
|
+
'@types/bun': 'latest',
|
|
106
|
+
'@types/react': '^19.0.0',
|
|
107
|
+
},
|
|
108
|
+
}, null, 2));
|
|
109
|
+
|
|
110
|
+
writeFileSync(join(dir, 'tsconfig.json'), JSON.stringify({
|
|
111
|
+
compilerOptions: {
|
|
112
|
+
target: 'ES2022',
|
|
113
|
+
module: 'ESNext',
|
|
114
|
+
moduleResolution: 'bundler',
|
|
115
|
+
lib: ['ES2022'],
|
|
116
|
+
jsx: 'react-jsx',
|
|
117
|
+
jsxImportSource: 'react',
|
|
118
|
+
strict: true,
|
|
119
|
+
esModuleInterop: true,
|
|
120
|
+
skipLibCheck: true,
|
|
121
|
+
resolveJsonModule: true,
|
|
122
|
+
},
|
|
123
|
+
include: ['src'],
|
|
124
|
+
}, null, 2));
|
|
125
|
+
|
|
126
|
+
let envContent = 'TELEGRAM_BOT_TOKEN=\n';
|
|
127
|
+
if (has('payments')) envContent += 'PAYMENT_PROVIDER_TOKEN=\n';
|
|
128
|
+
|
|
129
|
+
writeFileSync(join(dir, '.env'), envContent);
|
|
130
|
+
writeFileSync(join(dir, '.gitignore'), 'node_modules/\ndist/\n.env\n.teact/\n*.log\n.DS_Store\n');
|
|
131
|
+
|
|
132
|
+
const files = buildFiles(template, features);
|
|
133
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
134
|
+
const fullPath = join(dir, filePath);
|
|
135
|
+
mkdirSync(resolve(fullPath, '..'), { recursive: true });
|
|
136
|
+
writeFileSync(fullPath, content);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
spin.stop('Project generated');
|
|
140
|
+
|
|
141
|
+
if (shouldInstall) {
|
|
142
|
+
const installSpin = p.spinner();
|
|
143
|
+
installSpin.start(`Installing with ${pm}`);
|
|
144
|
+
try {
|
|
145
|
+
const proc = Bun.spawnSync([pm, 'install'], { cwd: dir, stderr: 'pipe', stdout: 'pipe' });
|
|
146
|
+
installSpin.stop(proc.exitCode === 0 ? 'Dependencies installed' : 'Install failed — run manually');
|
|
147
|
+
} catch {
|
|
148
|
+
installSpin.stop('Install failed — run manually');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const runCmd = pm === 'bun' ? 'bun dev' : `${pm} run dev`;
|
|
153
|
+
p.note(
|
|
154
|
+
[
|
|
155
|
+
`cd ${name}`,
|
|
156
|
+
'# Add TELEGRAM_BOT_TOKEN to .env',
|
|
157
|
+
runCmd,
|
|
158
|
+
].join('\n'),
|
|
159
|
+
'Next steps',
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
p.outro('Happy building!');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---- File generators ----
|
|
166
|
+
|
|
167
|
+
function buildFiles(template: string, features: string[]): Record<string, string> {
|
|
168
|
+
const has = (f: string) => template === 'full' || features.includes(f);
|
|
169
|
+
|
|
170
|
+
if (template === 'counter') return counterFiles();
|
|
171
|
+
if (template === 'empty') return emptyFiles();
|
|
172
|
+
|
|
173
|
+
return routerFiles(has);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emptyFiles(): Record<string, string> {
|
|
177
|
+
return {
|
|
178
|
+
'src/index.tsx': `import { createBot } from '@teactjs/core';
|
|
179
|
+
import { Message } from '@teactjs/ui';
|
|
180
|
+
import { TelegramAdapter } from '@teactjs/telegram';
|
|
181
|
+
|
|
182
|
+
function App() {
|
|
183
|
+
return <Message text="Hello from Teact!" />;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const bot = createBot({
|
|
187
|
+
component: App,
|
|
188
|
+
adapter: new TelegramAdapter(),
|
|
189
|
+
commands: { start: { description: 'Start the bot' } },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
bot.start();
|
|
193
|
+
`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function counterFiles(): Record<string, string> {
|
|
198
|
+
return {
|
|
199
|
+
'src/index.tsx': `import { useState } from 'react';
|
|
200
|
+
import { createBot } from '@teactjs/core';
|
|
201
|
+
import { Message, Button, InlineKeyboard, ButtonRow } from '@teactjs/ui';
|
|
202
|
+
import { TelegramAdapter } from '@teactjs/telegram';
|
|
203
|
+
|
|
204
|
+
function App() {
|
|
205
|
+
const [count, setCount] = useState(0);
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Message text={\`Count: \${count}\`}>
|
|
209
|
+
<InlineKeyboard>
|
|
210
|
+
<ButtonRow>
|
|
211
|
+
<Button text="-1" onClick={() => setCount(c => c - 1)} />
|
|
212
|
+
<Button text={\`[ \${count} ]\`} onClick={() => {}} />
|
|
213
|
+
<Button text="+1" onClick={() => setCount(c => c + 1)} />
|
|
214
|
+
</ButtonRow>
|
|
215
|
+
<ButtonRow>
|
|
216
|
+
<Button text="Reset" onClick={() => setCount(0)} />
|
|
217
|
+
</ButtonRow>
|
|
218
|
+
</InlineKeyboard>
|
|
219
|
+
</Message>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const bot = createBot({
|
|
224
|
+
component: App,
|
|
225
|
+
adapter: new TelegramAdapter(),
|
|
226
|
+
commands: { start: { description: 'Start the counter' } },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
bot.start();
|
|
230
|
+
`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function routerFiles(has: (f: string) => boolean): Record<string, string> {
|
|
235
|
+
const files: Record<string, string> = {};
|
|
236
|
+
|
|
237
|
+
// ---- teact.config.ts ----
|
|
238
|
+
const configPlugins: string[] = [];
|
|
239
|
+
const configImportsCore: string[] = ['defineConfig'];
|
|
240
|
+
const configImportsTelegram: string[] = [];
|
|
241
|
+
const configImportsStorage: string[] = [];
|
|
242
|
+
|
|
243
|
+
if (has('storage')) {
|
|
244
|
+
configImportsStorage.push('storagePlugin');
|
|
245
|
+
configPlugins.push("storagePlugin({ driver: 'file', path: '.teact/storage.json' })");
|
|
246
|
+
}
|
|
247
|
+
if (has('conversations')) {
|
|
248
|
+
configImportsTelegram.push('conversationsPlugin');
|
|
249
|
+
configPlugins.push('conversationsPlugin()');
|
|
250
|
+
}
|
|
251
|
+
if (has('streaming')) {
|
|
252
|
+
configImportsTelegram.push('streamPlugin');
|
|
253
|
+
configPlugins.push('streamPlugin()');
|
|
254
|
+
}
|
|
255
|
+
if (has('auth')) {
|
|
256
|
+
configImportsCore.push('authPlugin');
|
|
257
|
+
configPlugins.push('authPlugin({ admins: [] })');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let configSrc = `import { ${configImportsCore.join(', ')} } from '@teactjs/core';\n`;
|
|
261
|
+
if (configImportsTelegram.length > 0) {
|
|
262
|
+
configSrc += `import { ${configImportsTelegram.join(', ')} } from '@teactjs/telegram';\n`;
|
|
263
|
+
}
|
|
264
|
+
if (configImportsStorage.length > 0) {
|
|
265
|
+
configSrc += `import { ${configImportsStorage.join(', ')} } from '@teactjs/storage';\n`;
|
|
266
|
+
}
|
|
267
|
+
configSrc += `\nexport default defineConfig({\n mode: 'polling',\n`;
|
|
268
|
+
if (configPlugins.length > 0) {
|
|
269
|
+
configSrc += `\n plugins: [\n ${configPlugins.join(',\n ')},\n ],\n`;
|
|
270
|
+
}
|
|
271
|
+
configSrc += `});\n`;
|
|
272
|
+
files['teact.config.ts'] = configSrc;
|
|
273
|
+
|
|
274
|
+
// ---- Routes and pages ----
|
|
275
|
+
const routes: Array<{ path: string; component: string; file: string }> = [
|
|
276
|
+
{ path: '/', component: 'MainMenu', file: './pages/MainMenu' },
|
|
277
|
+
{ path: '/about', component: 'About', file: './pages/About' },
|
|
278
|
+
];
|
|
279
|
+
const commands: Record<string, string> = {
|
|
280
|
+
start: "'/'",
|
|
281
|
+
about: "'/about'",
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if (has('storage')) {
|
|
285
|
+
routes.push({ path: '/settings', component: 'Settings', file: './pages/Settings' });
|
|
286
|
+
commands['settings'] = "'/settings'";
|
|
287
|
+
}
|
|
288
|
+
if (has('streaming')) {
|
|
289
|
+
routes.push({ path: '/stream', component: 'StreamDemo', file: './pages/StreamDemo' });
|
|
290
|
+
commands['stream'] = "'/stream'";
|
|
291
|
+
}
|
|
292
|
+
if (has('i18n')) {
|
|
293
|
+
routes.push({ path: '/language', component: 'LanguagePage', file: './pages/LanguagePage' });
|
|
294
|
+
commands['language'] = "'/language'";
|
|
295
|
+
}
|
|
296
|
+
if (has('payments')) {
|
|
297
|
+
routes.push({ path: '/store', component: 'StorePage', file: './pages/StorePage' });
|
|
298
|
+
commands['store'] = "'/store'";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---- src/index.tsx ----
|
|
302
|
+
let indexSrc = `import React from 'react';\n`;
|
|
303
|
+
indexSrc += `import { createBot, createRouter${has('i18n') ? ', createI18n' : ''} } from '@teactjs/core';\n`;
|
|
304
|
+
indexSrc += `import { TelegramAdapter } from '@teactjs/telegram';\n\n`;
|
|
305
|
+
|
|
306
|
+
for (const r of routes) {
|
|
307
|
+
indexSrc += `import { ${r.component} } from '${r.file}';\n`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (has('i18n')) {
|
|
311
|
+
indexSrc += `\nimport en from './locales/en.json';\nimport am from './locales/am.json';\n`;
|
|
312
|
+
indexSrc += `\nconst i18n = createI18n({\n defaultLocale: 'en',\n resources: {\n en: { translation: en },\n am: { translation: am },\n },\n});\n`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
indexSrc += `\nconst router = createRouter({\n`;
|
|
316
|
+
for (const r of routes) {
|
|
317
|
+
indexSrc += ` '${r.path}': ${r.component},\n`;
|
|
318
|
+
}
|
|
319
|
+
indexSrc += `});\n`;
|
|
320
|
+
|
|
321
|
+
indexSrc += `\nconst bot = createBot({\n adapter: new TelegramAdapter(),\n router,\n`;
|
|
322
|
+
if (has('i18n')) {
|
|
323
|
+
indexSrc += ` providers: ({ children }) => (\n <i18n.Provider>{children}</i18n.Provider>\n ),\n`;
|
|
324
|
+
}
|
|
325
|
+
indexSrc += ` commands: {\n`;
|
|
326
|
+
for (const [cmd, route] of Object.entries(commands)) {
|
|
327
|
+
indexSrc += ` ${cmd}: { description: '${cmd.charAt(0).toUpperCase() + cmd.slice(1)}', route: ${route} },\n`;
|
|
328
|
+
}
|
|
329
|
+
indexSrc += ` },\n});\n\nbot.start();\n`;
|
|
330
|
+
files['src/index.tsx'] = indexSrc;
|
|
331
|
+
|
|
332
|
+
// ---- MainMenu ----
|
|
333
|
+
const menuButtons: string[] = [];
|
|
334
|
+
menuButtons.push(` <Button text="ℹ️ About" onClick={() => navigate('/about')} />`);
|
|
335
|
+
if (has('storage')) menuButtons.push(` <Button text="⚙️ Settings" onClick={() => navigate('/settings')} />`);
|
|
336
|
+
if (has('streaming')) menuButtons.push(` <Button text="⚡ Stream Demo" onClick={() => navigate('/stream')} />`);
|
|
337
|
+
if (has('i18n')) menuButtons.push(` <Button text="🌐 Language" onClick={() => navigate('/language')} />`);
|
|
338
|
+
if (has('payments')) menuButtons.push(` <Button text="💎 Store" onClick={() => navigate('/store')} />`);
|
|
339
|
+
|
|
340
|
+
const menuRows: string[] = [];
|
|
341
|
+
for (let i = 0; i < menuButtons.length; i += 2) {
|
|
342
|
+
const pair = menuButtons.slice(i, i + 2);
|
|
343
|
+
menuRows.push(` <ButtonRow>\n${pair.join('\n')}\n </ButtonRow>`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
files['src/pages/MainMenu.tsx'] = `import { useNavigate } from '@teactjs/core';
|
|
347
|
+
import { Message, Button, InlineKeyboard, ButtonRow } from '@teactjs/ui';
|
|
348
|
+
|
|
349
|
+
export function MainMenu() {
|
|
350
|
+
const navigate = useNavigate();
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<Message text="🤖 Welcome!\\n\\nPick an option:">
|
|
354
|
+
<InlineKeyboard>
|
|
355
|
+
${menuRows.join('\n')}
|
|
356
|
+
</InlineKeyboard>
|
|
357
|
+
</Message>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
// ---- About ----
|
|
363
|
+
files['src/pages/About.tsx'] = `import { useNavigate } from '@teactjs/core';
|
|
364
|
+
import { Message, Button, InlineKeyboard, ButtonRow } from '@teactjs/ui';
|
|
365
|
+
|
|
366
|
+
export function About() {
|
|
367
|
+
const navigate = useNavigate();
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<Message text="ℹ️ About\\n\\nBuilt with Teact — React for Telegram bots.\\nhttps://github.com/leuliance/teact">
|
|
371
|
+
<InlineKeyboard>
|
|
372
|
+
<ButtonRow>
|
|
373
|
+
<Button text="🏠 Menu" onClick={() => navigate('/')} />
|
|
374
|
+
</ButtonRow>
|
|
375
|
+
</InlineKeyboard>
|
|
376
|
+
</Message>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
`;
|
|
380
|
+
|
|
381
|
+
// ---- Settings (storage) ----
|
|
382
|
+
if (has('storage')) {
|
|
383
|
+
files['src/pages/Settings.tsx'] = `import { useNavigate } from '@teactjs/core';
|
|
384
|
+
import { useStorage } from '@teactjs/storage';
|
|
385
|
+
import { Message, Button, InlineKeyboard, ButtonRow } from '@teactjs/ui';
|
|
386
|
+
|
|
387
|
+
export function Settings() {
|
|
388
|
+
const navigate = useNavigate();
|
|
389
|
+
const [theme, setTheme] = useStorage('theme', 'light');
|
|
390
|
+
const [notify, setNotify] = useStorage('notifications', true);
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<Message text={\`⚙️ Settings\\n\\n🎨 Theme: \${theme}\\n🔔 Notifications: \${notify ? 'On' : 'Off'}\`}>
|
|
394
|
+
<InlineKeyboard>
|
|
395
|
+
<ButtonRow>
|
|
396
|
+
<Button text={\`🎨 \${theme}\`} onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')} />
|
|
397
|
+
<Button text={\`🔔 \${notify ? 'On' : 'Off'}\`} onClick={() => setNotify(n => !n)} />
|
|
398
|
+
</ButtonRow>
|
|
399
|
+
<ButtonRow>
|
|
400
|
+
<Button text="🏠 Menu" onClick={() => navigate('/')} />
|
|
401
|
+
</ButtonRow>
|
|
402
|
+
</InlineKeyboard>
|
|
403
|
+
</Message>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---- StreamDemo ----
|
|
410
|
+
if (has('streaming')) {
|
|
411
|
+
files['src/pages/StreamDemo.tsx'] = `import { useNavigate, useStream } from '@teactjs/core';
|
|
412
|
+
import { Message, Button, InlineKeyboard, ButtonRow } from '@teactjs/ui';
|
|
413
|
+
|
|
414
|
+
const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
|
|
415
|
+
|
|
416
|
+
async function* generateText() {
|
|
417
|
+
yield 'Generating';
|
|
418
|
+
await delay(500);
|
|
419
|
+
yield ' your ';
|
|
420
|
+
await delay(500);
|
|
421
|
+
yield 'response...\\n\\n';
|
|
422
|
+
await delay(600);
|
|
423
|
+
yield 'Hello from the stream!';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function StreamDemo() {
|
|
427
|
+
const navigate = useNavigate();
|
|
428
|
+
const { text, isStreaming, stream } = useStream({ throttleMs: 400 });
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<Message text={text || '⚡ Stream Demo\\n\\nWatch text generate live!'}>
|
|
432
|
+
<InlineKeyboard>
|
|
433
|
+
<ButtonRow>
|
|
434
|
+
<Button
|
|
435
|
+
text={isStreaming ? '⏳ Streaming...' : '▶️ Start Stream'}
|
|
436
|
+
onClick={() => { if (!isStreaming) stream(generateText()); }}
|
|
437
|
+
/>
|
|
438
|
+
</ButtonRow>
|
|
439
|
+
<ButtonRow>
|
|
440
|
+
<Button text="🏠 Menu" onClick={() => navigate('/')} />
|
|
441
|
+
</ButtonRow>
|
|
442
|
+
</InlineKeyboard>
|
|
443
|
+
</Message>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ---- LanguagePage (i18n) ----
|
|
450
|
+
if (has('i18n')) {
|
|
451
|
+
files['src/locales/en.json'] = `{
|
|
452
|
+
"welcome": "Welcome!",
|
|
453
|
+
"about": "About this bot",
|
|
454
|
+
"settings": "Settings",
|
|
455
|
+
"language": "Language",
|
|
456
|
+
"back": "Back"
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
files['src/locales/am.json'] = `{
|
|
460
|
+
"welcome": "እንኳን ደህና መጡ!",
|
|
461
|
+
"about": "ስለ ቦት",
|
|
462
|
+
"settings": "ቅንብሮች",
|
|
463
|
+
"language": "ቋንቋ",
|
|
464
|
+
"back": "ተመለስ"
|
|
465
|
+
}
|
|
466
|
+
`;
|
|
467
|
+
files['src/pages/LanguagePage.tsx'] = `import { useNavigate, useLocale } from '@teactjs/core';
|
|
468
|
+
import { Message, Button, InlineKeyboard, ButtonRow } from '@teactjs/ui';
|
|
469
|
+
|
|
470
|
+
export function LanguagePage() {
|
|
471
|
+
const navigate = useNavigate();
|
|
472
|
+
const { t, locale, setLocale } = useLocale();
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<Message text={\`🌐 \${t('language')}\\n\\nCurrent: \${locale}\`}>
|
|
476
|
+
<InlineKeyboard>
|
|
477
|
+
<ButtonRow>
|
|
478
|
+
<Button text="🇬🇧 English" onClick={() => setLocale('en')} />
|
|
479
|
+
<Button text="🇪🇹 አማርኛ" onClick={() => setLocale('am')} />
|
|
480
|
+
</ButtonRow>
|
|
481
|
+
<ButtonRow>
|
|
482
|
+
<Button text="🏠 Menu" onClick={() => navigate('/')} />
|
|
483
|
+
</ButtonRow>
|
|
484
|
+
</InlineKeyboard>
|
|
485
|
+
</Message>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ---- StorePage (payments) ----
|
|
492
|
+
if (has('payments')) {
|
|
493
|
+
files['src/pages/StorePage.tsx'] = `import { useNavigate, useInvoice } from '@teactjs/core';
|
|
494
|
+
import { Message, Button, InlineKeyboard, ButtonRow, Alert } from '@teactjs/ui';
|
|
495
|
+
|
|
496
|
+
export function StorePage() {
|
|
497
|
+
const navigate = useNavigate();
|
|
498
|
+
const invoice = useInvoice({
|
|
499
|
+
title: 'Premium Access',
|
|
500
|
+
description: 'Unlock all features for 30 days',
|
|
501
|
+
payload: 'premium-30d',
|
|
502
|
+
providerToken: process.env.PAYMENT_PROVIDER_TOKEN ?? '',
|
|
503
|
+
currency: 'USD',
|
|
504
|
+
prices: [{ label: 'Premium (30 days)', amount: 499 }],
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (invoice.status === 'success') {
|
|
508
|
+
return (
|
|
509
|
+
<Message text={\`✅ Payment successful!\\n\\nCharge ID: \${invoice.receipt?.telegramPaymentChargeId}\`}>
|
|
510
|
+
<InlineKeyboard>
|
|
511
|
+
<ButtonRow>
|
|
512
|
+
<Button text="🏠 Menu" onClick={() => navigate('/')} />
|
|
513
|
+
</ButtonRow>
|
|
514
|
+
</InlineKeyboard>
|
|
515
|
+
</Message>
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (invoice.status === 'failed') {
|
|
520
|
+
return (
|
|
521
|
+
<>
|
|
522
|
+
<Alert variant="error" title="Payment Failed">
|
|
523
|
+
{invoice.error ?? 'Unknown error'}
|
|
524
|
+
</Alert>
|
|
525
|
+
<Message text="Please check your payment configuration.">
|
|
526
|
+
<InlineKeyboard>
|
|
527
|
+
<ButtonRow>
|
|
528
|
+
<Button text="🔄 Retry" onClick={invoice.send} />
|
|
529
|
+
<Button text="🏠 Menu" onClick={() => navigate('/')} />
|
|
530
|
+
</ButtonRow>
|
|
531
|
+
</InlineKeyboard>
|
|
532
|
+
</Message>
|
|
533
|
+
</>
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<Message text={\`💎 Premium Access\\n\\nUnlock all features.\\n💰 Price: $4.99\${invoice.status === 'pending' ? '\\n\\n⏳ Processing...' : ''}\`}>
|
|
539
|
+
<InlineKeyboard>
|
|
540
|
+
<ButtonRow>
|
|
541
|
+
<Button text="💳 Buy — $4.99" onClick={invoice.send} />
|
|
542
|
+
</ButtonRow>
|
|
543
|
+
<ButtonRow>
|
|
544
|
+
<Button text="🏠 Menu" onClick={() => navigate('/')} />
|
|
545
|
+
</ButtonRow>
|
|
546
|
+
</InlineKeyboard>
|
|
547
|
+
</Message>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return files;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
main().catch((err) => {
|
|
557
|
+
console.error(err);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-teact",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Create a new Teact bot project — scaffolds a ready-to-run Telegram bot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-teact": "index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": ["index.ts", "README.md"],
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@clack/prompts": "^1.1.0"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT"
|
|
14
|
+
}
|