Skip to content

Commit 32c90c7

Browse files
committed
Icons in the grid block
1 parent 75f942d commit 32c90c7

File tree

12 files changed

+13226
-10384
lines changed

12 files changed

+13226
-10384
lines changed

packages/common-widgets/src/grid/admin-widget/index.tsx

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import React, { useEffect, useState } from "react";
4-
import Settings, { Item } from "../settings";
4+
import Settings, { Item, SvgStyle } from "../settings";
55
import ItemEditor from "./item-editor";
66
import { Address, Profile, Alignment } from "@courselit/common-models";
77
import {
@@ -15,12 +15,18 @@ import {
1515
ContentPaddingSelector,
1616
CssIdField,
1717
PageBuilderSlider,
18+
Tooltip,
19+
Checkbox,
20+
Button2,
1821
} from "@courselit/components-library";
1922
import {
2023
verticalPadding as defaultVerticalPadding,
2124
horizontalPadding as defaultHorizontalPadding,
2225
columns as defaultColumns,
2326
} from "../defaults";
27+
import { Help } from "@courselit/icons";
28+
import { WandSparkles } from "lucide-react";
29+
import SvgStyleEditor from "./svg-style-editor";
2430

2531
export interface AdminWidgetProps {
2632
settings: Settings;
@@ -122,6 +128,20 @@ export default function AdminWidget({
122128
);
123129
const [cssId, setCssId] = useState(settings.cssId);
124130
const [columns, setColumns] = useState(settings.columns || defaultColumns);
131+
const [svgStyle, setSvgStyle] = useState<SvgStyle>(
132+
settings.svgStyle || {
133+
width: 36,
134+
height: 36,
135+
svgColor: "#000000",
136+
backgroundColor: "#ffffff",
137+
borderRadius: 8,
138+
borderWidth: 1,
139+
borderStyle: "solid",
140+
borderColor: "#e2e8f0",
141+
},
142+
);
143+
const [svgInline, setSvgInline] = useState(settings.svgInline || false);
144+
const [editingSvgStyle, setEditingSvgStyle] = useState(false);
125145

126146
const onSettingsChanged = () =>
127147
onChange({
@@ -144,6 +164,8 @@ export default function AdminWidget({
144164
itemBorderRadius,
145165
cssId,
146166
columns,
167+
svgStyle,
168+
svgInline,
147169
});
148170

149171
useEffect(() => {
@@ -168,6 +190,8 @@ export default function AdminWidget({
168190
itemBorderRadius,
169191
cssId,
170192
columns,
193+
svgStyle,
194+
svgInline,
171195
]);
172196

173197
const onItemChange = (newItemData: Item) => {
@@ -206,6 +230,20 @@ export default function AdminWidget({
206230
onDelete={onDelete}
207231
profile={profile}
208232
address={address}
233+
svgStyle={svgStyle}
234+
/>
235+
);
236+
}
237+
238+
if (editingSvgStyle) {
239+
return (
240+
<SvgStyleEditor
241+
svgStyle={svgStyle}
242+
onChange={(style: SvgStyle) => {
243+
setSvgStyle(style);
244+
setEditingSvgStyle(false);
245+
hideActionButtons(false, {});
246+
}}
209247
/>
210248
);
211249
}
@@ -348,6 +386,33 @@ export default function AdminWidget({
348386
value={columns}
349387
onChange={setColumns}
350388
/>
389+
<div className="flex justify-between mt-2">
390+
<div className="flex grow items-center gap-1">
391+
<p>Icon inline</p>
392+
<Tooltip title="The icon (if used) will show inline with the item header">
393+
<Help />
394+
</Tooltip>
395+
</div>
396+
<Checkbox
397+
checked={svgInline}
398+
onChange={(value: boolean) => setSvgInline(value)}
399+
/>
400+
</div>
401+
<div className="flex justify-between mt-2">
402+
<div className="flex grow items-center gap-1">
403+
<p>Icon style</p>
404+
</div>
405+
<Button2
406+
size="icon"
407+
variant="outline"
408+
onClick={() => {
409+
setEditingSvgStyle(true);
410+
hideActionButtons(true, {});
411+
}}
412+
>
413+
<WandSparkles />
414+
</Button2>
415+
</div>
351416
</AdminWidgetPanel>
352417
<AdminWidgetPanel title="Advanced">
353418
<CssIdField value={cssId} onChange={setCssId} />

packages/common-widgets/src/grid/admin-widget/item-editor.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import React, { useState } from "react";
4-
import { Item } from "../settings";
4+
import { Item, SvgStyle } from "../settings";
55
import {
66
MediaSelector,
77
TextEditor,
@@ -18,9 +18,10 @@ import {
1818
Profile,
1919
VerticalAlignment,
2020
} from "@courselit/common-models";
21-
21+
import SvgEditor from "./svg-editor";
2222
interface ItemProps {
2323
item: Item;
24+
svgStyle: SvgStyle;
2425
index: number;
2526
onChange: (newItemData: Item) => void;
2627
onDelete: () => void;
@@ -30,6 +31,7 @@ interface ItemProps {
3031

3132
export default function ItemEditor({
3233
item,
34+
svgStyle,
3335
onChange,
3436
onDelete,
3537
address,
@@ -43,6 +45,7 @@ export default function ItemEditor({
4345
const [mediaAlignment, setMediaAlignment] = useState<VerticalAlignment>(
4446
item.mediaAlignment || "bottom",
4547
);
48+
const [svgText, setSvgText] = useState(item.svgText);
4649

4750
const itemChanged = () =>
4851
onChange({
@@ -52,6 +55,7 @@ export default function ItemEditor({
5255
buttonAction,
5356
media,
5457
mediaAlignment,
58+
svgText,
5559
});
5660

5761
return (
@@ -82,8 +86,14 @@ export default function ItemEditor({
8286
value={buttonAction}
8387
onChange={(e) => setButtonAction(e.target.value)}
8488
/>
89+
<p className="mb-1 font-medium">Icon</p>
90+
<SvgEditor
91+
svgText={svgText}
92+
svgStyle={svgStyle}
93+
onSvgChange={(svgText: string) => setSvgText(svgText)}
94+
/>
8595
<MediaSelector
86-
title=""
96+
title="Media"
8797
src={media && media.thumbnail}
8898
srcTitle={media && media.originalFileName}
8999
profile={profile}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { useState, useRef, useEffect } from "react";
2+
import { SvgStyle } from "../settings";
3+
import {
4+
Alert,
5+
Button2,
6+
Textarea,
7+
AlertDescription,
8+
} from "@courselit/components-library";
9+
import { Check, Pencil, Trash, AlertCircle } from "lucide-react";
10+
import { validateSvg, processedSvg } from "../helpers";
11+
12+
export default function SvgEditor({
13+
svgText,
14+
svgStyle,
15+
onSvgChange,
16+
}: {
17+
svgText: string;
18+
svgStyle: SvgStyle;
19+
onSvgChange: (svgCode: string) => void;
20+
}) {
21+
const [svgCode, setSvgCode] = useState<string>(svgText);
22+
const [isEditing, setIsEditing] = useState<boolean>(false);
23+
const [tempSvgCode, setTempSvgCode] = useState<string>(svgText);
24+
const [error, setError] = useState<string | null>(null);
25+
const textareaRef = useRef<HTMLTextAreaElement>(null);
26+
27+
useEffect(() => {
28+
if (isEditing && textareaRef.current) {
29+
textareaRef.current.focus();
30+
}
31+
}, [isEditing]);
32+
33+
useEffect(() => {
34+
onSvgChange(svgCode);
35+
}, [svgCode]);
36+
37+
const handleSvgChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
38+
setTempSvgCode(e.target.value);
39+
setError(null);
40+
};
41+
42+
// const validateSvg = (code: string): boolean => {
43+
// if (!code.trim()) return false
44+
45+
// // Basic validation - check if it starts with <svg and ends with </svg>
46+
// const hasSvgTags = code.trim().startsWith("<svg") && code.trim().endsWith("</svg>")
47+
48+
// if (!hasSvgTags) {
49+
// setError("Invalid SVG format. Make sure your code starts with <svg and ends with </svg>")
50+
// return false
51+
// }
52+
53+
// return true
54+
// }
55+
56+
const saveSvgChanges = () => {
57+
try {
58+
if (validateSvg(tempSvgCode)) {
59+
setSvgCode(tempSvgCode);
60+
setIsEditing(false);
61+
}
62+
} catch (error) {
63+
setError(error.message);
64+
}
65+
};
66+
67+
const cancelEditing = () => {
68+
setTempSvgCode(svgCode);
69+
setIsEditing(false);
70+
setError(null);
71+
};
72+
73+
const removeSvg = () => {
74+
setSvgCode("");
75+
setTempSvgCode("");
76+
setIsEditing(false);
77+
setError(null);
78+
};
79+
80+
// const processedSvg = () => {
81+
// if (!validateSvg(svgCode)) return ""
82+
83+
// // Replace currentColor with the selected color
84+
// return svgCode.replace(/currentColor/g, svgStyle.svgColor)
85+
// }
86+
87+
return !isEditing ? (
88+
<div>
89+
<div className="flex items-center justify-between gap-2">
90+
{svgCode ? (
91+
<div className="flex justify-center items-center py-4">
92+
<div
93+
className="flex justify-center items-center"
94+
style={{
95+
width: `${svgStyle.width}px`,
96+
height: `${svgStyle.height}px`,
97+
backgroundColor: svgStyle.backgroundColor,
98+
borderRadius: `${svgStyle.borderRadius}px`,
99+
borderWidth: `${svgStyle.borderWidth}px`,
100+
borderStyle: svgStyle.borderStyle,
101+
borderColor: svgStyle.borderColor,
102+
padding: "8px",
103+
}}
104+
dangerouslySetInnerHTML={{
105+
__html:
106+
processedSvg(svgCode, svgStyle) ||
107+
'<div class="text-red-500">Invalid SVG</div>',
108+
}}
109+
/>
110+
</div>
111+
) : (
112+
<div
113+
className="flex justify-center items-center"
114+
style={{
115+
width: `${svgStyle.width}px`,
116+
height: `${svgStyle.height}px`,
117+
backgroundColor: svgStyle.backgroundColor,
118+
borderRadius: `${svgStyle.borderRadius}px`,
119+
borderWidth: `${svgStyle.borderWidth}px`,
120+
borderStyle: svgStyle.borderStyle,
121+
borderColor: svgStyle.borderColor,
122+
padding: "8px",
123+
}}
124+
// dangerouslySetInnerHTML={{
125+
// __html: '<div class="text-center text-muted-foreground">No SVG added yet. Click the edit button to add SVG.</div>',
126+
// }}
127+
></div>
128+
)}
129+
<div className="flex gap-2">
130+
<Button2
131+
variant="outline"
132+
size="icon"
133+
onClick={() => {
134+
setIsEditing(true);
135+
}}
136+
>
137+
<Pencil />
138+
</Button2>
139+
{svgText && (
140+
<Button2
141+
variant="outline"
142+
size="icon"
143+
onClick={(e) => {
144+
e.preventDefault();
145+
removeSvg();
146+
}}
147+
>
148+
<Trash />
149+
</Button2>
150+
)}
151+
</div>
152+
</div>
153+
</div>
154+
) : (
155+
<div>
156+
{/* <FormField
157+
label="SVG Code"
158+
value={svgCode}
159+
onChange={(e) => setTempSvgCode(e.target.value)}
160+
multiline
161+
rows={10}
162+
/> */}
163+
<Textarea
164+
ref={textareaRef}
165+
placeholder="Enter SVG code here..."
166+
className="min-h-[150px] font-mono text-sm mb-4"
167+
rows={10}
168+
value={tempSvgCode}
169+
onChange={handleSvgChange}
170+
/>
171+
<p className="text-xs text-muted-foreground mb-4">
172+
<a
173+
href="https://lucide.dev/icons/"
174+
target="_blank"
175+
rel="noopener noreferrer"
176+
className="underline"
177+
>
178+
Lucide icons
179+
</a>{" "}
180+
works the best with CourseLit.
181+
</p>
182+
{error && (
183+
<Alert variant="destructive" className="mb-4">
184+
<AlertCircle className="h-4 w-4" />
185+
<AlertDescription>{error}</AlertDescription>
186+
</Alert>
187+
)}
188+
<div className="flex justify-end gap-2">
189+
<Button2
190+
type="button"
191+
onClick={(e) => {
192+
e.preventDefault();
193+
cancelEditing();
194+
}}
195+
variant="outline"
196+
size="sm"
197+
>
198+
Cancel
199+
</Button2>
200+
<Button2
201+
type="submit"
202+
variant="outline"
203+
size="sm"
204+
onClick={(e) => {
205+
e.preventDefault();
206+
saveSvgChanges();
207+
}}
208+
>
209+
<Check /> Save
210+
</Button2>
211+
</div>
212+
</div>
213+
);
214+
}

0 commit comments

Comments
 (0)