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
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
+6
-1
@@ -56,5 +56,10 @@
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^6.2.2",
|
||||
"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'],
|
||||
argTypes: {
|
||||
text: { control: 'text' },
|
||||
color: { control: 'color' },
|
||||
color: { options: ['client', 'productOwner', 'developer', 'admin'] },
|
||||
},
|
||||
} satisfies Meta<typeof Alert>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
export const Example: Story = {
|
||||
args: {
|
||||
text: 'Alert',
|
||||
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) => {
|
||||
return (
|
||||
<Wrapper color={color} size={size} className={className}>
|
||||
{text}
|
||||
{text[0]}
|
||||
</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';
|
||||
|
||||
export type BoxProps = {
|
||||
className?: string;
|
||||
children?: React.ReactNode | JSX.Element | string;
|
||||
children?: React.ReactNode | JSX.Element | JSX.Element[] | string;
|
||||
ref?: React.Ref<HTMLElement>;
|
||||
|
||||
onClick?: () => void;
|
||||
cursor?: 'pointer' | 'default';
|
||||
@@ -38,11 +39,11 @@ export type BoxProps = {
|
||||
|
||||
alignItems?: 'center' | 'flex-start' | 'flex-end' | 'stretch';
|
||||
justifyContent?:
|
||||
| 'center'
|
||||
| 'flex-start'
|
||||
| 'flex-end'
|
||||
| 'space-between'
|
||||
| 'space-around';
|
||||
| 'center'
|
||||
| 'flex-start'
|
||||
| 'flex-end'
|
||||
| 'space-between'
|
||||
| 'space-around';
|
||||
alignSelf?: 'center' | 'flex-start' | 'flex-end';
|
||||
justifySelf?: 'center' | 'flex-start' | 'flex-end';
|
||||
|
||||
@@ -86,14 +87,12 @@ export type BoxProps = {
|
||||
textDecoration?: string;
|
||||
};
|
||||
|
||||
const Box = React.forwardRef<HTMLDivElement, BoxProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Wrapper {...props} draggable='false' ref={ref}>
|
||||
{children}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
function Box({ children, ref, ...props }: BoxProps) {
|
||||
return (
|
||||
<Wrapper {...props} draggable='false' ref={ref}>
|
||||
{children}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
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';
|
||||
|
||||
type ButtonProps = {
|
||||
@@ -6,8 +7,8 @@ type ButtonProps = {
|
||||
size?: 'small' | 'big';
|
||||
variant?: 'primary-action' | 'secondary-action' | 'outlined' | 'text';
|
||||
type?: 'submit' | 'button' | 'reset';
|
||||
iconLeft?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
||||
iconRight?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
||||
iconLeft?: React.ReactNode;
|
||||
iconRight?: React.ReactNode;
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -4,8 +4,8 @@ type WrapperProps = {
|
||||
color: 'client' | 'productOwner' | 'developer' | 'admin' | 'error';
|
||||
size?: 'small' | 'big';
|
||||
variant?: 'primary-action' | 'secondary-action' | 'outlined' | 'text';
|
||||
iconLeft?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
||||
iconRight?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
||||
iconLeft?: React.ReactNode;
|
||||
iconRight?: React.ReactNode;
|
||||
load?: boolean;
|
||||
disabled?: 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 { CategoryOutput } from '../../graphql/types';
|
||||
import Box from '../Box';
|
||||
import Text from '../Text';
|
||||
|
||||
import { theme } from '../../themes';
|
||||
|
||||
type CategoryCardProps = {
|
||||
category: CategoryOutput;
|
||||
type CardProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
selectable?: boolean;
|
||||
selected?: boolean;
|
||||
toggleSelect?: () => void;
|
||||
color: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
};
|
||||
|
||||
const CategoryCard = ({
|
||||
category,
|
||||
const Card = ({
|
||||
title,
|
||||
description,
|
||||
selectable = false,
|
||||
selected = false,
|
||||
toggleSelect = () => {},
|
||||
color,
|
||||
}: CategoryCardProps) => {
|
||||
}: CardProps) => {
|
||||
return (
|
||||
<Box
|
||||
padding='10px'
|
||||
@@ -34,17 +37,17 @@ const CategoryCard = ({
|
||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='title' weight='bold'>
|
||||
{category.name}
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='row' alignItems='center'>
|
||||
<Box flexGrow='1'>
|
||||
<Text variant='body'>{category.description}</Text>
|
||||
<Text variant='body'>{description}</Text>
|
||||
</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 { Text } from '..';
|
||||
|
||||
import Text from '../Text';
|
||||
|
||||
import { Check } from '../../assets';
|
||||
|
||||
type CheckBoxProps = {
|
||||
className?: string;
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
label: string;
|
||||
name: string;
|
||||
checked: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const CheckBox = ({
|
||||
label,
|
||||
name,
|
||||
checked,
|
||||
onClick,
|
||||
...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 { Text } from '..';
|
||||
|
||||
import Text from '../Text';
|
||||
|
||||
type ChipProps = {
|
||||
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 { Wrapper } from './styles';
|
||||
import { Text } from '..';
|
||||
|
||||
import Text from '../Text';
|
||||
|
||||
type ContextMenuProps = {
|
||||
className?: string;
|
||||
@@ -10,7 +12,7 @@ type ContextMenuProps = {
|
||||
|
||||
const ContextMenu = ({ items, component, className }: ContextMenuProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const parentComponentRef = useRef<HTMLDivElement>();
|
||||
const parentComponentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
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('mouseleave', closeMenu);
|
||||
};
|
||||
}, []);
|
||||
}, [component]);
|
||||
|
||||
return (
|
||||
<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 = {
|
||||
color?: 'client' | 'productOwner' | 'developer' | 'admin';
|
||||
size?: 'small' | 'medium' | 'big';
|
||||
icon?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
||||
icon?: React.ReactNode;
|
||||
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;
|
||||
errorMessage?: string;
|
||||
name?: string;
|
||||
image: { name: string; src: string } | undefined;
|
||||
image?: { name: string; src: string } | undefined;
|
||||
deletable?: boolean;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => 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 { 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';
|
||||
|
||||
type LinkProps = {
|
||||
@@ -18,7 +18,7 @@ type LinkProps = {
|
||||
| string;
|
||||
selected?: boolean;
|
||||
className?: string;
|
||||
iconLeft?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
||||
iconLeft?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
target?: '_self' | '_blank';
|
||||
};
|
||||
@@ -35,10 +35,10 @@ const Link = ({
|
||||
return (
|
||||
<Wrapper {...props} selected={selected}>
|
||||
{href && !url ? (
|
||||
<RouterLink to={href} target={target}>
|
||||
<a href={href} target={target}>
|
||||
{iconLeft && <span className='icon left'>{iconLeft}</span>}
|
||||
{children}
|
||||
</RouterLink>
|
||||
</a>
|
||||
) : (
|
||||
<a href={href} target={target}>
|
||||
{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 { Wrapper } from './styles';
|
||||
import { Text } from '..';
|
||||
|
||||
import Text from '../Text';
|
||||
|
||||
type MenuProps = {
|
||||
className?: string;
|
||||
items: Array<{
|
||||
icon: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
|
||||
icon?: React.ReactNode;
|
||||
avoid?: boolean;
|
||||
label: string;
|
||||
action?: () => void;
|
||||
@@ -16,7 +17,7 @@ type MenuProps = {
|
||||
const Menu = ({ items, component, className }: MenuProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
const parentComponentRef = useRef<HTMLDivElement>();
|
||||
const parentComponentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const openMenu = () => setOpen(true);
|
||||
const closeMenu = () => setOpen(false);
|
||||
@@ -31,7 +32,7 @@ const Menu = ({ items, component, className }: MenuProps) => {
|
||||
parentComponentRef.current?.removeEventListener('mouseenter', openMenu);
|
||||
componentRef.current?.removeEventListener('mouseleave', closeMenu);
|
||||
};
|
||||
}, []);
|
||||
}, [component]);
|
||||
|
||||
return (
|
||||
<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 { Box, Button, Text } from '..';
|
||||
|
||||
import Box from '../Box';
|
||||
import Button from '../Button';
|
||||
import Text from '../Text';
|
||||
|
||||
import { Wrapper } from './styles';
|
||||
|
||||
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 { Search as SearchIcon } from '../../assets';
|
||||
|
||||
import SearchIcon from '../../assets/icons/search.svg?react';
|
||||
|
||||
type SearchProps = {
|
||||
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 { Text } from '..';
|
||||
|
||||
import Text from '../Text';
|
||||
|
||||
type SelectProps = {
|
||||
className?: string;
|
||||
@@ -15,9 +16,9 @@ type SelectProps = {
|
||||
| 'white';
|
||||
error?: boolean;
|
||||
errorMessage?: string;
|
||||
options: Array<{ value: any; label: string }>;
|
||||
options: Array<{ value: string | number; label: string }>;
|
||||
value: string;
|
||||
select?: any;
|
||||
selected?: string | number;
|
||||
name: string;
|
||||
label?: string;
|
||||
fullWidth?: boolean;
|
||||
@@ -30,7 +31,7 @@ const Select = ({
|
||||
label,
|
||||
name,
|
||||
value,
|
||||
select = null,
|
||||
selected = undefined,
|
||||
options,
|
||||
onChange,
|
||||
onBlur,
|
||||
@@ -55,7 +56,7 @@ const Select = ({
|
||||
<div className='select'>
|
||||
<div>
|
||||
<select
|
||||
value={select || value}
|
||||
value={selected || value}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
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';
|
||||
|
||||
type TextProps = {
|
||||
children?: React.ReactNode | JSX.Element | string;
|
||||
children?: string;
|
||||
className?: string;
|
||||
variant?: 'display' | 'headline' | 'title' | 'subheader' | 'body' | 'caption';
|
||||
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 { Text } from '..';
|
||||
|
||||
import Text from '../Text';
|
||||
|
||||
type TextAreaProps = {
|
||||
className?: string;
|
||||
|
||||
Vendored
+1
@@ -1 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), svgr()],
|
||||
resolve: {
|
||||
alias: {
|
||||
src: '/src',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user