smart-grid.web.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. import "react-grid-layout/css/styles.css";
  2. import "react-resizable/css/styles.css";
  3. import Image from 'app/ui/atoms/image';
  4. import { View, Pressable, Row } from 'app/design/view';
  5. import { Text } from 'app/design/typography';
  6. import { useState, useEffect, useMemo, memo } from 'react';
  7. import { NeoButton, Modal } from "app/design/controls";
  8. import { fetcher } from 'app/lib/fetcher';
  9. import Form from 'app/components/elements/form';
  10. import { appSetting } from 'app/lib/util'
  11. import { WidthProvider, Responsive } from "react-grid-layout";
  12. import { components } from 'app/components/registry';
  13. import { BlockWrapper } from 'app/components/block-wrapper'
  14. import { useTranslation } from 'react-i18next'
  15. const ResponsiveReactGridLayout = WidthProvider(Responsive);
  16. const ResponsiveReactGridLayoutM = memo(({ data, bAllowEdit, rowHeight, breakpoint, onBreakpointChange, onResize, onDrag, getCell }) => (
  17. <View className={bAllowEdit ? 'grid-editable' : 'grid-readonly'}><ResponsiveReactGridLayout
  18. className="layout w-full"
  19. layouts={data}
  20. isResizable={bAllowEdit}
  21. isDraggable={bAllowEdit}
  22. breakpoints={{ lg: 700, sm: 0 }}
  23. cols={{ lg: 4, sm: 1 }}
  24. rowHeight={rowHeight}
  25. onBreakpointChange={onBreakpointChange}
  26. onResizeStop={onResize}
  27. onDragStop={onDrag}
  28. >
  29. {data[breakpoint].map((block) => {
  30. return getCell(block, bAllowEdit);
  31. })}
  32. </ResponsiveReactGridLayout></View>
  33. ));
  34. export default function ({ is_allowed_edit, block_id, content_id, content_module, data: inited_data, blockWrapperProps }) {
  35. const Map = components['element']['map'];
  36. const { t } = useTranslation()
  37. const bAllowEdit = is_allowed_edit;
  38. const blockId = block_id;
  39. const contentId = content_id;
  40. const contentModule = content_module;
  41. const settings = { minW: 1, maxW: 2, minH: 1, maxH: 2, resizeHandles: ["s", "w", "e", "n", "sw", "nw", "se", "ne"] };
  42. const rowHeight = 260;
  43. const addSettings = (item) => {
  44. return {
  45. ...item,
  46. minW: settings.minW,
  47. maxW: settings.maxW,
  48. minH: settings.minH,
  49. maxH: settings.maxH,
  50. resizeHandles: settings.resizeHandles,
  51. }
  52. }
  53. let initedData = inited_data.content;
  54. if (!initedData.lg)
  55. initedData = { lg: [], sm: [] };
  56. const [data, setData] = useState(initedData)
  57. Object.keys(initedData).forEach(key => {
  58. initedData[key] = initedData[key].map(addSettings);
  59. });
  60. const [addType, setAddType] = useState(false);
  61. const [breakpoint, setBreakpoint] = useState('lg')
  62. const onDrag = (layout) => {
  63. const updatedData1 = data[breakpoint].map(item => {
  64. const layoutItem = layout.find(l => l.i === item.i);
  65. if (layoutItem) {
  66. return { ...item, x: layoutItem.x, y: layoutItem.y, w: layoutItem.w, h: layoutItem.h };
  67. }
  68. return item;
  69. });
  70. const updatedData = { ...data, [breakpoint]: updatedData1 };
  71. setData(updatedData);
  72. }
  73. const onResize = (layout) => {
  74. const updatedData1 = data[breakpoint].map(item => {
  75. const layoutItem = layout.find(l => l.i === item.i);
  76. if (layoutItem) {
  77. return { ...item, x: layoutItem.x, y: layoutItem.y, w: layoutItem.w, h: layoutItem.h };
  78. }
  79. return item;
  80. });
  81. const updatedData = { ...data, [breakpoint]: updatedData1 };
  82. setData(updatedData);
  83. }
  84. const onRemove = (e, key) => {
  85. e.preventDefault();
  86. e.stopPropagation();
  87. let updatedData = {};
  88. Object.keys(data).forEach(iter => {
  89. updatedData[iter] = data[iter].filter(item => item.i.toString() !== key.toString());
  90. });
  91. setData(updatedData);
  92. }
  93. const onChange = (e, key) => {
  94. e.preventDefault();
  95. e.stopPropagation();
  96. let a = data[breakpoint].find(item => item.i.toString() === key.toString());
  97. setAddType(a)
  98. }
  99. const onAdd = (type) => {
  100. setAddType({ i: 0, x: 0, y: 0, w: 2, h: 1, type: type })
  101. }
  102. useEffect(() => {
  103. saveData(data)
  104. }, [data]);
  105. const saveData = async (data) => {
  106. if (data) {
  107. const sResponse = await fetcher(['/api.php?r=system/set_page_block_data/TemplServicePages&params[]=' + blockId + '&params[]=' + contentId + '&params[]=' + contentModule, null, JSON.stringify(data)]);
  108. }
  109. }
  110. let control
  111. switch (addType.type) {
  112. case 'text':
  113. control = {
  114. name: 'content',
  115. type: 'textarea',
  116. required: true,
  117. value: addType.content,
  118. caption: '',
  119. placeholder: 'Content',
  120. };
  121. break;
  122. case 'link':
  123. control = {
  124. name: 'content',
  125. type: 'text',
  126. required: true,
  127. value: addType.content,
  128. caption: '',
  129. placeholder: 'Content',
  130. };
  131. break;
  132. case 'image':
  133. control = {
  134. name: 'content',
  135. type: 'files',
  136. required: true,
  137. value: addType.content,
  138. storage_object: 'sys_images_editor',
  139. images_transcoder: 'sys_images_editor',
  140. multiple: true,
  141. uploaders: [
  142. "sys_html5"
  143. ],
  144. useUrl: true,
  145. useSingle: true,
  146. ext_allow: "jpg,jpeg,jpe,gif,png,webp",
  147. ext_deny: "",
  148. caption: '',
  149. placeholder: 'Content',
  150. };
  151. break;
  152. case 'map':
  153. control = {
  154. name: 'content',
  155. type: 'location',
  156. required: true,
  157. value: '',
  158. caption: '',
  159. placeholder: 'Content',
  160. };
  161. break;
  162. }
  163. let form = {
  164. data:
  165. {
  166. inputs: [
  167. control,
  168. {
  169. name: 'submit',
  170. type: 'submit',
  171. label: 'Content',
  172. required: true,
  173. value: 'Save',
  174. },
  175. ],
  176. }
  177. }
  178. const onBreakpointChange = (breakpoint, cols) => {
  179. setBreakpoint(breakpoint);
  180. }
  181. const onFormSubmit = async (formData, d) => {
  182. let updatedData = {};
  183. let content_data = '';
  184. let content = d.content;
  185. if (addType.type === 'map') {
  186. content = d;
  187. }
  188. if (addType.type === 'image') {
  189. const a = await fetcher('/api.php?r=system/get_page_block_image_data/TemplServicePages&params[]=' + d.content);
  190. content_data = a.data;
  191. }
  192. if (addType.type === 'link') {
  193. const a = await fetcher('/api.php?r=' + appSetting("urls", "embeds_new") + d.content);
  194. content_data = a.data;
  195. }
  196. if (addType.i > 0) {
  197. const layout = [addType];
  198. Object.keys(data).forEach(iter => {
  199. updatedData[iter] = data[iter].map(item => {
  200. const layoutItem = layout.find(l => l.i === item.i);
  201. if (layoutItem) {
  202. return { ...item, content: content, content_data: content_data };
  203. }
  204. return item;
  205. });
  206. });
  207. }
  208. else {
  209. Object.keys(data).forEach(iter => {
  210. const maxI = data[iter].length > 0 ? data[iter].reduce((max, item) => (parseInt(item.i) > max ? parseInt(item.i) : max), data[iter][0].i) : 0;
  211. updatedData[iter] = [...data[iter], addSettings({ i: (maxI + 1).toString(), x: Infinity, y: Infinity, w: 1, h: 1, type: addType.type, content: content, content_data: content_data })];
  212. })
  213. }
  214. setData(updatedData);
  215. setAddType(false);
  216. }
  217. const memoizedCells = useMemo(() => {
  218. if (data && data[breakpoint]) {
  219. return data[breakpoint].map(getCell);
  220. }
  221. return [];
  222. }, [data, breakpoint, getCell]);
  223. function getCell(block, bAllowEdit) {
  224. let blockContent;
  225. switch (block.type) {
  226. case "image":
  227. blockContent = <><Image view='cover' className=" u-cover " alt='' src={block?.content_data?.src} /></>
  228. break;
  229. case "text":
  230. blockContent = <View className="py-2 px-4 items-start justify-start h-full"><Text className='text-lg'>{block.content}</Text></View>;
  231. break;
  232. case "link":
  233. const ImageComponent = ({ className, src }) => (
  234. <Image view='cover' className={className} alt='' src={src} />
  235. );
  236. blockContent = block.content_data && (
  237. <View className={`items-left justify-between h-full p-4 ${block.h == 1 && block.w == 2 ? 'flex-row' : ''}`}>
  238. <View className={block.h == 1 && block.w == 2 ? 'w-1/2' : ''}>
  239. <View className="h-8 w-8 rounded-full">
  240. <ImageComponent className="u-cover rounded-full" src={block.content_data.logo} />
  241. </View>
  242. <Text className=' text-base mt-2 font-semibold tracking-tight' numberOfLines={1}>{block.content_data.title}</Text>
  243. {(block.h > 1 || block.w > 1) && <Text className=' text-base my-2' numberOfLines={block.h == 1 && block.w == 2 ? 3 : 4}>{block.content_data.description}</Text>}
  244. <Text className='text-sm'>{block.content_data.domain}</Text>
  245. </View>
  246. <View className={`${block.h == 1 && block.w == 2 ? 'w-1/2 items-center justify-center pl-4' : 'w-full aspect-video rounded-2xl'}`}>
  247. <View className="aspect-video rounded-2xl w-full">
  248. <ImageComponent className="u-cover rounded-lg" src={block.content_data.image} />
  249. </View>
  250. </View>
  251. </View>
  252. );
  253. break;
  254. case "map":
  255. blockContent = <Map height={rowHeight * block.h} data={{ caption: block.content.content_state + ', ' + block.content.content_city + ', ' + block.content.content_street, location: { lat: block.content.content_lat, lng: block.content.content_lng } }} />
  256. break;
  257. default:
  258. blockContent = null;
  259. }
  260. return (
  261. <View key={block.i} className=" shadow groupweb:duration-200 overflow-hidden sm:rounded-2xl bg-card">
  262. {blockContent}
  263. {bAllowEdit && <View className="absolute left-1/4 w-1/2 flex-row justify-center gap-x-4 items-center bottom-5 z-50">
  264. <Pressable
  265. className="u-neo-btn-link hit-area-8"
  266. role="button"
  267. tabIndex={0}
  268. aria-label={t('Remove')}
  269. onMouseDown={(event) => onRemove(event, block.i)}
  270. onTouchStart={(event) => onRemove(event, block.i)}
  271. onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') onRemove(event, block.i) }}
  272. >
  273. <NeoButton style="glass" controlSize="mini" borderShape="circle" image="X" interactive />
  274. </Pressable>
  275. <Pressable
  276. className="u-neo-btn-link hit-area-8"
  277. role="button"
  278. tabIndex={0}
  279. aria-label={t('Edit')}
  280. onMouseDown={(event) => onChange(event, block.i)}
  281. onTouchStart={(event) => onChange(event, block.i)}
  282. onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') onChange(event, block.i) }}
  283. >
  284. <NeoButton style="glass" controlSize="mini" borderShape="circle" image="Pencil" interactive />
  285. </Pressable>
  286. </View>}
  287. </View>
  288. );
  289. }
  290. if (!bAllowEdit && Object.values(data).flat().length == 0) {
  291. return null
  292. }
  293. const input = form?.data?.inputs?.find(item => item?.type === "files");
  294. if (input) {
  295. input.placeholder = t("Add image");
  296. //input.previewPlaceHolder = true;
  297. }
  298. return (
  299. <BlockWrapper {...blockWrapperProps}>
  300. <View className="w-full overflow-hidden">
  301. {addType && <Modal
  302. onVisible={addType}
  303. onClose={() => {
  304. setAddType(false)
  305. }}
  306. transparent={true}
  307. headerBorder={true}
  308. title={(addType.i > 0 ? "Edit " : "Add new ") + addType.type}
  309. >
  310. <View className="p-4 sm:p-0">
  311. <Form {...form} resetOnSubmit={true} onFormSubmit={onFormSubmit} />
  312. </View>
  313. </Modal>}
  314. {bAllowEdit && <Row className="gap-x-4 items-center justify-center pt-1.5">
  315. <NeoButton style="borderless" borderShape="circle" image="NotepadText" accessibilityLabel={t('Add text')} onPress={() => { onAdd('text') }} />
  316. <NeoButton style="borderless" borderShape="circle" image="Link" accessibilityLabel={t('Add link')} onPress={() => { onAdd('link') }} />
  317. <NeoButton style="borderless" borderShape="circle" image="Image" accessibilityLabel={t('Add image')} onPress={() => { onAdd('image') }} />
  318. <NeoButton style="borderless" borderShape="circle" image="MapPin" accessibilityLabel={t('Add map')} onPress={() => { onAdd('map') }} />
  319. </Row>}
  320. <View className="-ml-2 -mr-2">
  321. <ResponsiveReactGridLayoutM
  322. rowHeight={rowHeight}
  323. data={data}
  324. bAllowEdit={bAllowEdit}
  325. breakpoint={breakpoint}
  326. onBreakpointChange={onBreakpointChange}
  327. onResize={onResize}
  328. onDrag={onDrag}
  329. getCell={getCell}
  330. resizeHandles={["s", "w", "e", "n", "sw", "nw", "se", "ne"]}
  331. />
  332. </View>
  333. </View>
  334. </BlockWrapper>
  335. );
  336. }