Clear git cache

This commit is contained in:
Hazem Krimi
2023-05-28 16:44:06 +01:00
parent 2a71d7927c
commit a5b7dfd72e
178 changed files with 29055 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
import { Wrapper } from './styles';
type AlertProps = {
className?: string;
color:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error';
text: string;
};
const Alert = ({ text, ...props }: AlertProps) => {
return <Wrapper {...props}>{text}</Wrapper>;
};
export default Alert;
+72
View File
@@ -0,0 +1,72 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error';
};
export const Wrapper = styled.div<WrapperProps>`
width: 100%;
height: auto;
padding: 0.938rem;
border-radius: 10px;
${({ color, theme }) => {
switch (color) {
case 'client':
return css`
border: 1px solid ${theme.colors.client.main};
color: ${theme.colors.client.main};
background: ${theme.colors.client.light};
`;
case 'productOwner':
return css`
border: 1px solid ${theme.colors.productOwner.main};
color: ${theme.colors.productOwner.main};
background: ${theme.colors.productOwner.light};
`;
case 'developer':
return css`
border: 1px solid ${theme.colors.developer.main};
color: ${theme.colors.developer.main};
background: ${theme.colors.developer.light};
`;
case 'admin':
return css`
border: 1px solid ${theme.colors.admin.main};
color: ${theme.colors.admin.main};
background: ${theme.colors.admin.light};
`;
case 'success':
return css`
border: 1px solid ${theme.colors.success.main};
color: ${theme.colors.success.main};
background: ${theme.colors.success.light};
`;
case 'warning':
return css`
border: 1px solid ${theme.colors.warning.main};
color: ${theme.colors.warning.main};
background: ${theme.colors.warning.light};
`;
case 'error':
return css`
border: 1px solid ${theme.colors.error.main};
color: ${theme.colors.error.main};
background: ${theme.colors.error.light};
`;
default:
return css`
border: 1px solid ${theme.colors.client.main};
color: ${theme.colors.client.main};
background: ${theme.colors.client.light};
`;
}
}}
`;
+18
View File
@@ -0,0 +1,18 @@
import { Wrapper } from './styles';
type AvatarProps = {
className?: string;
color?: 'client' | 'productOwner' | 'developer' | 'admin' | string;
size?: 'small' | 'big';
text: string;
};
const Avatar = ({ color, size = 'small', text, className }: AvatarProps) => {
return (
<Wrapper color={color} size={size} className={className}>
{text}
</Wrapper>
);
};
export default Avatar;
+41
View File
@@ -0,0 +1,41 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?: 'client' | 'productOwner' | 'developer' | 'admin' | string;
size?: 'small' | 'big';
};
export const Wrapper = styled.div<WrapperProps>`
user-select: none;
border-radius: 50%;
background: ${({ theme, color }) =>
color ? theme.colors[color].main : theme.colors.client.main};
color: ${({ theme }) => theme.colors.white.main};
display: inline-grid;
justify-content: center;
align-items: center;
font-weight: bold;
${({ size }) => {
switch (size) {
case 'small':
return css`
width: 25px;
height: 25px;
font-size: 12px;
`;
case 'big':
return css`
width: 50px;
height: 50px;
font-size: 24px;
`;
default:
return css`
width: 25px;
height: 25px;
font-size: 12px;
`;
}
}}
`;
@@ -0,0 +1,36 @@
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;
+99
View File
@@ -0,0 +1,99 @@
import React from 'react';
import { Wrapper } from './styles';
export type BoxProps = {
className?: string;
children?: React.ReactNode | JSX.Element | string;
onClick?: () => void;
cursor?: 'pointer' | 'default';
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
zIndex?: string;
top?: string;
right?: string;
bottom?: string;
left?: string;
transformOrigin?: string;
transform?: string;
display?: 'none' | 'block' | 'inline' | 'inline-block' | 'flex' | 'grid';
flex?: string;
flexDirection?: 'row' | 'column';
flexWrap?: 'wrap' | 'unwrap';
flexGrow?: string;
flexShrink?: string;
order?: string;
gridRow?: string;
gridColumn?: string;
gridTemplate?: string;
gridTemplateRows?: string;
gridTemplateColumns?: string;
gap?: string;
rowGap?: string;
columnGap?: string;
alignItems?: 'center' | 'flex-start' | 'flex-end' | 'stretch';
justifyContent?:
| 'center'
| 'flex-start'
| 'flex-end'
| 'space-between'
| 'space-around';
alignSelf?: 'center' | 'flex-start' | 'flex-end';
justifySelf?: 'center' | 'flex-start' | 'flex-end';
boxSizing?: 'content-box' | 'border-box';
width?: string;
minWidth?: string;
maxWidth?: string;
height?: string;
minHeight?: string;
maxHeight?: string;
margin?: string;
marginTop?: string;
marginRight?: string;
marginBottom?: string;
marginLeft?: string;
padding?: string;
paddingTop?: string;
paddingRight?: string;
paddingBottom?: string;
paddingLeft?: string;
overflow?: 'visible' | 'hidden' | 'scroll';
overflowX?: 'visible' | 'hidden' | 'scroll';
overflowY?: 'visible' | 'hidden' | 'scroll';
border?: string;
borderRadius?: string;
boxShadow?: string;
color?: string;
background?: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: string;
fontStyle?: string;
lineHeight?: string;
letterSpacing?: string;
textAlign?: 'center' | 'left' | 'right';
textDecoration?: string;
};
const Box = React.forwardRef<HTMLDivElement, BoxProps>(
({ children, ...props }, ref) => {
return (
<Wrapper {...props} draggable='false' ref={ref}>
{children}
</Wrapper>
);
}
);
export default Box;
+83
View File
@@ -0,0 +1,83 @@
import styled from 'styled-components';
import { BoxProps } from '.';
export const Wrapper = styled.div<BoxProps>`
${({ cursor }) => cursor && `cursor: ${cursor}`};
${({ position }) => position && `position: ${position}`};
${({ zIndex }) => zIndex && `z-index: ${zIndex}`};
${({ top }) => top && `top: ${top}`};
${({ right }) => right && `right: ${right}`};
${({ bottom }) => bottom && `bottom: ${bottom}`};
${({ left }) => left && `left: ${left}`};
${({ transformOrigin }) =>
transformOrigin && `transform-origin: ${transformOrigin}`};
${({ transform }) => transform && `transform: ${transform}`};
${({ display }) => display && `display: ${display}`};
${({ flex }) => flex && `flex: ${flex}`};
${({ flexDirection }) => flexDirection && `flex-direction: ${flexDirection}`};
${({ flexWrap }) => flexWrap && `flex-wrap: ${flexWrap}`};
${({ flexGrow }) => flexGrow && `flex-grow: ${flexGrow}`};
${({ flexShrink }) => flexShrink && `flex-shrink: ${flexShrink}`};
${({ order }) => order && `order: ${order}`};
${({ gridRow }) => gridRow && `grid-row: ${gridRow}`};
${({ gridColumn }) => gridColumn && `grid-column: ${gridColumn}`};
${({ gridTemplate }) => gridTemplate && `grid-template: ${gridTemplate}`};
${({ gridTemplateRows }) =>
gridTemplateRows && `grid-template-rows: ${gridTemplateRows}`};
${({ gridTemplateColumns }) =>
gridTemplateColumns && `grid-template-columns: ${gridTemplateColumns}`};
${({ gap }) => gap && `gap: ${gap}`};
${({ rowGap }) => rowGap && `row-gap: ${rowGap}`};
${({ columnGap }) => columnGap && `column-gap: ${columnGap}`};
${({ alignItems }) => alignItems && `align-items: ${alignItems}`};
${({ justifyContent }) =>
justifyContent && `justify-content: ${justifyContent}`};
${({ alignSelf }) => alignSelf && `align-self: ${alignSelf}`};
${({ justifySelf }) => justifySelf && `justify-self: ${justifySelf}`};
${({ boxSizing }) => boxSizing && `box-sizing: ${boxSizing}`};
${({ width }) => width && `width: ${width}`};
${({ minWidth }) => minWidth && `min-width: ${minWidth}`};
${({ maxWidth }) => maxWidth && `max-width: ${maxWidth}`};
${({ height }) => height && `height: ${height}`};
${({ minHeight }) => minHeight && `min-height: ${minHeight}`};
${({ maxHeight }) => maxHeight && `max-height: ${maxHeight}`};
${({ margin }) => margin && `margin: ${margin}`};
${({ marginTop }) => marginTop && `margin-top: ${marginTop}`};
${({ marginRight }) => marginRight && `margin-right: ${marginRight}`};
${({ marginBottom }) => marginBottom && `margin-bottom: ${marginBottom}`};
${({ marginLeft }) => marginLeft && `margin-left: ${marginLeft}`};
${({ padding }) => padding && `padding: ${padding}`};
${({ paddingTop }) => paddingTop && `padding-top: ${paddingTop}`};
${({ paddingRight }) => paddingRight && `padding-right: ${paddingRight}`};
${({ paddingBottom }) => paddingBottom && `padding-bottom: ${paddingBottom}`};
${({ paddingLeft }) => paddingLeft && `padding-left: ${paddingLeft}`};
${({ overflow }) => overflow && `overflow: ${overflow}`};
${({ overflowX }) => overflowX && `overflow-x: ${overflowX}`};
${({ overflowY }) => overflowY && `overflow-y: ${overflowY}`};
${({ border }) => border && `border: ${border}`};
${({ borderRadius }) => borderRadius && `border-radius: ${borderRadius}`};
${({ boxShadow }) => boxShadow && `box-shadow: ${boxShadow}`};
${({ color }) => color && `color: ${color}`};
${({ background }) => background && `background: ${background}`};
${({ fontFamily }) => fontFamily && `font-family: ${fontFamily}`};
${({ fontSize }) => fontSize && `font-size: ${fontSize}`};
${({ fontWeight }) => fontWeight && `font-weight: ${fontWeight}`};
${({ fontStyle }) => fontStyle && `font-style: ${fontStyle}`};
${({ lineHeight }) => lineHeight && `line-height: ${lineHeight}`};
${({ letterSpacing }) => letterSpacing && `letter-spacing: ${letterSpacing}`};
${({ textAlign }) => textAlign && `text-align: ${textAlign}`};
${({ textDecoration }) =>
textDecoration && `text-decoration: ${textDecoration}`};
`;
+56
View File
@@ -0,0 +1,56 @@
import { Spinner } from '..';
import { Wrapper } from './styles';
type ButtonProps = {
color: 'client' | 'productOwner' | 'developer' | 'admin' | 'error';
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>>;
fullWidth?: boolean;
loading?: boolean;
disabled?: boolean;
text: string;
onClick?: () => void;
};
const Button = ({
color,
size = 'small',
variant = 'text',
type = 'button',
iconLeft,
iconRight,
fullWidth = false,
loading = false,
disabled = false,
text,
onClick,
}: ButtonProps) => {
return (
<Wrapper
color={color}
size={size}
variant={variant}
type={type}
iconLeft={iconLeft || undefined}
iconRight={iconRight || undefined}
fullWidth={fullWidth}
load={loading}
disabled={disabled}
onClick={onClick}
>
{iconLeft && <span className='icon left'>{iconLeft}</span>}
{text}
{iconRight && !loading && <span className='icon right'>{iconRight}</span>}
{loading && (
<span>
<Spinner color='white' />
</span>
)}
</Wrapper>
);
};
export default Button;
+231
View File
@@ -0,0 +1,231 @@
import styled, { css } from 'styled-components';
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>>;
load?: boolean;
disabled?: boolean;
fullWidth?: boolean;
};
export const Wrapper = styled.button<WrapperProps>`
cursor: pointer;
outline: none;
border: none;
border-radius: 6px;
background: none;
font-weight: bold;
.icon svg {
display: flex;
align-items: center;
}
${({ iconLeft, iconRight, load }) => {
if (iconLeft || iconRight || load)
return css`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
`;
return '';
}}
.icon {
display: inline-flex;
align-items: center;
}
.icon.left {
margin-right: 0.5rem;
}
.icon.right {
margin-left: 0.5rem;
}
.lds-dual-ring {
display: inline !important;
border-width: 2px !important;
}
${({ size }) => {
switch (size) {
case 'small':
return css`
padding: 0.625rem 1.875rem;
font-size: 1rem;
.icon svg {
width: 1rem;
height: 1rem;
}
.lds-dual-ring {
width: 1rem !important;
height: 1rem !important;
&:after {
width: 0.5rem !important;
height: 0.5rem !important;
}
}
`;
case 'big':
return css`
padding: 0.625rem 1.875rem;
font-size: 1.25rem;
.icon svg {
width: 1.25rem;
height: 1.25rem;
}
.lds-dual-ring {
width: 1.25rem !important;
height: 1.25rem !important;
&:after {
width: 0.75rem !important;
height: 0.75rem !important;
}
}
`;
default:
return css`
padding: 0.625rem 1.875rem;
font-size: 1rem;
.icon svg {
width: 1rem;
height: 1rem;
}
.lds-dual-ring {
width: 1rem !important;
height: 1rem !important;
&:after {
width: 0.5rem !important;
height: 0.5rem !important;
}
}
`;
}
}}
${({ fullWidth }) =>
fullWidth &&
css`
width: 100%;
font-size: 1.25rem;
.icon svg {
width: 1.25rem;
height: 1.25rem;
}
.lds-dual-ring {
width: 1.25rem;
height: 1.25rem;
}
`};
${({ variant, color, theme, disabled }) => {
switch (variant) {
case 'primary-action':
return css`
background: ${!disabled
? theme.colors[color].main
: theme.colors[color].light};
color: ${theme.colors.white.main};
.icon svg path {
stroke: ${theme.colors.white.main};
}
&:hover {
background: ${!disabled
? theme.colors[color].dark
: theme.colors[color].light};
}
`;
case 'secondary-action':
return css`
background: ${theme.colors[color].light};
color: ${!disabled ? '#262628' : theme.colors[color].light};
.icon svg path {
stroke: ${!disabled ? '#262628' : theme.colors[color].light};
}
`;
case 'outlined':
return css`
background: none;
color: ${!disabled
? theme.colors[color].main
: theme.colors[color].light};
border: 2px solid
${!disabled ? theme.colors[color].main : theme.colors[color].light};
.icon svg path {
stroke: ${!disabled
? theme.colors[color].main
: theme.colors[color].light};
}
&:hover {
background: ${!disabled ? theme.colors[color].main : 'none'};
color: ${!disabled
? theme.colors.white.main
: theme.colors[color].light};
.icon svg path {
stroke: ${!disabled
? theme.colors.white.main
: theme.colors[color].light};
}
}
`;
case 'text':
return css`
background: none;
color: ${!disabled
? theme.colors[color].main
: theme.colors[color].light};
padding: 0;
.icon svg path {
stroke: ${!disabled
? theme.colors[color].main
: theme.colors[color].light};
}
`;
default:
return css`
background: none;
color: ${!disabled
? theme.colors[color].main
: theme.colors[color].light};
padding: 0;
.icon svg path {
stroke: ${!disabled
? theme.colors[color].main
: theme.colors[color].light};
}
`;
}
}}
${({ disabled }) =>
disabled &&
css`
cursor: default;
`}
`;
+50
View File
@@ -0,0 +1,50 @@
import { Box, Text } from '..';
import { CategoryOutput } from '../../graphql/types';
import { theme } from '../../themes';
type CategoryCardProps = {
category: CategoryOutput;
selectable?: boolean;
selected?: boolean;
toggleSelect?: () => void;
color: 'client' | 'productOwner' | 'developer' | 'admin';
};
const CategoryCard = ({
category,
selectable = false,
selected = false,
toggleSelect = () => {},
color,
}: CategoryCardProps) => {
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'>
{category.name}
</Text>
</Box>
</Box>
<Box display='flex' flexDirection='row' alignItems='center'>
<Box flexGrow='1'>
<Text variant='body'>{category.description}</Text>
</Box>
</Box>
</Box>
);
};
export default CategoryCard;
+31
View File
@@ -0,0 +1,31 @@
import { Wrapper } from './styles';
import { Text } from '..';
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
}: CheckBoxProps) => {
return (
<Wrapper checked={checked} {...props} onClick={onClick}>
<div className='checkbox'>
<Check />
</div>
<Text variant='body'>{label}</Text>
</Wrapper>
);
};
export default CheckBox;
+74
View File
@@ -0,0 +1,74 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?: 'client' | 'productOwner' | 'developer' | 'admin';
checked: boolean;
};
export const Wrapper = styled.div<WrapperProps>`
display: inline-flex;
flex-direction: row;
user-select: none;
.checkbox {
cursor: pointer;
border-radius: 3px;
margin-right: 10px;
width: 17px;
height: 17px;
display: flex;
align-items: center;
justify-content: center;
svg {
visibility: ${({ checked }) => (checked ? 'visible' : 'hidden')};
}
}
${({ checked, color, theme }) => {
if (!checked)
return css`
.checkbox {
border: 2px solid ${theme.colors.black.main};
background: ${theme.colors.white.main};
}
`;
switch (color) {
case 'client':
return css`
.checkbox {
border: none;
background: ${theme.colors.client.main};
}
`;
case 'productOwner':
return css`
.checkbox {
border: none;
background: ${theme.colors.productOwner.main};
}
`;
case 'developer':
return css`
.checkbox {
border: none;
background: ${theme.colors.developer.main};
}
`;
case 'admin':
return css`
.checkbox {
border: none;
background: ${theme.colors.admin.main};
}
`;
default:
return css`
.checkbox {
border: none;
background: ${theme.colors.client.main};
}
`;
}
}}
`;
+27
View File
@@ -0,0 +1,27 @@
import { Wrapper } from './styles';
import { Text } from '..';
type ChipProps = {
variant?: 'outlined' | 'filled';
color:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error';
text: string;
};
const Chip = ({ variant = 'outlined', color, text }: ChipProps) => {
return (
<Wrapper variant={variant} color={color}>
<Text variant='caption' weight='bold'>
{text}
</Text>
</Wrapper>
);
};
export default Chip;
+32
View File
@@ -0,0 +1,32 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error';
variant: 'outlined' | 'filled';
};
export const Wrapper = styled.div<WrapperProps>`
padding: 5px 15px;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
${({ variant, color, theme }) =>
variant === 'outlined'
? css`
border: 2px solid ${theme.colors[color].main};
color: ${theme.colors[color].main};
`
: css`
background: ${theme.colors[color].main};
color: ${theme.colors.white.main};
`}
`;
+63
View File
@@ -0,0 +1,63 @@
import { useEffect, useRef, useState } from 'react';
import { Wrapper } from './styles';
import { Text } from '..';
type ContextMenuProps = {
className?: string;
items: Array<{ label: string; action?: () => void }>;
component: string;
};
const ContextMenu = ({ items, component, className }: ContextMenuProps) => {
const [open, setOpen] = useState(false);
const parentComponentRef = useRef<HTMLDivElement>();
useEffect(() => {
parentComponentRef.current = document.querySelector(`#${component}`) as HTMLDivElement;
const openMenu = () => setOpen(true);
const closeMenu = () => setOpen(false);
parentComponentRef.current?.addEventListener(
'mouseenter',
openMenu
);
parentComponentRef.current?.addEventListener(
'mouseleave',
closeMenu
);
return () => {
parentComponentRef.current?.removeEventListener('mouseenter', openMenu);
parentComponentRef.current?.removeEventListener('mouseleave', closeMenu);
};
}, []);
return (
<Wrapper
className={className}
top={(parentComponentRef.current as HTMLDivElement)?.getBoundingClientRect().top + 30}
left={(parentComponentRef.current as HTMLDivElement)?.getBoundingClientRect().left + 10}
>
{open && (
<ul>
{items.map(({ label, action }) => (
<li
onClick={() => {
if (action) {
setOpen(false);
action();
}
}}
key={label}
>
<Text variant='caption'>{label}</Text>
</li>
))}
</ul>
)}
</Wrapper>
);
};
export default ContextMenu;
+25
View File
@@ -0,0 +1,25 @@
import styled from 'styled-components';
type WrapperProps = {
top: number;
left: number;
};
export const Wrapper = styled.div<WrapperProps>`
ul {
position: fixed;
top: ${({ top }) => top}px;
left: ${({ left }) => left}px;
background: #1f1b1b;
display: grid;
grid-template-columns: auto;
row-gap: 0.5rem;
color: ${({ theme }) => theme.colors.white.main};
border-radius: 3px;
padding: 5px 20px 5px 10px;
li {
cursor: pointer;
}
}
`;
+72
View File
@@ -0,0 +1,72 @@
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;
@@ -0,0 +1,61 @@
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;
+23
View File
@@ -0,0 +1,23 @@
import { Wrapper } from './styles';
type IconButtonProps = {
color?: 'client' | 'productOwner' | 'developer' | 'admin';
size?: 'small' | 'medium' | 'big';
icon?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
onClick: () => void;
};
const IconButton = ({
color,
size = 'medium',
icon,
onClick,
}: IconButtonProps) => {
return (
<Wrapper color={color} size={size} onClick={onClick}>
{icon}
</Wrapper>
);
};
export default IconButton;
+75
View File
@@ -0,0 +1,75 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?: 'client' | 'productOwner' | 'developer' | 'admin';
size?: 'small' | 'medium' | 'big';
icon?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
};
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};
display: grid;
justify-content: center;
align-items: center;
svg {
display: flex;
align-items: center;
path {
stroke: ${({ theme }) => theme.colors.white.main};
}
}
${({ size }) => {
switch (size) {
case 'small':
return css`
width: 25px;
height: 25px;
svg {
width: 12.5px;
height: 12.5px;
}
`;
case 'medium':
return css`
width: 35px;
height: 35px;
svg {
width: 17.5px;
height: 17.5px;
}
`;
case 'big':
return css`
width: 50px;
height: 50px;
svg {
width: 24.5px;
height: 24.5px;
}
`;
default:
return css`
width: 25px;
height: 25px;
svg {
width: 12.5px;
height: 12.5px;
}
`;
}
}}
`;
+53
View File
@@ -0,0 +1,53 @@
import { Wrapper } from './styles';
import { Upload, Close } from '../../assets';
type ImagePreviewProps = {
className?: string;
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white';
error?: boolean;
errorMessage?: string;
name?: string;
image: { name: string; src: string } | undefined;
deletable?: boolean;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onDelete?: () => void;
};
const ImagePreview = ({
name,
image,
deletable = false,
onChange,
onDelete,
...props
}: ImagePreviewProps) => {
return (
<Wrapper image={image} deletable={deletable} {...props}>
{image ? (
<div className='preview'>
{deletable && (
<div className='close'>
<Close onClick={onDelete} />
</div>
)}
</div>
) : (
<div className='upload'>
<input type='file' name={name} onChange={onChange} />
<Upload />
</div>
)}
</Wrapper>
);
};
export default ImagePreview;
+99
View File
@@ -0,0 +1,99 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white';
error?: boolean;
deletable?: boolean;
image: { name: string; src: string } | undefined;
};
export const Wrapper = styled.div<WrapperProps>`
.preview {
width: 175px;
height: 325px;
background: url(${({ image }) => image?.src});
background-repeat: no-repeat;
background-size: contain;
background-position: center;
padding: 150px 30px;
position: relative;
&:hover {
${({ deletable, color, theme }) =>
deletable &&
css`
border: 2px solid ${theme.colors[color || 'client'].main};
`}
.close {
display: block;
}
}
.close {
background: ${({ color, theme }) => theme.colors[color || 'client'].main};
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: -11.5px;
right: -11.5px;
padding: 5px;
cursor: pointer;
display: none;
svg {
width: 15px;
height: 15px;
stroke: ${({ theme }) => theme.colors.white.main};
display: flex;
align-items: center;
justify-content: center;
}
}
}
.upload {
padding: 150px 30px;
position: relative;
border: 2px solid
${({ color, theme }) => theme.colors[color || 'client'].main};
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
input {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
&::-webkit-file-upload-button {
display: none;
}
}
svg {
width: 25px;
height: 25px;
path {
stroke: ${({ color, theme }) => theme.colors[color || 'client'].main};
}
}
}
`;
+80
View File
@@ -0,0 +1,80 @@
import { Text } from '..';
import { Upload } from '../../assets';
import { Wrapper } from './styles';
type InputProps = {
className?: string;
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white';
error?: boolean;
errorMessage?: string;
value?: string | number;
label?: string;
name?: string;
type?: 'text' | 'email' | 'tel' | 'password' | 'file' | 'number';
file?: boolean;
placeholder?: string;
fullWidth?: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
};
const Input = ({
type = 'text',
file = false,
color = 'client',
label,
name,
placeholder,
value,
onChange,
onBlur,
error,
errorMessage,
...props
}: InputProps) => {
return (
<Wrapper label={label} error={error} type={type} color={color} {...props}>
<div className='info'>
{label && (
<Text variant='body' weight='bold' className='label'>
{label}
</Text>
)}
{error && errorMessage && (
<Text variant='body' color='error' className='error-message'>
{errorMessage}
</Text>
)}
</div>
<div className='input'>
<div>
{type === 'file' && (
<span className='icon left'>
<Upload />
</span>
)}
<input
type={type}
value={value}
onChange={onChange}
onBlur={onBlur}
name={name}
accept={type === 'file' && !file ? 'image/*' : undefined}
placeholder={placeholder}
/>
</div>
</div>
</Wrapper>
);
};
export default Input;
+297
View File
@@ -0,0 +1,297 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'gray'
| 'white';
error?: boolean;
errorMessage?: string;
type?: 'text' | 'email' | 'tel' | 'password' | 'file' | 'number';
label?: string;
fullWidth?: boolean;
};
export const Wrapper = styled.div<WrapperProps>`
.input {
width: inherit;
height: inherit;
border-radius: 5px;
padding: 2px;
color: ${({ theme }) => theme.colors.black.main};
div {
background: ${({ theme }) => theme.colors.white.main};
padding: 1rem;
border-radius: 5px;
}
}
.info {
margin-bottom: 5px;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
p {
background: ${({ theme }) => theme.colors.gray.dark};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.label {
justify-self: flex-start;
}
.error-message {
justify-self: flex-end;
}
}
input {
width: 100%;
background: none;
border: none;
color: ${({ theme }) => theme.colors.black.main};
}
input[type='file'] {
cursor: pointer;
&::-webkit-file-upload-button {
display: none;
}
}
${({ type }) => {
if (type === 'file')
return css`
.input div {
display: flex;
flex-direction: row;
align-items: center;
}
`;
return '';
}}
.icon {
${({ type }) => type === 'file' && `cursor: pointer`};
display: inline-flex;
align-items: center;
}
.icon.left {
margin-right: 0.5rem;
}
${({ color, theme }) => {
switch (color) {
case 'client':
return css`
.input {
background: ${theme.colors.client.light};
&:focus-within {
background: ${theme.colors.client.main};
}
}
input[type='file'] {
color: ${theme.colors.client.main};
}
.icon svg path {
stroke: ${theme.colors.client.main};
}
`;
case 'productOwner':
return css`
.input {
background: ${theme.colors.productOwner.light};
&:focus-within {
background: ${theme.colors.productOwner.main};
}
}
input[type='file'] {
color: ${theme.colors.productOwner.main};
}
.icon svg path {
stroke: ${theme.colors.productOwner.main};
}
`;
case 'developer':
return css`
.input {
background: ${theme.colors.developer.light};
&:focus-within {
background: ${theme.colors.developer.main};
}
}
input[type='file'] {
color: ${theme.colors.developer.main};
}
.icon svg path {
stroke: ${theme.colors.developer.main};
}
`;
case 'admin':
return css`
.input {
background: ${theme.colors.admin.light};
&:focus-within {
background: ${theme.colors.admin.main};
}
}
input[type='file'] {
color: ${theme.colors.admin.main};
}
.icon svg path {
stroke: ${theme.colors.admin.main};
}
`;
case 'success':
return css`
.input {
background: ${theme.colors.success.main};
}
input[type='file'] {
color: ${theme.colors.success.main};
}
.icon svg path {
stroke: ${theme.colors.success.main};
}
`;
case 'warning':
return css`
.input {
background: ${theme.colors.warning.main};
}
input[type='file'] {
color: ${theme.colors.warning.main};
}
.icon svg path {
stroke: ${theme.colors.warning.main};
}
`;
case 'error':
return css`
.input {
background: ${theme.colors.error.main};
}
input[type='file'] {
color: ${theme.colors.error.main};
}
.icon svg path {
stroke: ${theme.colors.error.main};
}
`;
case 'black':
return css`
.input {
background: ${theme.colors.black.main};
}
input[type='file'] {
color: ${theme.colors.black.main};
}
.icon svg path {
stroke: ${theme.colors.black.main};
}
`;
case 'white':
return css`
.input {
background: ${theme.colors.white.main};
}
input[type='file'] {
color: ${theme.colors.white.main};
}
.icon svg path {
stroke: ${theme.colors.white.main};
}
`;
default:
return css`
.input {
background: ${theme.colors.client.light};
&:focus-within {
background: ${theme.colors.client.main};
}
}
input[type='file'] {
color: ${theme.colors.client.main};
}
.icon svg path {
stroke: ${theme.colors.client.main};
}
`;
}
}}
${({ error, theme }) =>
error &&
css`
.info p {
background: ${theme.colors.error.main};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.input {
background: ${theme.colors.error.main};
&:focus-within {
background: ${theme.colors.error.main};
}
}
input[type='file'] {
color: ${theme.colors.error.main};
}
.icon svg path {
stroke: ${theme.colors.error.main};
}
`}
${({ fullWidth }) =>
fullWidth &&
css`
width: 100%;
font-size: 1.25rem;
.icon svg {
width: 1.25rem;
height: 1.25rem;
}
`};
`;
+52
View File
@@ -0,0 +1,52 @@
import { Link as RouterLink } from 'react-router-dom';
import { Wrapper } from './styles';
type LinkProps = {
href?: string;
url?: boolean;
children?: React.ReactNode | JSX.Element | string;
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white'
| string;
selected?: boolean;
className?: string;
iconLeft?: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
onClick?: () => void;
target?: '_self' | '_blank';
};
const Link = ({
href,
url = false,
children,
iconLeft,
selected = false,
target = '_self',
...props
}: LinkProps) => {
return (
<Wrapper {...props} selected={selected}>
{href && !url ? (
<RouterLink to={href} target={target}>
{iconLeft && <span className='icon left'>{iconLeft}</span>}
{children}
</RouterLink>
) : (
<a href={href} target={target}>
{iconLeft && <span className='icon left'>{iconLeft}</span>}
{children}
</a>
)}
</Wrapper>
);
};
export default Link;
+230
View File
@@ -0,0 +1,230 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white'
| string;
selected: boolean;
iconLeft?: React.SVGProps<SVGSVGElement>;
};
export const Wrapper = styled.div<WrapperProps>`
display: inline;
a {
text-decoration: ${({ selected }) => (selected ? 'underline' : 'none')};
&:hover {
text-decoration: underline;
}
}
${({ color, theme }) => {
if (!color)
return css`
color: #3e66fb;
a {
color: #3e66fb;
}
.icon svg path {
stroke: #3e66fb;
}
a:visited {
color: #3e66fb;
}
`;
switch (color) {
case 'client':
return css`
color: ${theme.colors.client.main};
a {
color: ${theme.colors.client.main};
}
.icon svg path {
stroke: ${theme.colors.client.main};
}
a:visited {
color: ${theme.colors.client.main};
}
`;
case 'productOwner':
return css`
color: ${theme.colors.productOwner.main};
a {
color: ${theme.colors.productOwner.main};
}
.icon svg path {
stroke: ${theme.colors.productOwner.main};
}
a:visited {
color: ${theme.colors.productOwner.main};
}
`;
case 'developer':
return css`
color: ${theme.colors.developer.main};
a {
color: ${theme.colors.developer.main};
}
.icon svg path {
stroke: ${theme.colors.developer.main};
}
a:visited {
color: ${theme.colors.developer.main};
}
`;
case 'admin':
return css`
color: ${theme.colors.admin.main};
a {
color: ${theme.colors.admin.main};
}
.icon svg path {
stroke: ${theme.colors.admin.main};
}
a:visited {
color: ${theme.colors.admin.main};
}
`;
case 'success':
return css`
color: ${theme.colors.success.main};
a {
color: ${theme.colors.success.main};
}
.icon svg path {
stroke: ${theme.colors.success.main};
}
a:visited {
color: ${theme.colors.success.main};
}
`;
case 'warning':
return css`
color: ${theme.colors.warning.main};
a {
color: ${theme.colors.warning.main};
}
.icon svg path {
stroke: ${theme.colors.warning.main};
}
a:visited {
color: ${theme.colors.warning.main};
}
`;
case 'error':
return css`
color: ${theme.colors.error.main};
a {
color: ${theme.colors.error.main};
}
.icon svg path {
stroke: ${theme.colors.error.main};
}
a:visited {
color: ${theme.colors.error.main};
}
`;
case 'black':
return css`
color: ${theme.colors.black.main};
a {
color: ${theme.colors.black.main};
}
.icon svg path {
stroke: ${theme.colors.black.main};
}
a:visited {
color: ${theme.colors.black.main};
}
`;
case 'white':
return css`
color: ${theme.colors.white.main};
a {
color: ${theme.colors.white.main};
}
.icon svg path {
stroke: ${theme.colors.white.main};
}
a:visited {
color: ${theme.colors.white.main};
}
`;
default:
return css`
color: ${color};
a {
color: ${color};
}
.icon svg path {
stroke: ${color};
}
a:visited {
color: ${color};
}
`;
}
}}
${({ iconLeft }) => {
if (iconLeft)
return css`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
`;
return '';
}}
.icon {
display: inline-flex;
align-items: center;
}
.icon.left {
margin-right: 5px;
}
`;
+69
View File
@@ -0,0 +1,69 @@
import { useEffect, useRef, useState } from 'react';
import { Wrapper } from './styles';
import { Text } from '..';
type MenuProps = {
className?: string;
items: Array<{
icon: React.FunctionComponentElement<React.SVGProps<SVGSVGElement>>;
avoid?: boolean;
label: string;
action?: () => void;
}>;
component: string;
};
const Menu = ({ items, component, className }: MenuProps) => {
const [open, setOpen] = useState(false);
const componentRef = useRef<HTMLDivElement>(null);
const parentComponentRef = useRef<HTMLDivElement>();
const openMenu = () => setOpen(true);
const closeMenu = () => setOpen(false);
useEffect(() => {
parentComponentRef.current = document.querySelector(`#${component}`) as HTMLDivElement;
parentComponentRef.current?.addEventListener('mouseenter', openMenu);
componentRef.current?.addEventListener('mouseleave', closeMenu);
return () => {
parentComponentRef.current?.removeEventListener('mouseenter', openMenu);
componentRef.current?.removeEventListener('mouseleave', closeMenu);
};
}, []);
return (
<Wrapper
ref={componentRef}
className={className}
top={
(parentComponentRef.current as HTMLDivElement)?.getBoundingClientRect().top + 30
}
left={
(parentComponentRef.current as HTMLDivElement)?.getBoundingClientRect()?.left
}
>
{open && (
<ul>
{items.map(({ icon, label, avoid, action }) => (
<li
onClick={() => {
if (action) {
setOpen(false);
action();
}
}}
key={label}
>
<span className={`icon ${avoid ? 'avoid' : ''}`}>{icon}</span>
<Text color={avoid ? 'error' : undefined}>{label}</Text>
</li>
))}
</ul>
)}
</Wrapper>
);
};
export default Menu;
+43
View File
@@ -0,0 +1,43 @@
import styled from 'styled-components';
type WrapperProps = {
top: number;
left: number;
};
export const Wrapper = styled.div<WrapperProps>`
ul {
position: fixed;
top: ${({ top }) => top}px;
left: ${({ left }) => left}px;
background: ${({ theme }) => theme.colors.white.main};
display: grid;
grid-template-columns: auto;
row-gap: 0.5rem;
border-radius: 3px;
padding: 15px 30px 15px 15px;
box-shadow: 1px 1px 15px 0px rgba(50, 59, 105, 0.25);
li {
cursor: pointer;
display: grid;
grid-template-columns: 24px 1fr;
column-gap: 10px;
justify-content: flex-start;
.icon {
display: flex;
align-items: center;
justify-content: center;
svg path {
stroke: ${({ theme }) => theme.colors.black.main};
}
&.avoid svg path {
stroke: ${({ theme }) => theme.colors.error.main};
}
}
}
}
`;
+54
View File
@@ -0,0 +1,54 @@
import { theme } from '../../themes';
import { Box, Button, Text } from '..';
import { Wrapper } from './styles';
type ModalProps = {
color: 'client' | 'productOwner' | 'developer' | 'admin';
title: string;
description: string;
children?: React.ReactNode | JSX.Element | string;
onConfirm: () => void;
onClose: () => void;
};
const Modal = ({
color,
title,
description,
children,
onConfirm,
onClose,
}: ModalProps) => {
return (
<Wrapper>
<Box
background={theme.colors.white.main}
borderRadius='10px'
padding='20px'
display='grid'
gridTemplateRows='auto'
alignItems='center'
rowGap='1rem'
>
<Text variant='headline' weight='bold' color={color}>
{title}
</Text>
<Text variant='body' color={theme.colors.black.main}>
{description}
</Text>
{children}
<Box
display='grid'
gridTemplateColumns='repeat(2, auto)'
justifyContent='flex-end'
columnGap='1rem'
>
<Button color={color} text='Confirm' onClick={onConfirm} />
<Button color={color} text='Cancel' onClick={onClose} />
</Box>
</Box>
</Wrapper>
);
};
export default Modal;
+14
View File
@@ -0,0 +1,14 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
position: fixed;
z-index: 200;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.15);
display: grid;
align-items: center;
justify-content: center;
`;
+172
View File
@@ -0,0 +1,172 @@
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', 'i').test(location.pathname)
? 'developer'
: 'black'
}
selected={new RegExp('template', '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', 'i').test(location.pathname)
? 'productOwner'
: 'black'
}
selected={new RegExp('project', 'i').test(location.pathname)}
>
Projects
</Link>
<Link
href='/template'
color={
new RegExp('template', 'i').test(location.pathname)
? 'productOwner'
: 'black'
}
selected={new RegExp('template', '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
@@ -0,0 +1,47 @@
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;
}
}
`;
+19
View File
@@ -0,0 +1,19 @@
import { useReactiveVar } from '@apollo/client';
import { Navigate } from 'react-router-dom';
import { tokenVar } from '../../graphql/state';
type Props = {
children: React.ReactNode;
};
const Protected = ({ children }: Props) => {
const token = useReactiveVar(tokenVar);
return (
<>
{token ? children : <Navigate to='/login' />}
</>
);
};
export default Protected;
+19
View File
@@ -0,0 +1,19 @@
import { useReactiveVar } from '@apollo/client';
import { Navigate } from 'react-router-dom';
import { tokenVar } from '../../graphql/state';
type Props = {
children: React.ReactNode;
};
const Public = ({ children }: Props) => {
const token = useReactiveVar(tokenVar);
return (
<>
{!token ? children : <Navigate to='/' />}
</>
);
};
export default Public;
+46
View File
@@ -0,0 +1,46 @@
import { Wrapper } from './styles';
import { Search as SearchIcon } from '../../assets';
type SearchProps = {
className?: string;
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white';
value: string;
fullWidth?: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
const Search = ({
color = 'client',
value,
onChange,
...props
}: SearchProps) => {
return (
<Wrapper color={color} {...props}>
<div className='search'>
<div>
<span className='icon left'>
<SearchIcon />
</span>
<input
type='text'
value={value}
onChange={onChange}
placeholder='Search'
/>
</div>
</div>
</Wrapper>
);
};
export default Search;
+170
View File
@@ -0,0 +1,170 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'gray'
| 'white';
type?: 'text' | 'email' | 'password' | 'file' | 'number';
fullWidth?: boolean;
};
export const Wrapper = styled.div<WrapperProps>`
.search {
width: inherit;
height: inherit;
border-radius: 5px;
padding: 2px;
color: ${({ theme }) => theme.colors.black.main};
div {
background: ${({ theme }) => theme.colors.white.main};
padding: 1rem;
border-radius: 5px;
display: flex;
flex-direction: row;
align-items: center;
}
}
input {
width: 100%;
background: none;
border: none;
color: ${({ theme }) => theme.colors.black.main};
}
.icon {
cursor: pointer;
display: inline-flex;
align-items: center;
}
.icon.left {
margin-right: 0.5rem;
}
${({ color, theme }) => {
switch (color) {
case 'client':
return css`
.search {
background: ${theme.colors.client.light};
}
.icon svg path {
stroke: ${theme.colors.client.main};
}
`;
case 'productOwner':
return css`
.search {
background: ${theme.colors.productOwner.light};
}
.icon svg path {
stroke: ${theme.colors.productOwner.main};
}
`;
case 'developer':
return css`
.search {
background: ${theme.colors.developer.light};
}
.icon svg path {
stroke: ${theme.colors.developer.main};
}
`;
case 'admin':
return css`
.search {
background: ${theme.colors.admin.light};
}
.icon svg path {
stroke: ${theme.colors.admin.main};
}
`;
case 'success':
return css`
.search {
background: ${theme.colors.success.main};
}
.icon svg path {
stroke: ${theme.colors.success.main};
}
`;
case 'warning':
return css`
.search {
background: ${theme.colors.warning.main};
}
.icon svg path {
stroke: ${theme.colors.warning.main};
}
`;
case 'error':
return css`
.search {
background: ${theme.colors.error.main};
}
.icon svg path {
stroke: ${theme.colors.error.main};
}
`;
case 'black':
return css`
.search {
background: ${theme.colors.black.main};
}
.icon svg path {
stroke: ${theme.colors.black.main};
}
`;
case 'white':
return css`
.search {
background: ${theme.colors.white.main};
}
.icon svg path {
stroke: ${theme.colors.white.main};
}
`;
default:
return css`
.search {
background: ${theme.colors.client.light};
}
.icon svg path {
stroke: ${theme.colors.client.main};
}
`;
}
}}
${({ fullWidth }) =>
fullWidth &&
css`
width: 100%;
font-size: 1.25rem;
.icon svg {
width: 1.25rem;
height: 1.25rem;
}
`};
`;
+34
View File
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,119 @@
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};
}
`};
`;
+75
View File
@@ -0,0 +1,75 @@
import { Wrapper } from './styles';
import { Text } from '..';
type SelectProps = {
className?: string;
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white';
error?: boolean;
errorMessage?: string;
options: Array<{ value: any; label: string }>;
value: string;
select?: any;
name: string;
label?: string;
fullWidth?: boolean;
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLSelectElement>) => void;
};
const Select = ({
color = 'client',
label,
name,
value,
select = null,
options,
onChange,
onBlur,
error,
errorMessage,
...props
}: SelectProps) => {
return (
<Wrapper label={label} error={error} color={color} {...props}>
<div className='info'>
{label && (
<Text variant='body' weight='bold' className='label'>
{label}
</Text>
)}
{error && errorMessage && (
<Text variant='body' color='error' className='error-message'>
{errorMessage}
</Text>
)}
</div>
<div className='select'>
<div>
<select
value={select || value}
name={name}
onChange={onChange}
onBlur={onBlur}
>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</Wrapper>
);
};
export default Select;
+154
View File
@@ -0,0 +1,154 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'gray'
| 'white';
error?: boolean;
errorMessage?: string;
type?: 'text' | 'email' | 'password' | 'file' | 'number';
label?: string;
fullWidth?: boolean;
};
export const Wrapper = styled.div<WrapperProps>`
.select {
width: inherit;
height: inherit;
border-radius: 5px;
padding: 2px;
color: ${({ theme }) => theme.colors.black.main};
div {
background: ${({ theme }) => theme.colors.white.main};
padding: 1rem;
border-radius: 5px;
display: flex;
flex-direction: row;
align-items: center;
}
}
.info {
margin-bottom: 5px;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
p {
background: ${({ theme }) => theme.colors.gray.dark};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.label {
justify-self: flex-start;
}
.error-message {
justify-self: flex-end;
}
}
select {
width: 100%;
background: none;
border: none;
color: ${({ theme }) => theme.colors.black.main};
background-image: url('../../assets/icons/chevron-down.svg');
}
${({ color, theme }) => {
switch (color) {
case 'client':
return css`
.select {
background: ${theme.colors.client.light};
}
`;
case 'productOwner':
return css`
.select {
background: ${theme.colors.productOwner.light};
}
`;
case 'developer':
return css`
.select {
background: ${theme.colors.developer.light};
}
`;
case 'admin':
return css`
.select {
background: ${theme.colors.admin.light};
}
`;
case 'success':
return css`
.select {
background: ${theme.colors.success.main};
}
`;
case 'warning':
return css`
.select {
background: ${theme.colors.warning.main};
}
`;
case 'error':
return css`
.select {
background: ${theme.colors.error.main};
}
`;
case 'black':
return css`
.select {
background: ${theme.colors.black.main};
}
`;
case 'white':
return css`
.select {
background: ${theme.colors.white.main};
}
`;
default:
return css`
.select {
background: ${theme.colors.client.light};
}
`;
}
}}
${({ error, theme }) =>
error &&
css`
.info p {
background: ${theme.colors.error.main};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.select {
background: ${theme.colors.error.main};
}
`}
${({ fullWidth }) =>
fullWidth &&
css`
width: 100%;
font-size: 1.25rem;
`};
`;
+286
View File
@@ -0,0 +1,286 @@
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/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/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/, '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/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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,53 @@
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;
`;
}
}}
`;
+204
View File
@@ -0,0 +1,204 @@
import { forwardRef } from 'react';
import { Box, Text } from '..';
import { FeatureOutput, SpecificationOutput } from '../../graphql/types';
import { Wrapper } from './styles';
type SpecificationProps = {
specification: SpecificationOutput;
features: Array<FeatureOutput>;
};
const Specification = forwardRef<HTMLDivElement, SpecificationProps>(
({ specification, features }, ref) => {
return (
<Wrapper ref={ref}>
<Box marginBottom='30px'>
<Text variant='title'>Customer Requirements Specifications</Text>
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
1. Introduction
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
1.1. Purpose
</Text>
<Text variant='body'>{specification.introduction.purpose}</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
1.2. Document Conventions
</Text>
<Text variant='body'>
{specification.introduction.documentConventions}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
1.3. Intended Audience and Reading Suggestions
</Text>
<Text variant='body'>
{specification.introduction.intendedAudience}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
1.4. Project Scope
</Text>
<Text variant='body'>
{specification.introduction.projectScope}
</Text>
</Box>
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
2. Overall Description
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
2.1. Project Perspective
</Text>
<Text variant='body'>
{specification.overallDescription.perspective}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
2.2. User Classes and Characteristics
</Text>
<Text variant='body'>
{specification.overallDescription.userCharacteristics}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
2.3. Operating Environment
</Text>
<Text variant='body'>
{specification.overallDescription.operatingEnvironment}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
2.4. Design and Implementation Constraints
</Text>
<Text variant='body'>
{specification.overallDescription.designImplementationConstraints}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
2.5. User Documentation
</Text>
<Text variant='body'>
{specification.overallDescription.userDocumentation}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
2.6. Assumptions and Dependencies
</Text>
<Text variant='body'>
{specification.overallDescription.assemptionsDependencies}
</Text>
</Box>
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
3. System Features
</Text>
</Box>
{features.map((feature, index) => (
<Box marginBottom='5px' key={feature.id}>
<Text variant='subheader' weight='bold' gutterBottom>
3.{index + 1}. {feature.name}
</Text>
<Text variant='body'>{feature.description}</Text>
</Box>
))}
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
4. Other Non-Functional Requirements
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
4.1. Performance Requirements
</Text>
<Text variant='body'>
{specification.nonFunctionalRequirements.performanceRequirements}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
4.2. Safety Requirements
</Text>
<Text variant='body'>
{specification.nonFunctionalRequirements.safetyRequirements}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
4.3. Security Requirements
</Text>
<Text variant='body'>
{specification.nonFunctionalRequirements.securityRequirements}
</Text>
</Box>
<Box marginBottom='5px'>
<Text variant='subheader' weight='bold' gutterBottom>
4.4. Software Quality Attributes
</Text>
<Text variant='body'>
{
specification.nonFunctionalRequirements
.softwareQualityAttributes
}
</Text>
</Box>
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
5. Other Requirements
</Text>
</Box>
<Text variant='body'>{specification.otherRequirements}</Text>
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
6. Glossary
</Text>
</Box>
<Text variant='body'>{specification.glossary}</Text>
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
6. Analysis Models
</Text>
</Box>
<Text variant='body'>{specification.analysisModels}</Text>
</Box>
<Box marginBottom='15px'>
<Box marginBottom='10px'>
<Text variant='headline' weight='bold'>
7. Issues List
</Text>
</Box>
<Text variant='body'>{specification.issuesList}</Text>
</Box>
</Wrapper>
);
}
);
export default Specification;
+5
View File
@@ -0,0 +1,5 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 1rem;
`;
+23
View File
@@ -0,0 +1,23 @@
import { Wrapper } from './styles';
type SpinnerProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'white'
| 'black'
| 'gray';
fullScreen?: boolean;
};
const Spinner = ({ fullScreen = false, color = 'client' }: SpinnerProps) => {
return (
<Wrapper fullScreen={fullScreen} color={color}>
<div className='lds-dual-ring'></div>
</Wrapper>
);
};
export default Spinner;
+57
View File
@@ -0,0 +1,57 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'white'
| 'black'
| 'gray';
fullScreen?: boolean;
};
export const Wrapper = styled.div<WrapperProps>`
${({ fullScreen }) =>
fullScreen &&
css`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99;
`}
.lds-dual-ring {
display: inline-block;
width: 80px;
height: 80px;
}
.lds-dual-ring:after {
content: ' ';
display: block;
width: 35px;
height: 35px;
margin: 8px;
border-radius: 50%;
border: 6px solid
${({ theme, color }) =>
color ? theme.colors[color].main : theme.colors.client.main};
border-color: ${({ theme, color }) =>
color ? theme.colors[color].main : theme.colors.client.main}
transparent
${({ theme, color }) =>
color ? theme.colors[color].main : theme.colors.client.main}
transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`;
+113
View File
@@ -0,0 +1,113 @@
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,
},
fetchPolicy: 'network-only',
});
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
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,50 @@
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;
+38
View File
@@ -0,0 +1,38 @@
import { Wrapper } from './styles';
type TextProps = {
children?: React.ReactNode | JSX.Element | string;
className?: string;
variant?: 'display' | 'headline' | 'title' | 'subheader' | 'body' | 'caption';
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white'
| string;
align?: 'inherit' | 'left' | 'center' | 'right' | 'justify';
display?: 'initial' | 'block' | 'inline';
gutterBottom?: boolean;
lineThrough?: boolean;
weight?: 'initial' | 'normal' | 'bold' | number;
};
const Text = ({
children,
variant = 'body',
className,
...props
}: TextProps) => {
return (
<Wrapper className={`${variant} ${className}`} {...props}>
{children}
</Wrapper>
);
};
export default Text;
+140
View File
@@ -0,0 +1,140 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white'
| string;
align?: 'inherit' | 'left' | 'center' | 'right' | 'justify';
display?: 'initial' | 'block' | 'inline';
gutterBottom?: boolean;
lineThrough?: boolean;
weight?: 'initial' | 'normal' | 'bold' | number;
};
export const Wrapper = styled.p<WrapperProps>`
${({ color, theme }) => {
if (!color)
return css`
color: inherit;
`;
switch (color) {
case 'client':
return css`
color: ${theme.colors.client.main};
`;
case 'productOwner':
return css`
color: ${theme.colors.productOwner.main};
`;
case 'developer':
return css`
color: ${theme.colors.developer.main};
`;
case 'admin':
return css`
color: ${theme.colors.admin.main};
`;
case 'success':
return css`
color: ${theme.colors.success.main};
`;
case 'warning':
return css`
color: ${theme.colors.warning.main};
`;
case 'error':
return css`
color: ${theme.colors.error.main};
`;
case 'black':
return css`
color: ${theme.colors.black.main};
`;
case 'white':
return css`
color: ${theme.colors.white.main};
`;
default:
return css`
color: ${color};
`;
}
}}
${({ display }) =>
display
? css`
display: ${display};
`
: css`
display: block;
`}
${({ gutterBottom }) =>
gutterBottom &&
css`
margin-bottom: 0.35rem;
`};
${({ lineThrough }) =>
lineThrough &&
css`
text-decoration: line-through;
`};
${({ align }) =>
align
? css`
align: ${align};
`
: css`
align: initial;
`}
${({ weight }) =>
weight
? css`
font-weight: ${weight};
`
: css`
font-weight: initial;
`}
&.display {
font-size: 2.25rem;
line-height: 3rem;
}
&.headline {
font-size: 1.5rem;
line-height: 2rem;
}
&.title {
font-size: 1.25rem;
line-height: 1.75rem;
}
&.subheader {
font-size: 1rem;
line-height: 1.5rem;
}
&.body {
font-size: 0.875rem;
line-height: 1.25rem;
}
&.caption {
font-size: 0.75rem;
line-height: 1rem;
}
`;
+69
View File
@@ -0,0 +1,69 @@
import { Wrapper } from './styles';
import { Text } from '..';
type TextAreaProps = {
className?: string;
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'white';
error?: boolean;
errorMessage?: string;
value: string;
label?: string;
name: string;
placeholder?: string;
fullWidth?: boolean;
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
};
const TextArea = ({
color = 'client',
label,
name,
placeholder,
value,
onChange,
onBlur,
error,
errorMessage,
...props
}: TextAreaProps) => {
return (
<Wrapper label={label} error={error} color={color} {...props}>
<div className='info'>
{label && (
<Text variant='body' weight='bold' className='label'>
{label}
</Text>
)}
{error && errorMessage && (
<Text variant='body' color='error' className='error-message'>
{errorMessage}
</Text>
)}
</div>
<div className='textarea'>
<div>
<textarea
rows={5}
value={value}
name={name}
onChange={onChange}
onBlur={onBlur}
placeholder={placeholder}
/>
</div>
</div>
</Wrapper>
);
};
export default TextArea;
+150
View File
@@ -0,0 +1,150 @@
import styled, { css } from 'styled-components';
type WrapperProps = {
color?:
| 'client'
| 'productOwner'
| 'developer'
| 'admin'
| 'success'
| 'warning'
| 'error'
| 'black'
| 'gray'
| 'white';
error?: boolean;
errorMessage?: string;
label?: string;
fullWidth?: boolean;
};
export const Wrapper = styled.div<WrapperProps>`
.textarea {
width: inherit;
height: inherit;
border-radius: 5px;
padding: 2px;
color: ${({ theme }) => theme.colors.black.main};
div {
background: ${({ theme }) => theme.colors.white.main};
padding: 1rem;
border-radius: 5px;
}
}
.info {
margin-bottom: 5px;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
p {
background: ${({ theme }) => theme.colors.gray.dark};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.label {
justify-self: flex-start;
}
.error-message {
justify-self: flex-end;
}
}
textarea {
width: 100%;
background: none;
border: none;
resize: none;
color: ${({ theme }) => theme.colors.black.main};
}
${({ color, theme }) => {
switch (color) {
case 'client':
return css`
.textarea {
background: ${theme.colors.client.light};
}
`;
case 'productOwner':
return css`
.textarea {
background: ${theme.colors.productOwner.light};
}
`;
case 'developer':
return css`
.textarea {
background: ${theme.colors.developer.light};
}
`;
case 'admin':
return css`
.textarea {
background: ${theme.colors.admin.light};
}
`;
case 'success':
return css`
.textarea {
background: ${theme.colors.success.main};
}
`;
case 'warning':
return css`
.textarea {
background: ${theme.colors.warning.main};
}
`;
case 'error':
return css`
.textarea {
background: ${theme.colors.error.main};
}
`;
case 'black':
return css`
.textarea {
background: ${theme.colors.black.main};
}
`;
case 'white':
return css`
.textarea {
background: ${theme.colors.white.main};
}
`;
default:
return css`
.textarea {
background: ${theme.colors.client.light};
}
`;
}
}}
${({ error, theme }) =>
error &&
css`
.info p {
background: ${theme.colors.error.main};
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.textarea {
background: ${theme.colors.error.main};
}
`}
${({ fullWidth }) =>
fullWidth &&
css`
width: 100%;
font-size: 1.25rem;
`};
`;
+65
View File
@@ -0,0 +1,65 @@
import Button from './Button';
import IconButton from './IconButton';
import Box from './Box';
import Text from './Text';
import Link from './Link';
import Input from './Input';
import TextArea from './TextArea';
import Select from './Select';
import Search from './Search';
import Avatar from './Avatar';
import ContextMenu from './ContextMenu';
import Spinner from './Spinner';
import Alert from './Alert';
import CheckBox from './CheckBox';
import Menu from './Menu';
import Navbar from './Navbar';
import Sidebar from './Sidebar';
import Protected from './Protected';
import Public from './Public';
import SectionSelector from './SectionSelector';
import Modal from './Modal';
import SidebarItem from './SidebarItem';
import ImagePreview from './ImagePreview';
import FeatureCard from './FeatureCard';
import FrontendFeatureCard from './FrontendFeatureCard';
import BackendFeatureCard from './BackendFeatureCard';
import Specification from './Specification';
import Chip from './Chip';
import CategoryCard from './CategoryCard';
import TemplateCard from './TemplateCard';
import SupportSidebar from './SupportSidebar';
export {
Button,
IconButton,
Box,
Text,
Link,
Input,
TextArea,
Select,
Search,
Avatar,
ContextMenu,
Menu,
Spinner,
Alert,
CheckBox,
Navbar,
Sidebar,
Protected,
Public,
SectionSelector,
Modal,
SidebarItem,
ImagePreview,
FeatureCard,
FrontendFeatureCard,
BackendFeatureCard,
Specification,
Chip,
CategoryCard,
TemplateCard,
SupportSidebar,
};