Skip to content

Commit 411a3eb

Browse files
committed
feat: simplify rating icon picker to 5 curated icons and remove icon search
1 parent 7268f96 commit 411a3eb

File tree

5 files changed

+53
-69
lines changed

5 files changed

+53
-69
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"fastify": "^5.7.4",
4242
"highlight.js": "^11.11.1",
4343
"lucide-react": "^0.562.0",
44-
"lucide-static": "^0.574.0",
4544
"marked": "^17.0.3",
4645
"pnpm": "^10.30.0",
4746
"react": "^19.2.4",

pnpm-lock.yaml

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/features/form/components/AdminFormDetailPages/components/SectionEditor/RangeQuestion.module.css

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,26 @@
2222
max-height: 14rem;
2323
}
2424

25-
.iconSearchWrapper {
25+
.iconSelected {
26+
margin-top: 0.75rem;
2627
display: flex;
2728
align-items: center;
2829
gap: 0.5rem;
30+
font-size: 0.875rem;
31+
color: var(--color-caption);
2932
}
3033

31-
.iconSearchIcon {
32-
color: var(--color-caption);
34+
.iconSelectedPreview {
35+
max-width: 1rem;
36+
max-height: 1rem;
37+
}
38+
39+
.iconSelectedName {
40+
font-size: 0.75rem;
41+
padding: 0.0625rem 0.25rem;
42+
border-radius: 0.25rem;
43+
background-color: var(--background-color-secondary);
44+
color: var(--foreground);
3345
}
3446

3547
.iconGrid {

src/features/form/components/AdminFormDetailPages/components/SectionEditor/RangeQuestion.tsx

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Question } from "@/features/form/components/AdminFormDetailPages/types/question";
22
import { InlineSvg, Input } from "@/shared/components";
3-
import { Search } from "lucide-react";
4-
import { useEffect, useMemo, useState } from "react";
53
import { OptionsInput } from "./OptionsInput";
64
import styles from "./RangeQuestion.module.css";
75

6+
const ADMIN_RATING_ICON_OPTIONS = ["star", "heart", "thumbs-up", "smile", "trophy"] as const;
7+
88
export interface RangeQuestionProps {
99
start: number;
1010
end: number;
@@ -20,36 +20,10 @@ export interface RangeQuestionProps {
2020
}
2121

2222
export const RangeQuestion = (props: RangeQuestionProps) => {
23-
const [iconKeyword, setIconKeyword] = useState("");
24-
const [iconNames, setIconNames] = useState<string[]>([]);
25-
26-
useEffect(() => {
27-
let isMounted = true;
28-
fetch("/icons/lucide/names.json")
29-
.then(res => (res.ok ? res.json() : []))
30-
.then((names: unknown) => {
31-
if (!isMounted) return;
32-
if (Array.isArray(names)) {
33-
setIconNames(names.filter((name): name is string => typeof name === "string"));
34-
}
35-
})
36-
.catch(() => {
37-
if (isMounted) setIconNames([]);
38-
});
39-
40-
return () => {
41-
isMounted = false;
42-
};
43-
}, []);
44-
45-
const filteredIcons = useMemo(() => {
46-
const keyword = iconKeyword.trim().toLowerCase();
47-
if (!keyword) return iconNames;
48-
return iconNames.filter(icon => icon.includes(keyword));
49-
}, [iconKeyword, iconNames]);
5023
const rangeWarning = props.start >= props.end ? "開始值必須小於結束值" : null;
5124
const startWarning = props.start < 0 || props.start > 1 ? "開始值需為 0 或 1" : null;
5225
const endWarning = props.end < 2 || props.end > 10 ? "結束值需介於 2 到 10" : null;
26+
const selectedIconName = props.icon ?? ADMIN_RATING_ICON_OPTIONS[0];
5327

5428
return (
5529
<>
@@ -61,12 +35,13 @@ export const RangeQuestion = (props: RangeQuestionProps) => {
6135
{(rangeWarning || startWarning || endWarning) && <p className={styles.warning}>{rangeWarning ?? startWarning ?? endWarning}</p>}
6236
{props.hasIcon && (
6337
<div className={styles.iconPickerSection}>
64-
<div className={styles.iconSearchWrapper}>
65-
<Search size={16} className={styles.iconSearchIcon} />
66-
<Input value={iconKeyword} onChange={event => setIconKeyword(event.target.value)} placeholder="搜尋圖示" variant="flushed" themeColor="--comment" />
38+
<div className={styles.iconSelected}>
39+
<span>選擇圖示:</span>
40+
<InlineSvg name={selectedIconName} filled size={18} className={styles.iconSelectedPreview} />
41+
<code className={styles.iconSelectedName}>{selectedIconName}</code>
6742
</div>
6843
<div className={styles.iconGrid}>
69-
{filteredIcons.map(iconName => (
44+
{ADMIN_RATING_ICON_OPTIONS.map(iconName => (
7045
<button
7146
key={iconName}
7247
type="button"
Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useEffect, useState } from "react";
1+
import type { LucideIcon } from "lucide-react";
2+
import { Heart, Smile, Star, ThumbsUp, Trophy } from "lucide-react";
23
import styles from "./InlineSvg.module.css";
34

4-
const svgCache = new Map<string, string>();
5+
const iconComponentCache = new Map<string, LucideIcon | null>();
56

67
interface InlineSvgProps {
78
name: string;
@@ -10,32 +11,31 @@ interface InlineSvgProps {
1011
className?: string;
1112
}
1213

13-
export const InlineSvg = ({ name, filled, size = 24, className }: InlineSvgProps) => {
14-
const [svg, setSvg] = useState<string>(() => svgCache.get(name) || "");
15-
16-
useEffect(() => {
17-
if (svgCache.has(name)) {
18-
return;
19-
}
20-
21-
let mounted = true;
22-
23-
fetch(`/icons/lucide/${name}.svg`)
24-
.then(res => res.text())
25-
.then(data => {
26-
if (!mounted) return;
27-
svgCache.set(name, data);
28-
setSvg(data);
29-
});
30-
31-
return () => {
32-
mounted = false;
33-
};
34-
}, [name]);
35-
36-
if (!svg) return null;
37-
38-
const processed = svg.replace("<svg", `<svg width="${size}" height="${size}" fill="${filled ? "currentColor" : "none"}" stroke="currentColor" class="${className ?? ""}"`);
14+
const ICON_MAP = {
15+
star: Star,
16+
heart: Heart,
17+
"thumbs-up": ThumbsUp,
18+
smile: Smile,
19+
trophy: Trophy
20+
} as const;
21+
22+
const resolveIconComponent = (name: string): LucideIcon | null => {
23+
if (iconComponentCache.has(name)) {
24+
return iconComponentCache.get(name) ?? null;
25+
}
26+
27+
const component = (ICON_MAP as Record<string, LucideIcon | undefined>)[name] ?? null;
28+
iconComponentCache.set(name, component);
29+
return component;
30+
};
3931

40-
return <span dangerouslySetInnerHTML={{ __html: processed }} className={styles.wrapper} />;
32+
export const InlineSvg = ({ name, filled, size = 24, className }: InlineSvgProps) => {
33+
const Icon = resolveIconComponent(name);
34+
if (!Icon) return null;
35+
36+
return (
37+
<span className={styles.wrapper}>
38+
<Icon size={size} fill={filled ? "currentColor" : "none"} stroke="currentColor" className={className} />
39+
</span>
40+
);
4141
};

0 commit comments

Comments
 (0)