From 1d71897e62ee3a22b685ace1adf0b30cf089cee7 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Wed, 12 Jan 2022 20:54:48 +0200 Subject: [PATCH] feature: Added Skeleton states Added skeleton loading states to project mini card, project card, and project rows --- .storybook/helpers.tsx | 22 +++-- .storybook/preview.js | 25 +++++- package-lock.json | 15 ++++ package.json | 1 + src/Components/Badge/Badge.stories.tsx | 70 ++++++++++++++++ src/Components/Badge/Badge.tsx | 72 ++++++++++++++++ src/Components/Button/Button.stories.tsx | 55 +++++++++++- src/Components/Button/Button.tsx | 83 ++++++++++++++++--- .../ExplorePage/Categories/Categories.tsx | 13 ++- .../ProjectCardMini.Skeleton.tsx | 15 ++++ .../ProjectCardMini.stories.tsx | 9 ++ .../ProjectsRow/ProjectsRow.Skeleton.tsx | 21 +++++ .../ProjectsSection/ProjectsSection.tsx | 6 +- .../ProjectCard/ProjectCard.Skeleton.tsx | 82 ++++++++++++++++++ .../ProjectCard/ProjectCard.stories.tsx | 5 ++ .../ProjectPage/ProjectCard/ProjectCard.tsx | 15 ++-- src/utils/Wrapper.tsx | 5 +- src/utils/hoc.tsx | 10 +++ src/utils/types/utils.ts | 11 +++ tailwind.config.js | 5 ++ 20 files changed, 498 insertions(+), 42 deletions(-) create mode 100644 src/Components/Badge/Badge.stories.tsx create mode 100644 src/Components/Badge/Badge.tsx create mode 100644 src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.Skeleton.tsx create mode 100644 src/pages/ExplorePage/ProjectsRow/ProjectsRow.Skeleton.tsx create mode 100644 src/pages/ProjectPage/ProjectCard/ProjectCard.Skeleton.tsx create mode 100644 src/utils/hoc.tsx create mode 100644 src/utils/types/utils.ts diff --git a/.storybook/helpers.tsx b/.storybook/helpers.tsx index 02a3477..df8b9c6 100644 --- a/.storybook/helpers.tsx +++ b/.storybook/helpers.tsx @@ -1,19 +1,23 @@ -import { motion } from 'framer-motion'; +import { AnimatePresence, motion } from 'framer-motion'; +import Modal from 'src/Components/Modals/Modal/Modal'; export const ModalsDecorator = (Story: any) => { const onClose = () => { }; return ( -
- + + + + +
); } \ No newline at end of file diff --git a/.storybook/preview.js b/.storybook/preview.js index e3bf129..ea2f2e8 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -3,12 +3,28 @@ import { configure, addDecorator } from "@storybook/react"; import { store } from "../src/redux/store"; import React from "react"; import { Provider } from "react-redux"; -import { QueryClient, QueryClientProvider } from "react-query"; + + +import { + ApolloClient, + InMemoryCache, + ApolloProvider +} from "@apollo/client"; + + + import "react-multi-carousel/lib/styles.css"; import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; +import 'react-loading-skeleton/dist/skeleton.css' + import { BrowserRouter } from "react-router-dom"; -const queryClient = new QueryClient(); + +const client = new ApolloClient({ + uri: 'https://deploy-preview-2--makers-bolt-fun.netlify.app/.netlify/functions/graphql', + cache: new InMemoryCache() +}); + export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, @@ -21,13 +37,14 @@ export const parameters = { }; addDecorator((S) => ( - + + - + )); configure(require.context("../src", true, /\.stories\.ts$/), module); diff --git a/package-lock.json b/package-lock.json index 310e8a3..00bb10f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "react-dom": "^17.0.2", "react-icons": "^4.3.1", "react-loader-spinner": "^4.0.0", + "react-loading-skeleton": "^3.0.2", "react-multi-carousel": "^2.6.5", "react-query": "^3.32.3", "react-redux": "^7.2.6", @@ -47620,6 +47621,14 @@ "react-dom": "*" } }, + "node_modules/react-loading-skeleton": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.0.2.tgz", + "integrity": "sha512-rlALwuZEcjazUDeIy3+fEhm20Uk9Yd/zZGeITU034K2ts5/yEf7RuZaV2FyrPWypIII4LAsFEo9WDTPKp6G0fQ==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-multi-carousel": { "version": "2.6.5", "integrity": "sha512-i5iuAm5XRT/h7uBL9/pGWeRsQXzqvjBrPVP1sobKgDKEvfZuKFpYp/alaQhTRM56Jtkb8jZpSqLn52Ku6jJbDg==", @@ -89771,6 +89780,12 @@ "prop-types": "^15.7.2" } }, + "react-loading-skeleton": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.0.2.tgz", + "integrity": "sha512-rlALwuZEcjazUDeIy3+fEhm20Uk9Yd/zZGeITU034K2ts5/yEf7RuZaV2FyrPWypIII4LAsFEo9WDTPKp6G0fQ==", + "requires": {} + }, "react-multi-carousel": { "version": "2.6.5", "integrity": "sha512-i5iuAm5XRT/h7uBL9/pGWeRsQXzqvjBrPVP1sobKgDKEvfZuKFpYp/alaQhTRM56Jtkb8jZpSqLn52Ku6jJbDg==" diff --git a/package.json b/package.json index 27f5434..90d2841 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react-dom": "^17.0.2", "react-icons": "^4.3.1", "react-loader-spinner": "^4.0.0", + "react-loading-skeleton": "^3.0.2", "react-multi-carousel": "^2.6.5", "react-query": "^3.32.3", "react-redux": "^7.2.6", diff --git a/src/Components/Badge/Badge.stories.tsx b/src/Components/Badge/Badge.stories.tsx new file mode 100644 index 0000000..f9a731a --- /dev/null +++ b/src/Components/Badge/Badge.stories.tsx @@ -0,0 +1,70 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import Badge from './Badge'; + +export default { + title: 'Shared/Badge', + component: Badge, + argTypes: { + backgroundColor: { control: 'color' }, + }, +} as ComponentMeta; + + +const Template: ComponentStory = (args) => Badge + +export const Default = Template.bind({}); + + +export const Primary = Template.bind({}); +Primary.args = { + color: 'primary' +} + +export const SmallSize = Template.bind({}); +SmallSize.args = { + color: 'primary', + size: 'sm' +} + +export const MediumSize = Template.bind({}); +MediumSize.args = { + color: 'primary', + size: 'md' +} + + +export const LargeSize = Template.bind({}); +LargeSize.args = { + color: 'primary', + size: 'lg' +} + +export const Removable = Template.bind({}); +Removable.args = { + color: 'primary', + onRemove: () => { } +} + +export const Loading = Template.bind({}); +Loading.args = { + isLoading: true +} + + + +export const Customized = Template.bind({}); +Customized.args = { + href: "#", + color: 'none', + className: 'bg-red-500 text-white underline font-bold' +} + + + + +const ListTemplate: ComponentStory = (args) =>
+ {Array(4).fill(0).map((_, idx) => Badge {idx + 1})} +
+ +export const BadgesList = ListTemplate.bind({}) \ No newline at end of file diff --git a/src/Components/Badge/Badge.tsx b/src/Components/Badge/Badge.tsx new file mode 100644 index 0000000..9e0fce5 --- /dev/null +++ b/src/Components/Badge/Badge.tsx @@ -0,0 +1,72 @@ +import { PropsWithChildren } from "react"; +import { IoMdCloseCircle } from "react-icons/io"; +import Skeleton from "react-loading-skeleton"; +import { wrapLink } from "src/utils/hoc"; +import { UnionToObjectKeys } from "src/utils/types/utils"; + +interface Props { + isLoading?: boolean; + color?: 'primary' | 'gray' | 'none' + size?: "sm" | 'md' | 'lg' + shadow?: 'sm' | 'md' | 'lg' | 'none' + className?: string; + href?: string; + onClick?: () => void + onRemove?: () => void +} + + +const badgrColor: UnionToObjectKeys = { + none: '', + primary: "bg-primary-600 border-0 text-white", + gray: 'bg-gray-100 text-gray-900', +} + +const badgeSize: UnionToObjectKeys = { + sm: "px-8 py-4 text-body6", + md: "px-16 py-8 text-body4", + lg: "px-24 py-12 text-body3" +} + +const loadingBadgeSize: UnionToObjectKeys = { + sm: "w-48 h-24 text-body6", + md: "w-64 h-32 text-body4", + lg: "w-64 h-42 text-body3" +} + + +export default function Badge( + { + color = 'gray', + size = 'md', + className, + href, + shadow = 'sm', + children, + isLoading, + onRemove, + onClick + } + : PropsWithChildren) { + + const classes = ` + rounded-48 shadow-${shadow} inline-block relative align-middle + ${badgrColor[color]} + ${badgeSize[size]} + ${className} + ${(onClick || href) && 'cursor-pointer'} + ` + + if (isLoading) + return + + + return ( + wrapLink( + + {children} + {onRemove && } + + , href) + ) +} diff --git a/src/Components/Button/Button.stories.tsx b/src/Components/Button/Button.stories.tsx index 2d74832..bc07d03 100644 --- a/src/Components/Button/Button.stories.tsx +++ b/src/Components/Button/Button.stories.tsx @@ -8,7 +8,7 @@ export default { } as ComponentMeta; -const Template: ComponentStory = (args) => ; +const Template: ComponentStory = (args) => ; export const Default = Template.bind({}); @@ -18,17 +18,53 @@ Primary.args = { color: 'primary' } +export const Red = Template.bind({}); +Red.args = { + color: 'red' +} + export const Gray = Template.bind({}); Gray.args = { color: 'gray' } +export const OutlinePrimary = Template.bind({}); +OutlinePrimary.args = { + color: 'primary', + variant: 'outline' +} + +export const OutlineRed = Template.bind({}); +OutlineRed.args = { + color: 'red', + variant: 'outline' +} + +export const OutlineGray = Template.bind({}); +OutlineGray.args = { + color: 'gray', + variant: 'outline' +} + +export const SmallSize = Template.bind({}); +SmallSize.args = { + color: 'primary', + size: 'sm' +} + export const MediumSize = Template.bind({}); MediumSize.args = { + color: 'primary', size: 'md' } +export const LargeSize = Template.bind({}); +LargeSize.args = { + color: 'primary', + size: 'lg' +} + export const FullWidth = Template.bind({}); FullWidth.args = { fullWidth: true @@ -37,4 +73,21 @@ FullWidth.args = { export const Link = Template.bind({}); Link.args = { href: '#' +} + +export const DefaultLoading = Template.bind({}); +DefaultLoading.args = { + isLoading: true, +} + +export const PrimaryLoading = Template.bind({}); +PrimaryLoading.args = { + isLoading: true, + color: 'primary' +} + +export const GrayLoading = Template.bind({}); +GrayLoading.args = { + isLoading: true, + color: 'gray' } \ No newline at end of file diff --git a/src/Components/Button/Button.tsx b/src/Components/Button/Button.tsx index 3f164c0..2333c44 100644 --- a/src/Components/Button/Button.tsx +++ b/src/Components/Button/Button.tsx @@ -1,32 +1,89 @@ -import { ReactNode } from 'react'; -import { Link } from 'react-router-dom' +import { ComponentProps, ReactNode } from 'react'; +import { wrapLink } from 'src/utils/hoc'; +import { UnionToObjectKeys } from 'src/utils/types/utils'; +// import Loading from '../Loading/Loading'; interface Props { - color?: 'primary' | 'white' | 'gray' - size?: 'md' | 'lg' + color?: 'primary' | 'red' | 'white' | 'gray' | 'none', + variant?: 'fill' | 'outline' + size?: 'sm' | 'md' | 'lg' children: ReactNode; href?: string; fullWidth?: boolean; onClick?: () => void; className?: string + isLoading?: boolean; + disableOnLoading?: boolean; + [rest: string]: any; } -export default function Button(props: Props) { - let classes = "btn inline-block"; +const btnStylesFill: UnionToObjectKeys = { + none: "", + primary: "bg-primary-500 border-0 hover:bg-primary-400 active:bg-primary-600 text-white", + gray: 'bg-gray-100 hover:bg-gray-200 text-gray-900 active:bg-gray-300', + white: 'text-gray-900 bg-gray-25 hover:bg-gray-50', + red: "bg-red-600 border-0 hover:bg-red-500 active:bg-red-700 text-white", +} - if (props.color === 'primary') classes += ' btn-primary'; - else if (props.color === 'gray') classes += ' btn-gray'; +const btnStylesOutline: UnionToObjectKeys = { + none: "", + primary: "text-primary-600", + gray: 'text-gray-700', + white: 'text-gray-900', + red: "text-red-500", +} - if (props.size === 'md') classes += ' py-12 px-24'; +const baseBtnStyles: UnionToObjectKeys = { + fill: " shadow-sm active:scale-95", + outline: "bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 " +} - if (props.fullWidth) classes += ' w-full' + +// const loadingColor: UnionToObjectKeys['color']> = { +// none: "white", +// primary: "white", +// gray: 'primary', +// white: 'primary', +// red: "white" +// } as const; + +const btnPadding: UnionToObjectKeys = { + sm: "py-8 px-12 text-body5", + md: "py-12 px-24 text-body4", + lg: 'py-12 px-36 text-body4' +} + +export default function Button({ color = 'white', variant = 'fill', isLoading, disableOnLoading = true, size = 'md', fullWidth, href, className, onClick, children, ...props }: Props) { + + let classes = ` + inline-block font-sans rounded-lg font-regular border border-gray-300 hover:cursor-pointer + ${baseBtnStyles[variant]} + ${btnPadding[size]} + ${variant === 'fill' ? btnStylesFill[color] : btnStylesOutline[color]} + ${isLoading && disableOnLoading && 'bg-opacity-70 pointer-events-none'} + `; + + if (size === 'md') classes += ' py-12 px-24'; + if (size === 'lg') + if (fullWidth) classes += ' w-full' const handleClick = () => { - if (props.onClick) props.onClick(); + if (isLoading && disableOnLoading) return; + if (onClick) onClick(); } - return ( - props.href ? {props.children} : + return ( + wrapLink( + + , href) ) } diff --git a/src/pages/ExplorePage/Categories/Categories.tsx b/src/pages/ExplorePage/Categories/Categories.tsx index 7d83e7d..59acbca 100644 --- a/src/pages/ExplorePage/Categories/Categories.tsx +++ b/src/pages/ExplorePage/Categories/Categories.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@apollo/client'; import { ALL_CATEGORIES_QUERY, ALL_CATEGORIES_QUERY_RES } from './query'; +import Badge from 'src/Components/Badge/Badge' export default function Categories() { @@ -9,12 +10,18 @@ export default function Categories() { } - if (loading) - return null; + if (loading || !data) + return
+ {Array(5).fill(0).map((_, idx) => + + )} +
return (
- {data?.allCategories.map(category => handleClick(category.id)}>{category.title})} + {data?.allCategories.map(category => + handleClick(category.id)}>{category.title} + )}
) } diff --git a/src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.Skeleton.tsx b/src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.Skeleton.tsx new file mode 100644 index 0000000..8fc758a --- /dev/null +++ b/src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.Skeleton.tsx @@ -0,0 +1,15 @@ +import Skeleton from 'react-loading-skeleton' + + +export default function ProjectCardMiniSkeleton() { + return ( +
+ +
+

+

+ +
+
+ ); +} diff --git a/src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.stories.tsx b/src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.stories.tsx index 9fc3935..f964e91 100644 --- a/src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.stories.tsx +++ b/src/pages/ExplorePage/ProjectCardMini/ProjectCardMini.stories.tsx @@ -2,6 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; import mockData from 'src/api/mockData.json' import ProjectCardMini from './ProjectCardMini'; +import ProjectCardMiniSkeleton from './ProjectCardMini.Skeleton'; export default { @@ -19,3 +20,11 @@ Default.args = { +const SkeletonTemplate: ComponentStory = (args) => ; + +export const LoadingState = SkeletonTemplate.bind({}); +LoadingState.args = { +} + + + diff --git a/src/pages/ExplorePage/ProjectsRow/ProjectsRow.Skeleton.tsx b/src/pages/ExplorePage/ProjectsRow/ProjectsRow.Skeleton.tsx new file mode 100644 index 0000000..751679b --- /dev/null +++ b/src/pages/ExplorePage/ProjectsRow/ProjectsRow.Skeleton.tsx @@ -0,0 +1,21 @@ + +import ProjectCardMiniSkeleton from "../ProjectCardMini/ProjectCardMini.Skeleton"; +import Skeleton from "react-loading-skeleton"; + +export default function ProjectsRowSkeleton() { + + return ( +
+

+ +

+
+ {Array(5).fill(0).map((_, idx) => ( + + ))} +
+ + +
+ ) +} diff --git a/src/pages/ExplorePage/ProjectsSection/ProjectsSection.tsx b/src/pages/ExplorePage/ProjectsSection/ProjectsSection.tsx index 1392f9c..eaa82c3 100644 --- a/src/pages/ExplorePage/ProjectsSection/ProjectsSection.tsx +++ b/src/pages/ExplorePage/ProjectsSection/ProjectsSection.tsx @@ -1,5 +1,7 @@ import ProjectsRow from "../ProjectsRow/ProjectsRow"; +import ProjectsRowSkeleton from "../ProjectsRow/ProjectsRow.Skeleton"; + import { MdLocalFireDepartment } from "react-icons/md"; import { useQuery } from "@apollo/client"; import { ALL_CATEGORIES_PROJECTS_QUERY, ALL_CATEGORIES_PROJECTS_RES } from "./query"; @@ -9,7 +11,9 @@ export default function ProjectsSection() { const { data, loading } = useQuery(ALL_CATEGORIES_PROJECTS_QUERY); - if (loading || !data) return null; + if (loading || !data) return
+ {Array(3).fill(0).map((_, idx) => )} +
; return (
diff --git a/src/pages/ProjectPage/ProjectCard/ProjectCard.Skeleton.tsx b/src/pages/ProjectPage/ProjectCard/ProjectCard.Skeleton.tsx new file mode 100644 index 0000000..e382b33 --- /dev/null +++ b/src/pages/ProjectPage/ProjectCard/ProjectCard.Skeleton.tsx @@ -0,0 +1,82 @@ +import { motion } from 'framer-motion' +import { MdClose, } from 'react-icons/md'; +import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'; +import Skeleton from 'react-loading-skeleton'; +import Badge from 'src/Components/Badge/Badge'; +import { useAppSelector } from 'src/utils/hooks'; + + +interface Props extends ModalCard { +} + +export default function ProjectCardSkeleton({ onClose, direction, ...props }: Props) { + + + + const { isMobileScreen } = useAppSelector(state => ({ + + isMobileScreen: state.theme.isMobileScreen + })); + + + + + return ( + +
+ + +
+
+
+
+ +
+
+

+ +
+ + +
+
+ +
+

+ + + + + +

+ +
+

Screenshots

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ ) +} diff --git a/src/pages/ProjectPage/ProjectCard/ProjectCard.stories.tsx b/src/pages/ProjectPage/ProjectCard/ProjectCard.stories.tsx index 66c4691..bf012c5 100644 --- a/src/pages/ProjectPage/ProjectCard/ProjectCard.stories.tsx +++ b/src/pages/ProjectPage/ProjectCard/ProjectCard.stories.tsx @@ -1,6 +1,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; import ProjectCard from './ProjectCard'; +import ProjectCardSkeleton from './ProjectCard.Skeleton'; import { ModalsDecorator } from '.storybook/helpers' @@ -15,3 +16,7 @@ const Template: ComponentStory = (args) => = (args) => ; +export const LoadingState = LoadingTemplate.bind({}) \ No newline at end of file diff --git a/src/pages/ProjectPage/ProjectCard/ProjectCard.tsx b/src/pages/ProjectPage/ProjectCard/ProjectCard.tsx index 4590ccd..7f7525c 100644 --- a/src/pages/ProjectPage/ProjectCard/ProjectCard.tsx +++ b/src/pages/ProjectPage/ProjectCard/ProjectCard.tsx @@ -11,6 +11,8 @@ import Button from 'src/Components/Button/Button'; import { requestProvider } from 'webln'; import { PROJECT_BY_ID_QUERY, PROJECT_BY_ID_RES, PROJECT_BY_ID_VARS } from './query' import { AiFillThunderbolt } from 'react-icons/ai'; +import ProjectCardSkeleton from './ProjectCard.Skeleton' + interface Props extends ModalCard { projectId: string @@ -39,7 +41,8 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }: - if (loading || !project) return <>; + if (loading || !project) + return ; const onConnectWallet = async () => { try { @@ -83,14 +86,8 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }: } return ( -
@@ -152,6 +149,6 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
- + ) } diff --git a/src/utils/Wrapper.tsx b/src/utils/Wrapper.tsx index 76dc512..b673972 100644 --- a/src/utils/Wrapper.tsx +++ b/src/utils/Wrapper.tsx @@ -1,11 +1,12 @@ import { QueryClient, QueryClientProvider } from 'react-query' - +import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { store } from '../redux/store'; import 'react-multi-carousel/lib/styles.css'; import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; -import { BrowserRouter } from 'react-router-dom'; +import 'react-loading-skeleton/dist/skeleton.css' + import { ApolloClient, diff --git a/src/utils/hoc.tsx b/src/utils/hoc.tsx new file mode 100644 index 0000000..1edec0c --- /dev/null +++ b/src/utils/hoc.tsx @@ -0,0 +1,10 @@ +import { Link } from "react-router-dom"; + + +export function wrapLink(Component: JSX.Element, href: string | undefined, className?: string) { + if (!href) return Component; + + return + {Component} + +} \ No newline at end of file diff --git a/src/utils/types/utils.ts b/src/utils/types/utils.ts new file mode 100644 index 0000000..398447e --- /dev/null +++ b/src/utils/types/utils.ts @@ -0,0 +1,11 @@ +export type UnionToObjectKeys = { [EE in NonNullable extends string ? NonNullable : never]: Value } + +export type Id = {} & { [P in keyof T]: T[P] } // flatens out the types to make them more readable can be removed + +export type RemoveCommonValues = { + [P in keyof T]: TOmit extends Record ? Exclude : T[P] +} + +export type ValueOf = Id; + +export type OmitId = Omit diff --git a/tailwind.config.js b/tailwind.config.js index dc52a29..6f920cf 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -123,8 +123,13 @@ module.exports = { 16: "16px", 20: "20px", 24: "24px", + 48: "48px", full: "50%", }, + lineHeight: { + 'inherit': "inherit", + 0: '0' + }, outline: { primary: ["2px solid #7B61FF", "1px"], },