mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
console: add Hasura familiarity survey component
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5829 Co-authored-by: Abhijeet Khangarot <26903230+abhi40308@users.noreply.github.com> GitOrigin-RevId: ca2ebd3c3ab15f9b3e75497517f6c0d11c8440b1
This commit is contained in:
parent
00b1acaf38
commit
cc6c026ca9
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta, Story } from '@storybook/react';
|
||||
import { Root } from './HasuraFamiliaritySurvey';
|
||||
|
||||
export default {
|
||||
title: 'features/HasuraFamiliaritySurvey/Root',
|
||||
component: Root,
|
||||
} as ComponentMeta<typeof Root>;
|
||||
|
||||
export const Base: Story = () => <Root onSkip={() => {}} />;
|
83
console/src/features/Surveys/HasuraFamiliaritySurvey.tsx
Normal file
83
console/src/features/Surveys/HasuraFamiliaritySurvey.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { FaStar, FaHeart, FaUser, FaBookmark } from 'react-icons/fa';
|
||||
import { Dialog } from '@/new-components/Dialog';
|
||||
import { IconCardGroup } from '@/new-components/IconCardGroup';
|
||||
|
||||
type CustomDialogFooterProps = {
|
||||
onSkip: () => void;
|
||||
};
|
||||
|
||||
const CustomDialogFooter: React.FC<CustomDialogFooterProps> = props => {
|
||||
const { onSkip } = props;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center border-t border-gray-300 bg-white p-sm">
|
||||
<div className="flex">
|
||||
<div className="ml-2">
|
||||
<a
|
||||
className="underline text-blue-600 cursor-pointer"
|
||||
onClick={onSkip}
|
||||
>
|
||||
Skip
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
// onSubmit: () => void;
|
||||
onSkip: () => void;
|
||||
};
|
||||
|
||||
// TODO: Subscribe this data object to db data using a custom hook for data fetching.
|
||||
const data: {
|
||||
value: string;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
body: string;
|
||||
}[] = [
|
||||
{
|
||||
value: '1',
|
||||
icon: <FaHeart className="text-yellow-500 text-xl" />,
|
||||
title: 'New User',
|
||||
body: `I'm completely new to Hasura`,
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
icon: <FaBookmark className="text-yellow-500 text-xl" />,
|
||||
title: 'Past User',
|
||||
body: `I've used Hasura before but not actively developing right now`,
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
icon: <FaUser className="text-yellow-500 text-xl" />,
|
||||
title: 'Recurring User',
|
||||
body: `I'm already using Hasura (CE/Cloud) weekly/monthly`,
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
icon: <FaStar className="text-yellow-500 text-xl" />,
|
||||
title: 'Active User',
|
||||
body: `I'm actively developing with Hasura (CE/Cloud) daily`,
|
||||
},
|
||||
];
|
||||
|
||||
export const Root: React.FC<Props> = ({ onSkip }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title="Welcome To Hasura!"
|
||||
description={`We'd love to get to know you before you get started with your first API.`}
|
||||
hasBackdrop
|
||||
footer={<CustomDialogFooter onSkip={onSkip} />}
|
||||
>
|
||||
<div className="mx-2 px-2">
|
||||
<div className="font-bold">How familiar are you with Hasura?</div>
|
||||
<div className="flex justify-center">
|
||||
<IconCardGroup items={data} disabled={false} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
3
console/src/features/Surveys/index.ts
Normal file
3
console/src/features/Surveys/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Root } from './HasuraFamiliaritySurvey';
|
||||
|
||||
export const HasuraFamiliaritySurvey = Root;
|
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ComponentMeta, Story } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import { FaStar, FaHeart, FaUser, FaBookmark } from 'react-icons/fa';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { IconCardGroup } from './IconCardGroup';
|
||||
|
||||
export default {
|
||||
title: 'components/IconCardGroup',
|
||||
component: IconCardGroup,
|
||||
argTypes: {
|
||||
onCardClick: { action: true },
|
||||
},
|
||||
} as ComponentMeta<typeof IconCardGroup>;
|
||||
|
||||
type Value = '1' | '2' | '3' | '4';
|
||||
|
||||
const data: {
|
||||
value: Value;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
body: string;
|
||||
}[] = [
|
||||
{
|
||||
value: '1',
|
||||
icon: <FaStar className="text-yellow-500 text-xl" />,
|
||||
title: 'Card-1',
|
||||
body: 'Description of card-1',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
icon: <FaHeart className="text-yellow-500 text-xl" />,
|
||||
title: 'Card-2',
|
||||
body: 'Description of card-2',
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
icon: <FaUser className="text-yellow-500 text-xl" />,
|
||||
title: 'Card-3',
|
||||
body: 'Description of card-3',
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
icon: <FaBookmark className="text-yellow-500 text-xl" />,
|
||||
title: 'Card-4',
|
||||
body: 'Description of card-4',
|
||||
},
|
||||
];
|
||||
|
||||
export const CardsWithValue: Story = () => {
|
||||
return (
|
||||
<IconCardGroup<Value> onChange={action('select')} items={data} value="1" />
|
||||
);
|
||||
};
|
||||
|
||||
export const CardsWithoutValue: Story = () => {
|
||||
return <IconCardGroup<Value> onChange={action('select')} items={data} />;
|
||||
};
|
||||
|
||||
export const Playground: Story = () => {
|
||||
const [selected, setSelected] = React.useState<string | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<IconCardGroup
|
||||
onChange={value => setSelected(value)}
|
||||
items={data}
|
||||
value={selected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlaygroundWithTest: Story = args => {
|
||||
return (
|
||||
<IconCardGroup onChange={value => args.onCardClick(value)} items={data} />
|
||||
);
|
||||
};
|
||||
|
||||
PlaygroundWithTest.play = async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Click on the first card, and expect the onChange prop to be called with `1`
|
||||
await userEvent.click(canvas.getByText('Card-1'));
|
||||
|
||||
await waitFor(() => expect(args.onCardClick).toHaveBeenCalledTimes(1));
|
||||
|
||||
expect(args.onCardClick).toBeCalledWith('1');
|
||||
|
||||
// Click on the fourth card, and expect the onChange prop to be called with `4`
|
||||
await userEvent.click(canvas.getByText('Card-4'));
|
||||
|
||||
await waitFor(() => expect(args.onCardClick).toHaveBeenCalledTimes(2));
|
||||
|
||||
expect(args.onCardClick).toBeCalledWith('4');
|
||||
};
|
60
console/src/new-components/IconCardGroup/IconCardGroup.tsx
Normal file
60
console/src/new-components/IconCardGroup/IconCardGroup.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FaAngleRight } from 'react-icons/fa';
|
||||
|
||||
interface IconCardGroupItem<T> {
|
||||
value: T;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
body: string | React.ReactNode;
|
||||
}
|
||||
|
||||
interface IconCardGroupProps<T> {
|
||||
items: Array<IconCardGroupItem<T>>;
|
||||
onChange: (option: T) => void;
|
||||
disabled?: boolean;
|
||||
value?: T;
|
||||
}
|
||||
|
||||
export const IconCardGroup = <T extends string = string>(
|
||||
props: IconCardGroupProps<T>
|
||||
) => {
|
||||
const { value, items, disabled = false, onChange } = props;
|
||||
|
||||
return (
|
||||
<div className="grid gap-sm grid-rows-auto w-full">
|
||||
{items.map(item => {
|
||||
const { value: iValue, title, body } = item;
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-white shadow-sm rounded p-md border border-gray-300 flex',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
||||
value === iValue && 'border-yellow-400'
|
||||
)}
|
||||
key={iValue}
|
||||
onClick={() => !disabled && onChange(iValue)}
|
||||
>
|
||||
<div className="mt-2">{item.icon}</div>
|
||||
<div className="w-9/12 ml-md">
|
||||
<label
|
||||
htmlFor={`card-select-${iValue}`}
|
||||
className={clsx(
|
||||
'mb-sm font-semibold mt-0.5',
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</label>
|
||||
<p className="text-muted">{body}</p>
|
||||
</div>
|
||||
<div className="mt-2 ml-auto">
|
||||
<FaAngleRight className="text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
};
|
1
console/src/new-components/IconCardGroup/index.tsx
Normal file
1
console/src/new-components/IconCardGroup/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { IconCardGroup } from './IconCardGroup';
|
Loading…
Reference in New Issue
Block a user