diff --git a/package-lock.json b/package-lock.json index 7e1e4d3..8e19aaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react-hook-form": "^7.52.0", "react-icons": "^5.2.1", "react-json-view-lite": "^1.4.0", + "react-player": "^2.16.0", "recharts": "^2.12.7", "recoil": "^0.7.7", "sonner": "^1.5.0", @@ -5856,6 +5857,12 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==", + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -5934,6 +5941,12 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6429,6 +6442,12 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, "node_modules/react-firebase-hooks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz", @@ -6477,6 +6496,22 @@ "react": "^16.13.1 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-player": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz", + "integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.0.0", + "load-script": "^1.0.0", + "memoize-one": "^5.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.0.1" + }, + "peerDependencies": { + "react": ">=16.6.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.10", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.10.tgz", diff --git a/package.json b/package.json index ff21043..cc6c820 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-hook-form": "^7.52.0", "react-icons": "^5.2.1", "react-json-view-lite": "^1.4.0", + "react-player": "^2.16.0", "recharts": "^2.12.7", "recoil": "^0.7.7", "sonner": "^1.5.0", diff --git a/src/components/editor/fastboard-components/card/FastboardCard.tsx b/src/components/editor/fastboard-components/card/FastboardCard.tsx new file mode 100644 index 0000000..ccea3b2 --- /dev/null +++ b/src/components/editor/fastboard-components/card/FastboardCard.tsx @@ -0,0 +1,210 @@ +import { ComponentId, ComponentType } from "@/types/editor"; +import { + CardComponentProperties, + CardComponentType, + CardProperties, + ImageComponentProperties, + LinkComponentProperties, + SpacerComponentProperties, + TextComponentProperties, + VideoComponentProperties, +} from "@/types/editor/card-types"; +import { Spinner } from "@nextui-org/react"; +import scrollbarStyles from "@/styles/scrollbar.module.css"; +import useData from "@/hooks/useData"; +import { toast } from "sonner"; +import { useEffect, useMemo } from "react"; +import useNavigation from "@/hooks/useNavigation"; +import { useSetRecoilState } from "recoil"; +import { propertiesDrawerState } from "@/atoms/editor"; +import useDashboard from "@/hooks/dashboards/useDashboard"; +import TextComponent from "./TextComponent"; +import ImageComponent from "./ImageComponent"; +import VideoComponent from "./VideoComponent"; +import LinkComponent from "./LinkComponent"; +import SpacerComponent from "./SpacerComponent"; +import { useTheme } from "next-themes"; + +export default function FastboardCard({ + id, + properties, +}: { + id: ComponentId; + properties: CardProperties; +}) { + const { theme } = useTheme(); + const { + sourceQueryData, + queryParameters, + components, + spacing, + showShadow, + backgroundColor, + } = properties; + const { updateComponentProperties } = useDashboard(); + const { getQueryParam } = useNavigation(); + const traducedQueryParameters = useMemo(() => { + if (!queryParameters) return {}; + + return Object.entries(queryParameters).reduce((acc, parameter) => { + // Test if the value of the parametes has the synstax {{URL.queryValue}} + const pattern = /\{\{\s*URL\.([^\s]+)\s*\}\}/; + const match = parameter[1].match(pattern); + if (match) { + const queryValue = getQueryParam(match[1]); + return { ...acc, [parameter[0]]: queryValue }; + } + return { ...acc, [parameter[0]]: parameter[1] }; + }, {}); + }, [queryParameters]); + const { + data, + keys, + isFetching: dataFetching, + isError: isDataError, + error: dataError, + refetch, + } = useData( + `${ComponentType.Card}-${id}`, + sourceQueryData, + traducedQueryParameters + ); + const setPropertiesState = useSetRecoilState(propertiesDrawerState); + + useEffect(() => { + updateComponentProperties(id, { + ...properties, + dataKeys: keys, + }); + setPropertiesState((previous) => { + if (previous.selectedComponentId !== id) { + return previous; + } + return { + ...previous, + properties: { + ...previous.properties, + dataKeys: keys, + }, + }; + }); + }, [keys]); + + useEffect(() => { + refetch(); + }, [queryParameters]); + + useEffect(() => { + if (isDataError) { + toast.error(dataError?.message); + } + }, [isDataError]); + + function isValidData(data: any[]) { + if (data.length > 1) { + return false; + } + if (data.length === 0) { + return false; + } + return typeof data[0] === "object"; + } + + function renderComponent(component: CardComponentProperties, data: any[]) { + const item = data[0]; + + switch (component.type) { + case CardComponentType.Text: { + return ( + + ); + } + case CardComponentType.Image: + return ( + + ); + case CardComponentType.Link: + return ( + + ); + case CardComponentType.Video: + return ( + + ); + case CardComponentType.Spacer: + return ( + + ); + default: + return

Component not found

