mirror of
https://github.com/hazemKrimi/crimson-quirks-ui.git
synced 2026-05-01 18:20:28 +00:00
Stories scaffolding
This commit is contained in:
@@ -17,7 +17,6 @@ codegen-*
|
|||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
.env.*
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|||||||
+6
-1
@@ -56,5 +56,10 @@
|
|||||||
"typescript-eslint": "^8.26.1",
|
"typescript-eslint": "^8.26.1",
|
||||||
"vite": "^6.2.2",
|
"vite": "^6.2.2",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
}
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"styled-components": "^6.1.15"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ const meta = {
|
|||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
text: { control: 'text' },
|
text: { control: 'text' },
|
||||||
color: { control: 'color' },
|
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof Alert>;
|
} satisfies Meta<typeof Alert>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Primary: Story = {
|
export const Example: Story = {
|
||||||
args: {
|
args: {
|
||||||
text: 'Alert',
|
text: 'Alert',
|
||||||
color: 'client'
|
color: 'client'
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Avatar from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Avatar',
|
||||||
|
component: Avatar,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
text: { control: 'text' },
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||||
|
size: { options: ['big', 'small'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Avatar>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
text: 'A',
|
||||||
|
color: 'admin'
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@ type AvatarProps = {
|
|||||||
const Avatar = ({ color, size = 'small', text, className }: AvatarProps) => {
|
const Avatar = ({ color, size = 'small', text, className }: AvatarProps) => {
|
||||||
return (
|
return (
|
||||||
<Wrapper color={color} size={size} className={className}>
|
<Wrapper color={color} size={size} className={className}>
|
||||||
{text}
|
{text[0]}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Box, Text } from '..';
|
|
||||||
import { Backend } from '../../assets';
|
|
||||||
import { FeatureOutput } from '../../graphql/types';
|
|
||||||
|
|
||||||
type BackendFeatureCardProps = {
|
|
||||||
feature: FeatureOutput;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BackendFeatureCard = ({ feature }: BackendFeatureCardProps) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
padding='15px 10px'
|
|
||||||
background='white'
|
|
||||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
|
||||||
display='grid'
|
|
||||||
gridTemplateRows='auto'
|
|
||||||
alignItems='center'
|
|
||||||
rowGap='10px'
|
|
||||||
borderRadius='10px'
|
|
||||||
cursor='pointer'
|
|
||||||
>
|
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
|
||||||
<Box flexGrow='1'>
|
|
||||||
<Text variant='title' weight='bold'>
|
|
||||||
{feature.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Backend />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BackendFeatureCard;
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Box from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Box',
|
||||||
|
component: Box,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { control: 'text' },
|
||||||
|
children: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Box>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: '#000000',
|
||||||
|
children: 'Hello, World!',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { JSX } from 'react';
|
||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
|
|
||||||
export type BoxProps = {
|
export type BoxProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode | JSX.Element | string;
|
children?: React.ReactNode | JSX.Element | JSX.Element[] | string;
|
||||||
|
ref?: React.Ref<HTMLElement>;
|
||||||
|
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
cursor?: 'pointer' | 'default';
|
cursor?: 'pointer' | 'default';
|
||||||
@@ -38,11 +39,11 @@ export type BoxProps = {
|
|||||||
|
|
||||||
alignItems?: 'center' | 'flex-start' | 'flex-end' | 'stretch';
|
alignItems?: 'center' | 'flex-start' | 'flex-end' | 'stretch';
|
||||||
justifyContent?:
|
justifyContent?:
|
||||||
| 'center'
|
| 'center'
|
||||||
| 'flex-start'
|
| 'flex-start'
|
||||||
| 'flex-end'
|
| 'flex-end'
|
||||||
| 'space-between'
|
| 'space-between'
|
||||||
| 'space-around';
|
| 'space-around';
|
||||||
alignSelf?: 'center' | 'flex-start' | 'flex-end';
|
alignSelf?: 'center' | 'flex-start' | 'flex-end';
|
||||||
justifySelf?: 'center' | 'flex-start' | 'flex-end';
|
justifySelf?: 'center' | 'flex-start' | 'flex-end';
|
||||||
|
|
||||||
@@ -86,14 +87,12 @@ export type BoxProps = {
|
|||||||
textDecoration?: string;
|
textDecoration?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Box = React.forwardRef<HTMLDivElement, BoxProps>(
|
function Box({ children, ref, ...props }: BoxProps) {
|
||||||
({ children, ...props }, ref) => {
|
return (
|
||||||
return (
|
<Wrapper {...props} draggable='false' ref={ref}>
|
||||||
<Wrapper {...props} draggable='false' ref={ref}>
|
{children}
|
||||||
{children}
|
</Wrapper>
|
||||||
</Wrapper>
|
);
|
||||||
);
|
};
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Box;
|
export default Box;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Button from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Button',
|
||||||
|
component: Button,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
text: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Button>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'admin',
|
||||||
|
text: 'Hello, World!',
|
||||||
|
variant: 'primary-action'
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Spinner } from '..';
|
import Spinner from '../Spinner';
|
||||||
|
|
||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
|
|
||||||
type ButtonProps = {
|
type ButtonProps = {
|
||||||
@@ -6,8 +7,8 @@ type ButtonProps = {
|
|||||||
size?: 'small' | 'big';
|
size?: 'small' | 'big';
|
||||||
variant?: 'primary-action' | 'secondary-action' | 'outlined' | 'text';
|
variant?: 'primary-action' | 'secondary-action' | 'outlined' | 'text';
|
||||||
type?: 'submit' | 'button' | 'reset';
|
type?: 'submit' | 'button' | 'reset';
|
||||||
iconLeft?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
iconLeft?: React.ReactNode;
|
||||||
iconRight?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
iconRight?: React.ReactNode;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ type WrapperProps = {
|
|||||||
color: 'client' | 'productOwner' | 'developer' | 'admin' | 'error';
|
color: 'client' | 'productOwner' | 'developer' | 'admin' | 'error';
|
||||||
size?: 'small' | 'big';
|
size?: 'small' | 'big';
|
||||||
variant?: 'primary-action' | 'secondary-action' | 'outlined' | 'text';
|
variant?: 'primary-action' | 'secondary-action' | 'outlined' | 'text';
|
||||||
iconLeft?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
iconLeft?: React.ReactNode;
|
||||||
iconRight?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
iconRight?: React.ReactNode;
|
||||||
load?: boolean;
|
load?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Card from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Card',
|
||||||
|
component: Card,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Card>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'developer',
|
||||||
|
title: 'Card title',
|
||||||
|
description: 'Card description',
|
||||||
|
selected: true
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
import { Box, Text } from '..';
|
import Box from '../Box';
|
||||||
import { CategoryOutput } from '../../graphql/types';
|
import Text from '../Text';
|
||||||
|
|
||||||
import { theme } from '../../themes';
|
import { theme } from '../../themes';
|
||||||
|
|
||||||
type CategoryCardProps = {
|
type CardProps = {
|
||||||
category: CategoryOutput;
|
title: string;
|
||||||
|
description: string;
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
toggleSelect?: () => void;
|
toggleSelect?: () => void;
|
||||||
color: 'client' | 'productOwner' | 'developer' | 'admin';
|
color: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||||
};
|
};
|
||||||
|
|
||||||
const CategoryCard = ({
|
const Card = ({
|
||||||
category,
|
title,
|
||||||
|
description,
|
||||||
selectable = false,
|
selectable = false,
|
||||||
selected = false,
|
selected = false,
|
||||||
toggleSelect = () => {},
|
toggleSelect = () => {},
|
||||||
color,
|
color,
|
||||||
}: CategoryCardProps) => {
|
}: CardProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
padding='10px'
|
padding='10px'
|
||||||
@@ -34,17 +37,17 @@ const CategoryCard = ({
|
|||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||||
<Box flexGrow='1'>
|
<Box flexGrow='1'>
|
||||||
<Text variant='title' weight='bold'>
|
<Text variant='title' weight='bold'>
|
||||||
{category.name}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||||
<Box flexGrow='1'>
|
<Box flexGrow='1'>
|
||||||
<Text variant='body'>{category.description}</Text>
|
<Text variant='body'>{description}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CategoryCard;
|
export default Card;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import CheckBox from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'CheckBox',
|
||||||
|
component: CheckBox,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||||
|
label: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof CheckBox>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'admin',
|
||||||
|
label: 'Hello, World!',
|
||||||
|
checked: false,
|
||||||
|
onClick: () => window.alert('Clicked!'),
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
import { Text } from '..';
|
|
||||||
|
import Text from '../Text';
|
||||||
|
|
||||||
import { Check } from '../../assets';
|
import { Check } from '../../assets';
|
||||||
|
|
||||||
type CheckBoxProps = {
|
type CheckBoxProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CheckBox = ({
|
const CheckBox = ({
|
||||||
label,
|
label,
|
||||||
name,
|
|
||||||
checked,
|
checked,
|
||||||
onClick,
|
onClick,
|
||||||
...props
|
...props
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Chip from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Chip',
|
||||||
|
component: Chip,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||||
|
text: { control: 'text' },
|
||||||
|
variant: { options: ['outlined', 'filled'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Chip>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'admin',
|
||||||
|
text: 'Hello, World!',
|
||||||
|
variant: 'filled'
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
import { Text } from '..';
|
|
||||||
|
import Text from '../Text';
|
||||||
|
|
||||||
type ChipProps = {
|
type ChipProps = {
|
||||||
variant?: 'outlined' | 'filled';
|
variant?: 'outlined' | 'filled';
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import ContextMenu from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ContextMenu',
|
||||||
|
component: ContextMenu,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof ContextMenu>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Hello, World!',
|
||||||
|
action: () => window.alert('Hello, World!')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
component: 'component',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
import { Text } from '..';
|
|
||||||
|
import Text from '../Text';
|
||||||
|
|
||||||
type ContextMenuProps = {
|
type ContextMenuProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -10,7 +12,7 @@ type ContextMenuProps = {
|
|||||||
|
|
||||||
const ContextMenu = ({ items, component, className }: ContextMenuProps) => {
|
const ContextMenu = ({ items, component, className }: ContextMenuProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const parentComponentRef = useRef<HTMLDivElement>();
|
const parentComponentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
parentComponentRef.current = document.querySelector(`#${component}`) as HTMLDivElement;
|
parentComponentRef.current = document.querySelector(`#${component}`) as HTMLDivElement;
|
||||||
@@ -31,7 +33,7 @@ const ContextMenu = ({ items, component, className }: ContextMenuProps) => {
|
|||||||
parentComponentRef.current?.removeEventListener('mouseenter', openMenu);
|
parentComponentRef.current?.removeEventListener('mouseenter', openMenu);
|
||||||
parentComponentRef.current?.removeEventListener('mouseleave', closeMenu);
|
parentComponentRef.current?.removeEventListener('mouseleave', closeMenu);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [component]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Box, Text } from '..';
|
|
||||||
import { Backend, Frontend } from '../../assets';
|
|
||||||
import { FeatureOutput } from '../../graphql/types';
|
|
||||||
import { theme } from '../../themes';
|
|
||||||
|
|
||||||
type FeatureCardProps = {
|
|
||||||
feature: FeatureOutput;
|
|
||||||
selectable?: boolean;
|
|
||||||
selected?: boolean;
|
|
||||||
toggleSelect?: () => void;
|
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
};
|
|
||||||
|
|
||||||
const FeatureCard = ({
|
|
||||||
feature,
|
|
||||||
selectable = false,
|
|
||||||
selected = false,
|
|
||||||
toggleSelect = () => {},
|
|
||||||
color,
|
|
||||||
}: FeatureCardProps) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
padding='10px'
|
|
||||||
background='white'
|
|
||||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
|
||||||
border={
|
|
||||||
selected
|
|
||||||
? `2px solid ${color ? theme.colors[color].main : '#3CC13B'}`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onClick={selectable ? toggleSelect : () => {}}
|
|
||||||
display='grid'
|
|
||||||
gridTemplateRows='auto'
|
|
||||||
alignItems='center'
|
|
||||||
rowGap='10px'
|
|
||||||
borderRadius='10px'
|
|
||||||
cursor={selectable ? 'pointer' : undefined}
|
|
||||||
>
|
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
|
||||||
<Box flexGrow='1'>
|
|
||||||
<Text variant='title' weight='bold'>
|
|
||||||
{feature.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
|
||||||
<Box
|
|
||||||
marginRight={
|
|
||||||
feature.featureType === 'fullstack' ? '10px' : undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{feature.featureType === 'frontend' ||
|
|
||||||
(feature.featureType === 'fullstack' && <Frontend />)}
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
{feature.featureType === 'backend' ||
|
|
||||||
(feature.featureType === 'fullstack' && <Backend />)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
|
||||||
<Box flexGrow='1'>
|
|
||||||
<Text variant='body'>{feature.description}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text variant='title'>${feature.price}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FeatureCard;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Handle, Position } from 'reactflow';
|
|
||||||
import { Box, Text } from '..';
|
|
||||||
import { FeatureOutput } from '../../graphql/types';
|
|
||||||
|
|
||||||
type FrontendFeatureCardProps = {
|
|
||||||
data: FeatureOutput;
|
|
||||||
isConnectable?: boolean;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FrontendFeatureCard = ({
|
|
||||||
data,
|
|
||||||
isConnectable = false,
|
|
||||||
className,
|
|
||||||
}: FrontendFeatureCardProps) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Handle type="target" position={Position.Top} isConnectable={isConnectable} />
|
|
||||||
<Box
|
|
||||||
className={className}
|
|
||||||
padding='10px'
|
|
||||||
background='white'
|
|
||||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
|
||||||
display='grid'
|
|
||||||
gridTemplateRows='auto'
|
|
||||||
alignItems='center'
|
|
||||||
rowGap='10px'
|
|
||||||
borderRadius='10px'
|
|
||||||
cursor='pointer'
|
|
||||||
textAlign='left'
|
|
||||||
>
|
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
|
||||||
<Box flexGrow='1'>
|
|
||||||
<Text variant='title' weight='bold'>
|
|
||||||
{data.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
display='flex'
|
|
||||||
flexDirection='row'
|
|
||||||
alignItems='center'
|
|
||||||
justifyContent='space-between'
|
|
||||||
padding='5px 20px'
|
|
||||||
>
|
|
||||||
{data.wireframes?.map((wireframe) => (
|
|
||||||
<img
|
|
||||||
src={wireframe.src}
|
|
||||||
alt={wireframe.name}
|
|
||||||
key={wireframe.id}
|
|
||||||
style={{ width: '100px', height: 'auto', marginRight: '10px' }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Handle type="source" position={Position.Bottom} isConnectable={isConnectable} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FrontendFeatureCard;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import IconButton from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'IconButton',
|
||||||
|
component: IconButton,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||||
|
size: { options: ['small', 'medium', 'big'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof IconButton>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'admin',
|
||||||
|
size: 'medium',
|
||||||
|
onClick: () => window.alert('Hello, World!')
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { Wrapper } from './styles';
|
|||||||
type IconButtonProps = {
|
type IconButtonProps = {
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||||
size?: 'small' | 'medium' | 'big';
|
size?: 'small' | 'medium' | 'big';
|
||||||
icon?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
icon?: React.ReactNode;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import ImagePreview from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'ImagePreview',
|
||||||
|
component: ImagePreview,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof ImagePreview>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'developer',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ type ImagePreviewProps = {
|
|||||||
error?: boolean;
|
error?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
image: { name: string; src: string } | undefined;
|
image?: { name: string; src: string } | undefined;
|
||||||
deletable?: boolean;
|
deletable?: boolean;
|
||||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Input from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Input',
|
||||||
|
component: Input,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Input>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'developer',
|
||||||
|
onChange: () => {}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Text } from '..';
|
import Text from '../Text';
|
||||||
|
|
||||||
import { Upload } from '../../assets';
|
import { Upload } from '../../assets';
|
||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Link from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Link',
|
||||||
|
component: Link,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['success', 'error', 'warning', 'black'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Link>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'success',
|
||||||
|
children: 'Link',
|
||||||
|
url: true,
|
||||||
|
href: 'https://hazemkrimi.tech',
|
||||||
|
target: '_blank'
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { JSX } from 'react';
|
||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
|
|
||||||
type LinkProps = {
|
type LinkProps = {
|
||||||
@@ -18,7 +18,7 @@ type LinkProps = {
|
|||||||
| string;
|
| string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
iconLeft?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
iconLeft?: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
target?: '_self' | '_blank';
|
target?: '_self' | '_blank';
|
||||||
};
|
};
|
||||||
@@ -35,10 +35,10 @@ const Link = ({
|
|||||||
return (
|
return (
|
||||||
<Wrapper {...props} selected={selected}>
|
<Wrapper {...props} selected={selected}>
|
||||||
{href && !url ? (
|
{href && !url ? (
|
||||||
<RouterLink to={href} target={target}>
|
<a href={href} target={target}>
|
||||||
{iconLeft && <span className='icon left'>{iconLeft}</span>}
|
{iconLeft && <span className='icon left'>{iconLeft}</span>}
|
||||||
{children}
|
{children}
|
||||||
</RouterLink>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<a href={href} target={target}>
|
<a href={href} target={target}>
|
||||||
{iconLeft && <span className='icon left'>{iconLeft}</span>}
|
{iconLeft && <span className='icon left'>{iconLeft}</span>}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Menu from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Menu',
|
||||||
|
component: Menu,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
} satisfies Meta<typeof Menu>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Hello, World!',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
component: 'component',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
import { Text } from '..';
|
|
||||||
|
import Text from '../Text';
|
||||||
|
|
||||||
type MenuProps = {
|
type MenuProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
items: Array<{
|
items: Array<{
|
||||||
icon: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
icon?: React.ReactNode;
|
||||||
avoid?: boolean;
|
avoid?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
action?: () => void;
|
action?: () => void;
|
||||||
@@ -16,7 +17,7 @@ type MenuProps = {
|
|||||||
const Menu = ({ items, component, className }: MenuProps) => {
|
const Menu = ({ items, component, className }: MenuProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const componentRef = useRef<HTMLDivElement>(null);
|
const componentRef = useRef<HTMLDivElement>(null);
|
||||||
const parentComponentRef = useRef<HTMLDivElement>();
|
const parentComponentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const openMenu = () => setOpen(true);
|
const openMenu = () => setOpen(true);
|
||||||
const closeMenu = () => setOpen(false);
|
const closeMenu = () => setOpen(false);
|
||||||
@@ -31,7 +32,7 @@ const Menu = ({ items, component, className }: MenuProps) => {
|
|||||||
parentComponentRef.current?.removeEventListener('mouseenter', openMenu);
|
parentComponentRef.current?.removeEventListener('mouseenter', openMenu);
|
||||||
componentRef.current?.removeEventListener('mouseleave', closeMenu);
|
componentRef.current?.removeEventListener('mouseleave', closeMenu);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [component]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Modal from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Modal',
|
||||||
|
component: Modal,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
title: { control: 'text' },
|
||||||
|
description: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Modal>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'productOwner',
|
||||||
|
title: 'Modal',
|
||||||
|
description: 'This is a modal!',
|
||||||
|
onConfirm: () => {},
|
||||||
|
onClose: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import { JSX } from 'react';
|
||||||
import { theme } from '../../themes';
|
import { theme } from '../../themes';
|
||||||
import { Box, Button, Text } from '..';
|
|
||||||
|
import Box from '../Box';
|
||||||
|
import Button from '../Button';
|
||||||
|
import Text from '../Text';
|
||||||
|
|
||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
|
|
||||||
type ModalProps = {
|
type ModalProps = {
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
import { useReactiveVar } from '@apollo/client';
|
|
||||||
import { useNavigate, useLocation } from 'react-router';
|
|
||||||
import { roleVar, tokenVar, userVar } from '../../graphql/state';
|
|
||||||
import { Wrapper } from './styles';
|
|
||||||
import { Avatar, Link, Menu, Text } from '..';
|
|
||||||
import { Settings, Logout, Logo } from '../../assets';
|
|
||||||
|
|
||||||
const Navbar = () => {
|
|
||||||
const user = useReactiveVar(userVar);
|
|
||||||
const role = useReactiveVar(roleVar);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper color={role}>
|
|
||||||
<Link href='/'>
|
|
||||||
<Logo />
|
|
||||||
</Link>
|
|
||||||
<nav>
|
|
||||||
{role === 'admin' && (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
href='/clients'
|
|
||||||
color={
|
|
||||||
new RegExp('clients', 'i').test(location.pathname)
|
|
||||||
? 'admin'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp('clients', 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Clients
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/product-owners'
|
|
||||||
color={
|
|
||||||
new RegExp('product-owners', 'i').test(location.pathname)
|
|
||||||
? 'admin'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp('product-owners', 'i').test(
|
|
||||||
location.pathname
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Product Owners
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/developers'
|
|
||||||
color={
|
|
||||||
new RegExp('developers', 'i').test(location.pathname)
|
|
||||||
? 'admin'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp('developers', 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Developers
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{role === 'developer' && (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
href='/project'
|
|
||||||
color={
|
|
||||||
new RegExp('project', 'i').test(location.pathname)
|
|
||||||
? 'developer'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp('project', 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Projects
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/template'
|
|
||||||
color={
|
|
||||||
new RegExp(/(template|prototype)/, 'i').test(location.pathname)
|
|
||||||
? 'developer'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp(/(template|prototype)/, 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/feature'
|
|
||||||
color={
|
|
||||||
new RegExp('feature', 'i').test(location.pathname)
|
|
||||||
? 'developer'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp('feature', 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Features
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/category'
|
|
||||||
color={
|
|
||||||
new RegExp('category', 'i').test(location.pathname)
|
|
||||||
? 'developer'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp('category', 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Categories
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{role === 'productOwner' && (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
href='/project'
|
|
||||||
color={
|
|
||||||
new RegExp(/(project|support)/, 'i').test(location.pathname)
|
|
||||||
? 'productOwner'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp(/(project|support)/, 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Projects
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href='/template'
|
|
||||||
color={
|
|
||||||
new RegExp(/(template|prototype)/, 'i').test(location.pathname)
|
|
||||||
? 'productOwner'
|
|
||||||
: 'black'
|
|
||||||
}
|
|
||||||
selected={new RegExp(/(template|prototype)/, 'i').test(location.pathname)}
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
<div className='menu'></div>
|
|
||||||
<div className='user' id='user'>
|
|
||||||
<Avatar
|
|
||||||
text={
|
|
||||||
(user?.firstName && user?.firstName[0].toLocaleUpperCase()) ||
|
|
||||||
(role && role[0].toLocaleUpperCase()) ||
|
|
||||||
'C'
|
|
||||||
}
|
|
||||||
color={role}
|
|
||||||
/>
|
|
||||||
<Text variant='body' weight='bold'>
|
|
||||||
{user?.firstName} {user?.lastName}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Menu
|
|
||||||
component='user'
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
icon: <Settings />,
|
|
||||||
label: 'Settings',
|
|
||||||
action: () => navigate('/settings'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Logout />,
|
|
||||||
label: 'Logout',
|
|
||||||
action: () => {
|
|
||||||
tokenVar(undefined);
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
navigate('/login');
|
|
||||||
},
|
|
||||||
avoid: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Navbar;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type WrapperProps = {
|
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Wrapper = styled.div<WrapperProps>`
|
|
||||||
background: ${({ theme }) => theme.colors.white.main};
|
|
||||||
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.25);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding: 15px 45px 15px 120px;
|
|
||||||
user-select: none;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 99;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-icon {
|
|
||||||
fill: ${({ theme, color }) =>
|
|
||||||
color ? theme.colors[color].main : theme.colors.client.main};
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-left: 60px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, auto);
|
|
||||||
column-gap: 20px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Search from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Search',
|
||||||
|
component: Search,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
value: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Search>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'productOwner',
|
||||||
|
value: '',
|
||||||
|
onChange: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
import { Search as SearchIcon } from '../../assets';
|
|
||||||
|
import SearchIcon from '../../assets/icons/search.svg?react';
|
||||||
|
|
||||||
type SearchProps = {
|
type SearchProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Wrapper } from './styles';
|
|
||||||
|
|
||||||
type SectionSelectorProps = {
|
|
||||||
icon: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
|
||||||
text: string;
|
|
||||||
color: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
selected?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionSelector = ({
|
|
||||||
icon,
|
|
||||||
text,
|
|
||||||
color,
|
|
||||||
selected = false,
|
|
||||||
disabled = false,
|
|
||||||
onClick,
|
|
||||||
}: SectionSelectorProps) => {
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
color={color}
|
|
||||||
icon={icon}
|
|
||||||
selected={selected}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{icon && <span className='icon left'>{icon}</span>}
|
|
||||||
{text}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SectionSelector;
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import styled, { css } from 'styled-components';
|
|
||||||
|
|
||||||
type WrapperProps = {
|
|
||||||
icon: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
|
||||||
color: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
selected: boolean;
|
|
||||||
disabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Wrapper = styled.div<WrapperProps>`
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-height: 50px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
${({ icon }) => {
|
|
||||||
if (icon)
|
|
||||||
return css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
return '';
|
|
||||||
}}
|
|
||||||
|
|
||||||
.icon svg {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon.left {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
${({ color, theme, selected }) => {
|
|
||||||
switch (color) {
|
|
||||||
case 'client':
|
|
||||||
return css`
|
|
||||||
color: ${selected
|
|
||||||
? theme.colors.client.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
background: ${selected ? theme.colors.client.light : 'none'};
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
stroke: ${selected
|
|
||||||
? theme.colors.client.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
case 'productOwner':
|
|
||||||
return css`
|
|
||||||
color: ${selected
|
|
||||||
? theme.colors.productOwner.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
background: ${selected ? theme.colors.productOwner.light : 'none'};
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
stroke: ${selected
|
|
||||||
? theme.colors.productOwner.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
case 'developer':
|
|
||||||
return css`
|
|
||||||
color: ${selected
|
|
||||||
? theme.colors.developer.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
background: ${selected ? theme.colors.developer.light : 'none'};
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
stroke: ${selected
|
|
||||||
? theme.colors.developer.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
case 'admin':
|
|
||||||
return css`
|
|
||||||
color: ${selected
|
|
||||||
? theme.colors.admin.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
background: ${selected ? theme.colors.admin.light : 'none'};
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
stroke: ${selected
|
|
||||||
? theme.colors.admin.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
default:
|
|
||||||
return css`
|
|
||||||
color: ${selected
|
|
||||||
? theme.colors.client.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
background: ${selected ? theme.colors.client.light : 'none'};
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
stroke: ${selected
|
|
||||||
? theme.colors.client.main
|
|
||||||
: theme.colors.black.main};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
|
|
||||||
${({ disabled, theme }) =>
|
|
||||||
disabled &&
|
|
||||||
css`
|
|
||||||
cursor: default;
|
|
||||||
color: ${theme.colors.gray.main};
|
|
||||||
background: none;
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
stroke: ${theme.colors.gray.main};
|
|
||||||
}
|
|
||||||
`};
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Select from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Select',
|
||||||
|
component: Select,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
value: { control: 'text' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Select>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'productOwner',
|
||||||
|
value: '',
|
||||||
|
options: [
|
||||||
|
{ value: '1', label: 'Option 1' },
|
||||||
|
{ value: '2', label: 'Option 2' },
|
||||||
|
],
|
||||||
|
name: 'select',
|
||||||
|
onChange: () => { },
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
import { Text } from '..';
|
|
||||||
|
import Text from '../Text';
|
||||||
|
|
||||||
type SelectProps = {
|
type SelectProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -15,9 +16,9 @@ type SelectProps = {
|
|||||||
| 'white';
|
| 'white';
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
options: Array<{ value: any; label: string }>;
|
options: Array<{ value: string | number; label: string }>;
|
||||||
value: string;
|
value: string;
|
||||||
select?: any;
|
selected?: string | number;
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
@@ -30,7 +31,7 @@ const Select = ({
|
|||||||
label,
|
label,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
select = null,
|
selected = undefined,
|
||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
@@ -55,7 +56,7 @@ const Select = ({
|
|||||||
<div className='select'>
|
<div className='select'>
|
||||||
<div>
|
<div>
|
||||||
<select
|
<select
|
||||||
value={select || value}
|
value={selected || value}
|
||||||
name={name}
|
name={name}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useNavigate, useLocation } from 'react-router';
|
|
||||||
import { useLazyQuery, useReactiveVar } from '@apollo/client';
|
|
||||||
import { roleVar, userVar } from '../../graphql/state';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
ContextMenu,
|
|
||||||
IconButton,
|
|
||||||
SupportSidebar,
|
|
||||||
SidebarItem,
|
|
||||||
} from '..';
|
|
||||||
import { Add, Messaging } from '../../assets';
|
|
||||||
import { Wrapper } from './styles';
|
|
||||||
import {
|
|
||||||
CategoryOutput,
|
|
||||||
FeatureOutput,
|
|
||||||
GetAllCategoriesQuery,
|
|
||||||
GetAllCategoriesQueryVariables,
|
|
||||||
GetAllFeaturesQuery,
|
|
||||||
GetAllFeaturesQueryVariables,
|
|
||||||
GetAllProjectsByClientIdQuery,
|
|
||||||
GetAllProjectsByClientIdQueryVariables,
|
|
||||||
GetAllProjectsQuery,
|
|
||||||
GetAllProjectsQueryVariables,
|
|
||||||
GetAllTemplatesQuery,
|
|
||||||
GetAllTemplatesQueryVariables,
|
|
||||||
ProjectOutput,
|
|
||||||
TemplateOutput,
|
|
||||||
} from '../../graphql/types';
|
|
||||||
import { GET_ALL_CATEGORIES } from '../../graphql/category.api';
|
|
||||||
import {
|
|
||||||
GET_ALL_PROJECTS,
|
|
||||||
GET_ALL_PROJECTS_BY_CLIENT_ID,
|
|
||||||
} from '../../graphql/project.api';
|
|
||||||
import { GET_ALL_TEMPLATES } from '../../graphql/template.api';
|
|
||||||
import { GET_ALL_FEATURES } from '../../graphql/feature.api';
|
|
||||||
|
|
||||||
const Sidebar = () => {
|
|
||||||
const role = useReactiveVar(roleVar);
|
|
||||||
const currentUser = useReactiveVar(userVar);
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [projects, setProjects] = useState<Array<ProjectOutput>>();
|
|
||||||
const [templates, setTemplates] = useState<Array<TemplateOutput>>();
|
|
||||||
const [features, setFeatures] = useState<Array<FeatureOutput>>();
|
|
||||||
const [categories, setCategories] = useState<Array<CategoryOutput>>();
|
|
||||||
const [supportSideBarOpen, setSupportSideBarOpen] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
const [getProjectsByClientId] = useLazyQuery<
|
|
||||||
GetAllProjectsByClientIdQuery,
|
|
||||||
GetAllProjectsByClientIdQueryVariables
|
|
||||||
>(GET_ALL_PROJECTS_BY_CLIENT_ID, {
|
|
||||||
variables: {
|
|
||||||
id: currentUser?.id!,
|
|
||||||
},
|
|
||||||
onCompleted({ getAllProjectsByClientId }) {
|
|
||||||
setProjects(getAllProjectsByClientId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [getProjects] = useLazyQuery<
|
|
||||||
GetAllProjectsQuery,
|
|
||||||
GetAllProjectsQueryVariables
|
|
||||||
>(GET_ALL_PROJECTS, {
|
|
||||||
onCompleted({ getAllProjects }) {
|
|
||||||
setProjects(getAllProjects);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [getTemplates] = useLazyQuery<
|
|
||||||
GetAllTemplatesQuery,
|
|
||||||
GetAllTemplatesQueryVariables
|
|
||||||
>(GET_ALL_TEMPLATES, {
|
|
||||||
onCompleted({ getAllTemplates }) {
|
|
||||||
setTemplates(getAllTemplates);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [getFeatures] = useLazyQuery<
|
|
||||||
GetAllFeaturesQuery,
|
|
||||||
GetAllFeaturesQueryVariables
|
|
||||||
>(GET_ALL_FEATURES, {
|
|
||||||
onCompleted({ getAllFeatures }) {
|
|
||||||
setFeatures(getAllFeatures);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [getCategories] = useLazyQuery<
|
|
||||||
GetAllCategoriesQuery,
|
|
||||||
GetAllCategoriesQueryVariables
|
|
||||||
>(GET_ALL_CATEGORIES, {
|
|
||||||
onCompleted({ getAllCategories }) {
|
|
||||||
setCategories(getAllCategories);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (/(project|support)/i.test(location.pathname)) {
|
|
||||||
if (role !== 'client') getProjects();
|
|
||||||
else getProjectsByClientId({ variables: { id: currentUser?.id! } });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/template/i.test(location.pathname)) {
|
|
||||||
getTemplates();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/feature/i.test(location.pathname)) {
|
|
||||||
getFeatures();
|
|
||||||
}
|
|
||||||
if (/category/i.test(location.pathname)) {
|
|
||||||
getCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setProjects([]);
|
|
||||||
setTemplates([]);
|
|
||||||
setFeatures([]);
|
|
||||||
setCategories([]);
|
|
||||||
};
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
const showAddButton = (role: string, pathname: string) => {
|
|
||||||
switch (role) {
|
|
||||||
case 'client':
|
|
||||||
return /(project|support)/i.test(pathname);
|
|
||||||
case 'productOwner':
|
|
||||||
return /template/i.test(pathname);
|
|
||||||
case 'developer':
|
|
||||||
return /feature/i.test(pathname) || /category/i.test(pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper color={role}>
|
|
||||||
{role !== 'admin' && (
|
|
||||||
<>
|
|
||||||
<Box display='flex' flexDirection='column'>
|
|
||||||
{projects &&
|
|
||||||
new RegExp(/(project|support)/, 'i').test(location.pathname) &&
|
|
||||||
projects.map((project, index) => (
|
|
||||||
<Box marginBottom='20px' key={project.id}>
|
|
||||||
<div id={`project-${project.id}`}>
|
|
||||||
<SidebarItem
|
|
||||||
color={role}
|
|
||||||
selected={
|
|
||||||
new RegExp(project.id, 'i').test(location.pathname) ||
|
|
||||||
(index === 0 && location.pathname === '/project')
|
|
||||||
}
|
|
||||||
text={project.name[0]}
|
|
||||||
onClick={() => navigate(`/project/${project.id}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ContextMenu
|
|
||||||
component={`project-${project.id}`}
|
|
||||||
items={[{ label: project.name }]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
{templates &&
|
|
||||||
new RegExp(/template/, 'i').test(location.pathname) &&
|
|
||||||
templates.map((template, index) => (
|
|
||||||
<Box marginBottom='20px' key={template.id}>
|
|
||||||
<div id={`template-${template.id}`}>
|
|
||||||
<SidebarItem
|
|
||||||
color={role}
|
|
||||||
selected={
|
|
||||||
new RegExp(template.id, 'i').test(location.pathname) ||
|
|
||||||
(index === 0 && location.pathname === '/template')
|
|
||||||
}
|
|
||||||
text={template.name[0]}
|
|
||||||
onClick={() => navigate(`/template/${template.id}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ContextMenu
|
|
||||||
component={`template-${template.id}`}
|
|
||||||
items={[{ label: template.name }]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
{features &&
|
|
||||||
new RegExp(/feature/, 'i').test(location.pathname) &&
|
|
||||||
features.map((feature, index) => (
|
|
||||||
<Box marginBottom='20px' key={feature.id}>
|
|
||||||
<div id={`feature-${feature.id}`}>
|
|
||||||
<SidebarItem
|
|
||||||
color={role}
|
|
||||||
selected={
|
|
||||||
new RegExp(feature.id, 'i').test(location.pathname) ||
|
|
||||||
(index === 0 && location.pathname === '/feature')
|
|
||||||
}
|
|
||||||
text={feature.name[0]}
|
|
||||||
onClick={() => navigate(`/feature/${feature.id}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ContextMenu
|
|
||||||
component={`feature-${feature.id}`}
|
|
||||||
items={[{ label: feature.name }]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
{categories &&
|
|
||||||
new RegExp(/category/, 'i').test(location.pathname) &&
|
|
||||||
categories.map((category, index) => (
|
|
||||||
<Box marginBottom='20px' key={category.id}>
|
|
||||||
<div id={`category-${category.id}`}>
|
|
||||||
<SidebarItem
|
|
||||||
color={role}
|
|
||||||
selected={
|
|
||||||
new RegExp(category.id, 'i').test(location.pathname) ||
|
|
||||||
(index === 0 && location.pathname === '/category')
|
|
||||||
}
|
|
||||||
text={category.name[0]}
|
|
||||||
onClick={() => navigate(`/category/${category.id}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ContextMenu
|
|
||||||
component={`category-${category.id}`}
|
|
||||||
items={[{ label: category.name }]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<Box display='flex' flexDirection='column'>
|
|
||||||
{showAddButton(role as string, location.pathname) && (
|
|
||||||
<Box marginBottom='20px'>
|
|
||||||
<IconButton
|
|
||||||
icon={<Add />}
|
|
||||||
color={role}
|
|
||||||
onClick={() => {
|
|
||||||
switch (role) {
|
|
||||||
case 'client':
|
|
||||||
default: {
|
|
||||||
if (/project/i.test(location.pathname)) {
|
|
||||||
navigate('/add-project');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'productOwner': {
|
|
||||||
if (/project/i.test(location.pathname)) {
|
|
||||||
navigate('/add-project');
|
|
||||||
}
|
|
||||||
if (/template/i.test(location.pathname)) {
|
|
||||||
navigate('/add-template');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'developer': {
|
|
||||||
if (/feature/i.test(location.pathname)) {
|
|
||||||
navigate('/add-feature');
|
|
||||||
}
|
|
||||||
if (/category/i.test(location.pathname)) {
|
|
||||||
navigate('/add-category');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{/(project|support)/i.test(location.pathname) &&
|
|
||||||
['client', 'productOwner'].includes(role as string) && (
|
|
||||||
<Box>
|
|
||||||
<IconButton
|
|
||||||
icon={<Messaging />}
|
|
||||||
color={role}
|
|
||||||
onClick={() =>
|
|
||||||
setSupportSideBarOpen(!supportSideBarOpen)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{supportSideBarOpen && (
|
|
||||||
<SupportSidebar onClose={() => setSupportSideBarOpen(false)} />
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type WrapperProps = {
|
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Wrapper = styled.div<WrapperProps>`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 100;
|
|
||||||
width: 75px;
|
|
||||||
height: 100%;
|
|
||||||
background: ${({ theme, color }) =>
|
|
||||||
color ? theme.colors[color].light : theme.colors.client.light};
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr auto;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 55px 0px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Wrapper } from './styles';
|
|
||||||
|
|
||||||
type SidebarItemProps = {
|
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
size?: 'small' | 'medium' | 'big';
|
|
||||||
selected?: boolean;
|
|
||||||
text: string;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SidebarItem = ({
|
|
||||||
color,
|
|
||||||
size = 'medium',
|
|
||||||
selected = false,
|
|
||||||
text,
|
|
||||||
onClick,
|
|
||||||
}: SidebarItemProps) => {
|
|
||||||
return (
|
|
||||||
<Wrapper color={color} size={size} selected={selected} onClick={onClick}>
|
|
||||||
{text}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SidebarItem;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import styled, { css } from 'styled-components';
|
|
||||||
|
|
||||||
type WrapperProps = {
|
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
size?: 'small' | 'medium' | 'big';
|
|
||||||
selected?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Wrapper = styled.button<WrapperProps>`
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: none;
|
|
||||||
font-weight: bold;
|
|
||||||
background: ${({ theme, color }) =>
|
|
||||||
color ? theme.colors[color].main : theme.colors.client.main};
|
|
||||||
color: ${({ theme }) => theme.colors.white.main};
|
|
||||||
display: grid;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
${({ selected, theme }) =>
|
|
||||||
selected &&
|
|
||||||
css`
|
|
||||||
border: 2px solid ${theme.colors.white.main};
|
|
||||||
`}
|
|
||||||
|
|
||||||
${({ size }) => {
|
|
||||||
switch (size) {
|
|
||||||
case 'small':
|
|
||||||
return css`
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
`;
|
|
||||||
case 'medium':
|
|
||||||
return css`
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
`;
|
|
||||||
case 'big':
|
|
||||||
return css`
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
`;
|
|
||||||
default:
|
|
||||||
return css`
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Spinner from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Spinner',
|
||||||
|
component: Spinner,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Spinner>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'client',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useNavigate, useLocation } from 'react-router';
|
|
||||||
import { useReactiveVar } from '@apollo/client';
|
|
||||||
import { roleVar } from '../../graphql/state';
|
|
||||||
import { Box, Button, Text } from '..';
|
|
||||||
import { Wrapper } from './styles';
|
|
||||||
import {
|
|
||||||
GetProjectThreadsQuery,
|
|
||||||
GetProjectThreadsQueryVariables,
|
|
||||||
Support,
|
|
||||||
} from '../../graphql/types.support';
|
|
||||||
import { GET_PROJECT_THREADS } from '../../graphql/chat.api.support';
|
|
||||||
import { Add, Empty } from '../../assets';
|
|
||||||
import { clientSupport } from '../..';
|
|
||||||
|
|
||||||
type SupportSideBarProps = {
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SupportSidebar = ({ onClose }: SupportSideBarProps) => {
|
|
||||||
const role = useReactiveVar(roleVar);
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [projectThreads, setProjectThreads] = useState<Array<Support>>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (/\/project/i.test(location.pathname)) {
|
|
||||||
const threads = await clientSupport.query<
|
|
||||||
GetProjectThreadsQuery,
|
|
||||||
GetProjectThreadsQueryVariables
|
|
||||||
>({
|
|
||||||
query: GET_PROJECT_THREADS,
|
|
||||||
variables: {
|
|
||||||
projectId: location.pathname.split('/')[2] as string,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setProjectThreads(threads?.data?.threads!);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper color={role || 'client'}>
|
|
||||||
<Box className='overlay' onClick={onClose}></Box>
|
|
||||||
<Box padding='25px 20px'>
|
|
||||||
<Box
|
|
||||||
display='flex'
|
|
||||||
flexDirection='row'
|
|
||||||
alignItems='center'
|
|
||||||
marginBottom='20px'
|
|
||||||
>
|
|
||||||
<Box flexGrow='1'>
|
|
||||||
<Text variant='title' weight='bold' color='white'>
|
|
||||||
Support
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
variant='secondary-action'
|
|
||||||
color={role || 'client'}
|
|
||||||
text='Add'
|
|
||||||
iconLeft={<Add />}
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
navigate(`/support/${location.pathname.split('/')[2]}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{projectThreads && projectThreads.length > 0 ? (
|
|
||||||
<Box
|
|
||||||
display='grid'
|
|
||||||
gridTemplateColumns='1fr'
|
|
||||||
rowGap='10px'
|
|
||||||
alignItems='center'
|
|
||||||
>
|
|
||||||
{projectThreads.map((thread) => (
|
|
||||||
<Box
|
|
||||||
key={thread.id}
|
|
||||||
padding='10px 15px'
|
|
||||||
background='white'
|
|
||||||
cursor='pointer'
|
|
||||||
borderRadius='10px'
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
navigate(
|
|
||||||
`/support/${location.pathname.split('/')[2]}/${thread.id}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text variant='body'>{thread.title}</Text>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box
|
|
||||||
width='100%'
|
|
||||||
height='100vh'
|
|
||||||
display='grid'
|
|
||||||
alignItems='center'
|
|
||||||
justifyContent='center'
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<Empty />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SupportSidebar;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
type WrapperProps = {
|
|
||||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Wrapper = styled.div<WrapperProps>`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 75px;
|
|
||||||
z-index: 100;
|
|
||||||
width: 500px;
|
|
||||||
height: 100vh;
|
|
||||||
background: ${({ theme, color }) =>
|
|
||||||
color ? theme.colors[color].main : theme.colors.client.main};
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 575px;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
fill: ${({ theme, color }) =>
|
|
||||||
color ? theme.colors[color].main : theme.colors.client.main};
|
|
||||||
}
|
|
||||||
|
|
||||||
.messaging-empty {
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Box, Text } from '..';
|
|
||||||
import { TemplateOutput } from '../../graphql/types';
|
|
||||||
import { theme } from '../../themes';
|
|
||||||
|
|
||||||
type TemplateCardProps = {
|
|
||||||
template: TemplateOutput;
|
|
||||||
selectable?: boolean;
|
|
||||||
selected?: boolean;
|
|
||||||
toggleSelect?: () => void;
|
|
||||||
color: 'client' | 'productOwner' | 'developer' | 'admin';
|
|
||||||
};
|
|
||||||
|
|
||||||
const TemplateCard = ({
|
|
||||||
template,
|
|
||||||
selectable = false,
|
|
||||||
selected = false,
|
|
||||||
toggleSelect = () => {},
|
|
||||||
color,
|
|
||||||
}: TemplateCardProps) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
padding='10px'
|
|
||||||
background='white'
|
|
||||||
boxShadow='1px 1px 10px rgba(50, 59, 105, 0.25)'
|
|
||||||
border={selected ? `2px solid ${theme.colors[color].main}` : undefined}
|
|
||||||
onClick={selectable ? toggleSelect : () => {}}
|
|
||||||
display='grid'
|
|
||||||
gridTemplateRows='auto'
|
|
||||||
alignItems='center'
|
|
||||||
rowGap='10px'
|
|
||||||
borderRadius='10px'
|
|
||||||
cursor='pointer'
|
|
||||||
>
|
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
|
||||||
<Box flexGrow='1'>
|
|
||||||
<Text variant='title' weight='bold'>
|
|
||||||
{template.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
|
||||||
<Box flexGrow='1'>
|
|
||||||
<Text variant='body'>{template.description}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TemplateCard;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import Text from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Text',
|
||||||
|
component: Text,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
variant: { options: ['display', 'headline', 'title', 'subheader', 'body', 'caption'] },
|
||||||
|
weight: { options: ['initial', 'normal', 'bold'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Text>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'developer',
|
||||||
|
children: 'Text',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
|
|
||||||
type TextProps = {
|
type TextProps = {
|
||||||
children?: React.ReactNode | JSX.Element | string;
|
children?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: 'display' | 'headline' | 'title' | 'subheader' | 'body' | 'caption';
|
variant?: 'display' | 'headline' | 'title' | 'subheader' | 'body' | 'caption';
|
||||||
color?:
|
color?:
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import TextArea from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'TextArea',
|
||||||
|
component: TextArea,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
color: { options: ['client', 'productOwner', 'developer', 'admin', 'error'] },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof TextArea>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Example: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'developer',
|
||||||
|
value: 'TextArea',
|
||||||
|
name: 'TextArea',
|
||||||
|
onChange: () => {}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Wrapper } from './styles';
|
import { Wrapper } from './styles';
|
||||||
import { Text } from '..';
|
|
||||||
|
import Text from '../Text';
|
||||||
|
|
||||||
type TextAreaProps = {
|
type TextAreaProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
Vendored
+1
@@ -1 +1,2 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), svgr()],
|
plugins: [react(), svgr()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
src: '/src',
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user