duocnv 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/index.js +400 -0
- package/package.json +40 -0
package/index.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { useState, useRef } from "react";
|
|
4
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
5
|
+
import SelectInput from "ink-select-input";
|
|
6
|
+
import gradient from "gradient-string";
|
|
7
|
+
import figlet from "figlet";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import { platform } from "os";
|
|
10
|
+
|
|
11
|
+
const coolGradient = gradient(["#00d4ff", "#7c3aed", "#f472b6"]);
|
|
12
|
+
|
|
13
|
+
// Terminal hyperlink (OSC 8)
|
|
14
|
+
const link = (url, text) => `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
15
|
+
|
|
16
|
+
// Open URL in browser safely using spawn
|
|
17
|
+
const openBrowser = (url) => {
|
|
18
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
19
|
+
const args = platform() === "win32" ? ["/c", "start", url] : [url];
|
|
20
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Konami code sequence
|
|
24
|
+
const KONAMI = ["up", "up", "down", "down", "left", "right", "left", "right", "b", "a"];
|
|
25
|
+
|
|
26
|
+
const links = {
|
|
27
|
+
github: "https://github.com/nguyenvanduocit",
|
|
28
|
+
twitter: "https://x.com/duocdev",
|
|
29
|
+
linkedin: "https://linkedin.com/in/duocnv",
|
|
30
|
+
blog: "https://12bit.vn",
|
|
31
|
+
website: "https://onepercent.plus",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const projectDetails = {
|
|
35
|
+
clik: {
|
|
36
|
+
title: "Clik - Screenshot Tool for AI Workflows",
|
|
37
|
+
stats: [
|
|
38
|
+
"5 annotation tools",
|
|
39
|
+
"Counter markers for AI reference",
|
|
40
|
+
"Multi-capture sessions",
|
|
41
|
+
"Keyboard-first, ~15MB",
|
|
42
|
+
],
|
|
43
|
+
tech: "Tauri + Rust + React",
|
|
44
|
+
url: "https://clik.aiocean.io",
|
|
45
|
+
},
|
|
46
|
+
justread: {
|
|
47
|
+
title: "Just Read - AI-Powered Reading Platform",
|
|
48
|
+
stats: [
|
|
49
|
+
"Smart translation for learners",
|
|
50
|
+
"EPUB/PDF support",
|
|
51
|
+
"AI-assisted comprehension",
|
|
52
|
+
"Vocabulary building",
|
|
53
|
+
],
|
|
54
|
+
tech: "TypeScript, AI/ML",
|
|
55
|
+
url: "https://aiocean.io",
|
|
56
|
+
},
|
|
57
|
+
obsidian: {
|
|
58
|
+
title: "Obsidian Open Gate - Web Integration Plugin",
|
|
59
|
+
stats: ["35K+ installations", "211 GitHub stars", "Embed any webpage", "Zero-config setup"],
|
|
60
|
+
tech: "TypeScript, Obsidian API",
|
|
61
|
+
url: "https://github.com/nguyenvanduocit/obsidian-open-gate",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Konami code hook
|
|
66
|
+
function useKonamiCode(onActivate) {
|
|
67
|
+
const sequence = useRef([]);
|
|
68
|
+
|
|
69
|
+
useInput((input, key) => {
|
|
70
|
+
let pressed = null;
|
|
71
|
+
if (key.upArrow) pressed = "up";
|
|
72
|
+
else if (key.downArrow) pressed = "down";
|
|
73
|
+
else if (key.leftArrow) pressed = "left";
|
|
74
|
+
else if (key.rightArrow) pressed = "right";
|
|
75
|
+
else if (input === "b") pressed = "b";
|
|
76
|
+
else if (input === "a") pressed = "a";
|
|
77
|
+
|
|
78
|
+
if (pressed) {
|
|
79
|
+
sequence.current.push(pressed);
|
|
80
|
+
if (sequence.current.length > KONAMI.length) {
|
|
81
|
+
sequence.current.shift();
|
|
82
|
+
}
|
|
83
|
+
if (sequence.current.join(",") === KONAMI.join(",")) {
|
|
84
|
+
sequence.current = [];
|
|
85
|
+
onActivate();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Header() {
|
|
92
|
+
const logoText = figlet.textSync("DUOC NV", { font: "ANSI Shadow", horizontalLayout: "fitted" });
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Box flexDirection="column">
|
|
96
|
+
<Text> </Text>
|
|
97
|
+
<Text>{coolGradient(logoText)}</Text>
|
|
98
|
+
<Text dimColor> Tech Entrepreneur • Community Builder • Knowledge Sharer</Text>
|
|
99
|
+
<Text> </Text>
|
|
100
|
+
<Text>
|
|
101
|
+
{" "}
|
|
102
|
+
<Text color="cyan" bold>
|
|
103
|
+
13+
|
|
104
|
+
</Text>{" "}
|
|
105
|
+
<Text dimColor>years coding</Text>
|
|
106
|
+
<Text dimColor> • </Text>
|
|
107
|
+
<Text color="cyan" bold>
|
|
108
|
+
5K+
|
|
109
|
+
</Text>{" "}
|
|
110
|
+
<Text dimColor>commits</Text>
|
|
111
|
+
<Text dimColor> • </Text>
|
|
112
|
+
<Text color="cyan" bold>
|
|
113
|
+
425
|
|
114
|
+
</Text>{" "}
|
|
115
|
+
<Text dimColor>repositories</Text>
|
|
116
|
+
<Text dimColor> • </Text>
|
|
117
|
+
<Text color="cyan" bold>
|
|
118
|
+
1.8K
|
|
119
|
+
</Text>{" "}
|
|
120
|
+
<Text dimColor>GitHub stars</Text>
|
|
121
|
+
</Text>
|
|
122
|
+
<Text> </Text>
|
|
123
|
+
<Text dimColor italic>
|
|
124
|
+
{" "}
|
|
125
|
+
"Leave the world better than we found it"
|
|
126
|
+
</Text>
|
|
127
|
+
<Text> </Text>
|
|
128
|
+
<Text dimColor> ─────────────────────────────────────────────────────────────────</Text>
|
|
129
|
+
<Text> </Text>
|
|
130
|
+
<Text>
|
|
131
|
+
{" "}
|
|
132
|
+
<Text bold>Hi, I'm Được Nguyễn</Text>{" "}
|
|
133
|
+
<Text dimColor>- Tech Ecosystem Builder from Vietnam</Text>
|
|
134
|
+
</Text>
|
|
135
|
+
<Text> </Text>
|
|
136
|
+
<Text dimColor> I build developer tools and production systems at scale.</Text>
|
|
137
|
+
<Text dimColor> Currently focused on AI-powered productivity tools:</Text>
|
|
138
|
+
<Text dimColor>
|
|
139
|
+
{" "}
|
|
140
|
+
Clik (screenshot annotations for AI) and Just Read (AI reading assistant).
|
|
141
|
+
</Text>
|
|
142
|
+
<Text> </Text>
|
|
143
|
+
<Text dimColor> Co-founded Vue.js Vietnam community. I believe in building tools</Text>
|
|
144
|
+
<Text dimColor> that make developers more productive and leaving the world better.</Text>
|
|
145
|
+
<Text> </Text>
|
|
146
|
+
<Text>
|
|
147
|
+
{" "}
|
|
148
|
+
<Text color="cyan">→</Text> <Text dimColor>GitHub:</Text>{" "}
|
|
149
|
+
<Text color="cyan">{link(links.github, "@nguyenvanduocit")}</Text>
|
|
150
|
+
</Text>
|
|
151
|
+
<Text>
|
|
152
|
+
{" "}
|
|
153
|
+
<Text color="cyan">→</Text> <Text dimColor>Twitter:</Text>{" "}
|
|
154
|
+
<Text color="cyan">{link(links.twitter, "@duocdev")}</Text>
|
|
155
|
+
</Text>
|
|
156
|
+
<Text>
|
|
157
|
+
{" "}
|
|
158
|
+
<Text color="cyan">→</Text> <Text dimColor>Blog:</Text>{" "}
|
|
159
|
+
<Text color="cyan">{link(links.blog, "12bit.vn")}</Text>
|
|
160
|
+
</Text>
|
|
161
|
+
<Text> </Text>
|
|
162
|
+
</Box>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function HintBar({ hints }) {
|
|
167
|
+
return (
|
|
168
|
+
<Box flexDirection="column" marginTop={1}>
|
|
169
|
+
<Text dimColor> {hints}</Text>
|
|
170
|
+
<Text> </Text>
|
|
171
|
+
</Box>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function EasterEgg({ onClose }) {
|
|
176
|
+
useInput((_, key) => {
|
|
177
|
+
if (key.escape || key.return) onClose();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const art = `
|
|
181
|
+
╔══════════════════════════════════════════╗
|
|
182
|
+
║ ║
|
|
183
|
+
║ 🎮 KONAMI CODE ACTIVATED! 🎮 ║
|
|
184
|
+
║ ║
|
|
185
|
+
║ You found the secret! ║
|
|
186
|
+
║ ║
|
|
187
|
+
║ Fun fact: I've been coding since ║
|
|
188
|
+
║ 2011 and still use "console.log" ║
|
|
189
|
+
║ for debugging. Some things never ║
|
|
190
|
+
║ change. 😄 ║
|
|
191
|
+
║ ║
|
|
192
|
+
║ Thanks for exploring my card! ║
|
|
193
|
+
║ ║
|
|
194
|
+
╚══════════════════════════════════════════╝
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
199
|
+
<Text color="magenta">{art}</Text>
|
|
200
|
+
<HintBar hints="Press any key to continue" />
|
|
201
|
+
</Box>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function ProjectDetail({ project, onBack }) {
|
|
206
|
+
const detail = projectDetails[project];
|
|
207
|
+
|
|
208
|
+
useInput((_, key) => {
|
|
209
|
+
if (key.escape) onBack();
|
|
210
|
+
if (key.return) openBrowser(detail.url);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
215
|
+
<Text> </Text>
|
|
216
|
+
<Text color="cyan" bold>
|
|
217
|
+
{detail.title}
|
|
218
|
+
</Text>
|
|
219
|
+
<Text> </Text>
|
|
220
|
+
{detail.stats.map((stat, i) => (
|
|
221
|
+
<Text key={i}>
|
|
222
|
+
{" "}
|
|
223
|
+
• <Text color="yellow">{stat}</Text>
|
|
224
|
+
</Text>
|
|
225
|
+
))}
|
|
226
|
+
<Text> </Text>
|
|
227
|
+
<Text dimColor> {detail.tech}</Text>
|
|
228
|
+
<Text> </Text>
|
|
229
|
+
<Text>
|
|
230
|
+
{" "}
|
|
231
|
+
<Text color="cyan">{link(detail.url, detail.url)}</Text>
|
|
232
|
+
</Text>
|
|
233
|
+
<HintBar hints="Enter to open in browser · Esc to back" />
|
|
234
|
+
</Box>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function LinkDetail({ platform: plat, onBack }) {
|
|
239
|
+
const url = links[plat];
|
|
240
|
+
|
|
241
|
+
useState(() => {
|
|
242
|
+
openBrowser(url);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
useInput((_, key) => {
|
|
246
|
+
if (key.escape) onBack();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
251
|
+
<Text> </Text>
|
|
252
|
+
<Text color="green">✓ Opened in browser!</Text>
|
|
253
|
+
<Text> </Text>
|
|
254
|
+
<Text color="cyan">{link(url, url)}</Text>
|
|
255
|
+
<HintBar hints="Esc to back" />
|
|
256
|
+
</Box>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function MainMenu({ onSelect, onEasterEgg }) {
|
|
261
|
+
const items = [
|
|
262
|
+
{ label: "View projects", value: "projects", hint: "Things I've built" },
|
|
263
|
+
{ label: "Connect with me", value: "connect", hint: "Social links" },
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const { exit } = useApp();
|
|
267
|
+
|
|
268
|
+
useKonamiCode(onEasterEgg);
|
|
269
|
+
|
|
270
|
+
useInput((_, key) => {
|
|
271
|
+
if (key.escape) exit();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<Box flexDirection="column">
|
|
276
|
+
<SelectInput
|
|
277
|
+
items={items}
|
|
278
|
+
onSelect={(item) => onSelect(item.value)}
|
|
279
|
+
itemComponent={({ isSelected, label, hint }) => (
|
|
280
|
+
<Text>
|
|
281
|
+
<Text color={isSelected ? "cyan" : undefined}>{label}</Text>
|
|
282
|
+
{hint && <Text dimColor> · {hint}</Text>}
|
|
283
|
+
</Text>
|
|
284
|
+
)}
|
|
285
|
+
/>
|
|
286
|
+
<HintBar hints="↑/↓ to select · Enter to confirm · Esc to exit" />
|
|
287
|
+
</Box>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function ProjectsMenu({ onSelect, onBack }) {
|
|
292
|
+
const items = [
|
|
293
|
+
{ label: "Clik", value: "clik", hint: "Screenshot tool for AI workflows • Tauri + Rust" },
|
|
294
|
+
{ label: "Just Read", value: "justread", hint: "AI-powered reading • Smart translation" },
|
|
295
|
+
{ label: "Obsidian Open Gate", value: "obsidian", hint: "35K+ installations • 211 stars" },
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
useInput((_, key) => {
|
|
299
|
+
if (key.escape) onBack();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<Box flexDirection="column">
|
|
304
|
+
<Text dimColor> Projects</Text>
|
|
305
|
+
<Text> </Text>
|
|
306
|
+
<SelectInput
|
|
307
|
+
items={items}
|
|
308
|
+
onSelect={(item) => onSelect(item.value)}
|
|
309
|
+
itemComponent={({ isSelected, label, hint }) => (
|
|
310
|
+
<Text>
|
|
311
|
+
<Text color={isSelected ? "cyan" : undefined}>{label}</Text>
|
|
312
|
+
{hint && <Text dimColor> · {hint}</Text>}
|
|
313
|
+
</Text>
|
|
314
|
+
)}
|
|
315
|
+
/>
|
|
316
|
+
<HintBar hints="↑/↓ to select · Enter to confirm · Esc to back" />
|
|
317
|
+
</Box>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function ConnectMenu({ onSelect, onBack }) {
|
|
322
|
+
const items = [
|
|
323
|
+
{ label: "GitHub", value: "github", hint: "@nguyenvanduocit" },
|
|
324
|
+
{ label: "X (Twitter)", value: "twitter", hint: "@duocdev" },
|
|
325
|
+
{ label: "LinkedIn", value: "linkedin", hint: "/in/duocnv" },
|
|
326
|
+
{ label: "Blog", value: "blog", hint: "12bit.vn" },
|
|
327
|
+
{ label: "Website", value: "website", hint: "onepercent.plus" },
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
useInput((_, key) => {
|
|
331
|
+
if (key.escape) onBack();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<Box flexDirection="column">
|
|
336
|
+
<Text dimColor> Connect</Text>
|
|
337
|
+
<Text> </Text>
|
|
338
|
+
<SelectInput
|
|
339
|
+
items={items}
|
|
340
|
+
onSelect={(item) => onSelect(item.value)}
|
|
341
|
+
itemComponent={({ isSelected, label, hint }) => (
|
|
342
|
+
<Text>
|
|
343
|
+
<Text color={isSelected ? "cyan" : undefined}>{label}</Text>
|
|
344
|
+
{hint && <Text dimColor> · {hint}</Text>}
|
|
345
|
+
</Text>
|
|
346
|
+
)}
|
|
347
|
+
/>
|
|
348
|
+
<HintBar hints="↑/↓ to select · Enter to open in browser · Esc to back" />
|
|
349
|
+
</Box>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function App() {
|
|
354
|
+
const [screen, setScreen] = useState("main");
|
|
355
|
+
const [selectedProject, setSelectedProject] = useState(null);
|
|
356
|
+
const [selectedPlatform, setSelectedPlatform] = useState(null);
|
|
357
|
+
const [showEasterEgg, setShowEasterEgg] = useState(false);
|
|
358
|
+
|
|
359
|
+
const handleMainSelect = (value) => setScreen(value);
|
|
360
|
+
const handleProjectSelect = (value) => {
|
|
361
|
+
setSelectedProject(value);
|
|
362
|
+
setScreen("project-detail");
|
|
363
|
+
};
|
|
364
|
+
const handleConnectSelect = (value) => {
|
|
365
|
+
setSelectedPlatform(value);
|
|
366
|
+
setScreen("connect-detail");
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const goToMain = () => setScreen("main");
|
|
370
|
+
const goToProjects = () => setScreen("projects");
|
|
371
|
+
const goToConnect = () => setScreen("connect");
|
|
372
|
+
|
|
373
|
+
if (showEasterEgg) {
|
|
374
|
+
return (
|
|
375
|
+
<Box flexDirection="column">
|
|
376
|
+
<Header />
|
|
377
|
+
<EasterEgg onClose={() => setShowEasterEgg(false)} />
|
|
378
|
+
</Box>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<Box flexDirection="column">
|
|
384
|
+
<Header />
|
|
385
|
+
{screen === "main" && (
|
|
386
|
+
<MainMenu onSelect={handleMainSelect} onEasterEgg={() => setShowEasterEgg(true)} />
|
|
387
|
+
)}
|
|
388
|
+
{screen === "projects" && <ProjectsMenu onSelect={handleProjectSelect} onBack={goToMain} />}
|
|
389
|
+
{screen === "connect" && <ConnectMenu onSelect={handleConnectSelect} onBack={goToMain} />}
|
|
390
|
+
{screen === "project-detail" && (
|
|
391
|
+
<ProjectDetail project={selectedProject} onBack={goToProjects} />
|
|
392
|
+
)}
|
|
393
|
+
{screen === "connect-detail" && (
|
|
394
|
+
<LinkDetail platform={selectedPlatform} onBack={goToConnect} />
|
|
395
|
+
)}
|
|
396
|
+
</Box>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
render(<App />);
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "duocnv",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Terminal business card for Được Nguyễn - Tech Ecosystem Builder",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bunx",
|
|
7
|
+
"business-card",
|
|
8
|
+
"cli",
|
|
9
|
+
"npx",
|
|
10
|
+
"terminal"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/nguyenvanduocit/duocnv#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nguyenvanduocit/duocnv/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Được Nguyễn <duoc@aiocean.io>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/nguyenvanduocit/duocnv.git"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"duocnv": "index.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"index.js"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "index.js",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"start": "bun index.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"figlet": "^1.9.4",
|
|
35
|
+
"gradient-string": "^3.0.0",
|
|
36
|
+
"ink": "^6.6.0",
|
|
37
|
+
"ink-select-input": "^6.2.0",
|
|
38
|
+
"react": "19"
|
|
39
|
+
}
|
|
40
|
+
}
|