; + } + } + + return ( +
+ {dataFetching && } + {!dataFetching && isDataError && ( +

+ {dataError?.message} +

+ )} + + {!dataFetching && !isDataError && ( + <> + {!sourceQueryData && ( +

+ Nothing to show. There is no query selected +

+ )} + {sourceQueryData && isValidData(data) && ( + <> + {components.length === 0 && ( +

+ Add components +

+ )} + + {components.length > 0 && ( +
+ {components.map((component) => + renderComponent(component, data) + )} +
+ )} + + )} + {sourceQueryData && !isValidData(data) && ( +

+ Seems like the data is not an object or is empty. +

+ )} + + )} +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/FastboardCardDraggable.tsx b/src/components/editor/fastboard-components/card/FastboardCardDraggable.tsx new file mode 100644 index 0000000..d5bce51 --- /dev/null +++ b/src/components/editor/fastboard-components/card/FastboardCardDraggable.tsx @@ -0,0 +1,20 @@ +import Draggable from "../Draggable"; +import { ComponentType } from "@/types/editor"; +import DraggableImage from "@/components/shared/DraggableImage"; +import { CardProperties } from "@/types/editor/card-types"; + +export default function FastboardCardDraggable() { + return ( + + + + ); +} diff --git a/src/components/editor/fastboard-components/card/ImageComponent.tsx b/src/components/editor/fastboard-components/card/ImageComponent.tsx new file mode 100644 index 0000000..70b92e0 --- /dev/null +++ b/src/components/editor/fastboard-components/card/ImageComponent.tsx @@ -0,0 +1,66 @@ +import { Alignment } from "@/components/shared/AlignmentProperty"; +import { ImageComponentProperties } from "@/types/editor/card-types"; +import { FastboardHeaderPhotoSize } from "@/types/editor/header-types"; +import { Image } from "@nextui-org/react"; +import { useEffect, useState } from "react"; + +export default function ImageComponent({ + properties, + item, +}: { + properties: ImageComponentProperties; + item: any; +}) { + const { dataKey, alignment, border, size } = properties; + const [imageError, setImageError] = useState(false); + + useEffect(() => { + setImageError(false); + }, [dataKey]); + + function getSize(size: FastboardHeaderPhotoSize) { + switch (size) { + case FastboardHeaderPhotoSize.Small: + return "100px"; + case FastboardHeaderPhotoSize.Medium: + return "150px"; + case FastboardHeaderPhotoSize.Large: + return "300px"; + } + } + + return ( +
+ Card image setImageError(true)} + /> +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/LinkComponent.tsx b/src/components/editor/fastboard-components/card/LinkComponent.tsx new file mode 100644 index 0000000..9c14378 --- /dev/null +++ b/src/components/editor/fastboard-components/card/LinkComponent.tsx @@ -0,0 +1,76 @@ +import { Alignment } from "@/components/shared/AlignmentProperty"; +import { Icon } from "@/components/shared/IconPicker"; +import { LinkComponentProperties } from "@/types/editor/card-types"; +import { Link } from "@nextui-org/react"; +import { useTheme } from "next-themes"; + +export default function LinkComponent({ + properties, + item, +}: { + properties: LinkComponentProperties; + item: any; +}) { + const { theme } = useTheme(); + const { + dataKey, + alignment, + label, + defaultText, + isExternal, + externalIcon, + showExternalIcon, + fontSize, + textColor, + labelColor, + } = properties; + + function addPrefix(url: string) { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + return `https://${url}`; + } + return url; + } + + return ( +
+

+ {label} +

+ + ) : ( + + ) + } + style={{ + color: theme === "dark" ? textColor.dark : textColor.light, + fontSize: `${fontSize}px`, + }} + > + {dataKey !== "" ? item[dataKey] : defaultText} + +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/SpacerComponent.tsx b/src/components/editor/fastboard-components/card/SpacerComponent.tsx new file mode 100644 index 0000000..4eac58f --- /dev/null +++ b/src/components/editor/fastboard-components/card/SpacerComponent.tsx @@ -0,0 +1,25 @@ +import { + SpacerComponentProperties, + VideoComponentProperties, +} from "@/types/editor/card-types"; +import { Spacer } from "@nextui-org/react"; +import React from "react"; +import ReactPlayer from "react-player/lazy"; + +export default function SpacerComponent({ + properties, + item, +}: { + properties: SpacerComponentProperties; + item: any; +}) { + const { height } = properties; + + return ( + + ); +} diff --git a/src/components/editor/fastboard-components/card/TextComponent.tsx b/src/components/editor/fastboard-components/card/TextComponent.tsx new file mode 100644 index 0000000..d77cd8a --- /dev/null +++ b/src/components/editor/fastboard-components/card/TextComponent.tsx @@ -0,0 +1,63 @@ +import { Alignment } from "@/components/shared/AlignmentProperty"; +import { FontType } from "@/components/shared/FontTypeProperty"; +import { TextComponentProperties } from "@/types/editor/card-types"; +import { useTheme } from "next-themes"; + +export default function TextComponent({ + properties, + item, +}: { + properties: TextComponentProperties; + item: any; +}) { + const { theme } = useTheme(); + const { + dataKey, + label, + defaultText, + alignment, + fontSize, + fontTypes, + textColor, + labelColor, + } = properties; + + return ( +
+ {label !== "" && ( +

+ {label} +

+ )} + +

+ {dataKey !== "" ? item[dataKey] : defaultText} +

+
+ ); +} diff --git a/src/components/editor/fastboard-components/card/VideoComponent.tsx b/src/components/editor/fastboard-components/card/VideoComponent.tsx new file mode 100644 index 0000000..afb62a7 --- /dev/null +++ b/src/components/editor/fastboard-components/card/VideoComponent.tsx @@ -0,0 +1,37 @@ +import { VideoComponentProperties } from "@/types/editor/card-types"; +import React from "react"; +import ReactPlayer from "react-player/lazy"; + +export default function VideoComponent({ + properties, + item, +}: { + properties: VideoComponentProperties; + item: any; +}) { + const { dataKey } = properties; + + return ( +
+ { + console.error(e.target.error); + }} + /> +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardComponent.tsx b/src/components/editor/fastboard-components/card/properties/CardComponent.tsx new file mode 100644 index 0000000..96e62c2 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardComponent.tsx @@ -0,0 +1,109 @@ +import { + CardComponentProperties, + CardComponentType, + DefaultCardComponentProperties, + ImageComponentProperties, + LinkComponentProperties, + SpacerComponentProperties, + TextComponentProperties, + VideoComponentProperties, +} from "@/types/editor/card-types"; +import CardTextComponentProperties from "./CardTextComponentProperties"; +import { Select, SelectItem } from "@nextui-org/react"; +import CardComponentIcon from "./CardComponentIcon"; +import CardImageComponentProperties from "./CardImageComponentProperties"; +import CardLinkComponentProperties from "./CardLinkComponentProperties"; +import CardVideoComponentProperties from "./CardVideoComponentProperties"; +import CardSpacerComponentProperties from "./CardSpacerComponentProperties"; + +export default function CardComponent({ + component, + dataKeys, + onComponentChange, +}: { + component: CardComponentProperties; + dataKeys: string[]; + onComponentChange: (componentProperties: CardComponentProperties) => void; +}) { + const { type } = component; + + function changeComponentType( + type: CardComponentType + ): CardComponentProperties { + const newProperties = DefaultCardComponentProperties.of(type); + return { + ...newProperties, + }; + } + + return ( +
+ + + {type === CardComponentType.Text && ( + { + onComponentChange(component); + }} + /> + )} + {type === CardComponentType.Image && ( + { + onComponentChange(component); + }} + /> + )} + {type === CardComponentType.Link && ( + { + onComponentChange(component); + }} + /> + )} + {type === CardComponentType.Video && ( + { + onComponentChange(component); + }} + /> + )} + {type === CardComponentType.Spacer && ( + { + onComponentChange(component); + }} + /> + )} +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardComponentIcon.tsx b/src/components/editor/fastboard-components/card/properties/CardComponentIcon.tsx new file mode 100644 index 0000000..d86e866 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardComponentIcon.tsx @@ -0,0 +1,36 @@ +import { CardComponentType } from "@/types/editor/card-types"; +import { + IconProps, + Image, + Link, + Pharagraphspacing, + Text, + VideoSquare, +} from "iconsax-react"; + +export default function CardComponentIcon({ + type, + size, + variant, + className, +}: { + type: CardComponentType; + size: number; + variant?: IconProps["variant"]; + className?: string; +}) { + switch (type) { + case CardComponentType.Text: + return ; + case CardComponentType.Image: + return ; + case CardComponentType.Link: + return ; + case CardComponentType.Video: + return ; + case CardComponentType.Spacer: + return ; + default: + return null; + } +} diff --git a/src/components/editor/fastboard-components/card/properties/CardComponentsList.tsx b/src/components/editor/fastboard-components/card/properties/CardComponentsList.tsx new file mode 100644 index 0000000..7fc5e41 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardComponentsList.tsx @@ -0,0 +1,74 @@ +import { + Button, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownTrigger, +} from "@nextui-org/react"; +import { Add } from "iconsax-react"; + +import Option from "@/components/shared/Option"; +import { + CardComponentProperties, + CardComponentType, + DefaultCardComponentProperties, +} from "@/types/editor/card-types"; +import CardComponentIcon from "./CardComponentIcon"; + +export default function CardComponentsList({ + components, + onSelectComponent, + onChange, +}: { + components: CardComponentProperties[]; + onSelectComponent?: (component: CardComponentProperties) => void; + onChange?: (newComponents: CardComponentProperties[]) => void; +}) { + return ( +
+
+ + + + + + {Object.values(CardComponentType).map((type) => ( + { + onChange?.([ + ...components, + DefaultCardComponentProperties.of(type), + ]); + }} + startContent={} + > + {type} + + ))} + + +
+ +
    + {components.map((component, index) => ( +
+
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardImageComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardImageComponentProperties.tsx new file mode 100644 index 0000000..5aaa2f6 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardImageComponentProperties.tsx @@ -0,0 +1,93 @@ +import AlignmentProperty from "@/components/shared/AlignmentProperty"; +import ImageBoderProperty from "@/components/shared/ImageBorderProperty"; +import { ImageComponentProperties } from "@/types/editor/card-types"; +import { FastboardHeaderPhotoSize } from "@/types/editor/header-types"; +import { Select, SelectItem, Slider } from "@nextui-org/react"; + +export default function CardImageComponentProperties({ + properties, + dataKeys, + onValueChange, +}: { + properties: ImageComponentProperties; + dataKeys: string[]; + onValueChange: (properties: ImageComponentProperties) => void; +}) { + const { dataKey, alignment, border, size } = properties; + + return ( +
+ + + onValueChange({ ...properties, alignment: position }) + } + /> + onValueChange({ ...properties, border })} + /> + { + const selectedValue = e as number; + const size = + selectedValue === 0 + ? FastboardHeaderPhotoSize.Small + : selectedValue === 1 + ? FastboardHeaderPhotoSize.Medium + : FastboardHeaderPhotoSize.Large; + onValueChange({ + ...properties, + size, + }); + }} + /> +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardLinkComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardLinkComponentProperties.tsx new file mode 100644 index 0000000..5eb5dba --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardLinkComponentProperties.tsx @@ -0,0 +1,163 @@ +import AlignmentProperty from "@/components/shared/AlignmentProperty"; +import ColorPicker from "@/components/shared/ColorPicker"; +import IconPicker from "@/components/shared/IconPicker"; +import { LinkComponentProperties } from "@/types/editor/card-types"; +import { Checkbox, Input, Select, SelectItem } from "@nextui-org/react"; +import { useTheme } from "next-themes"; + +export default function CardLinkComponentProperties({ + properties, + dataKeys, + onValueChange, +}: { + properties: LinkComponentProperties; + dataKeys: string[]; + onValueChange: (properties: LinkComponentProperties) => void; +}) { + const { theme } = useTheme(); + const { + dataKey, + label, + defaultText, + isExternal, + externalIcon, + showExternalIcon, + alignment, + fontSize, + textColor, + labelColor, + } = properties; + + return ( +
+ + { + onValueChange({ ...properties, label: value }); + }} + /> + { + onValueChange({ ...properties, defaultText: value }); + }} + /> +
+

Text size

+ { + onValueChange({ ...properties, fontSize: Number(value) }); + }} + /> +
+ + + onValueChange({ ...properties, alignment: position }) + } + /> + +
+ Is external + { + onValueChange({ + ...properties, + isExternal: value, + }); + }} + /> +
+
+ Show anchow icon + { + onValueChange({ + ...properties, + showExternalIcon: value, + }); + }} + /> +
+
+ Anchor icon + { + onValueChange({ + ...properties, + externalIcon: icon, + }); + }} + /> +
+ + { + if (theme === "dark") { + onValueChange({ + ...properties, + textColor: { dark: color, light: textColor.light }, + }); + } else { + onValueChange({ + ...properties, + textColor: { dark: textColor.dark, light: color }, + }); + } + }} + /> + { + if (theme === "dark") { + onValueChange({ + ...properties, + labelColor: { dark: color, light: labelColor.light }, + }); + } else { + onValueChange({ + ...properties, + labelColor: { dark: labelColor.dark, light: color }, + }); + } + }} + /> +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardSpacerComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardSpacerComponentProperties.tsx new file mode 100644 index 0000000..dd64359 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardSpacerComponentProperties.tsx @@ -0,0 +1,35 @@ +import { SpacerComponentProperties } from "@/types/editor/card-types"; +import { Input } from "@nextui-org/react"; + +export default function CardSpacerComponentProperties({ + properties, + dataKeys, + onValueChange, +}: { + properties: SpacerComponentProperties; + dataKeys: string[]; + onValueChange: (properties: SpacerComponentProperties) => void; +}) { + const { height } = properties; + + return ( +
+
+

Spacing

+ { + onValueChange({ + ...properties, + height: parseInt(e.target.value), + }); + }} + /> +
+
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardStyle.tsx b/src/components/editor/fastboard-components/card/properties/CardStyle.tsx new file mode 100644 index 0000000..918e8d5 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardStyle.tsx @@ -0,0 +1,57 @@ +import ColorPicker from "@/components/shared/ColorPicker"; +import { CardProperties } from "@/types/editor/card-types"; +import { Checkbox, Input } from "@nextui-org/react"; +import { useTheme } from "next-themes"; + +export default function CardStyle({ + properties, + onValueChange, +}: { + properties: CardProperties; + onValueChange: (properties: CardProperties) => void; +}) { + const { theme } = useTheme(); + const { spacing, backgroundColor, showShadow } = properties; + + return ( +
+
+

Show border

+ { + onValueChange({ + ...properties, + showShadow: isSelected, + }); + }} + /> +
+ { + if (theme === "light") { + onValueChange({ + ...properties, + backgroundColor: { + light: color, + dark: backgroundColor.dark, + }, + }); + } else { + onValueChange({ + ...properties, + backgroundColor: { + light: backgroundColor.light, + dark: color, + }, + }); + } + }} + /> +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardTextComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardTextComponentProperties.tsx new file mode 100644 index 0000000..6679691 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardTextComponentProperties.tsx @@ -0,0 +1,129 @@ +import AlignmentProperty from "@/components/shared/AlignmentProperty"; +import ColorPicker from "@/components/shared/ColorPicker"; +import FontTypeProperty from "@/components/shared/FontTypeProperty"; +import { TextComponentProperties } from "@/types/editor/card-types"; +import { Input, Select, SelectItem } from "@nextui-org/react"; +import { useTheme } from "next-themes"; + +export default function CardTextComponentProperties({ + properties, + dataKeys, + onValueChange, +}: { + properties: TextComponentProperties; + dataKeys: string[]; + onValueChange: (properties: TextComponentProperties) => void; +}) { + const { theme } = useTheme(); + const { + dataKey, + label, + defaultText, + alignment, + fontSize, + fontTypes, + textColor, + labelColor, + } = properties; + + return ( +
+ + { + onValueChange({ ...properties, label: value }); + }} + /> + { + onValueChange({ ...properties, defaultText: value }); + }} + /> +
+

Text size

+ { + onValueChange({ ...properties, fontSize: Number(value) }); + }} + /> +
+ + + onValueChange({ ...properties, alignment: position }) + } + /> + { + onValueChange({ ...properties, fontTypes: newFontTypes }); + }} + /> + { + if (theme === "dark") { + onValueChange({ + ...properties, + textColor: { dark: color, light: textColor.light }, + }); + } else { + onValueChange({ + ...properties, + textColor: { dark: textColor.dark, light: color }, + }); + } + }} + /> + { + if (theme === "dark") { + onValueChange({ + ...properties, + labelColor: { dark: color, light: labelColor.light }, + }); + } else { + onValueChange({ + ...properties, + labelColor: { dark: labelColor.dark, light: color }, + }); + } + }} + /> +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/CardVideoComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardVideoComponentProperties.tsx new file mode 100644 index 0000000..a242ddf --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/CardVideoComponentProperties.tsx @@ -0,0 +1,37 @@ +import { VideoComponentProperties } from "@/types/editor/card-types"; +import { Select, SelectItem } from "@nextui-org/react"; + +export default function CardVideoComponentProperties({ + properties, + dataKeys, + onValueChange, +}: { + properties: VideoComponentProperties; + dataKeys: string[]; + onValueChange: (properties: VideoComponentProperties) => void; +}) { + const { dataKey } = properties; + + return ( +
+ +
+ ); +} diff --git a/src/components/editor/fastboard-components/card/properties/FastboardCardProperties.tsx b/src/components/editor/fastboard-components/card/properties/FastboardCardProperties.tsx new file mode 100644 index 0000000..ff91390 --- /dev/null +++ b/src/components/editor/fastboard-components/card/properties/FastboardCardProperties.tsx @@ -0,0 +1,220 @@ +import QuerySelection from "@/components/editor/QuerySelection"; +import { + CardComponentProperties, + CardProperties, +} from "@/types/editor/card-types"; +import { + Accordion, + AccordionItem, + BreadcrumbItem, + Breadcrumbs, + Code, + Input, + Spacer, + Tooltip, +} from "@nextui-org/react"; +import { RiQuestionLine } from "react-icons/ri"; +import CardComponentsList from "./CardComponentsList"; +import { useEffect, useState } from "react"; +import { useRecoilValue } from "recoil"; +import { propertiesDrawerState } from "@/atoms/editor"; +import CardComponent from "./CardComponent"; +import { QueryType } from "@/types/connections"; +import CardStyle from "./CardStyle"; +import DebounceInput from "@/components/shared/DebounceInput"; + +export default function FastboardCardProperties({ + properties, + onValueChange, +}: { + properties: CardProperties; + onValueChange: (properties: CardProperties) => void; +}) { + const { sourceQueryData, queryParameters, components, dataKeys } = properties; + const { selectedComponentId } = useRecoilValue(propertiesDrawerState); + const [componentSelectedIndex, setComponentSelectedIndex] = useState< + number | null + >(null); + + useEffect(() => { + // Reset selectinos when component is changed + setComponentSelectedIndex(null); + }, [selectedComponentId]); + + function onComponentChange(component: CardComponentProperties) { + if (componentSelectedIndex === null) { + return; + } + const newComponents = [...components]; + newComponents[componentSelectedIndex] = component; + + onValueChange({ + ...properties, + components: newComponents, + }); + } + + return ( +
+ + { + setComponentSelectedIndex(null); + }} + > + Card + + + {componentSelectedIndex != null && ( + Component + )} + + + + {componentSelectedIndex === null && ( + + + { + if (newQuery.id === sourceQueryData?.queryId) { + return; + } + onValueChange({ + ...properties, + sourceQueryData: { + queryId: newQuery.id, + connectionId: newQuery.connection_id, + method: newQuery.metadata?.method, + }, + queryParameters: newQuery.metadata?.parameters?.reduce( + (acc: Record, param: any) => { + acc[param.name] = param.preview; + return acc; + }, + {} + ), + dataKeys: [], + components: components.map((component) => ({ + ...component, + dataKey: "", + })), + }); + }} + /> + {sourceQueryData && queryParameters && ( + <> +
+

Parameters

+ + Use{" "} + + {"{{URL.queryValue}}"} + {" "} + syntax to access url query strings. +
+ } + className={"p-3 w-[275px] -translate-x-[35px] text-xs"} + placement={"bottom"} + offset={10} + closeDelay={0} + > +
+ +
+ +
+ +
+ {Object.entries(queryParameters).map((parameter, index) => ( +
+

{parameter[0]}

+ { + onValueChange({ + ...properties, + queryParameters: { + ...queryParameters, + [parameter[0]]: value, + }, + }); + }} + /> +
+ ))} +
+ + )} + + + {(!sourceQueryData || dataKeys.length === 0) && ( +
+ Select a query to enable components +
+ )} + {sourceQueryData && dataKeys.length > 0 && ( + { + setComponentSelectedIndex(components.indexOf(component)); + }} + onChange={(newComponents) => + onValueChange({ ...properties, components: newComponents }) + } + /> + )} +
+ + + + + )} + + {componentSelectedIndex !== null && ( + + )} + + ); +} diff --git a/src/components/editor/fastboard-components/header/FastboardHeader.tsx b/src/components/editor/fastboard-components/header/FastboardHeader.tsx index 7a78bf5..b460d57 100644 --- a/src/components/editor/fastboard-components/header/FastboardHeader.tsx +++ b/src/components/editor/fastboard-components/header/FastboardHeader.tsx @@ -1,10 +1,10 @@ import { ComponentId } from "@/types/editor"; import { FastboardHeaderProperties } from "@/types/editor/header-types"; import { Image, Navbar, NavbarContent, NavbarItem } from "@nextui-org/react"; -import { FastboardHeaderPosition } from "@/types/editor/header-types"; import { useEffect, useState } from "react"; import { ThemeSwitcher } from "@/components/layout/ThemeSwitcher"; import { useTheme } from "next-themes"; +import { Alignment } from "@/components/shared/AlignmentProperty"; export default function FastboardHeader({ id, @@ -43,17 +43,20 @@ export default function FastboardHeader({ theme === "light" ? backgroundColor.light : backgroundColor.dark, }} > - + {photo.url && ( Header {title.size && (
- {showThemeSwitcher && position === FastboardHeaderPosition.Right && ( + {showThemeSwitcher && position === Alignment.Right && ( )} - {showThemeSwitcher && position !== FastboardHeaderPosition.Right && ( + {showThemeSwitcher && position !== Alignment.Right && (
-

Position

- - {Object.entries(FastboardHeaderPosition).map(([key, value]) => ( - - ))} - + + onValueChange({ + ...properties, + position: position, + }) + } + />

Photo Border Radius

diff --git a/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx b/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx index a6aeac8..52a4780 100644 --- a/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx +++ b/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx @@ -13,7 +13,6 @@ export default function FastboardSidebar({ const { theme } = useTheme(); const { currentPage, changePage } = useNavigation(); const { menuItems, backgroundColor, textColor, selectedColor } = properties; - const cursorClassName = `bg-[${selectedColor.light}]`; function handleSelectionChange(key: Key) { changePage(key.toString()); diff --git a/src/components/editor/fastboard-components/table/FastboardTable.tsx b/src/components/editor/fastboard-components/table/FastboardTable.tsx index 5b57acf..5b0ae00 100644 --- a/src/components/editor/fastboard-components/table/FastboardTable.tsx +++ b/src/components/editor/fastboard-components/table/FastboardTable.tsx @@ -1,4 +1,3 @@ -import CustomSkeleton from "@/components/shared/CustomSkeleton"; import useExecuteQuery from "@/hooks/adapter/useExecuteQuery"; import { FastboardTableProperties, @@ -53,15 +52,16 @@ import { Table, useReactTable, } from "@tanstack/react-table"; -import { HTTP_METHOD } from "@/types/connections"; import { fillParameters, getExportData, getFilterFunction, getFinalColumns, sortFunction, + traduceQueryStrings, } from "@/lib/table.utils"; -import Filters, { NumberFilter, StringFilter } from "./Filters"; +import Filters from "./Filters"; +import useNavigation from "@/hooks/useNavigation"; export default function FastboardTable({ id, @@ -73,6 +73,7 @@ export default function FastboardTable({ const { theme } = useTheme(); const { id: dashboardId } = useParams(); const { updateComponentProperties } = useDashboard(); + const { changePage } = useNavigation(); const { openModal } = useModalFrame(); const { sourceQueryData, @@ -345,16 +346,29 @@ export default function FastboardTable({ } onPress={() => { setSelectedRowAction({ action, item }); + updateComponentProperties(id, { + ...properties, + selectedRow: item, + }); if (action.type === "view") { - executeAction({ action, item }); - setViewModalOpen(action.query ? true : false); + if (action.mode === "modal") { + executeAction({ action, item }); + setViewModalOpen(action.query ? true : false); + } else if (action.mode === "page") { + if (!action.pageId) { + toast.warning( + "No page found for this action, please add one in action properties" + ); + return; + } + changePage( + action.pageId, + traduceQueryStrings(action.queryStrings, item) + ); + } } else if (action.type === "delete") { setDeleteModalOpen(true); } else if (action.type === "edit") { - updateComponentProperties(id, { - ...properties, - selectedRow: item, - }); openModal(action.modalId ?? ""); } }} diff --git a/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx b/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx index d7a08e8..8217404 100644 --- a/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx +++ b/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx @@ -31,6 +31,7 @@ import { QueryType } from "@/types/connections"; import FiltersList from "./filters/FiltersList"; import TableStringFilterProperties from "./filters/TableStringFilterProperties"; import TableNumberFilterProperties from "./filters/TableNumberFilterProperties"; +import { ViewActionProperties } from "./ViewActionProperties"; import { queryToQueryData } from "@/lib/rest-queries"; const FastboardTablePropertiesComponent = ({ @@ -285,7 +286,7 @@ const FastboardTablePropertiesComponent = ({ )} - {actionSelected && ( + {actionSelected && actionSelected.type == "delete" && ( c.column)} @@ -298,6 +299,19 @@ const FastboardTablePropertiesComponent = ({ }} /> )} + {actionSelected && actionSelected.type == "view" && ( + c.column)} + onChange={(action) => { + setActionSelected(action); + onValueChange({ + ...properties, + actions: actions.map((a) => (a.key === action.key ? action : a)), + }); + }} + /> + )} {filterIndexSelected !== null && filters[filterIndexSelected]?.type === FilterType.StringFilter && ( diff --git a/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx b/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx index 9eedefa..2f58dec 100644 --- a/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx +++ b/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx @@ -24,7 +24,7 @@ export default function TableActionsList({ onActionSelect: (action: TableActionProperty) => void; onChange?: (actions: TableActionProperty[]) => void; }) { - const { createModalFrame, deleteModalFrame } = useDashboard(); + const { createModalFrame, deleteModalFrame, deletePage } = useDashboard(); const [actions, setActions] = useState(actionsProperties); useEffect(() => { @@ -38,6 +38,7 @@ export default function TableActionsList({ type, query: null, parameters: [], + queryStrings: [], }; setActions((previous) => [...previous, newAction]); @@ -66,6 +67,7 @@ export default function TableActionsList({ query: null, parameters: [], modalId, + queryStrings: [], }; setActions((previous) => [...previous, newAction]); @@ -78,6 +80,9 @@ export default function TableActionsList({ if (actions[index].modalId) { deleteModalFrame(actions[index].modalId as string); } + if (actions[index].pageId) { + deletePage(actions[index].pageId); + } const newActions = actions.filter((action) => action.key !== key); setActions(newActions); if (onChange) { diff --git a/src/components/editor/fastboard-components/table/properties/ViewActionProperties.tsx b/src/components/editor/fastboard-components/table/properties/ViewActionProperties.tsx new file mode 100644 index 0000000..cf11053 --- /dev/null +++ b/src/components/editor/fastboard-components/table/properties/ViewActionProperties.tsx @@ -0,0 +1,249 @@ +import { + Button, + Code, + Input, + Select, + SelectItem, + Tab, + Tabs, + Tooltip, +} from "@nextui-org/react"; +import { RiQuestionLine } from "react-icons/ri"; +import { Column, TableActionProperty } from "@/types/editor/table-types"; +import QuerySelection from "@/components/editor/QuerySelection"; +import useDashboard from "@/hooks/dashboards/useDashboard"; +import { Add } from "iconsax-react"; +import useNavigation from "@/hooks/useNavigation"; +import { IoIosClose } from "react-icons/io"; +import { QueryType } from "@/types/connections"; +import { queryToQueryData } from "@/lib/rest-queries"; + +export function ViewActionProperties({ + action, + columns, + onChange, +}: { + action: TableActionProperty; + columns: Column[]; + onChange: (action: TableActionProperty) => void; +}) { + const { label, mode, query, parameters, pageId, queryStrings } = action; + const { addPage } = useDashboard(); + const { currentPage } = useNavigation(); + + function createViewPage() { + const pageId = addPage(currentPage); + if (!pageId) return; + + onChange({ + ...action, + pageId: pageId, + }); + } + + return ( +
+ { + onChange({ + ...action, + label: newValue, + }); + }} + /> + + { + onChange({ + ...action, + mode: key.toString() as "modal" | "page", + }); + }} + > + + { + onChange({ + ...action, + query: queryToQueryData(query), + parameters: query.metadata.parameters?.map( + (p: { name: string; preview: string }) => ({ + name: p.name, + columnKey: "", + value: p.preview, + }) + ), + }); + }} + /> + {parameters?.length > 0 && ( +
+

Parameters

+ + If no column is selected for a parameter we use{" "} + {"preview value"} setting + on queries editor. +
+ } + className={"p-3 w-[275px] -translate-x-[35px] text-xs"} + placement={"bottom"} + offset={10} + closeDelay={0} + > +
+ +
+ +
+ )} +
+ {parameters?.map((parameter, index) => ( +
+

{parameter.name}

+ +
+ ))} +
+ + + + {!pageId && ( + + )} + {pageId && ( +
+
+ +
+ + {queryStrings?.map((queryString, index) => ( +
+ { + const newQueryStrings = queryStrings.map((qs, i) => { + if (i === index) { + return { + ...qs, + name: value, + }; + } + return qs; + }); + onChange({ + ...action, + queryStrings: newQueryStrings, + }); + }} + /> + + + + +
+ ))} +
+ )} +
+ +
+ ); +} diff --git a/src/components/editor/fastboard-components/utils.tsx b/src/components/editor/fastboard-components/utils.tsx index 0823c87..3a5f76e 100644 --- a/src/components/editor/fastboard-components/utils.tsx +++ b/src/components/editor/fastboard-components/utils.tsx @@ -44,6 +44,10 @@ import { SidebarProperties } from "@/types/editor/sidebar-types"; import FastboardHeader from "./header/FastboardHeader"; import FastboardHeaderPropertiesComponent from "./header/FastboardHeaderProperties"; import { FastboardHeaderProperties } from "@/types/editor/header-types"; +import FastboardCard from "./card/FastboardCard"; +import { CardProperties } from "@/types/editor/card-types"; +import FastboardCardProperties from "./card/properties/FastboardCardProperties"; +import FastboardCardDraggable from "./card/FastboardCardDraggable"; export function getDraggableComponent(id: ComponentType) { const components = { @@ -54,6 +58,7 @@ export function getDraggableComponent(id: ComponentType) { ), [ComponentType.Cards]: , + [ComponentType.Card]: , [ComponentType.Sidebar]: null, [ComponentType.Header]: null, }; @@ -104,7 +109,20 @@ export function getComponent( editable: null, view: null, }, - + [ComponentType.Card]: { + editable: ( + + ), + view: ( + + ), + }, [ComponentType.Cards]: { editable: ( ), + [ComponentType.Card]: ( + { + if (onValueChange) { + onValueChange(properties); + } + }} + /> + ), [ComponentType.Cards]: ( {!(hasSidebar && sidebarVisible) && ( { changeLayout("home", 0, layoutType); }} diff --git a/src/components/shared/AlignmentProperty.tsx b/src/components/shared/AlignmentProperty.tsx new file mode 100644 index 0000000..7165f86 --- /dev/null +++ b/src/components/shared/AlignmentProperty.tsx @@ -0,0 +1,48 @@ +import { Button, ButtonGroup } from "@nextui-org/react"; +import AlignIcon from "./icons/AlignIcon"; + +export enum Alignment { + Left = "Start", + Center = "Center", + Right = "End", +} + +export default function AlignmentProperty({ + label, + position, + onPositionChange, +}: { + label?: string; + position: Alignment; + onPositionChange: (position: Alignment) => void; +}) { + return ( +
+

{label}

+ + {Object.entries(Alignment).map(([key, value]) => ( + + ))} + +
+ ); +} diff --git a/src/components/shared/DebounceInput.tsx b/src/components/shared/DebounceInput.tsx new file mode 100644 index 0000000..4d3cf89 --- /dev/null +++ b/src/components/shared/DebounceInput.tsx @@ -0,0 +1,37 @@ +import { Input } from "@nextui-org/react"; +import React, { useEffect, useState } from "react"; + +export default function DebounceInput({ + value, + onValueChange, + delay = 500, + className, + type, +}: { + value: string; + onValueChange: (value: string) => void; + delay?: number; + className?: string; + type?: string; +}) { + const [inputValue, setInputValue] = useState(value); + + useEffect(() => { + const timeoutId = setTimeout(() => { + onValueChange(inputValue); + }, delay); + return () => clearTimeout(timeoutId); + }, [inputValue, delay]); + + return ( + { + setInputValue(value); + }} + width={"100%"} + /> + ); +} diff --git a/src/components/shared/FontTypeProperty.tsx b/src/components/shared/FontTypeProperty.tsx new file mode 100644 index 0000000..966a439 --- /dev/null +++ b/src/components/shared/FontTypeProperty.tsx @@ -0,0 +1,68 @@ +import { Button, ButtonGroup } from "@nextui-org/react"; + +export enum FontType { + Bold = "Bold", + Italic = "Italic", + Underline = "Underline", +} + +export default function FontTypeProperty({ + fontTypes, + onFontTypesChange, +}: { + fontTypes: FontType[]; + onFontTypesChange: (fontTypes: FontType[]) => void; +}) { + return ( + + + + + + ); +} diff --git a/src/components/shared/IconPicker.tsx b/src/components/shared/IconPicker.tsx index 1d539d1..5f9ac77 100644 --- a/src/components/shared/IconPicker.tsx +++ b/src/components/shared/IconPicker.tsx @@ -51,11 +51,11 @@ import { Heart, Hierarchy, Home, + Link, Folder, Trash, User, } from "iconsax-react"; -import { IoIosClose } from "react-icons/io"; export function Icon({ icon, @@ -113,6 +113,7 @@ export function Icon({ [IconType.Heart]: , [IconType.Hierarchy]: , [IconType.Home]: , + [IconType.Link]: , [IconType.Folder]: , [IconType.Trash]: , [IconType.User]: , diff --git a/src/components/shared/ImageBorderProperty.tsx b/src/components/shared/ImageBorderProperty.tsx new file mode 100644 index 0000000..2574574 --- /dev/null +++ b/src/components/shared/ImageBorderProperty.tsx @@ -0,0 +1,59 @@ +import { Button, ButtonGroup } from "@nextui-org/react"; + +export enum ImageBorder { + None = "none", + Round = "lg", + Circle = "full", +} + +export default function ImageBoderProperty({ + label, + border, + onBorderChange, +}: { + label?: string; + border: ImageBorder; + onBorderChange: (border: ImageBorder) => void; +}) { + return ( +
+

{label}

+ + {Object.entries(ImageBorder).map(([key, value]) => ( + + ))} + +
+ ); +} diff --git a/src/components/shared/Viewport.tsx b/src/components/shared/Viewport.tsx index bfce694..ebcca47 100644 --- a/src/components/shared/Viewport.tsx +++ b/src/components/shared/Viewport.tsx @@ -6,9 +6,12 @@ import { previewAccessTokenState } from "@/atoms/editor"; import useNavigation from "@/hooks/useNavigation"; import { useEffect } from "react"; import { getLayout } from "../editor/fastboard-components/utils"; -import { Spinner } from "@nextui-org/react"; +import { Button, Spinner } from "@nextui-org/react"; import { AxiosError } from "axios"; import { notFound } from "next/navigation"; +import { Back } from "iconsax-react"; + +const RETURN_BUTTON_HEIGHT = 48; export default function Viewport({ mode, @@ -19,7 +22,7 @@ export default function Viewport({ mode === "editor" || mode === "preview" ? "editor" : "published" ); const setPreviewAccessToken = useSetRecoilState(previewAccessTokenState); - const { currentPage } = useNavigation(); + const { currentPage, changePage } = useNavigation(); useEffect(() => { if (mode === "editor" && dashboard?.metadata?.auth?.previewAccessToken) { @@ -39,9 +42,7 @@ export default function Viewport({ const sidebar = dashboard?.metadata?.sidebar?.id ? getComponent(dashboard.metadata.sidebar?.id) : null; - const selectedPage = dashboard?.metadata?.pages[currentPage] - ? currentPage - : "home"; + const selectedPage = dashboard?.metadata?.pages[currentPage]; if (loading) { return ( @@ -103,15 +104,48 @@ export default function Viewport({ />
)} -
- {dashboard?.metadata?.pages[selectedPage].map((layout, index) => - getLayout( - layout, - currentPage, - index, - mode === "editor" ? "editable" : "view" - ) +
+ {selectedPage && selectedPage.returnPage && ( +
+ +
)} +
+ {selectedPage && + selectedPage.layouts.map((layout, index) => + getLayout( + layout, + currentPage, + index, + mode === "editor" ? "editable" : "view" + ) + )} +
diff --git a/src/hooks/dashboards/useDashboard.ts b/src/hooks/dashboards/useDashboard.ts index a23c325..ccd9850 100644 --- a/src/hooks/dashboards/useDashboard.ts +++ b/src/hooks/dashboards/useDashboard.ts @@ -146,10 +146,13 @@ const useDashboard = (mode: "editor" | "published" = "editor") => { changePage("home"); }; - const addPage = (): string | null => { + const addPage = (returnPage?: string): string | null => { if (!dashboard) return null; - const [newMetadata, pageId] = editorUtils.addPage(dashboard.metadata); + const [newMetadata, pageId] = editorUtils.addPage( + dashboard.metadata, + returnPage + ); updateDashboard((prev) => ({ ...prev, metadata: newMetadata, @@ -167,7 +170,7 @@ const useDashboard = (mode: "editor" | "published" = "editor") => { const getBaseLayout = (): LayoutType | null => { if (!dashboard) return null; - return dashboard.metadata.pages["home"][0].type; + return dashboard.metadata.pages["home"].layouts[0].type; }; const changeLayout = ( diff --git a/src/hooks/useData.ts b/src/hooks/useData.ts index 7d1c2a4..0c09269 100644 --- a/src/hooks/useData.ts +++ b/src/hooks/useData.ts @@ -6,7 +6,11 @@ import { useRecoilValue } from "recoil"; import { previewAccessTokenState } from "@/atoms/editor"; import { useParams } from "next/navigation"; -const useData = (componentId: string, queryData: QueryData | null) => { +const useData = ( + componentId: string, + queryData: QueryData | null, + queryParameters?: Record +) => { const { queryId, connectionId } = queryData || {}; const { id: dashboardId } = useParams(); const previewAccessToken = useRecoilValue(previewAccessTokenState); @@ -37,7 +41,7 @@ const useData = (componentId: string, queryData: QueryData | null) => { const response = await adapterService.executeQuery( queryId ?? null, dashboardId as string, - {}, + queryParameters ?? {}, previewAccessToken, { headers: { diff --git a/src/hooks/useNavigation.ts b/src/hooks/useNavigation.ts index 37447f8..347a88d 100644 --- a/src/hooks/useNavigation.ts +++ b/src/hooks/useNavigation.ts @@ -1,10 +1,13 @@ +import { isPropertiesDrawerOpen } from "@/atoms/editor"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useCallback, useMemo } from "react"; +import { useSetRecoilState } from "recoil"; const useNavigation = () => { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const setIsPropertiesDrawerOpen = useSetRecoilState(isPropertiesDrawerOpen); // Get a new searchParams string by merging the current // searchParams with a provided key/value pair @@ -22,13 +25,29 @@ const useNavigation = () => { return searchParams.get("page") || "home"; }, [searchParams]); - function changePage(pageId: string) { - router.push(pathname + "?" + createQueryString("page", pageId)); + function changePage( + pageId: string, + queryStrings?: { name: string; value: string }[] + ) { + const newParams = new URLSearchParams(); + newParams.set("page", pageId); + if (queryStrings) { + queryStrings.forEach((qs) => { + newParams.set(qs.name, qs.value); + }); + } + router.push(pathname + "?" + newParams.toString()); + setIsPropertiesDrawerOpen(false); + } + + function getQueryParam(key: string) { + return searchParams.get(key); } return { currentPage, changePage, + getQueryParam, }; }; diff --git a/src/lib/editor.utils.ts b/src/lib/editor.utils.ts index 6405c3c..89a16b5 100644 --- a/src/lib/editor.utils.ts +++ b/src/lib/editor.utils.ts @@ -43,14 +43,19 @@ function deleteTableModalsFrame( component: FastboardComponent, dashboardMetadata: DashboardMetadata ): DashboardMetadata { + const viewModalId = component.properties?.actions?.find( + (action: TableActionProperty) => action.type === "view" + )?.modalId; const editModalId = component.properties?.actions?.find( (action: TableActionProperty) => action.type === "edit" )?.modalId; const addRowModalId = component.properties?.addOns?.addRowForm?.modalId; + if (viewModalId) { + dashboardMetadata = removeModalFrame(viewModalId, dashboardMetadata); + } if (editModalId) { dashboardMetadata = removeModalFrame(editModalId, dashboardMetadata); } - if (addRowModalId) { dashboardMetadata = removeModalFrame(addRowModalId, dashboardMetadata); } @@ -58,6 +63,20 @@ function deleteTableModalsFrame( return dashboardMetadata; } +function deleteTablePageView( + component: FastboardComponent, + dashboardMetadata: DashboardMetadata +): DashboardMetadata { + const pageId = component.properties?.actions?.find( + (action: TableActionProperty) => action.type === "view" + )?.pageId; + + if (pageId) { + dashboardMetadata = deletePage(pageId, dashboardMetadata); + } + return dashboardMetadata; +} + export function deleteComponent( id: ComponentId, dashboardMetadata: DashboardMetadata @@ -67,8 +86,9 @@ export function deleteComponent( return dashboardMetadata; } if (component.type === ComponentType.Table) { - //If the component is a table, then we need to remove the modal frame that is associated with it + //If the component is a table, then we need to remove the modal frame that is associated with it and view pages also dashboardMetadata = deleteTableModalsFrame(component, dashboardMetadata); + dashboardMetadata = deleteTablePageView(component, dashboardMetadata); } const { [id]: removedComponent, ...newComponents } = dashboardMetadata.components; @@ -111,7 +131,7 @@ export function addComponentToLayout( container: containerIndex, } = index; // If there is already a component in the container, delete it - const layout = dashboardMetadata.pages[pageIndex][layoutIndex]; + const layout = dashboardMetadata.pages[pageIndex].layouts[layoutIndex]; const curretnComponentId: ComponentId | null = layout[containerIndex as keyof Layout]; if (curretnComponentId) { @@ -127,15 +147,18 @@ export function addComponentToLayout( ...newMetadata, pages: { ...newMetadata.pages, - [pageIndex]: newMetadata.pages[pageIndex].map((layout, index) => { - if (index === layoutIndex) { - return { - ...layout, - [containerIndex]: componentId, - }; - } - return layout; - }), + [pageIndex]: { + ...newMetadata.pages[pageIndex], + layouts: newMetadata.pages[pageIndex].layouts.map((layout, index) => { + if (index === layoutIndex) { + return { + ...layout, + [containerIndex]: componentId, + }; + } + return layout; + }), + }, }, }; } @@ -145,7 +168,7 @@ export function deleteComponentFromLayout( dashboardMetadata: DashboardMetadata ): DashboardMetadata { const { page, layout: layoutIndex, container } = index; - const layout = dashboardMetadata.pages[page][layoutIndex]; + const layout = dashboardMetadata.pages[page].layouts[layoutIndex]; const componentId: ComponentId = layout[container as keyof Layout]; if (!componentId) { return dashboardMetadata; @@ -156,15 +179,18 @@ export function deleteComponentFromLayout( ...newMetadata, pages: { ...newMetadata.pages, - [page]: newMetadata.pages[page].map((layout, index) => { - if (index === layoutIndex) { - return { - ...layout, - [container]: null, - }; - } - return layout; - }), + [page]: { + ...newMetadata.pages[page], + layouts: newMetadata.pages[page].layouts.map((layout, index) => { + if (index === layoutIndex) { + return { + ...layout, + [container]: null, + }; + } + return layout; + }), + }, }, }; } @@ -176,7 +202,7 @@ function changeLayout( dashboardMetadata: DashboardMetadata ): DashboardMetadata { let to = Layout.of(to_type); - const from = dashboardMetadata.pages[pageIndex][layoutIndex]; + const from = dashboardMetadata.pages[pageIndex].layouts[layoutIndex]; const keysFrom = Object.keys(from); const keysTo = Object.keys(to); @@ -192,12 +218,17 @@ function changeLayout( ...dashboardMetadata, pages: { ...dashboardMetadata.pages, - [pageIndex]: dashboardMetadata.pages[pageIndex].map((layout, index) => { - if (index === layoutIndex) { - return convertLayout(from, to_type); - } - return layout; - }), + [pageIndex]: { + ...dashboardMetadata.pages[pageIndex], + layouts: dashboardMetadata.pages[pageIndex].layouts.map( + (layout, index) => { + if (index === layoutIndex) { + return convertLayout(from, to_type); + } + return layout; + } + ), + }, }, }; } @@ -333,7 +364,8 @@ function deleteSidebar( } function addPage( - dashboardMetadata: DashboardMetadata + dashboardMetadata: DashboardMetadata, + returnPage?: string ): [DashboardMetadata, string] { const pageId = uuidv4(); return [ @@ -341,7 +373,10 @@ function addPage( ...dashboardMetadata, pages: { ...dashboardMetadata.pages, - [pageId]: [Layout.of(LayoutType.Full)], + [pageId]: { + layouts: [Layout.of(LayoutType.Full)], + returnPage: returnPage, + }, }, }, pageId, @@ -353,7 +388,7 @@ function deletePage( dashboardMetadata: DashboardMetadata ): DashboardMetadata { //Remove all components in the page - const layouts = dashboardMetadata.pages[pageId]; + const layouts = dashboardMetadata.pages[pageId].layouts; layouts.forEach((layout) => { Object.keys(layout).forEach((key) => { if (key === "type") { diff --git a/src/lib/services/adapter.ts b/src/lib/services/adapter.ts index 5601fe8..74b1da1 100644 --- a/src/lib/services/adapter.ts +++ b/src/lib/services/adapter.ts @@ -18,7 +18,7 @@ const previewQuery = async ( | RestQueryMetadata | MongoVectorSearchMetadata, parameters: QueryParameter[], - config?: AxiosRequestConfig, + config?: AxiosRequestConfig ) => { const contentType = (queryMetadata as RestQueryMetadata)?.contentType; @@ -41,7 +41,7 @@ const previewQuery = async ( } } return param; - }), + }) ) ).reduce((acc: any, param: any) => { return { ...acc, [param.name]: param.preview }; @@ -54,7 +54,7 @@ const previewQuery = async ( connection_metadata: queryMetadata, parameters: transformedParameters, }, - config, + config ); return response.data; @@ -65,7 +65,7 @@ async function executeQuery( dashboardId: string, parameters?: Record, previewAccessToken?: string, - config?: AxiosRequestConfig, + config?: AxiosRequestConfig ) { try { if (!queryId) { @@ -73,19 +73,18 @@ async function executeQuery( } const token = localStorage.getItem(`auth-${dashboardId}`); - - const parametersToSend = parameters ?? {}; - const viewMode = isPreviewPage() || isPublishPage(); - - parametersToSend.token = viewMode ? token : previewAccessToken; + const parametersToSend = { + ...parameters, + token: viewMode ? token : previewAccessToken, + }; const response = await axiosInstance.post( `/adapter/execute/${queryId}`, { parameters: parametersToSend, }, - config, + config ); if (response?.data.status_code && response?.data.status_code !== 200) { @@ -98,7 +97,7 @@ async function executeQuery( const error = response.data?.body?.error; throw new Error( - `Error ${response.data.status_code}: ${error?.description ?? ""}`, + `Error ${response.data.status_code}: ${error?.description ?? ""}` ); } return response.data; @@ -109,7 +108,7 @@ async function executeQuery( async function createEmbeddings(queryId: string, indexField: string) { const response = await axiosInstance.post( - `/embeddings/${queryId}?index_field=${indexField}`, + `/embeddings/${queryId}?index_field=${indexField}` ); return mapQuery(response?.data?.body); diff --git a/src/lib/table.utils.ts b/src/lib/table.utils.ts index 040266f..e17b13d 100644 --- a/src/lib/table.utils.ts +++ b/src/lib/table.utils.ts @@ -84,6 +84,19 @@ export function fillParameters( }, {}); } +export function traduceQueryStrings( + queryStrings: { name: string; columnKey: string }[], + item: any +) { + return queryStrings.reduce((acc, queryString) => { + const value = getKeyValue(item, queryString.columnKey); + if (queryString.name === "" || value === undefined) { + return [...acc]; + } + return [...acc, { name: queryString.name, value }]; + }, [] as { name: string; value: any }[]); +} + export function getFilterFunction( columnKey: string, filters: FilterProperties[] diff --git a/src/types/editor/card-types.ts b/src/types/editor/card-types.ts new file mode 100644 index 0000000..71a0cc9 --- /dev/null +++ b/src/types/editor/card-types.ts @@ -0,0 +1,133 @@ +import { Alignment } from "@/components/shared/AlignmentProperty"; +import { Color } from "./style-types"; +import { FontType } from "@/components/shared/FontTypeProperty"; +import { ImageBorder } from "@/components/shared/ImageBorderProperty"; +import { IconType } from "./icon-types"; +import { QueryData } from "../connections"; +import { FastboardHeaderPhotoSize } from "./header-types"; + +export enum CardComponentType { + Text = "Text", + Image = "Image", + Link = "Link", + Video = "Video", + Spacer = "Spacer", +} + +interface BaseCardComponentProperties { + type: CardComponentType; + dataKey: string; +} + +export interface TextComponentProperties extends BaseCardComponentProperties { + label: string; + defaultText: string; + alignment: Alignment; + fontSize: number; + fontTypes: FontType[]; + textColor: Color; + labelColor: Color; +} + +export interface ImageComponentProperties extends BaseCardComponentProperties { + alignment: Alignment; + border: ImageBorder; + size: FastboardHeaderPhotoSize; +} + +export interface LinkComponentProperties extends BaseCardComponentProperties { + label: string; + defaultText: string; + isExternal: boolean; + externalIcon: IconType; + showExternalIcon: boolean; + alignment: Alignment; + fontSize: number; + textColor: Color; + labelColor: Color; +} + +export interface VideoComponentProperties extends BaseCardComponentProperties {} + +export interface SpacerComponentProperties extends BaseCardComponentProperties { + height: number; +} + +export type CardComponentProperties = + | TextComponentProperties + | ImageComponentProperties + | LinkComponentProperties + | VideoComponentProperties + | SpacerComponentProperties; + +export class DefaultCardComponentProperties { + static of(type: CardComponentType): CardComponentProperties { + const baseProperties = { + type, + dataKey: "", + }; + switch (type) { + case CardComponentType.Text: + return { + ...baseProperties, + label: "", + defaultText: "Some text", + alignment: Alignment.Left, + fontSize: 18, + fontTypes: [], + textColor: new Color("#000000", "#ffffff"), + labelColor: new Color("#000000", "#ffffff"), + }; + case CardComponentType.Image: + return { + ...baseProperties, + alignment: Alignment.Left, + border: ImageBorder.Round, + size: FastboardHeaderPhotoSize.Medium, + }; + case CardComponentType.Link: + return { + ...baseProperties, + label: "", + defaultText: "https://fastboard-xgski.ondigitalocean.app/", + isExternal: true, + externalIcon: IconType.Link, + showExternalIcon: false, + alignment: Alignment.Left, + fontSize: 18, + textColor: Color.primary(), + labelColor: new Color("#000000", "#ffffff"), + }; + case CardComponentType.Video: + return { + ...baseProperties, + }; + case CardComponentType.Spacer: + return { + ...baseProperties, + height: 5, + }; + default: + return { + ...baseProperties, + alignment: Alignment.Left, + border: ImageBorder.Round, + }; + } + } +} + +export class CardProperties { + sourceQueryData: QueryData | null = null; + queryParameters: Record = {}; + components: CardComponentProperties[] = []; + dataKeys: string[] = []; + + spacing: number = 2; + backgroundColor: Color = new Color("#ffffff", "#18181b"); + showShadow: boolean = true; + + static default(): CardProperties { + return new CardProperties(); + } +} diff --git a/src/types/editor/header-types.ts b/src/types/editor/header-types.ts index 1846c5a..2f11ab9 100644 --- a/src/types/editor/header-types.ts +++ b/src/types/editor/header-types.ts @@ -1,11 +1,6 @@ +import { Alignment } from "@/components/shared/AlignmentProperty"; import { Color } from "./style-types"; -export enum FastboardHeaderPosition { - Left = "start", - Center = "center", - Right = "end", -} - export enum FastboardHeaderFontSize { Small = "x-large", Medium = "xx-large", @@ -39,7 +34,7 @@ export class FastboardHeaderProperties { size: FastboardHeaderPhotoSize.Medium, }; showThemeSwitcher: boolean = false; - position: FastboardHeaderPosition = FastboardHeaderPosition.Center; + position: Alignment = Alignment.Center; divider: boolean = false; backgroundColor: Color = new Color("#ffffff", "#000000"); textColor: Color = new Color("#11181C", "#ECEDEE"); diff --git a/src/types/editor/icon-types.ts b/src/types/editor/icon-types.ts index c2af540..98955bf 100644 --- a/src/types/editor/icon-types.ts +++ b/src/types/editor/icon-types.ts @@ -44,6 +44,7 @@ export enum IconType { Heart = "Heart", Hierarchy = "Hierarchy", Home = "Home", + Link = "Link", Folder = "Folder", Trash = "Trash", User = "User", diff --git a/src/types/editor/index.ts b/src/types/editor/index.ts index c38c375..ab3f061 100644 --- a/src/types/editor/index.ts +++ b/src/types/editor/index.ts @@ -12,6 +12,7 @@ export enum ComponentType { Image = "image", GroupChart = "group-chart", Form = "form", + Card = "card", Cards = "cards", Sidebar = "sidebar", Header = "header", @@ -49,6 +50,11 @@ export class DashboardAuth { } } +export interface Page { + layouts: Layout[]; + returnPage: string | undefined; +} + export class DashboardMetadata { components: Record = {}; header: { componentId: ComponentId | null; isVisible: boolean } = { @@ -57,8 +63,11 @@ export class DashboardMetadata { }; sidebar: { id: ComponentId; visible: boolean } | null = null; modals: ModalFrame[] = []; - pages: Record = { - home: [Layout.of(LayoutType.Full)], + pages: Record = { + home: { + layouts: [Layout.of(LayoutType.Full)], + returnPage: undefined, + }, }; auth: DashboardAuth = DashboardAuth.default(); pageTitle: string = ""; diff --git a/src/types/editor/table-types.ts b/src/types/editor/table-types.ts index 25aee7a..19f3b0d 100644 --- a/src/types/editor/table-types.ts +++ b/src/types/editor/table-types.ts @@ -15,9 +15,12 @@ export interface TableActionProperty { key: string; label: string; type: "view" | "edit" | "delete"; + mode?: "modal" | "page"; query: QueryData | null; parameters: { name: string; columnKey: string; value: string }[]; modalId?: string; + pageId?: string; + queryStrings: { name: string; columnKey: string }[]; } export interface AddRowFormProperties {