Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
210 changes: 210 additions & 0 deletions src/components/editor/fastboard-components/card/FastboardCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TextComponent
properties={component as TextComponentProperties}
item={item}
/>
);
}
case CardComponentType.Image:
return (
<ImageComponent
properties={component as ImageComponentProperties}
item={item}
/>
);
case CardComponentType.Link:
return (
<LinkComponent
properties={component as LinkComponentProperties}
item={item}
/>
);
case CardComponentType.Video:
return (
<VideoComponent
properties={component as VideoComponentProperties}
item={item}
/>
);
case CardComponentType.Spacer:
return (
<SpacerComponent
properties={component as SpacerComponentProperties}
item={item}
/>
);
default:
return <p>Component not found</p>;
}
}

return (
<div
className={
"flex grow-0 h-full w-full p-5 rounded-xl overflow-auto " +
(showShadow ? "border dark:border-content2 shadow-xl " : " ") +
scrollbarStyles.scrollbar
}
style={{
backgroundColor:
theme === "dark" ? backgroundColor.dark : backgroundColor.light,
}}
>
{dataFetching && <Spinner className="w-full h-full" />}
{!dataFetching && isDataError && (
<p className="flex justify-center items-center w-full h-full text-danger">
{dataError?.message}
</p>
)}

{!dataFetching && !isDataError && (
<>
{!sourceQueryData && (
<h2 className="flex w-full h-full justify-center items-center">
Nothing to show. There is no query selected
</h2>
)}
{sourceQueryData && isValidData(data) && (
<>
{components.length === 0 && (
<h2 className="flex w-full h-full justify-center items-center">
Add components
</h2>
)}

{components.length > 0 && (
<div className="h-full w-full">
{components.map((component) =>
renderComponent(component, data)
)}
</div>
)}
</>
)}
{sourceQueryData && !isValidData(data) && (
<p className="flex justify-center items-center w-full h-full text-warning">
Seems like the data is not an object or is empty.
</p>
)}
</>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Draggable
id="card-draggable"
data={{
type: ComponentType.Card,
defaultProperties: CardProperties.default(),
}}
dragSnapToOrigin
name={"Card"}
>
<DraggableImage name={"card"} alt={"Card"} />
</Draggable>
);
}
66 changes: 66 additions & 0 deletions src/components/editor/fastboard-components/card/ImageComponent.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={
"flex size-auto " +
(alignment === Alignment.Center
? "justify-center"
: alignment === Alignment.Right
? "justify-end"
: "justify-start")
}
>
<Image
src={
imageError
? "/ImageErrorImage.svg"
: dataKey !== ""
? item[dataKey]
: "/ImageErrorImage.svg"
}
className={
"object-cover " + (imageError || dataKey === "" ? "dark:invert " : "")
}
style={{
maxHeight: getSize(size),
maxWidth: getSize(size),
}}
alt="Card image"
radius={border as any}
width={getSize(size)}
height={getSize(size)}
onError={() => setImageError(true)}
/>
</div>
);
}
Loading