mirror of
https://github.com/hazemKrimi/crimson-quirks-ui.git
synced 2026-05-02 02:30:29 +00:00
Clear git cache
This commit is contained in:
@@ -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;
|
||||
@@ -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};
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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}`};
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
`}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
}
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
`}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`};
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`};
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
}
|
||||
`};
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
`};
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 1rem;
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
`};
|
||||
`;
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user