Stories scaffolding

This commit is contained in:
Hazem Krimi
2025-03-18 17:12:38 +01:00
parent 13f88a09b3
commit dadf638631
55 changed files with 596 additions and 1189 deletions
-1
View File
@@ -17,7 +17,6 @@ codegen-*
# misc
.DS_Store
.env
.env.*
npm-debug.log*
yarn-debug.log*
+6 -1
View File
@@ -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"
}
}
+3 -2
View File
@@ -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'
+28
View File
@@ -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'
},
};
+1 -1
View File
@@ -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;
+27
View File
@@ -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!',
},
};
+15 -16
View File
@@ -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;
+28
View File
@@ -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'
},
};
+4 -3
View File
@@ -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;
+2 -2
View File
@@ -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;
+28
View File
@@ -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;
+29
View File
@@ -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!'),
},
};
+3 -3
View File
@@ -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
+29
View File
@@ -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'
},
};
+2 -1
View File
@@ -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',
},
};
+5 -3
View File
@@ -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
-72
View File
@@ -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!')
},
};
+1 -1
View File
@@ -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',
},
};
+1 -1
View File
@@ -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;
+26
View File
@@ -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: () => {}
},
};
+2 -1
View File
@@ -1,4 +1,5 @@
import { Text } from '..';
import Text from '../Text';
import { Upload } from '../../assets';
import { Wrapper } from './styles';
+29
View File
@@ -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'
},
};
+4 -4
View File
@@ -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>}
+27
View File
@@ -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',
},
};
+5 -4
View File
@@ -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
+31
View File
@@ -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: () => {},
},
};
+6 -1
View File
@@ -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 = {
-172
View File
@@ -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;
-47
View File
@@ -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;
}
}
`;
+28
View File
@@ -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: () => {},
},
};
+2 -1
View File
@@ -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;
-34
View File
@@ -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;
-119
View File
@@ -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};
}
`};
`;
+33
View File
@@ -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: () => { },
},
};
+6 -5
View File
@@ -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}
-286
View File
@@ -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;
-25
View File
@@ -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;
}
`;
-25
View File
@@ -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;
-53
View File
@@ -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;
`;
}
}}
`;
+25
View File
@@ -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',
},
};
-112
View File
@@ -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;
-34
View File
@@ -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;
}
`;
-50
View File
@@ -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;
+28
View File
@@ -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 -1
View File
@@ -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?:
+28
View File
@@ -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: () => {}
},
};
+2 -1
View File
@@ -1,5 +1,6 @@
import { Wrapper } from './styles';
import { Text } from '..';
import Text from '../Text';
type TextAreaProps = {
className?: string;
+1
View File
@@ -1 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
+6
View File
@@ -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',
},
},
})