Skip to content

Commit fd88fa9

Browse files
wontoryclaude
andcommitted
feat(web): add badge UI components
Add BadgePreview, BadgeCustomize, BadgeHighlightToggle, and BadgeSelectedIcon components for badge customization interface. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dca27b5 commit fd88fa9

4 files changed

Lines changed: 206 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
'use client'
2+
3+
import { Input } from '@tech-stack/ui/components/input'
4+
import { Label } from '@tech-stack/ui/components/label'
5+
import { CheckIcon } from 'lucide-react'
6+
7+
import { BadgeHighlightToggle } from '#components/badge-highlight-toggle'
8+
import { BadgeSelectedIcon } from '#components/badge-selected-icon'
9+
import { Customize } from '#components/customize'
10+
import { IconSearch } from '#components/icon-search'
11+
import { TextInput } from '#components/text-input'
12+
import { useBadgeState } from '#stores/badge-context'
13+
14+
export function BadgeCustomize() {
15+
const {
16+
slug,
17+
setSlug,
18+
text,
19+
setText,
20+
textColor,
21+
setTextColor,
22+
iconColor,
23+
setIconColor,
24+
bgColor,
25+
setBgColor,
26+
} = useBadgeState()
27+
28+
return (
29+
<Customize>
30+
<div className="grid w-full items-center gap-2">
31+
<Label htmlFor="text">Text</Label>
32+
<TextInput id="text" text={text} setText={setText} />
33+
</div>
34+
<div className="grid w-full items-center gap-2">
35+
<Label htmlFor="bgColor">Badge Color</Label>
36+
<div className="flex gap-2">
37+
<input
38+
type="color"
39+
id="bgColor"
40+
value={`#${bgColor || 'CCD2D9'}`}
41+
onChange={(e) => setBgColor(e.target.value.replace('#', ''))}
42+
className="h-9 w-9 shrink-0 cursor-pointer rounded-md border"
43+
/>
44+
<Input
45+
type="text"
46+
value={bgColor}
47+
onChange={(e) => setBgColor(e.target.value.replace('#', ''))}
48+
placeholder="Default"
49+
className="w-full"
50+
/>
51+
</div>
52+
</div>
53+
<div className="grid w-full grid-cols-2 gap-2">
54+
<div className="grid items-center gap-2">
55+
<Label htmlFor="textColor">Text Color</Label>
56+
<div className="flex gap-2">
57+
<input
58+
type="color"
59+
id="textColor"
60+
value={`#${textColor || '000000'}`}
61+
onChange={(e) => setTextColor(e.target.value.replace('#', ''))}
62+
className="h-9 w-9 shrink-0 cursor-pointer rounded-md border"
63+
/>
64+
<Input
65+
type="text"
66+
value={textColor}
67+
onChange={(e) => setTextColor(e.target.value.replace('#', ''))}
68+
placeholder="000000"
69+
className="w-full"
70+
/>
71+
</div>
72+
</div>
73+
<div className="grid items-center gap-2">
74+
<Label htmlFor="iconColor">Icon Color</Label>
75+
<div className="flex gap-2">
76+
<input
77+
type="color"
78+
id="iconColor"
79+
value={`#${iconColor || '000000'}`}
80+
onChange={(e) => setIconColor(e.target.value.replace('#', ''))}
81+
className="h-9 w-9 shrink-0 cursor-pointer rounded-md border"
82+
/>
83+
<Input
84+
type="text"
85+
value={iconColor}
86+
onChange={(e) => setIconColor(e.target.value.replace('#', ''))}
87+
placeholder="Icon default"
88+
className="w-full"
89+
/>
90+
</div>
91+
</div>
92+
</div>
93+
<div className="grid w-full items-center gap-2">
94+
<Label>Highlight</Label>
95+
<BadgeHighlightToggle />
96+
</div>
97+
<div className="grid w-full items-center gap-2">
98+
<Label>Selected Icon</Label>
99+
<BadgeSelectedIcon />
100+
</div>
101+
<div className="grid w-full items-center gap-2">
102+
<Label htmlFor="search">Search Icons</Label>
103+
<IconSearch
104+
id="search"
105+
onIconClick={(iconSlug) => setSlug(slug === iconSlug ? '' : iconSlug)}
106+
isSelected={(iconSlug) => slug === iconSlug}
107+
renderOverlay={(selected) =>
108+
selected ? (
109+
<CheckIcon
110+
className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 text-primary"
111+
size={32}
112+
/>
113+
) : null
114+
}
115+
/>
116+
</div>
117+
</Customize>
118+
)
119+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use client'
2+
3+
import { Button } from '@tech-stack/ui/components/button'
4+
import { SparklesIcon } from 'lucide-react'
5+
6+
import { useBadgeState } from '#stores/badge-context'
7+
8+
export function BadgeHighlightToggle() {
9+
const { highlight, setHighlight } = useBadgeState()
10+
11+
return (
12+
<Button
13+
variant={highlight ? 'default' : 'outline'}
14+
size="sm"
15+
onClick={() => setHighlight((prev) => !prev)}
16+
>
17+
<SparklesIcon className="size-4" />
18+
<span>{highlight ? 'Highlight On' : 'Highlight Off'}</span>
19+
</Button>
20+
)
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use client'
2+
3+
import { Preview } from '#components/preview'
4+
import { useBadgeState } from '#stores/badge-context'
5+
import { constructBadgeUrl } from '#utils/construct-badge-url'
6+
7+
export function BadgePreview() {
8+
const { slug, text, highlight, textColor, iconColor, bgColor } =
9+
useBadgeState()
10+
11+
const svgSrc = constructBadgeUrl(
12+
slug,
13+
text,
14+
highlight,
15+
textColor,
16+
iconColor,
17+
bgColor,
18+
)
19+
const mdCode = `![${text || 'badge'}](${process.env.NEXT_PUBLIC_API_BASE_URL}${svgSrc})`
20+
21+
return <Preview src={svgSrc} code={mdCode} variant="center" />
22+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client'
2+
3+
import { XIcon } from 'lucide-react'
4+
5+
import { useBadgeState } from '#stores/badge-context'
6+
import { colorizeIconSvg, resolveSimpleIcon } from '#utils/simple-icon'
7+
8+
export function BadgeSelectedIcon() {
9+
const { slug, setSlug } = useBadgeState()
10+
11+
if (!slug) {
12+
return <div className="text-muted-foreground text-xs">No icon selected</div>
13+
}
14+
15+
const icon = resolveSimpleIcon(slug)
16+
17+
if (!icon) {
18+
return <div className="text-muted-foreground text-xs">Icon not found</div>
19+
}
20+
21+
const { svg, hex } = icon
22+
23+
return (
24+
<div className="flex w-full flex-wrap gap-2">
25+
<button
26+
type="button"
27+
onClick={() => setSlug('')}
28+
className="group relative cursor-pointer p-2"
29+
>
30+
<div
31+
// biome-ignore lint/security/noDangerouslySetInnerHtml: simple-icons
32+
dangerouslySetInnerHTML={{
33+
__html: colorizeIconSvg(svg, hex),
34+
}}
35+
className="size-6 drop-shadow-svg transition-opacity duration-200 group-hover:opacity-50"
36+
/>
37+
<XIcon
38+
className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 opacity-0 transition-opacity duration-200 group-hover:opacity-80"
39+
size={12}
40+
/>
41+
</button>
42+
</div>
43+
)
44+
}

0 commit comments

Comments
 (0)