@nualang/nualang-ui-components 0.1.1409 → 0.1.1410
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/dist/Assignments/AssignmentBotSelection/AssignmentBotSelection.js +1 -1
- package/dist/Assignments/AssignmentExerciseSelection/AssignmentExerciseSelection.js +2 -1
- package/dist/Assignments/AssignmentPhraseListSelection/AssignmentPhraseListSelection.js +8 -4
- package/dist/Assignments/AssignmentRoleplaySelection/AssignmentRoleplaySelection.js +1 -1
- package/dist/Containers/App/App.js +12 -1
- package/dist/Dialogs/ImageBuilder/ImageBuilderControls.js +250 -0
- package/dist/Dialogs/ImageBuilder/ImageBuilderDialog.js +138 -0
- package/dist/Dialogs/ImageBuilder/ImageBuilderPreview.js +79 -0
- package/dist/Dialogs/ImageBuilder/iconPaths.js +117 -0
- package/dist/Dialogs/ImageBuilder/imageBuilderUtils.js +159 -0
- package/dist/Dialogs/QuickCreateClassroomDialog/QuickCreateClassroomDialog.js +21 -4
- package/dist/Dialogs/QuickCreateCourseDialog/QuickCreateCourseDialog.js +17 -3
- package/dist/Dialogs/QuickCreateTopicDialog/QuickCreateTopicDialog.js +31 -9
- package/dist/Dialogs/ResponsiveDialog/ResponsiveDialog.js +2 -1
- package/dist/Dialogs/UploadPhrases/UploadPhrases.js +23 -1
- package/dist/Editors/Bot/Editor/BotDesigner/VarList.js +2 -1
- package/dist/Exercises/PhraseGroup/PhraseGroup.js +3 -1
- package/dist/Exercises/PhraseGroup/PhraseGroup.test.js +60 -16
- package/dist/Exercises/PhraseGroup/PhraseGroupGameOptions/PhraseGroupGame/PhraseGroupGame.js +0 -1
- package/dist/Exercises/PhraseGroup/PhraseGroupGameOptions/PhraseGroupGames.js +30 -27
- package/dist/Forms/CreateClassroom/CreateClassroom.js +6 -3
- package/dist/Forms/CreateClassroom/Steps/Customization/Customization.js +31 -6
- package/dist/Forms/CreateCourse/CreateCourse.js +6 -2
- package/dist/Forms/CreateCourse/Steps/Customization/Customization.js +28 -3
- package/dist/Forms/CreateTopic/Steps/Customization/Customization.js +28 -3
- package/dist/Forms/ImageSelector/ImageSelector.js +116 -17
- package/dist/Forms/Settings/Tabs/General/General.js +44 -22
- package/dist/Forms/UpdateClassroom/UpdateClassroom.js +9 -1
- package/dist/Forms/UpdateCourse/UpdateCourse.js +9 -1
- package/dist/Forms/UpdatePhraseList/UpdatePhraseList.js +2 -1
- package/dist/Forms/UpdateTopic/UpdateTopic.js +9 -1
- package/dist/Lists/CourseOutline/CourseOutline.js +5 -1
- package/dist/Lists/Phrases/Phrases.js +172 -24
- package/dist/Misc/FeatureNudge/FeatureNudge.js +160 -0
- package/dist/Misc/FeatureNudge/FeatureNudge.test.js +113 -0
- package/dist/Misc/FeatureNudge/package.json +6 -0
- package/dist/Misc/HtmlTooltipComponent/HtmlTooltipComponent.js +10 -1
- package/dist/Misc/newGenerateCardBackground/Generate Card Background (standalone) (1).html +186 -0
- package/dist/Navigation/BottomBar/BottomBar.js +25 -3
- package/dist/Navigation/ResponsiveDrawer/ResponsiveDrawer.js +128 -112
- package/dist/img/bots_background.svg +62 -0
- package/dist/img/classroom_background.svg +43 -0
- package/dist/img/courses_background.svg +92 -0
- package/dist/img/nua-glyph.svg +6 -0
- package/dist/img/nuala-silhouette.svg +43 -0
- package/dist/img/roleplay_background.svg +62 -0
- package/dist/utils/placeholder-images/index.js +9 -10
- package/package.json +1 -1
|
@@ -54,7 +54,7 @@ function Bot({
|
|
|
54
54
|
classroomId
|
|
55
55
|
} = params;
|
|
56
56
|
const [courseId, sectionId, topicId] = courseSectionTopicId ? courseSectionTopicId.split("|") : "";
|
|
57
|
-
const [
|
|
57
|
+
const [lastClickedAssignmentId, lastClickedBotId] = lastClickedExerciseId?.split("|").slice(-2) || [];
|
|
58
58
|
const addSearchParams = () => {
|
|
59
59
|
if (!classroomId || !courseId || !sectionId || !topicId || !bot?.botId) {
|
|
60
60
|
console.error("Missing required parameters for navigation");
|
|
@@ -221,7 +221,8 @@ function Exercise({
|
|
|
221
221
|
completionHelpers: completionHelpers,
|
|
222
222
|
username: username,
|
|
223
223
|
preferred_username: preferred_username,
|
|
224
|
-
isCreator: isCreator
|
|
224
|
+
isCreator: isCreator,
|
|
225
|
+
lastClickedExerciseId: lastClickedExerciseId
|
|
225
226
|
})
|
|
226
227
|
})
|
|
227
228
|
}), name.toLowerCase() === "roleplays" && /*#__PURE__*/_jsx(Collapse, {
|
|
@@ -110,7 +110,8 @@ function PhraseList({
|
|
|
110
110
|
progressHelpers,
|
|
111
111
|
completionHelpers,
|
|
112
112
|
username,
|
|
113
|
-
preferred_username
|
|
113
|
+
preferred_username,
|
|
114
|
+
lastClickedExerciseId
|
|
114
115
|
}) {
|
|
115
116
|
const navigate = useNavigate();
|
|
116
117
|
const {
|
|
@@ -120,8 +121,9 @@ function PhraseList({
|
|
|
120
121
|
const {
|
|
121
122
|
classroomId
|
|
122
123
|
} = params;
|
|
124
|
+
const [lastClickedAssignmentId, lastClickedPhraseListId] = lastClickedExerciseId?.split("|").slice(-2) || [];
|
|
123
125
|
const [isSelected, setIsSelected] = useState(false);
|
|
124
|
-
const [isPhraseListOpen, setIsPhraseListOpen] = useState(
|
|
126
|
+
const [isPhraseListOpen, setIsPhraseListOpen] = useState(lastClickedPhraseListId === phraseList.phraseListId && lastClickedAssignmentId === assignment.assignmentId);
|
|
125
127
|
const [isSelectAllChecked, setIsSelectAllChecked] = useState(false);
|
|
126
128
|
const [indeterminate, setIndeterminate] = useState(false);
|
|
127
129
|
const activePhrases = phraseList?.phrases || [];
|
|
@@ -436,7 +438,8 @@ export default function AssignmentPhraseListSelection({
|
|
|
436
438
|
completionHelpers,
|
|
437
439
|
username,
|
|
438
440
|
preferred_username,
|
|
439
|
-
isCreator
|
|
441
|
+
isCreator,
|
|
442
|
+
lastClickedExerciseId
|
|
440
443
|
}) {
|
|
441
444
|
const selectedPhraseListIds = new Set(selectedExercises?.map(exercise => exercise.phraseListId) || []);
|
|
442
445
|
const filteredPhraseLists = !viewOnly ? phraseLists : phraseLists.filter(phraseList => selectedPhraseListIds.has(phraseList.phraseListId));
|
|
@@ -458,7 +461,8 @@ export default function AssignmentPhraseListSelection({
|
|
|
458
461
|
completionHelpers: completionHelpers,
|
|
459
462
|
username: username,
|
|
460
463
|
preferred_username: preferred_username,
|
|
461
|
-
isCreator: isCreator
|
|
464
|
+
isCreator: isCreator,
|
|
465
|
+
lastClickedExerciseId: lastClickedExerciseId
|
|
462
466
|
}, index)), orphanedPhraseListIds.map(id => /*#__PURE__*/_jsx(Box, {
|
|
463
467
|
sx: {
|
|
464
468
|
display: "flex",
|
|
@@ -97,7 +97,7 @@ function Roleplay({
|
|
|
97
97
|
const [selectedGames, setSelectedGames] = useState([]);
|
|
98
98
|
const [indeterminate, setIndeterminate] = useState(false);
|
|
99
99
|
const [courseId, sectionId, topicId] = courseSectionTopicId ? courseSectionTopicId.split("|") : "";
|
|
100
|
-
const [lastClickedAssignmentId, lastClickedRoleplayId] = lastClickedExerciseId?.split("|") || [];
|
|
100
|
+
const [lastClickedAssignmentId, lastClickedRoleplayId] = lastClickedExerciseId?.split("|").slice(-2) || [];
|
|
101
101
|
const addSearchParams = exercise => {
|
|
102
102
|
if (!classroomId || !courseId || !sectionId || !topicId || !roleplay?.roleplayId) {
|
|
103
103
|
console.error("Missing required parameters for navigation");
|
|
@@ -513,7 +513,18 @@ function App({
|
|
|
513
513
|
...user
|
|
514
514
|
}) : /*#__PURE__*/_jsx(BottomBar, {
|
|
515
515
|
navigation: bottomNav,
|
|
516
|
-
t: t
|
|
516
|
+
t: t,
|
|
517
|
+
nudges: [{
|
|
518
|
+
// Mobile counterpart of the sidebar profile nudge: the settings
|
|
519
|
+
// item is the one-tap route to the last-name field.
|
|
520
|
+
url: navToSettings,
|
|
521
|
+
active: !!(user && user.given_name && !user.family_name),
|
|
522
|
+
nudgeId: "add-family-name-bottom-nav",
|
|
523
|
+
tooltipTitle: t("nudge_add_family_name_title"),
|
|
524
|
+
tooltipDesc: t("nudge_add_family_name_profile_desc"),
|
|
525
|
+
placement: "top",
|
|
526
|
+
"data-cy": "nudge-add-family-name-bottom-nav"
|
|
527
|
+
}]
|
|
517
528
|
}), /*#__PURE__*/_jsxs("main", {
|
|
518
529
|
className: classes.main,
|
|
519
530
|
children: [/*#__PURE__*/_jsx("div", {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { makeStyles } from "tss-react/mui";
|
|
2
|
+
import { Typography, ButtonBase, Box, Slider } from "@mui/material";
|
|
3
|
+
import PaletteIcon from "@mui/icons-material/Palette";
|
|
4
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
5
|
+
import GroupsIcon from "@mui/icons-material/Groups";
|
|
6
|
+
import TranslateIcon from "@mui/icons-material/Translate";
|
|
7
|
+
import SchoolIcon from "@mui/icons-material/School";
|
|
8
|
+
import MenuBookIcon from "@mui/icons-material/MenuBook";
|
|
9
|
+
import AutoStoriesIcon from "@mui/icons-material/AutoStories";
|
|
10
|
+
import RecordVoiceOverIcon from "@mui/icons-material/RecordVoiceOver";
|
|
11
|
+
import ForumIcon from "@mui/icons-material/Forum";
|
|
12
|
+
import MicIcon from "@mui/icons-material/Mic";
|
|
13
|
+
import HeadphonesIcon from "@mui/icons-material/Headphones";
|
|
14
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
15
|
+
import QuizIcon from "@mui/icons-material/Quiz";
|
|
16
|
+
import LanguageIcon from "@mui/icons-material/Language";
|
|
17
|
+
import WavingHandIcon from "@mui/icons-material/WavingHand";
|
|
18
|
+
import LightbulbIcon from "@mui/icons-material/Lightbulb";
|
|
19
|
+
import FlagIcon from "@mui/icons-material/Flag";
|
|
20
|
+
import TravelExploreIcon from "@mui/icons-material/TravelExplore";
|
|
21
|
+
import { PRESET_COLORS, ICON_PATHS, ICON_KEYS } from "./iconPaths";
|
|
22
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
23
|
+
const ICON_COMPONENTS = {
|
|
24
|
+
groups: GroupsIcon,
|
|
25
|
+
translate: TranslateIcon,
|
|
26
|
+
school: SchoolIcon,
|
|
27
|
+
menu_book: MenuBookIcon,
|
|
28
|
+
auto_stories: AutoStoriesIcon,
|
|
29
|
+
record_voice_over: RecordVoiceOverIcon,
|
|
30
|
+
forum: ForumIcon,
|
|
31
|
+
mic: MicIcon,
|
|
32
|
+
headphones: HeadphonesIcon,
|
|
33
|
+
edit: EditIcon,
|
|
34
|
+
quiz: QuizIcon,
|
|
35
|
+
language: LanguageIcon,
|
|
36
|
+
waving_hand: WavingHandIcon,
|
|
37
|
+
lightbulb: LightbulbIcon,
|
|
38
|
+
flag: FlagIcon,
|
|
39
|
+
travel_explore: TravelExploreIcon
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Build mask-image URLs for SVG-based icons (computed once at module level)
|
|
43
|
+
const SVG_MASK_URLS = {};
|
|
44
|
+
for (const key of ICON_KEYS) {
|
|
45
|
+
if (!ICON_COMPONENTS[key]) {
|
|
46
|
+
const iconData = ICON_PATHS[key];
|
|
47
|
+
const vb = iconData.viewBox || {
|
|
48
|
+
width: 24,
|
|
49
|
+
height: 24
|
|
50
|
+
};
|
|
51
|
+
const pathsStr = iconData.paths.map(d => `<path d="${d}"/>`).join("");
|
|
52
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${vb.width} ${vb.height}" fill="black">${pathsStr}</svg>`;
|
|
53
|
+
SVG_MASK_URLS[key] = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const useStyles = makeStyles()(theme => ({
|
|
57
|
+
root: {
|
|
58
|
+
width: 440,
|
|
59
|
+
flexShrink: 0,
|
|
60
|
+
padding: "28px 32px",
|
|
61
|
+
borderRight: `1px solid ${theme.palette.divider}`,
|
|
62
|
+
overflowY: "auto",
|
|
63
|
+
[theme.breakpoints.down("md")]: {
|
|
64
|
+
width: "100%",
|
|
65
|
+
borderRight: "none",
|
|
66
|
+
borderBottom: `1px solid ${theme.palette.divider}`
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
sectionLabel: {
|
|
70
|
+
fontSize: 14,
|
|
71
|
+
fontWeight: 600,
|
|
72
|
+
marginBottom: 12
|
|
73
|
+
},
|
|
74
|
+
swatchRow: {
|
|
75
|
+
display: "flex",
|
|
76
|
+
flexWrap: "wrap",
|
|
77
|
+
gap: 12,
|
|
78
|
+
marginBottom: 14
|
|
79
|
+
},
|
|
80
|
+
swatch: {
|
|
81
|
+
width: 40,
|
|
82
|
+
height: 40,
|
|
83
|
+
borderRadius: 8,
|
|
84
|
+
border: "none",
|
|
85
|
+
cursor: "pointer",
|
|
86
|
+
padding: 0,
|
|
87
|
+
transition: "box-shadow 0.15s"
|
|
88
|
+
},
|
|
89
|
+
customSwatch: {
|
|
90
|
+
position: "relative",
|
|
91
|
+
display: "flex",
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
width: 40,
|
|
95
|
+
height: 40,
|
|
96
|
+
borderRadius: 8,
|
|
97
|
+
cursor: "pointer",
|
|
98
|
+
overflow: "hidden"
|
|
99
|
+
},
|
|
100
|
+
colorInput: {
|
|
101
|
+
position: "absolute",
|
|
102
|
+
inset: 0,
|
|
103
|
+
opacity: 0,
|
|
104
|
+
cursor: "pointer",
|
|
105
|
+
width: "100%",
|
|
106
|
+
height: "100%",
|
|
107
|
+
border: "none"
|
|
108
|
+
},
|
|
109
|
+
iconGrid: {
|
|
110
|
+
display: "grid",
|
|
111
|
+
gridTemplateColumns: "repeat(6, 1fr)",
|
|
112
|
+
gap: 10,
|
|
113
|
+
marginBottom: 8
|
|
114
|
+
},
|
|
115
|
+
iconCell: {
|
|
116
|
+
display: "flex",
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
justifyContent: "center",
|
|
119
|
+
aspectRatio: "1",
|
|
120
|
+
borderRadius: 8,
|
|
121
|
+
cursor: "pointer",
|
|
122
|
+
padding: 0,
|
|
123
|
+
transition: "background-color 0.15s, border-color 0.15s"
|
|
124
|
+
}
|
|
125
|
+
}));
|
|
126
|
+
function getSwatchSx(colorValue, isSelected) {
|
|
127
|
+
return {
|
|
128
|
+
backgroundColor: colorValue,
|
|
129
|
+
boxShadow: isSelected ? theme => `0 0 0 3px ${theme.palette.background.paper}, 0 0 0 5px ${theme.palette.primary.main}` : "inset 0 0 0 1px rgba(0,0,0,0.12)"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export default function ImageBuilderControls({
|
|
133
|
+
t,
|
|
134
|
+
color,
|
|
135
|
+
onColorChange,
|
|
136
|
+
customColor,
|
|
137
|
+
onCustomColorChange,
|
|
138
|
+
iconKey,
|
|
139
|
+
onIconChange,
|
|
140
|
+
iconOpacity,
|
|
141
|
+
onIconOpacityChange
|
|
142
|
+
}) {
|
|
143
|
+
const {
|
|
144
|
+
classes
|
|
145
|
+
} = useStyles();
|
|
146
|
+
const isPreset = PRESET_COLORS.some(c => c.value.toLowerCase() === color.toLowerCase());
|
|
147
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
148
|
+
className: classes.root,
|
|
149
|
+
children: [/*#__PURE__*/_jsx(Typography, {
|
|
150
|
+
className: classes.sectionLabel,
|
|
151
|
+
children: t("background_colour")
|
|
152
|
+
}), /*#__PURE__*/_jsxs("div", {
|
|
153
|
+
className: classes.swatchRow,
|
|
154
|
+
children: [PRESET_COLORS.map(c => {
|
|
155
|
+
const selected = c.value.toLowerCase() === color.toLowerCase();
|
|
156
|
+
return /*#__PURE__*/_jsx(ButtonBase, {
|
|
157
|
+
className: classes.swatch,
|
|
158
|
+
sx: getSwatchSx(c.value, selected),
|
|
159
|
+
onClick: () => onColorChange(c.value),
|
|
160
|
+
title: c.name,
|
|
161
|
+
"aria-label": c.name,
|
|
162
|
+
children: selected && /*#__PURE__*/_jsx(CheckIcon, {
|
|
163
|
+
sx: {
|
|
164
|
+
color: "#fff",
|
|
165
|
+
fontSize: 20
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}, c.value);
|
|
169
|
+
}), /*#__PURE__*/_jsxs(ButtonBase, {
|
|
170
|
+
className: classes.customSwatch,
|
|
171
|
+
sx: getSwatchSx(customColor, !isPreset),
|
|
172
|
+
component: "label",
|
|
173
|
+
title: t("custom_colour"),
|
|
174
|
+
"aria-label": t("custom_colour"),
|
|
175
|
+
children: [/*#__PURE__*/_jsx(PaletteIcon, {
|
|
176
|
+
sx: {
|
|
177
|
+
color: "#fff",
|
|
178
|
+
fontSize: 20,
|
|
179
|
+
mixBlendMode: "difference"
|
|
180
|
+
}
|
|
181
|
+
}), /*#__PURE__*/_jsx("input", {
|
|
182
|
+
type: "color",
|
|
183
|
+
value: customColor,
|
|
184
|
+
onChange: e => onCustomColorChange(e.target.value),
|
|
185
|
+
className: classes.colorInput
|
|
186
|
+
})]
|
|
187
|
+
})]
|
|
188
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
|
189
|
+
className: classes.sectionLabel,
|
|
190
|
+
sx: {
|
|
191
|
+
mt: 3
|
|
192
|
+
},
|
|
193
|
+
children: t("icon")
|
|
194
|
+
}), /*#__PURE__*/_jsx("div", {
|
|
195
|
+
className: classes.iconGrid,
|
|
196
|
+
children: ICON_KEYS.map(key => {
|
|
197
|
+
const IconComp = ICON_COMPONENTS[key];
|
|
198
|
+
const maskUrl = SVG_MASK_URLS[key];
|
|
199
|
+
const selected = key === iconKey;
|
|
200
|
+
return /*#__PURE__*/_jsx(ButtonBase, {
|
|
201
|
+
className: classes.iconCell,
|
|
202
|
+
sx: theme => ({
|
|
203
|
+
backgroundColor: selected ? theme.palette.primary.light + "30" : theme.palette.background.paper,
|
|
204
|
+
border: selected ? `2px solid ${theme.palette.primary.main}` : `1px solid ${theme.palette.divider}`
|
|
205
|
+
}),
|
|
206
|
+
onClick: () => onIconChange(key),
|
|
207
|
+
title: key.replace(/_/g, " "),
|
|
208
|
+
"aria-label": key.replace(/_/g, " "),
|
|
209
|
+
children: IconComp ? /*#__PURE__*/_jsx(IconComp, {
|
|
210
|
+
sx: theme => ({
|
|
211
|
+
color: selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
|
212
|
+
fontSize: 24
|
|
213
|
+
})
|
|
214
|
+
}) : /*#__PURE__*/_jsx(Box, {
|
|
215
|
+
sx: theme => ({
|
|
216
|
+
width: 24,
|
|
217
|
+
height: 24,
|
|
218
|
+
backgroundColor: selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
|
219
|
+
maskImage: maskUrl,
|
|
220
|
+
WebkitMaskImage: maskUrl,
|
|
221
|
+
maskSize: "contain",
|
|
222
|
+
WebkitMaskSize: "contain",
|
|
223
|
+
maskRepeat: "no-repeat",
|
|
224
|
+
WebkitMaskRepeat: "no-repeat",
|
|
225
|
+
maskPosition: "center",
|
|
226
|
+
WebkitMaskPosition: "center"
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
}, key);
|
|
230
|
+
})
|
|
231
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
|
232
|
+
className: classes.sectionLabel,
|
|
233
|
+
sx: {
|
|
234
|
+
mt: 3
|
|
235
|
+
},
|
|
236
|
+
children: t("icon_opacity")
|
|
237
|
+
}), /*#__PURE__*/_jsx(Slider, {
|
|
238
|
+
value: iconOpacity,
|
|
239
|
+
min: 0.05,
|
|
240
|
+
max: 0.5,
|
|
241
|
+
step: 0.01,
|
|
242
|
+
onChange: (e, val) => onIconOpacityChange(val),
|
|
243
|
+
valueLabelDisplay: "auto",
|
|
244
|
+
valueLabelFormat: v => `${Math.round(v * 100)}%`,
|
|
245
|
+
sx: {
|
|
246
|
+
maxWidth: 360
|
|
247
|
+
}
|
|
248
|
+
})]
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { Box, Button } from "@mui/material";
|
|
3
|
+
import { makeStyles } from "tss-react/mui";
|
|
4
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
5
|
+
import CasinoIcon from "@mui/icons-material/Casino";
|
|
6
|
+
import ImageBuilderControls from "./ImageBuilderControls";
|
|
7
|
+
import ImageBuilderPreview from "./ImageBuilderPreview";
|
|
8
|
+
import ResponsiveDialog from "../ResponsiveDialog/ResponsiveDialog";
|
|
9
|
+
import { PRESET_COLORS, ICON_KEYS } from "./iconPaths";
|
|
10
|
+
import { generatePatternCanvas, getCenterCropDataUrl, getOptimalIconOpacity, shouldUseDarkIcons } from "./imageBuilderUtils";
|
|
11
|
+
|
|
12
|
+
// Matches placeholder SVG ratio (21144 x 4890), scaled down 10x
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
const BANNER_W = 2114;
|
|
15
|
+
const BANNER_H = 489;
|
|
16
|
+
const CARD_SIZE = 489;
|
|
17
|
+
const useStyles = makeStyles()(theme => ({
|
|
18
|
+
body: {
|
|
19
|
+
display: "flex",
|
|
20
|
+
[theme.breakpoints.down("md")]: {
|
|
21
|
+
flexDirection: "column"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
export default function ImageBuilderDialog({
|
|
26
|
+
t,
|
|
27
|
+
open,
|
|
28
|
+
onClose,
|
|
29
|
+
onConfirm,
|
|
30
|
+
initialColor,
|
|
31
|
+
initialIconKey
|
|
32
|
+
}) {
|
|
33
|
+
const {
|
|
34
|
+
classes
|
|
35
|
+
} = useStyles();
|
|
36
|
+
const defaultColor = initialColor || PRESET_COLORS[0].value;
|
|
37
|
+
const [color, setColor] = useState(defaultColor);
|
|
38
|
+
const [customColor, setCustomColor] = useState(defaultColor);
|
|
39
|
+
const [iconKey, setIconKey] = useState(initialIconKey || "school");
|
|
40
|
+
const [iconOpacity, setIconOpacity] = useState(() => getOptimalIconOpacity(defaultColor));
|
|
41
|
+
const darkIcons = shouldUseDarkIcons(color);
|
|
42
|
+
|
|
43
|
+
// Generate preview — only recalculates when inputs change
|
|
44
|
+
const {
|
|
45
|
+
bannerDataUrl,
|
|
46
|
+
cardDataUrl
|
|
47
|
+
} = useMemo(() => {
|
|
48
|
+
if (typeof document === "undefined") {
|
|
49
|
+
return {
|
|
50
|
+
bannerDataUrl: null,
|
|
51
|
+
cardDataUrl: null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const canvas = generatePatternCanvas({
|
|
55
|
+
color,
|
|
56
|
+
iconKey,
|
|
57
|
+
density: "dense",
|
|
58
|
+
iconOpacity,
|
|
59
|
+
darkIcons,
|
|
60
|
+
width: BANNER_W,
|
|
61
|
+
height: BANNER_H
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
bannerDataUrl: canvas.toDataURL("image/png"),
|
|
65
|
+
cardDataUrl: getCenterCropDataUrl(canvas, CARD_SIZE)
|
|
66
|
+
};
|
|
67
|
+
}, [color, iconKey, iconOpacity, darkIcons]);
|
|
68
|
+
|
|
69
|
+
// Reset state when dialog opens
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (open) {
|
|
72
|
+
const c = initialColor || PRESET_COLORS[0].value;
|
|
73
|
+
setColor(c);
|
|
74
|
+
setCustomColor(c);
|
|
75
|
+
setIconKey(initialIconKey || "school");
|
|
76
|
+
setIconOpacity(getOptimalIconOpacity(c));
|
|
77
|
+
}
|
|
78
|
+
}, [open, initialColor, initialIconKey]);
|
|
79
|
+
const handleColorChange = val => {
|
|
80
|
+
setColor(val);
|
|
81
|
+
setCustomColor(val);
|
|
82
|
+
setIconOpacity(getOptimalIconOpacity(val));
|
|
83
|
+
};
|
|
84
|
+
const handleCustomColorChange = val => {
|
|
85
|
+
setCustomColor(val);
|
|
86
|
+
setColor(val);
|
|
87
|
+
setIconOpacity(getOptimalIconOpacity(val));
|
|
88
|
+
};
|
|
89
|
+
const handleRandomize = () => {
|
|
90
|
+
const c = PRESET_COLORS[Math.floor(Math.random() * PRESET_COLORS.length)].value;
|
|
91
|
+
const ic = ICON_KEYS[Math.floor(Math.random() * ICON_KEYS.length)];
|
|
92
|
+
setColor(c);
|
|
93
|
+
setCustomColor(c);
|
|
94
|
+
setIconKey(ic);
|
|
95
|
+
setIconOpacity(getOptimalIconOpacity(c));
|
|
96
|
+
};
|
|
97
|
+
const handleConfirm = () => {
|
|
98
|
+
onConfirm(bannerDataUrl, {
|
|
99
|
+
color,
|
|
100
|
+
iconKey
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
return /*#__PURE__*/_jsx(ResponsiveDialog, {
|
|
104
|
+
open: open,
|
|
105
|
+
handleClose: onClose,
|
|
106
|
+
handleSubmit: handleConfirm,
|
|
107
|
+
dialogTitle: t("image_builder"),
|
|
108
|
+
submitText: t("use_image"),
|
|
109
|
+
closeText: t("cancel"),
|
|
110
|
+
maxWidth: "lg",
|
|
111
|
+
startIcon: /*#__PURE__*/_jsx(CheckIcon, {}),
|
|
112
|
+
t: t,
|
|
113
|
+
extraActions: /*#__PURE__*/_jsx(Button, {
|
|
114
|
+
variant: "outlined",
|
|
115
|
+
onClick: handleRandomize,
|
|
116
|
+
startIcon: /*#__PURE__*/_jsx(CasinoIcon, {}),
|
|
117
|
+
children: t("randomise")
|
|
118
|
+
}),
|
|
119
|
+
children: /*#__PURE__*/_jsxs(Box, {
|
|
120
|
+
className: classes.body,
|
|
121
|
+
children: [/*#__PURE__*/_jsx(ImageBuilderControls, {
|
|
122
|
+
t: t,
|
|
123
|
+
color: color,
|
|
124
|
+
onColorChange: handleColorChange,
|
|
125
|
+
customColor: customColor,
|
|
126
|
+
onCustomColorChange: handleCustomColorChange,
|
|
127
|
+
iconKey: iconKey,
|
|
128
|
+
onIconChange: setIconKey,
|
|
129
|
+
iconOpacity: iconOpacity,
|
|
130
|
+
onIconOpacityChange: setIconOpacity
|
|
131
|
+
}), /*#__PURE__*/_jsx(ImageBuilderPreview, {
|
|
132
|
+
t: t,
|
|
133
|
+
bannerDataUrl: bannerDataUrl,
|
|
134
|
+
cardDataUrl: cardDataUrl
|
|
135
|
+
})]
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { makeStyles } from "tss-react/mui";
|
|
2
|
+
import { Typography, Box } from "@mui/material";
|
|
3
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
|
+
const useStyles = makeStyles()(theme => ({
|
|
5
|
+
root: {
|
|
6
|
+
flex: 1,
|
|
7
|
+
padding: "28px 32px",
|
|
8
|
+
backgroundColor: theme.palette.mode === "dark" ? theme.palette.background.default : "#ffffff",
|
|
9
|
+
[theme.breakpoints.down("md")]: {
|
|
10
|
+
padding: "20px 16px"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
label: {
|
|
14
|
+
fontSize: 13,
|
|
15
|
+
fontWeight: 600,
|
|
16
|
+
color: theme.palette.text.secondary,
|
|
17
|
+
marginBottom: 10,
|
|
18
|
+
letterSpacing: "0.02em"
|
|
19
|
+
},
|
|
20
|
+
bannerImg: {
|
|
21
|
+
width: "100%",
|
|
22
|
+
aspectRatio: "2114 / 489",
|
|
23
|
+
borderRadius: 8,
|
|
24
|
+
overflow: "hidden",
|
|
25
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
26
|
+
marginBottom: 24,
|
|
27
|
+
objectFit: "cover"
|
|
28
|
+
},
|
|
29
|
+
cardRow: {
|
|
30
|
+
display: "flex",
|
|
31
|
+
alignItems: "flex-end",
|
|
32
|
+
gap: 20
|
|
33
|
+
},
|
|
34
|
+
cardImg: {
|
|
35
|
+
width: 150,
|
|
36
|
+
height: 150,
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
borderRadius: 10,
|
|
39
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
40
|
+
objectFit: "cover"
|
|
41
|
+
},
|
|
42
|
+
helperText: {
|
|
43
|
+
margin: 0,
|
|
44
|
+
fontSize: 13,
|
|
45
|
+
lineHeight: 1.5,
|
|
46
|
+
color: theme.palette.text.secondary,
|
|
47
|
+
maxWidth: 180
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
export default function ImageBuilderPreview({
|
|
51
|
+
t,
|
|
52
|
+
bannerDataUrl,
|
|
53
|
+
cardDataUrl
|
|
54
|
+
}) {
|
|
55
|
+
const {
|
|
56
|
+
classes
|
|
57
|
+
} = useStyles();
|
|
58
|
+
return /*#__PURE__*/_jsxs(Box, {
|
|
59
|
+
className: classes.root,
|
|
60
|
+
children: [/*#__PURE__*/_jsx(Typography, {
|
|
61
|
+
className: classes.label,
|
|
62
|
+
children: t("banner")
|
|
63
|
+
}), bannerDataUrl && /*#__PURE__*/_jsx("img", {
|
|
64
|
+
src: bannerDataUrl,
|
|
65
|
+
alt: t("banner_preview"),
|
|
66
|
+
className: classes.bannerImg
|
|
67
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
|
68
|
+
className: classes.label,
|
|
69
|
+
children: t("card")
|
|
70
|
+
}), /*#__PURE__*/_jsx("div", {
|
|
71
|
+
className: classes.cardRow,
|
|
72
|
+
children: cardDataUrl && /*#__PURE__*/_jsx("img", {
|
|
73
|
+
src: cardDataUrl,
|
|
74
|
+
alt: t("card_preview"),
|
|
75
|
+
className: classes.cardImg
|
|
76
|
+
})
|
|
77
|
+
})]
|
|
78
|
+
});
|
|
79
|
+
}
|