@nan0web/ui 1.0.0
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/.datasets/README.dataset.jsonl +12 -0
- package/.editorconfig +20 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +15 -0
- package/README.md +238 -0
- package/docs/uk/README.md +240 -0
- package/package.json +64 -0
- package/playground/User.js +52 -0
- package/playground/currency.exchange.js +48 -0
- package/playground/i18n/index.js +21 -0
- package/playground/i18n/uk.js +53 -0
- package/playground/language.form.js +25 -0
- package/playground/main.js +72 -0
- package/playground/registration.form.js +58 -0
- package/playground/topup.telephone.js +62 -0
- package/src/App/Command/Options.js +78 -0
- package/src/App/Command/index.js +9 -0
- package/src/App/Core/CoreApp.js +129 -0
- package/src/App/Core/UI.js +116 -0
- package/src/App/Core/Widget.js +67 -0
- package/src/App/Core/index.js +11 -0
- package/src/App/Scenario.js +45 -0
- package/src/App/User/Command/Message.js +44 -0
- package/src/App/User/Command/Options.js +48 -0
- package/src/App/User/Command/index.js +11 -0
- package/src/App/User/UserApp.js +73 -0
- package/src/App/User/UserApp.test.js +56 -0
- package/src/App/User/UserUI.js +20 -0
- package/src/App/User/UserUI.test.js +51 -0
- package/src/App/User/index.js +15 -0
- package/src/App/index.js +22 -0
- package/src/Component/Process/Input.js +70 -0
- package/src/Component/Process/Process.js +26 -0
- package/src/Component/Process/index.js +5 -0
- package/src/Component/Welcome/Input.js +50 -0
- package/src/Component/Welcome/Welcome.js +26 -0
- package/src/Component/Welcome/index.js +5 -0
- package/src/Component/index.js +9 -0
- package/src/Frame/Frame.js +591 -0
- package/src/Frame/Frame.test.js +429 -0
- package/src/Frame/Props.js +102 -0
- package/src/Locale.js +119 -0
- package/src/Model/User/User.js +56 -0
- package/src/Model/index.js +7 -0
- package/src/README.md.js +371 -0
- package/src/StdIn.js +111 -0
- package/src/StdOut.js +99 -0
- package/src/View/RenderOptions.js +48 -0
- package/src/View/View.js +289 -0
- package/src/View/View.test.js +77 -0
- package/src/core/Form/Form.js +289 -0
- package/src/core/Form/Form.test.js +116 -0
- package/src/core/Form/Input.js +116 -0
- package/src/core/Form/Input.test.js +58 -0
- package/src/core/Form/Message.js +86 -0
- package/src/core/Form/Message.test.js +54 -0
- package/src/core/Form/index.js +11 -0
- package/src/core/InputAdapter.js +41 -0
- package/src/core/InputAdapter.test.js +35 -0
- package/src/core/Message/InputMessage.js +119 -0
- package/src/core/Message/InputMessage.test.js +45 -0
- package/src/core/Message/Message.js +77 -0
- package/src/core/Message/Message.test.js +58 -0
- package/src/core/Message/OutputMessage.js +143 -0
- package/src/core/Message/OutputMessage.test.js +61 -0
- package/src/core/Message/index.js +7 -0
- package/src/core/OutputAdapter.js +50 -0
- package/src/core/OutputAdapter.test.js +35 -0
- package/src/core/Stream.js +71 -0
- package/src/core/Stream.test.js +78 -0
- package/src/core/StreamEntry.js +59 -0
- package/src/core/index.js +13 -0
- package/src/functions.js +38 -0
- package/src/index.js +34 -0
- package/src/index.test.js +14 -0
- package/src/models/SimpleUser.js +18 -0
- package/stories/App/AppView.js +15 -0
- package/stories/App/AppView.test.js +22 -0
- package/stories/App/RenderOptions.js +14 -0
- package/stories/nodejs/interface.test.js +27 -0
- package/system.md +187 -0
- package/system1.md +137 -0
- package/task.md +181 -0
- package/tsconfig.json +23 -0
- package/types/App/Command/Options.d.ts +46 -0
- package/types/App/Command/index.d.ts +8 -0
- package/types/App/Core/CoreApp.d.ts +70 -0
- package/types/App/Core/UI.d.ts +49 -0
- package/types/App/Core/Widget.d.ts +40 -0
- package/types/App/Core/index.d.ts +10 -0
- package/types/App/Scenario.d.ts +26 -0
- package/types/App/User/Command/Message.d.ts +30 -0
- package/types/App/User/Command/Options.d.ts +27 -0
- package/types/App/User/Command/index.d.ts +8 -0
- package/types/App/User/UserApp.d.ts +31 -0
- package/types/App/User/UserUI.d.ts +18 -0
- package/types/App/User/index.d.ts +12 -0
- package/types/App/index.d.ts +14 -0
- package/types/Component/Process/Input.d.ts +48 -0
- package/types/Component/Process/Process.d.ts +13 -0
- package/types/Component/Process/index.d.ts +4 -0
- package/types/Component/Welcome/Input.d.ts +34 -0
- package/types/Component/Welcome/Welcome.d.ts +13 -0
- package/types/Component/Welcome/index.d.ts +4 -0
- package/types/Component/index.d.ts +8 -0
- package/types/Frame/Frame.d.ts +186 -0
- package/types/Frame/Props.d.ts +77 -0
- package/types/Locale.d.ts +55 -0
- package/types/Model/User/User.d.ts +36 -0
- package/types/Model/index.d.ts +6 -0
- package/types/StdIn.d.ts +62 -0
- package/types/StdOut.d.ts +52 -0
- package/types/View/RenderOptions.d.ts +29 -0
- package/types/View/View.d.ts +115 -0
- package/types/core/Form/Form.d.ts +123 -0
- package/types/core/Form/Input.d.ts +69 -0
- package/types/core/Form/Message.d.ts +28 -0
- package/types/core/Form/index.d.ts +5 -0
- package/types/core/InputAdapter.d.ts +28 -0
- package/types/core/Message/InputMessage.d.ts +71 -0
- package/types/core/Message/Message.d.ts +50 -0
- package/types/core/Message/OutputMessage.d.ts +53 -0
- package/types/core/Message/index.d.ts +5 -0
- package/types/core/OutputAdapter.d.ts +33 -0
- package/types/core/Stream.d.ts +27 -0
- package/types/core/StreamEntry.d.ts +45 -0
- package/types/core/index.d.ts +9 -0
- package/types/functions.d.ts +3 -0
- package/types/index.d.ts +20 -0
- package/types/models/SimpleUser.d.ts +21 -0
- package/vitest.config.js +26 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import FormInput from "../src/core/Form/Input.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple domain model representing a user.
|
|
5
|
+
*
|
|
6
|
+
* The static `formFields` property defines the UI fields that should be
|
|
7
|
+
* displayed when generating a form for this model.
|
|
8
|
+
*
|
|
9
|
+
* Each field is described using `FormInput`, which the UI core library
|
|
10
|
+
* understands. The `required` flag, `type`, and other properties are
|
|
11
|
+
* respected by the validation routine inside `UIForm`.
|
|
12
|
+
*/
|
|
13
|
+
export default class User {
|
|
14
|
+
/**
|
|
15
|
+
* Constructs a new User instance.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} data - Initial data.
|
|
18
|
+
* @param {string} data.name
|
|
19
|
+
* @param {string} data.email
|
|
20
|
+
* @param {number} data.age
|
|
21
|
+
*/
|
|
22
|
+
constructor({ name = "", email = "", age = null } = {}) {
|
|
23
|
+
this.name = String(name)
|
|
24
|
+
this.email = String(email)
|
|
25
|
+
this.age = age !== null ? Number(age) : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @type {FormInput[]} UI fields for the User model */
|
|
29
|
+
static formFields = [
|
|
30
|
+
new FormInput({
|
|
31
|
+
name: "name",
|
|
32
|
+
label: "Name",
|
|
33
|
+
type: FormInput.TYPES.TEXT,
|
|
34
|
+
required: true,
|
|
35
|
+
placeholder: "Enter full name",
|
|
36
|
+
}),
|
|
37
|
+
new FormInput({
|
|
38
|
+
name: "email",
|
|
39
|
+
label: "Email",
|
|
40
|
+
type: FormInput.TYPES.EMAIL,
|
|
41
|
+
required: true,
|
|
42
|
+
placeholder: "example@domain.com",
|
|
43
|
+
}),
|
|
44
|
+
new FormInput({
|
|
45
|
+
name: "age",
|
|
46
|
+
label: "Age",
|
|
47
|
+
type: FormInput.TYPES.NUMBER,
|
|
48
|
+
required: false,
|
|
49
|
+
placeholder: "Optional",
|
|
50
|
+
}),
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple currency exchange console tool.
|
|
3
|
+
*
|
|
4
|
+
* Prompts for:
|
|
5
|
+
* - From currency (options)
|
|
6
|
+
* - To currency (options)
|
|
7
|
+
* - Amount (number)
|
|
8
|
+
*
|
|
9
|
+
* Controls:
|
|
10
|
+
* - Input "0" on any prompt to cancel.
|
|
11
|
+
* - Empty input on final confirmation submits.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CancelError, select } from "@nan0web/ui-cli"
|
|
15
|
+
|
|
16
|
+
const rates = {
|
|
17
|
+
USD: 1,
|
|
18
|
+
EUR: 0.9,
|
|
19
|
+
UAH: 27,
|
|
20
|
+
GBP: 0.8,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Main exchange flow */
|
|
24
|
+
export async function runExchange(t, ask, console, prompt, invalidPrompt) {
|
|
25
|
+
/** Choose a currency from available list */
|
|
26
|
+
async function chooseCurrency(title, selected = []) {
|
|
27
|
+
const input = await select({
|
|
28
|
+
title,
|
|
29
|
+
prompt, invalidPrompt, console,
|
|
30
|
+
options: Object.keys(rates).filter(c => !selected.includes(c))
|
|
31
|
+
})
|
|
32
|
+
return input.value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.info("\n=== " + t("Currency Exchange") + " ===")
|
|
36
|
+
const from = await chooseCurrency(t("From Currency"))
|
|
37
|
+
if (!from) throw new CancelError()
|
|
38
|
+
const to = await chooseCurrency(t("To Currency"), [from])
|
|
39
|
+
if (!to) throw new CancelError()
|
|
40
|
+
const input = await ask(
|
|
41
|
+
t("Amount") + ": ",
|
|
42
|
+
input => isNaN(input) || Number(input) <= 0,
|
|
43
|
+
t("Invalid amount.") + ": ",
|
|
44
|
+
)
|
|
45
|
+
const amount = Number(input)
|
|
46
|
+
const result = (amount / rates[from]) * rates[to]
|
|
47
|
+
console.success(`\n${amount} ${from} = ${result.toFixed(2)} ${to}\n`)
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import i18n, { createT } from "@nan0web/i18n"
|
|
2
|
+
import uk from "./uk.js"
|
|
3
|
+
|
|
4
|
+
const getVocab = i18n({ uk })
|
|
5
|
+
|
|
6
|
+
export function detectLocale(argv = []) {
|
|
7
|
+
// Detect language from CLI argument or environment variable
|
|
8
|
+
const langArg = argv.find(a => a.startsWith("--lang="))
|
|
9
|
+
let locale = "en"
|
|
10
|
+
if (langArg) {
|
|
11
|
+
locale = langArg.split("=")[1]
|
|
12
|
+
}
|
|
13
|
+
return locale
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const localesMap = new Map([
|
|
17
|
+
["en", "English"],
|
|
18
|
+
["uk", "Українська"],
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
export default (locale) => createT(getVocab(locale))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ukrainian translations for the playground.
|
|
3
|
+
*
|
|
4
|
+
* The object keys are English identifiers, the values are Ukrainian strings.
|
|
5
|
+
*
|
|
6
|
+
* Example usage:
|
|
7
|
+
* import vocabMap from "./i18n/index.js"
|
|
8
|
+
* const vocab = vocabMap("uk")
|
|
9
|
+
* const t = createT(vocab)
|
|
10
|
+
* console.log(t("Registration Form")) // → "Форма реєстрації"
|
|
11
|
+
* console.log(vocab["Registration Form"]) // → "Форма реєстрації"
|
|
12
|
+
*/
|
|
13
|
+
export default {
|
|
14
|
+
"Select demo:": "Оберіть демонстрацію:",
|
|
15
|
+
"Registration Form": "Форма реєстрації",
|
|
16
|
+
"Currency Exchange": "Обмін валют",
|
|
17
|
+
"Top‑up Telephone": "Поповнення телефону",
|
|
18
|
+
"Enter number (or 0 to quit):": "Введіть номер (або 0 для виходу):",
|
|
19
|
+
"Good‑bye.": "До побачення.",
|
|
20
|
+
"Invalid choice.": "Невірний вибір.",
|
|
21
|
+
"Username": "Ім'я користувача",
|
|
22
|
+
"Password": "Пароль",
|
|
23
|
+
"Confirm Password": "Підтвердження пароля",
|
|
24
|
+
"Email or Telephone": "Електронна пошта або телефон",
|
|
25
|
+
"Press ENTER to submit, type 0 to cancel": "Натисніть ENTER для відправки, введіть 0 для скасування",
|
|
26
|
+
"Cancelled.": "Скасовано.",
|
|
27
|
+
"Form submitted successfully!": "Форма успішно надіслана!",
|
|
28
|
+
"Phone Number": "Номер телефону",
|
|
29
|
+
"Top‑up Amount": "Сума поповнення",
|
|
30
|
+
"Select currency": "Виберіть валюту",
|
|
31
|
+
"Currency options:": "Валютні опції:",
|
|
32
|
+
"Invalid choice.": "Неправильний вибір.",
|
|
33
|
+
"Phone number is required.": "Необхідно вказати номер телефону.",
|
|
34
|
+
"Invalid phone number. Use digits only, 7‑15 characters.": "Неправильний номер телефону. Використовуйте лише цифри, 7-15 символів.",
|
|
35
|
+
"Invalid amount.": "Неправильна сума.",
|
|
36
|
+
"Top‑up of": "Поповнення на",
|
|
37
|
+
"to": "до",
|
|
38
|
+
"scheduled.": "заплановано.",
|
|
39
|
+
"options:": "опції:",
|
|
40
|
+
"Select": "Зробіть вибір",
|
|
41
|
+
"From Currency": "З якої валюти міняємо",
|
|
42
|
+
"To Currency": "У яку валюту",
|
|
43
|
+
"Amount": "Сума",
|
|
44
|
+
"Invalid choice, try again.": "Неправильний вибір, спробуйте знову.",
|
|
45
|
+
"Password (min 4 chars)": "Пароль (мінімум 4 символи)",
|
|
46
|
+
"Passwords do not match. Try again.": "Паролі не збігаються. Спробуйте знову.",
|
|
47
|
+
"[me]": "[Я]",
|
|
48
|
+
"[me invalid]": "[Я помилився]",
|
|
49
|
+
"! Invalid choice.": "! Неправильний вибір.",
|
|
50
|
+
"⨉ Cancelled.": "⨉ Скасовано.",
|
|
51
|
+
"Select a language": "Виберіть мову",
|
|
52
|
+
"Language Selector": "Вибір мови",
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple currency exchange console tool.
|
|
3
|
+
*
|
|
4
|
+
* Prompts for:
|
|
5
|
+
* - From currency (options)
|
|
6
|
+
* - To currency (options)
|
|
7
|
+
* - Amount (number)
|
|
8
|
+
*
|
|
9
|
+
* Controls:
|
|
10
|
+
* - Input "0" on any prompt to cancel.
|
|
11
|
+
* - Empty input on final confirmation submits.
|
|
12
|
+
*/
|
|
13
|
+
import { localesMap } from "./i18n/index.js"
|
|
14
|
+
import { select } from "@nan0web/ui-cli"
|
|
15
|
+
|
|
16
|
+
/** Main exchange flow */
|
|
17
|
+
export async function runLanguage(t, ask, console, prompt, invalidPrompt) {
|
|
18
|
+
const lang = await select({
|
|
19
|
+
title: "\n=== " + t("Language Selector") + " ===",
|
|
20
|
+
prompt, invalidPrompt, console,
|
|
21
|
+
options: Array.from(localesMap.keys()),
|
|
22
|
+
})
|
|
23
|
+
console.success(`\n${lang.value}\n`)
|
|
24
|
+
return lang.value
|
|
25
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playground entry point.
|
|
3
|
+
*
|
|
4
|
+
* Choose which demo to run:
|
|
5
|
+
* 1) Select a language
|
|
6
|
+
* 2) Registration Form
|
|
7
|
+
* 3) Currency Exchange
|
|
8
|
+
* 4) Top‑up Telephone
|
|
9
|
+
*
|
|
10
|
+
* Type 0 to exit.
|
|
11
|
+
*/
|
|
12
|
+
import { argv } from "node:process"
|
|
13
|
+
import Logger from "@nan0web/log"
|
|
14
|
+
import { runLanguage } from "./language.form.js"
|
|
15
|
+
import { runRegistration } from "./registration.form.js"
|
|
16
|
+
import { runExchange } from "./currency.exchange.js"
|
|
17
|
+
import { runTopup } from "./topup.telephone.js"
|
|
18
|
+
import createT, { detectLocale } from "./i18n/index.js"
|
|
19
|
+
import createInput, { CancelError, select } from "./ui/index.js"
|
|
20
|
+
|
|
21
|
+
const console = new Logger(Logger.detectLevel(argv))
|
|
22
|
+
let t = createT(detectLocale(argv))
|
|
23
|
+
const ask = createInput(["0"])
|
|
24
|
+
const menuOptions = [
|
|
25
|
+
"Select a language", // t("Select a language")
|
|
26
|
+
"Registration Form", // t("Registration Form")
|
|
27
|
+
"Currency Exchange", // t("Currency Exchange")
|
|
28
|
+
"Top‑up Telephone", // t("Top‑up Telephone")
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
console.info(Logger.style(Logger.LOGO, { color: "magenta" }))
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
try {
|
|
36
|
+
const prompt = t("[me]") + ": "
|
|
37
|
+
const invalidPrompt = Logger.style(t("[me invalid]", t("[me]")), { bgColor: "yellow" }) + ": "
|
|
38
|
+
const choice = await select({
|
|
39
|
+
title: t("Select demo:"),
|
|
40
|
+
prompt: t("[me]") + ": ",
|
|
41
|
+
invalidPrompt: Logger.style(t("[me]"), { bgColor: "yellow" }) + ": ",
|
|
42
|
+
options: menuOptions.map(t),
|
|
43
|
+
console,
|
|
44
|
+
})
|
|
45
|
+
switch (choice.index) {
|
|
46
|
+
case 0:
|
|
47
|
+
const lang = await runLanguage(t, ask, console, prompt, invalidPrompt)
|
|
48
|
+
t = createT(lang)
|
|
49
|
+
break
|
|
50
|
+
case 1:
|
|
51
|
+
await runRegistration(t, ask, console, prompt, invalidPrompt)
|
|
52
|
+
break
|
|
53
|
+
case 2:
|
|
54
|
+
await runExchange(t, ask, console, prompt, invalidPrompt)
|
|
55
|
+
break
|
|
56
|
+
case 3:
|
|
57
|
+
await runTopup(t, ask, console, prompt, invalidPrompt)
|
|
58
|
+
break
|
|
59
|
+
default:
|
|
60
|
+
console.warn(t("! Invalid choice."))
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err instanceof CancelError) {
|
|
64
|
+
console.warn("\n" + t("⨉ Cancelled."))
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
throw err
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple registration form using stdin.
|
|
3
|
+
*
|
|
4
|
+
* Fields: username, password, confirm, emailOrTel
|
|
5
|
+
*
|
|
6
|
+
* Controls:
|
|
7
|
+
* - Input "0" -> cancel (onCancel)
|
|
8
|
+
* - Empty input on final prompt -> submit (onSubmit)
|
|
9
|
+
*/
|
|
10
|
+
import { CancelError } from "@nan0web/ui-cli"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Main registration flow
|
|
14
|
+
* @todo add proper jsdoc to make autocomplete work for input as result of ask, console, t.
|
|
15
|
+
*/
|
|
16
|
+
export async function runRegistration(t, ask, console) {
|
|
17
|
+
console.info("\n=== " + t("Registration Form") + " ===")
|
|
18
|
+
const data = {}
|
|
19
|
+
|
|
20
|
+
// username
|
|
21
|
+
{
|
|
22
|
+
const input = await ask(t("Username") + ": ", true)
|
|
23
|
+
if (input.cancelled) throw new CancelError()
|
|
24
|
+
data.username = input.value
|
|
25
|
+
}
|
|
26
|
+
// password
|
|
27
|
+
{
|
|
28
|
+
const input = await ask(
|
|
29
|
+
t("Password (min 4 chars)") + ": ",
|
|
30
|
+
(input) => input.value.length < 4,
|
|
31
|
+
t("! Password must be at least 4 characters") + ": "
|
|
32
|
+
)
|
|
33
|
+
if (input.cancelled) throw new CancelError()
|
|
34
|
+
data.password = input.value
|
|
35
|
+
}
|
|
36
|
+
// confirm password
|
|
37
|
+
{
|
|
38
|
+
await ask(t("Confirm Password") + ": ", (input) => {
|
|
39
|
+
if (input.value !== data.password) {
|
|
40
|
+
console.error(t("Passwords do not match. Try again."))
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
// email or telephone
|
|
47
|
+
{
|
|
48
|
+
const input = await ask(t("Email or Telephone") + ": ", true)
|
|
49
|
+
if (input.cancelled) throw new CancelError()
|
|
50
|
+
data.emailOrTel = input.value
|
|
51
|
+
}
|
|
52
|
+
// final confirmation – empty line submits, "0" cancels
|
|
53
|
+
{
|
|
54
|
+
await ask(t("Press ENTER to submit, type 0 to cancel") + ": ", i => "" != i.value)
|
|
55
|
+
}
|
|
56
|
+
console.success("\n" + t("Form submitted successfully!"))
|
|
57
|
+
console.info(JSON.stringify({ ...data, password: "****" }, null, 2))
|
|
58
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple telephone top‑up form.
|
|
3
|
+
*
|
|
4
|
+
* Fields: number, amount, currency
|
|
5
|
+
*
|
|
6
|
+
* Controls:
|
|
7
|
+
* - Input "0" on any prompt to cancel.
|
|
8
|
+
* - Empty input on final confirmation submits.
|
|
9
|
+
*
|
|
10
|
+
* Validation:
|
|
11
|
+
* - Phone number must contain only digits and be 7‑15 characters long.
|
|
12
|
+
*/
|
|
13
|
+
import { CancelError } from "@nan0web/ui-cli"
|
|
14
|
+
|
|
15
|
+
const rates = {
|
|
16
|
+
USD: 1,
|
|
17
|
+
EUR: 0.9,
|
|
18
|
+
UAH: 27,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Main top‑up flow */
|
|
22
|
+
export async function runTopup(t, ask, console) {
|
|
23
|
+
/** Choose currency */
|
|
24
|
+
async function chooseCurrency(t) {
|
|
25
|
+
const list = Object.keys(rates)
|
|
26
|
+
console.info(t("Currency options:"))
|
|
27
|
+
list.forEach((c, i) => console.info(` ${i + 1}) ${c}`))
|
|
28
|
+
const input = await ask(
|
|
29
|
+
`${t("Select currency")} (1-${list.length}): `,
|
|
30
|
+
input => {
|
|
31
|
+
input.idx = Number(input.value) - 1
|
|
32
|
+
return ! (input.idx >= 0 && input.idx < list.length)
|
|
33
|
+
},
|
|
34
|
+
[t("Invalid choice."), t("Try again") + ":", ""].join(" ")
|
|
35
|
+
)
|
|
36
|
+
return list[input.idx] ?? null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Validate phone number */
|
|
40
|
+
function isValidPhone(number) {
|
|
41
|
+
return /^[0-9]{7,15}$/.test(number)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.info("\n=== " + t("Top‑up Telephone") + " ===")
|
|
45
|
+
const numberInp = await ask(
|
|
46
|
+
t("Phone Number") + ": ",
|
|
47
|
+
input => !isValidPhone(input.value),
|
|
48
|
+
t("Invalid phone number. Use digits only, 7‑15 characters.") + ": "
|
|
49
|
+
)
|
|
50
|
+
if (numberInp.cancelled) throw new CancelError()
|
|
51
|
+
const number = numberInp.value
|
|
52
|
+
const amountInp = await ask(
|
|
53
|
+
t("Top‑up Amount") + ": ",
|
|
54
|
+
input => isNaN(input.value) || Number(input.value) <= 0 || Number(input.value) > 1_000_000,
|
|
55
|
+
t("Amount must be a positive number below 1 million.") + ": "
|
|
56
|
+
)
|
|
57
|
+
if (amountInp.cancelled) throw new CancelError()
|
|
58
|
+
const amount = Number(amountInp.value)
|
|
59
|
+
const currency = await chooseCurrency(t)
|
|
60
|
+
if (!currency) throw new CancelError()
|
|
61
|
+
console.success(`\n${t("Top‑up of")} ${amount} ${currency} ${t("to")} ${number} ${t("scheduled.")}\n`)
|
|
62
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents command options with default values.
|
|
3
|
+
* Provides utilities for handling command line options.
|
|
4
|
+
*/
|
|
5
|
+
class CommandOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Default option values.
|
|
8
|
+
* @type {object}
|
|
9
|
+
* @property {boolean} help - Whether help is requested
|
|
10
|
+
* @property {string} cwd - Current working directory
|
|
11
|
+
*/
|
|
12
|
+
static DEFAULTS = {
|
|
13
|
+
help: false,
|
|
14
|
+
cwd: "",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @type {boolean} Whether help is requested */
|
|
18
|
+
help
|
|
19
|
+
|
|
20
|
+
/** @type {string} Current working directory */
|
|
21
|
+
cwd
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new CommandOptions instance.
|
|
25
|
+
* @param {object} props - The properties for command options
|
|
26
|
+
* @param {boolean} [props.help=false] - Whether help is requested
|
|
27
|
+
* @param {string} [props.cwd=""] - Current working directory
|
|
28
|
+
*/
|
|
29
|
+
constructor(props = {}) {
|
|
30
|
+
const {
|
|
31
|
+
help = CommandOptions.DEFAULTS.help,
|
|
32
|
+
cwd = CommandOptions.DEFAULTS.cwd,
|
|
33
|
+
} = props
|
|
34
|
+
this.help = Boolean(help)
|
|
35
|
+
this.cwd = String(cwd)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @type {Record<string, any>} */
|
|
39
|
+
get DEFAULTS() {
|
|
40
|
+
return /** @type {typeof CommandOptions} */ (this.constructor).DEFAULTS
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if all options have their default values.
|
|
45
|
+
* @returns {boolean} True if all options are at their default values, false otherwise
|
|
46
|
+
*/
|
|
47
|
+
get empty() {
|
|
48
|
+
return Object.entries(this).every(
|
|
49
|
+
([key, value]) => CommandOptions.DEFAULTS[key] === value
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Converts the options to a string representation.
|
|
55
|
+
* @returns {string} String representation of the options or "<no options>" if none set
|
|
56
|
+
*/
|
|
57
|
+
toString() {
|
|
58
|
+
const opts = Object.entries(this).map(([key, value]) => {
|
|
59
|
+
if (this.DEFAULTS[key] === value) return false
|
|
60
|
+
const prefix = true === value ? "--" : "-"
|
|
61
|
+
return `${prefix}${key}${value ? ` ${value}` : ""}`
|
|
62
|
+
}).filter(Boolean)
|
|
63
|
+
if (0 === opts.length) return "<no options>"
|
|
64
|
+
return opts.join(" ")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates a CommandOptions instance from the given props.
|
|
69
|
+
* @param {CommandOptions|object} props - The properties to create from
|
|
70
|
+
* @returns {CommandOptions} A CommandOptions instance
|
|
71
|
+
*/
|
|
72
|
+
static from(props) {
|
|
73
|
+
if (props instanceof CommandOptions) return props
|
|
74
|
+
return new this(props)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default CommandOptions
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { typeOf } from "@nan0web/types"
|
|
2
|
+
import { CommandMessage } from "../Command/index.js"
|
|
3
|
+
import UI from "./UI.js"
|
|
4
|
+
|
|
5
|
+
/** @typedef {Function} CommandFn */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Abstract base class for all apps.
|
|
9
|
+
* Each app processes input commands and produces output.
|
|
10
|
+
*/
|
|
11
|
+
export default class CoreApp {
|
|
12
|
+
/** @type {string} App name */
|
|
13
|
+
name
|
|
14
|
+
|
|
15
|
+
/** @type {Map<string, CommandFn>} Registered command handlers */
|
|
16
|
+
commands
|
|
17
|
+
|
|
18
|
+
/** @type {object} App state */
|
|
19
|
+
state
|
|
20
|
+
|
|
21
|
+
/** @type {CommandMessage} Starting command parsed from argv */
|
|
22
|
+
startCommand
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new CoreApp instance.
|
|
26
|
+
* @param {object} props - CoreApp properties
|
|
27
|
+
* @param {string} [props.name="CoreApp"] - App name
|
|
28
|
+
* @param {object} [props.state={}] - Initial state object
|
|
29
|
+
* @param {string[]} [props.argv=[]] - Command line arguments to parse
|
|
30
|
+
*/
|
|
31
|
+
constructor(props = {}) {
|
|
32
|
+
const {
|
|
33
|
+
name = "CoreApp",
|
|
34
|
+
state = {},
|
|
35
|
+
argv = [],
|
|
36
|
+
} = props
|
|
37
|
+
this.name = String(name)
|
|
38
|
+
this.state = state
|
|
39
|
+
this.commands = new Map()
|
|
40
|
+
this.startCommand = CommandMessage.parse(argv)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sets app state.
|
|
45
|
+
* @param {string|object} state - State key or object with multiple keys
|
|
46
|
+
* @param {any} [value] - State value if state is a string key
|
|
47
|
+
* @returns {object} Updated state
|
|
48
|
+
*/
|
|
49
|
+
set(state, value) {
|
|
50
|
+
if ("string" === typeof state) {
|
|
51
|
+
this.state[state] = value
|
|
52
|
+
} else {
|
|
53
|
+
Object.assign(this.state, state)
|
|
54
|
+
}
|
|
55
|
+
// @todo save state
|
|
56
|
+
return this.state
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register a command handler.
|
|
61
|
+
* @param {string} commandName - Name of the command to register
|
|
62
|
+
* @param {Function} handler - async function or sync function that accepts params and returns output
|
|
63
|
+
*/
|
|
64
|
+
registerCommand(commandName, handler) {
|
|
65
|
+
if (!typeOf(Function)(handler)) {
|
|
66
|
+
throw new TypeError("Handler must be a function")
|
|
67
|
+
}
|
|
68
|
+
this.commands.set(commandName, handler)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns a string representation of the app.
|
|
73
|
+
* @returns {string} String representation including name and state
|
|
74
|
+
*/
|
|
75
|
+
toString() {
|
|
76
|
+
return `${this.constructor.name}(name=${this.name}, state=${JSON.stringify(this.state)})`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Process a command message.
|
|
81
|
+
* @param {CommandMessage} commandMessage - Command to process
|
|
82
|
+
* @param {UI} ui - UI instance to use for rendering
|
|
83
|
+
* @returns {Promise<any>} Output of the command
|
|
84
|
+
* @throws {Error} If the command is not registered
|
|
85
|
+
*/
|
|
86
|
+
async processCommand(commandMessage, ui) {
|
|
87
|
+
const cmd = commandMessage.args[0]
|
|
88
|
+
const handler = this.commands.get(cmd)
|
|
89
|
+
if (!handler) {
|
|
90
|
+
throw new Error([
|
|
91
|
+
"Unknown command", ": ",
|
|
92
|
+
cmd, "\n",
|
|
93
|
+
"Available commands", ": ",
|
|
94
|
+
[...this.commands.keys()].join(", "),
|
|
95
|
+
].join(""))
|
|
96
|
+
}
|
|
97
|
+
const Class = /** @type {typeof CommandMessage} */ (commandMessage.constructor)
|
|
98
|
+
const command = Class.from({
|
|
99
|
+
args: commandMessage.args.slice(1),
|
|
100
|
+
opts: commandMessage.opts,
|
|
101
|
+
})
|
|
102
|
+
return await handler.apply(this, [command, ui])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Process an array of command messages sequentially.
|
|
107
|
+
* @param {CommandMessage[]} commandMessages - Array of commands to process
|
|
108
|
+
* @param {UI} ui - UI instance to use for rendering
|
|
109
|
+
* @returns {Promise<any[]>} Array of command outputs
|
|
110
|
+
*/
|
|
111
|
+
async processCommands(commandMessages, ui) {
|
|
112
|
+
const results = []
|
|
113
|
+
for (const cmdMsg of commandMessages) {
|
|
114
|
+
const result = await this.processCommand(cmdMsg, ui)
|
|
115
|
+
results.push(result)
|
|
116
|
+
}
|
|
117
|
+
return results
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Select a command to run. Must be implemented by subclasses.
|
|
122
|
+
* @param {UI} ui - UI instance for interaction
|
|
123
|
+
* @returns {Promise<string>} Command name to execute
|
|
124
|
+
* @throws {Error} Always thrown as this method must be implemented by subclasses
|
|
125
|
+
*/
|
|
126
|
+
async selectCommand(ui) {
|
|
127
|
+
throw new Error("Not implemented, must be implemented by subclass")
|
|
128
|
+
}
|
|
129
|
+
}
|