Skip to content

Commit c77792d

Browse files
refactor: put everything under the /source file
1 parent 2cdce1b commit c77792d

22 files changed

+1220
-0
lines changed

source/components/Ask.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Box, Text } from 'ink'
2+
import TextInput from 'ink-text-input'
3+
import React, { type FC, useMemo, useState } from 'react'
4+
import { isAnswerConfirmed } from '../utils/utils.js'
5+
6+
interface Props {
7+
answer?: string
8+
errorMessage?: string
9+
onSubmit: (value: string) => void
10+
question: string
11+
tip?: string
12+
placeholder?: string
13+
}
14+
15+
const Ask: FC<Props> = ({ question, onSubmit, answer, tip, errorMessage, placeholder }) => {
16+
const [input, setInput] = useState('')
17+
const answered = useMemo(() => isAnswerConfirmed(answer, errorMessage), [answer, errorMessage])
18+
19+
return (
20+
<Box
21+
flexDirection={'column'}
22+
rowGap={1}
23+
>
24+
<Box flexDirection={'column'}>
25+
<Box>
26+
<Text color={'whiteBright'}>{question}: </Text>
27+
{answered ? (
28+
<Text
29+
bold
30+
color={'green'}
31+
>
32+
{answer}
33+
</Text>
34+
) : (
35+
<TextInput
36+
onChange={setInput}
37+
onSubmit={onSubmit}
38+
placeholder={placeholder}
39+
value={input}
40+
/>
41+
)}
42+
</Box>
43+
{tip && <Text color={'gray'}>{tip}</Text>}
44+
</Box>
45+
{errorMessage && (
46+
<Text
47+
bold
48+
color={'red'}
49+
>
50+
{errorMessage}
51+
</Text>
52+
)}
53+
</Box>
54+
)
55+
}
56+
57+
export default Ask

source/components/Divider.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import BaseDivider from 'ink-divider'
2+
import React, { type FC } from 'react'
3+
4+
const Divider: FC<{ title: string }> = ({ title }) => (
5+
<BaseDivider
6+
titlePadding={2}
7+
titleColor={'whiteBright'}
8+
title={title}
9+
/>
10+
)
11+
12+
export default Divider

source/components/MainTitle.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import BigText from 'ink-big-text'
2+
import Gradient from 'ink-gradient'
3+
import React, { type FC } from 'react'
4+
5+
const MainTitle: FC = () => (
6+
<Gradient colors={['#ff438c', '#bb1d79', '#8b46a4', '#6a2581']}>
7+
<BigText
8+
lineHeight={1}
9+
font={'chrome'}
10+
text="dAppBooster"
11+
/>
12+
</Gradient>
13+
)
14+
15+
export default MainTitle
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Box, useInput } from 'ink'
2+
import React, { useCallback, useState } from 'react'
3+
import CheckBox from './components/Checkbox.js'
4+
import Indicator from './components/Indicator.js'
5+
import ItemComponent from './components/Item.js'
6+
7+
type Item<T> = {
8+
label: string
9+
value: T
10+
key?: string | number
11+
}
12+
13+
type MultiSelectProps<T> = {
14+
items: Item<T>[]
15+
defaultSelected?: Item<T>[]
16+
focus?: boolean
17+
initialIndex?: number
18+
indicatorComponent?: React.FC<{ isHighlighted: boolean }>
19+
checkboxComponent?: React.FC<{ isSelected: boolean }>
20+
itemComponent?: React.FC<{ isHighlighted: boolean; label: string }>
21+
limit?: number | null
22+
onSelect?: (selectedItem: Item<T>) => void
23+
onUnselect?: (unselectedItem: Item<T>) => void
24+
onSubmit?: (selectedItems: Item<T>[]) => void
25+
onHighlight?: (highlightedItem: Item<T>) => void
26+
}
27+
28+
const MultiSelect = <T,>({
29+
items = [],
30+
defaultSelected = [],
31+
focus = true,
32+
initialIndex = 0,
33+
indicatorComponent = Indicator,
34+
checkboxComponent = CheckBox,
35+
itemComponent = ItemComponent,
36+
limit = null,
37+
onSelect = () => {},
38+
onUnselect = () => {},
39+
onSubmit = () => {},
40+
onHighlight = () => {},
41+
}: MultiSelectProps<T>) => {
42+
const [highlightedIndex, setHighlightedIndex] = useState(initialIndex)
43+
const [selectedItems, setSelectedItems] = useState(defaultSelected)
44+
45+
const hasLimit = limit !== null && limit < items.length
46+
47+
const slicedItems = hasLimit ? items.slice(0, limit) : items
48+
49+
const includesItems = useCallback((item: Item<T>, selectedItems: Item<T>[]) => {
50+
return (
51+
selectedItems.filter(
52+
(selectedItem) => selectedItem.value === item.value && selectedItem.label === item.label,
53+
).length > 0
54+
)
55+
}, [])
56+
57+
const handleSelect = useCallback(
58+
(item: Item<T>) => {
59+
if (includesItems(item, selectedItems)) {
60+
const newSelectedItems = selectedItems.filter(
61+
(selectedItem) => selectedItem.value !== item.value && selectedItem.label !== item.label,
62+
)
63+
setSelectedItems(newSelectedItems)
64+
onUnselect(item)
65+
} else {
66+
const newSelectedItems = [...selectedItems, item]
67+
setSelectedItems(newSelectedItems)
68+
onSelect(item)
69+
}
70+
},
71+
[selectedItems, onSelect, onUnselect, includesItems],
72+
)
73+
74+
const handleSubmit = useCallback(() => {
75+
onSubmit(selectedItems)
76+
}, [selectedItems, onSubmit])
77+
78+
useInput(
79+
useCallback(
80+
(input, key) => {
81+
if (key.upArrow) {
82+
setHighlightedIndex((prevIndex) => {
83+
const index = prevIndex === 0 ? slicedItems.length - 1 : prevIndex - 1
84+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
85+
onHighlight(slicedItems[index]!)
86+
return index
87+
})
88+
} else if (key.downArrow) {
89+
setHighlightedIndex((prevIndex) => {
90+
const index = prevIndex === slicedItems.length - 1 ? 0 : prevIndex + 1
91+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
92+
onHighlight(slicedItems[index]!)
93+
return index
94+
})
95+
} else if (key.return) {
96+
handleSubmit()
97+
} else if (input === ' ') {
98+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
99+
handleSelect(slicedItems[highlightedIndex]!)
100+
}
101+
},
102+
[onHighlight, handleSelect, handleSubmit, slicedItems, highlightedIndex],
103+
),
104+
{ isActive: focus },
105+
)
106+
107+
return (
108+
<Box flexDirection="column">
109+
{slicedItems.map((item, index) => {
110+
const key = item.key || item.label
111+
const isHighlighted = index === highlightedIndex
112+
const isSelected = includesItems(item, selectedItems)
113+
114+
return (
115+
<Box key={key}>
116+
{React.createElement(indicatorComponent, { isHighlighted })}
117+
{React.createElement(checkboxComponent, { isSelected })}
118+
{React.createElement(itemComponent, {
119+
...item,
120+
isHighlighted,
121+
})}
122+
</Box>
123+
)
124+
})}
125+
</Box>
126+
)
127+
}
128+
129+
export default MultiSelect
130+
131+
export { Indicator, ItemComponent, CheckBox, type Item }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import figures from 'figures'
2+
import { Box, Text } from 'ink'
3+
import React from 'react'
4+
5+
type CheckBoxProps = {
6+
isSelected: boolean
7+
}
8+
9+
const CheckBox = ({ isSelected = false }: CheckBoxProps) => (
10+
<Box marginRight={1}>
11+
<Text color={isSelected ? 'green' : 'white'}>
12+
{isSelected ? figures.circleFilled : figures.circle}
13+
</Text>
14+
</Box>
15+
)
16+
17+
export default CheckBox
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import figures from 'figures'
2+
import { Box, Text } from 'ink'
3+
import React from 'react'
4+
5+
type IndicatorProps = {
6+
isHighlighted: boolean
7+
}
8+
9+
const Indicator = ({ isHighlighted = false }: IndicatorProps) => (
10+
<Box marginRight={1}>
11+
<Text color={isHighlighted ? 'green' : undefined}>{isHighlighted ? figures.pointer : ' '}</Text>
12+
</Box>
13+
)
14+
15+
export default Indicator
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Text } from 'ink'
2+
import React from 'react'
3+
4+
type ItemProps = {
5+
isHighlighted: boolean
6+
label: string
7+
}
8+
9+
const Item = ({ isHighlighted = false, label }: ItemProps) => (
10+
<Text
11+
color={isHighlighted ? 'green' : 'white'}
12+
bold={isHighlighted}
13+
>
14+
{label}
15+
</Text>
16+
)
17+
18+
export default Item
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default, type Item, Indicator, CheckBox, ItemComponent } from './MultiSelect.js'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React, { type FC } from 'react'
2+
import Divider from '../../Divider.js'
3+
import Commands from './Commands.js'
4+
5+
interface Props {
6+
projectName: string
7+
onCompletion: () => void
8+
}
9+
10+
/**
11+
* Step for cloning the repository.
12+
* @param projectName
13+
* @param onCompletion
14+
* @constructor
15+
*/
16+
const CloneRepo: FC<Props> = ({ projectName, onCompletion }) => (
17+
<>
18+
<Divider title={'Git tasks'} />
19+
<Commands
20+
projectName={projectName}
21+
onCompletion={onCompletion}
22+
/>
23+
</>
24+
)
25+
26+
export default CloneRepo
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { join } from 'node:path'
2+
import * as process from 'node:process'
3+
import { Box, Text } from 'ink'
4+
import { Script, Spawn } from 'ink-spawn'
5+
import React, { type FC } from 'react'
6+
import { repoUrl } from '../../../constants/config.js'
7+
8+
interface Props {
9+
projectName: string
10+
onCompletion: () => void
11+
}
12+
13+
/**
14+
* Executes all the commands to clone the dAppBooster repository.
15+
* @param projectName
16+
* @param onCompletion
17+
*/
18+
const Commands: FC<Props> = ({ projectName, onCompletion }) => {
19+
const projectFolder = join(process.cwd(), projectName)
20+
21+
return (
22+
<Box
23+
flexDirection={'column'}
24+
gap={0}
25+
>
26+
<Script>
27+
<Box columnGap={1}>
28+
<Text color={'whiteBright'}>Cloning dAppBooster in</Text>
29+
<Text italic>{projectName}</Text>
30+
</Box>
31+
<Spawn
32+
shell
33+
silent
34+
successText={'Done!'}
35+
failureText={`Failed to clone the project, check if a folder called "${projectName}" already exists and your read/write permissions...`}
36+
runningText={'Working...'}
37+
command="git"
38+
args={['clone', '--depth', '1', '--no-checkout', repoUrl, projectName]}
39+
/>
40+
<Text color={'whiteBright'}>Fetching tags</Text>
41+
<Spawn
42+
shell
43+
cwd={projectFolder}
44+
silent
45+
command={'git'}
46+
args={['fetch', '--tags']}
47+
runningText={'Working...'}
48+
successText={'Done!'}
49+
failureText={'Error...'}
50+
/>
51+
<Text color={'whiteBright'}>Checking out latest tag</Text>
52+
<Spawn
53+
shell
54+
cwd={projectFolder}
55+
command="git"
56+
args={['checkout $(git describe --tags `git rev-list --tags --max-count=1`)']}
57+
successText="Done!"
58+
failureText={'Error...'}
59+
/>
60+
<Text color={'whiteBright'}>Removing .git folder</Text>
61+
<Spawn
62+
shell
63+
cwd={projectFolder}
64+
command="rm"
65+
args={['-rf', '.git']}
66+
successText="Done!"
67+
failureText={'Error...'}
68+
/>
69+
<Text color={'whiteBright'}>Initializing Git repository</Text>
70+
<Spawn
71+
shell
72+
cwd={projectFolder}
73+
command="git"
74+
args={['init']}
75+
successText="Done!"
76+
failureText={'Error...'}
77+
onCompletion={onCompletion}
78+
/>
79+
</Script>
80+
</Box>
81+
)
82+
}
83+
84+
export default Commands

0 commit comments

Comments
 (0)